From 9b41c11b9727e7eb53dbbc47a632f6eba5bdf7cc Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 22:40:43 +0100 Subject: [PATCH 01/32] refactor: simplify SurfCoreApi companion object and instance retrieval --- .../kotlin/dev/slne/surf/surfapi/core/api/SurfCoreApi.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/SurfCoreApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/SurfCoreApi.kt index 2f39642b7..ed086e73e 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/SurfCoreApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/SurfCoreApi.kt @@ -21,17 +21,17 @@ interface SurfCoreApi { */ fun getPlayer(playerUuid: UUID): Any? - companion object { + companion object : SurfCoreApi by surfCoreApi { /** * The instance of the SurfCoreApi. */ @JvmStatic - val instance = requiredService() + val instance = surfCoreApi } } /** * The instance of the SurfCoreApi. */ -val surfCoreApi get() = SurfCoreApi.instance \ No newline at end of file +val surfCoreApi = requiredService() \ No newline at end of file From 9865169fc6e3a9d19102da6071b20ced454a18d4 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:26:09 +0100 Subject: [PATCH 02/32] refactor: introduce SurfMiniMessageHolder for centralized MiniMessage handling and deprecate DazzlConf usage --- .../surfapi/core/api/config/SurfConfigApi.kt | 13 ++- .../config/manager/DazzlConfConfigManager.kt | 16 ++- .../api/config/manager/SpongeConfigManager.kt | 13 +-- .../serializer/DefaultDazzlConfSerializers.kt | 63 ++++-------- .../serializer/SpongeConfigSerializers.kt | 30 +++--- .../api/minimessage/SurfMiniMessageHolder.kt | 99 +++++++++++++++++++ 6 files changed, 162 insertions(+), 72 deletions(-) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt index 18300e47a..7472d8a0c 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt @@ -1,5 +1,6 @@ package dev.slne.surf.surfapi.core.api.config +import dev.slne.surf.surfapi.core.api.config.manager.DazzlConfDeprecationMessageHolder import dev.slne.surf.surfapi.core.api.config.manager.PreferUsingSpongeConfigOverDazzlConf import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager import dev.slne.surf.surfapi.core.api.util.requiredService @@ -21,6 +22,7 @@ interface SurfConfigApi { * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf + @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) fun createDazzlConfig( configClass: Class, configFolder: Path, @@ -35,6 +37,7 @@ interface SurfConfigApi { * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf + @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) fun getDazzlConfig(configClass: Class): C /** @@ -45,6 +48,7 @@ interface SurfConfigApi { * @return The reloaded instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf + @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) fun reloadDazzlConfig(configClass: Class): C /** @@ -134,18 +138,18 @@ interface SurfConfigApi { */ fun getSpongeConfigManagerForConfig(configClass: Class): SpongeConfigManager - companion object { + companion object: SurfConfigApi by surfConfigApi { /** * Retrieves the singleton instance of [SurfConfigApi]. */ - val instance = requiredService() + val instance = surfConfigApi } } /** * Retrieves the singleton instance of [SurfConfigApi]. */ -val surfConfigApi get() = SurfConfigApi.instance +val surfConfigApi = requiredService() /** * Creates a DazzlConf configuration using a reified type. @@ -156,6 +160,7 @@ val surfConfigApi get() = SurfConfigApi.instance * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf +@Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) inline fun SurfConfigApi.createDazzlConfig( configFolder: Path, configFileName: @YamlConfigFileNamePattern String, @@ -168,6 +173,7 @@ inline fun SurfConfigApi.createDazzlConfig( * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf +@Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) inline fun SurfConfigApi.getDazzlConfig() = getDazzlConfig(C::class.java) /** @@ -177,6 +183,7 @@ inline fun SurfConfigApi.getDazzlConfig() = getDazzlConfig(C::class. * @return The reloaded instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf +@Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) inline fun SurfConfigApi.reloadDazzlConfig() = reloadDazzlConfig(C::class.java) /** diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt index a0f46eb65..7a92b6f7a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt @@ -3,6 +3,7 @@ package dev.slne.surf.surfapi.core.api.config.manager import dev.slne.surf.surfapi.core.api.config.YamlConfigFileNamePattern import dev.slne.surf.surfapi.core.api.config.serializer.DefaultDazzlConfSerializers import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi import space.arim.dazzleconf.ConfigurationOptions import space.arim.dazzleconf.error.ConfigFormatSyntaxException import space.arim.dazzleconf.error.InvalidConfigException @@ -16,12 +17,19 @@ import java.nio.file.Path import java.util.concurrent.TimeUnit @RequiresOptIn( - level = RequiresOptIn.Level.WARNING, - message = "Prefer using Sponge's Configurate library over DazzlConf" + level = RequiresOptIn.Level.ERROR, + message = DazzlConfDeprecationMessageHolder.MESSAGE ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) -annotation class PreferUsingSpongeConfigOverDazzlConf +annotation class PreferUsingSpongeConfigOverDazzlConf { +} + +@InternalSurfApi +object DazzlConfDeprecationMessageHolder { + const val MESSAGE = + "Prefer using Sponge's Configurate library over DazzlConf. DazzlConf will be removed in a future release. If you need to use DazzlConf, please contact the developers to discuss your use case." +} /** * Manages configurations using the DazzlConf library, including loading, saving, and reloading configurations. @@ -31,6 +39,7 @@ annotation class PreferUsingSpongeConfigOverDazzlConf * @property config The current configuration instance, or `null` if not yet loaded. */ @PreferUsingSpongeConfigOverDazzlConf +@Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) class DazzlConfConfigManager private constructor(private val helper: ConfigurationHelper) { @Volatile var config: C? = null @@ -102,6 +111,7 @@ class DazzlConfConfigManager private constructor(private val helper: Configur * @return A new instance of [DazzlConfConfigManager]. */ @JvmStatic + @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) fun create( configClass: Class, configFolder: Path, diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt index f384fd2ec..f5c9ae9ad 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt @@ -4,7 +4,6 @@ import dev.slne.surf.surfapi.core.api.config.JsonConfigFileNamePattern import dev.slne.surf.surfapi.core.api.config.YamlConfigFileNamePattern import dev.slne.surf.surfapi.core.api.config.serializer.SpongeConfigSerializers import dev.slne.surf.surfapi.core.api.util.logger -import org.jetbrains.annotations.Contract import org.spongepowered.configurate.ConfigurateException import org.spongepowered.configurate.ConfigurationNode import org.spongepowered.configurate.ScopedConfigurationNode @@ -15,6 +14,7 @@ import org.spongepowered.configurate.serialize.SerializationException import org.spongepowered.configurate.yaml.NodeStyle import org.spongepowered.configurate.yaml.YamlConfigurationLoader import java.io.Serial +import java.io.UncheckedIOException import java.nio.file.Path /** @@ -24,7 +24,7 @@ import java.nio.file.Path * @param C The type of the configuration class. * @property config The current configuration instance. */ -class SpongeConfigManager @Contract(pure = true) private constructor( +class SpongeConfigManager private constructor( private val configClass: Class, @JvmField @field:Volatile var config: C, private val loader: ConfigurationLoader, @@ -34,8 +34,9 @@ class SpongeConfigManager @Contract(pure = true) private constructor( /** * Saves the current configuration to the file. * - * @throws RuntimeException if an I/O error or serialization error occurs. + * @throws UncheckedIOException if an I/O error or serialization error occurs. */ + @Throws(UncheckedIOException::class) fun save() { try { node.set(configClass, config) @@ -44,7 +45,7 @@ class SpongeConfigManager @Contract(pure = true) private constructor( log.atSevere() .withCause(e) .log("Failed to save config") - throw RuntimeException(e) + throw UncheckedIOException(e) } } @@ -52,7 +53,7 @@ class SpongeConfigManager @Contract(pure = true) private constructor( * Reloads the configuration from the file. If loading fails, the current configuration remains unchanged. * * @return The reloaded configuration instance. - * @throws RuntimeException if a critical error occurs during reload. + * @throws UncheckedIOException if an I/O error occurs during reload. */ fun reloadFromFile(): C { try { @@ -74,7 +75,7 @@ class SpongeConfigManager @Contract(pure = true) private constructor( log.atSevere() .withCause(e) .log("Failed to reload config") - throw RuntimeException(e) + throw UncheckedIOException(e) } } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers.kt index db95b0528..4fb95adc1 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers.kt @@ -1,15 +1,14 @@ package dev.slne.surf.surfapi.core.api.config.serializer import dev.slne.surf.surfapi.core.api.config.manager.PreferUsingSpongeConfigOverDazzlConf -import dev.slne.surf.surfapi.core.api.messages.Colors +import dev.slne.surf.surfapi.core.api.minimessage.SurfMiniMessageHolder import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.TextColor import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.ParsingException -import net.kyori.adventure.text.minimessage.tag.Tag import space.arim.dazzleconf.serialiser.Decomposer import space.arim.dazzleconf.serialiser.FlexibleType import space.arim.dazzleconf.serialiser.ValueSerialiser +import java.util.concurrent.CopyOnWriteArrayList /** * Default serializers for DazzlConf configuration files. Provides support for custom types such as Adventure [Component]. @@ -20,7 +19,11 @@ object DefaultDazzlConfSerializers { /** * The default list of serializers used in DazzlConf configurations. */ - val DEFAULTS = mutableListOf>(ComponentSerializer()) + val DEFAULTS = CopyOnWriteArrayList>() + + init { + DEFAULTS.add(ComponentSerializer()) + } /** * Serializer for [Component] objects in DazzlConf configurations. @@ -29,7 +32,7 @@ object DefaultDazzlConfSerializers { override fun getTargetClass() = Component::class.java override fun deserialise(flexibleType: FlexibleType): Component { try { - return miniMessage.deserialize(flexibleType.string) + return SurfMiniMessageHolder.miniMessage().deserialize(flexibleType.string) } catch (e: ParsingException) { throw flexibleType.badValueExceptionBuilder() .message( @@ -45,48 +48,24 @@ object DefaultDazzlConfSerializers { } override fun serialise(value: Component, decomposer: Decomposer?) = - miniMessage.serialize(value) + SurfMiniMessageHolder.miniMessage().serialize(value) companion object { - private val builder = MiniMessage.builder() - .editTags { - val tags = mapOf( - "primary" to Colors.PRIMARY, - "secondary" to Colors.SECONDARY, - "info" to Colors.INFO, - "success" to Colors.SUCCESS, - "warning" to Colors.WARNING, - "error" to Colors.ERROR, - "variable_key" to Colors.VARIABLE_KEY, - "variable_value" to Colors.VARIABLE_VALUE, - "spacer" to Colors.SPACER, - "dark_spacer" to Colors.DARK_SPACER, - "prefix_color" to Colors.PREFIX_COLOR - ) - - tags.forEach { (tag, color) -> - it.tag(tag) { _, _ -> colorTag(color) } - } - } - @JvmStatic - var miniMessage: MiniMessage = builder.build() - get() { - if (modified) { - field = builder.build() - modified = false - } - - return field - } - private set - private var modified = false - - private fun colorTag(color: TextColor) = Tag.styling { it.color(color) } + @Deprecated( + message = "Configs now use the MiniMessage instance supplied by the SurfMiniMessageHolder", + replaceWith = ReplaceWith( + "SurfMiniMessageHolder.miniMessage()", + "dev.slne.surf.surfapi.core.api.minimessage.SurfMiniMessageHolder" + ) + ) + val miniMessage: MiniMessage = SurfMiniMessageHolder.miniMessage() + @Deprecated( + message = "Cannot customize MiniMessage anymore. If you need custom tags, use your own MiniMessage instance instead and use a String in the config, then parse it manually (Consider caching the parsed value in a lazy value though).", + level = DeprecationLevel.ERROR + ) fun customizeMiniMessage(modifier: (MiniMessage.Builder) -> Unit) { - modifier(builder) - modified = true } } } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/SpongeConfigSerializers.kt index d54280c39..afc9265f6 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/serializer/SpongeConfigSerializers.kt @@ -1,7 +1,7 @@ package dev.slne.surf.surfapi.core.api.config.serializer import dev.slne.surf.surfapi.core.api.config.manager.PreferUsingSpongeConfigOverDazzlConf -import dev.slne.surf.surfapi.core.api.config.serializer.DefaultDazzlConfSerializers.ComponentSerializer.Companion.miniMessage +import dev.slne.surf.surfapi.core.api.minimessage.SurfMiniMessageHolder import io.leangen.geantyref.TypeToken import net.kyori.adventure.text.Component import org.spongepowered.configurate.ConfigurationNode @@ -11,8 +11,8 @@ import org.spongepowered.configurate.serialize.SerializationException import org.spongepowered.configurate.serialize.TypeSerializer import org.spongepowered.configurate.serialize.TypeSerializerCollection import org.spongepowered.configurate.util.CheckedConsumer +import java.lang.reflect.AnnotatedParameterizedType import java.lang.reflect.AnnotatedType -import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* import java.util.function.Consumer @@ -38,13 +38,9 @@ object SpongeConfigSerializers { @OptIn(PreferUsingSpongeConfigOverDazzlConf::class) override fun deserialize(type: Type?, node: ConfigurationNode): Component { - val message = node.string + val message = node.string ?: return Component.empty() - if (message == null) { - return Component.empty() - } - - return miniMessage.deserialize(message) + return SurfMiniMessageHolder.miniMessage().deserialize(message) } @OptIn(PreferUsingSpongeConfigOverDazzlConf::class) @@ -53,7 +49,7 @@ object SpongeConfigSerializers { return } - node.set(miniMessage.serialize(obj)) + node.set(SurfMiniMessageHolder.miniMessage().serialize(obj)) } } @@ -62,18 +58,16 @@ object SpongeConfigSerializers { */ class LinkedListSerializer : AbstractListChildSerializer>() { - override fun elementType(containerType: Type?): Type { - if (containerType !is ParameterizedType) { - throw SerializationException( - containerType, - "Raw types are not supported for collections" - ) + override fun elementType(containerType: AnnotatedType): AnnotatedType? { + if (containerType !is AnnotatedParameterizedType) { + throw SerializationException(containerType, "Raw types are not supported for collections") } - return containerType.actualTypeArguments[0] + + return containerType.annotatedActualTypeArguments[0] } - override fun createNew(length: Int, elementType: AnnotatedType?): LinkedList? { - return LinkedList() + override fun createNew(length: Int, elementType: AnnotatedType?): LinkedList { + return LinkedList() } @Throws(SerializationException::class) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder.kt new file mode 100644 index 000000000..8405f1c9e --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder.kt @@ -0,0 +1,99 @@ +package dev.slne.surf.surfapi.core.api.minimessage + +import dev.slne.surf.surfapi.core.api.messages.Colors +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.Tag + +/** + * Holds a preconfigured [MiniMessage] instance with custom color tags and prefix support for the Surf API. + * + * This object provides a centralized MiniMessage parser that includes custom tags for consistent + * color theming throughout the application. All custom tags are registered at initialization and + * map to predefined colors from [Colors]. + * + * ## Available Custom Tags + * + * ### Color Tags + * - `` - Primary theme color + * - `` - Secondary theme color + * - `` - Information message color + * - `` - Success message color + * - `` - Warning message color + * - `` - Error message color + * - `` - Variable key color + * - `` - Variable value color + * - `` - Spacer color + * - `` - Dark spacer color + * - `` - Prefix text color + * + * ### Prefix Tag + * The `` tag is a self-closing tag that inserts a predefined prefix component. + * It supports optional types: + * - `` - Default prefix + * - `` - Info prefix + * - `` - Success prefix + * - `` - Warning prefix + * - `` - Error prefix + * + * @see Colors + */ +object SurfMiniMessageHolder { + private val minimessage = MiniMessage.builder() + .editTags { tagBuilder -> + val tags = mapOf( + "primary" to Colors.PRIMARY, + "secondary" to Colors.SECONDARY, + "info" to Colors.INFO, + "success" to Colors.SUCCESS, + "warning" to Colors.WARNING, + "error" to Colors.ERROR, + "variable_key" to Colors.VARIABLE_KEY, + "variable_value" to Colors.VARIABLE_VALUE, + "spacer" to Colors.SPACER, + "dark_spacer" to Colors.DARK_SPACER, + "prefix_color" to Colors.PREFIX_COLOR, + ) + + tags.forEach { (tag, color) -> + tagBuilder.tag(tag) { _, _ -> colorTag(color) } + } + + tagBuilder + .tag("prefix") { queue, context -> + val prefix = when (val type = queue.peek()?.lowerValue()) { + null -> Colors.PREFIX + "info" -> Colors.INFO_PREFIX + "success" -> Colors.SUCCESS_PREFIX + "warning" -> Colors.WARNING_PREFIX + "error" -> Colors.ERROR_PREFIX + else -> throw context.newException("Unknown prefix type: $type", queue) + } + + Tag.selfClosingInserting(prefix) + } + } + .build() + + /** + * Creates a color styling tag for the given [TextColor]. + * + * @param color The text color to apply + * @return A [Tag] that applies the color styling + */ + private fun colorTag(color: TextColor) = Tag.styling { it.color(color) } + + /** + * Returns the preconfigured [MiniMessage] instance. + * + * @return The MiniMessage parser with custom Surf API tags + */ + fun miniMessage() = minimessage +} + +/** + * Convenience property for accessing the preconfigured [MiniMessage] instance. + * + * @see SurfMiniMessageHolder + */ +val miniMessage get() = SurfMiniMessageHolder.miniMessage() \ No newline at end of file From b5ddbd6569e4b620a5633fd5265d8af23b4287c7 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:26:50 +0100 Subject: [PATCH 03/32] refactor: remove empty file --- .../surf/surfapi/core/api/extensions/glm/Vec3iExtensions.kt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/extensions/glm/Vec3iExtensions.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/extensions/glm/Vec3iExtensions.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/extensions/glm/Vec3iExtensions.kt deleted file mode 100644 index b8a32b55f..000000000 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/extensions/glm/Vec3iExtensions.kt +++ /dev/null @@ -1,2 +0,0 @@ -package dev.slne.surf.surfapi.core.api.extensions.glm - From bff820d8a9e66cb60a923bf71746fb458862a2b1 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:35:03 +0100 Subject: [PATCH 04/32] refactor: suppress deprecation warnings for DazzlConf usage in configuration files --- .../dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt | 3 +++ .../surfapi/core/api/config/manager/DazzlConfConfigManager.kt | 2 ++ .../surf/surfapi/core/server/config/DazzlConfConfigTracker.kt | 1 + 3 files changed, 6 insertions(+) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt index 7472d8a0c..6efcdba5a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt @@ -161,6 +161,7 @@ val surfConfigApi = requiredService() */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) +@Suppress("DEPRECATION_ERROR") inline fun SurfConfigApi.createDazzlConfig( configFolder: Path, configFileName: @YamlConfigFileNamePattern String, @@ -174,6 +175,7 @@ inline fun SurfConfigApi.createDazzlConfig( */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) +@Suppress("DEPRECATION_ERROR") inline fun SurfConfigApi.getDazzlConfig() = getDazzlConfig(C::class.java) /** @@ -184,6 +186,7 @@ inline fun SurfConfigApi.getDazzlConfig() = getDazzlConfig(C::class. */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) +@Suppress("DEPRECATION_ERROR") inline fun SurfConfigApi.reloadDazzlConfig() = reloadDazzlConfig(C::class.java) /** diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt index 7a92b6f7a..43b18fd30 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager.kt @@ -112,6 +112,8 @@ class DazzlConfConfigManager private constructor(private val helper: Configur */ @JvmStatic @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) + @PreferUsingSpongeConfigOverDazzlConf + @Suppress("DEPRECATION_ERROR") fun create( configClass: Class, configFolder: Path, diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/DazzlConfConfigTracker.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/DazzlConfConfigTracker.kt index 89290d762..ea61d8b0c 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/DazzlConfConfigTracker.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/DazzlConfConfigTracker.kt @@ -1,3 +1,4 @@ +@file:Suppress("DEPRECATION_ERROR") package dev.slne.surf.surfapi.core.server.config import dev.slne.surf.surfapi.core.api.config.YamlConfigFileNamePattern From e36b5636888d66ce38821e25bdfea72d77f34df9 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:42:17 +0100 Subject: [PATCH 05/32] refactor: enhance small caps conversion with CharSequence extension and deprecate String overload --- .../surf/surfapi/core/api/font/small-caps.kt | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/font/small-caps.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/font/small-caps.kt index 36805507d..91865a1f3 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/font/small-caps.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/font/small-caps.kt @@ -2,6 +2,12 @@ package dev.slne.surf.surfapi.core.api.font import dev.slne.surf.surfapi.core.api.util.char2CharMapOf +/** + * A character mapping table from lowercase ASCII letters and digits to their Unicode small caps equivalents. + * + * Maps lowercase letters a-z to their corresponding small caps characters (ᴀ-ᴢ) and preserves digits 0-9. + * Some letters like 's' and 'x' map to themselves as they lack distinct small caps Unicode characters. + */ private val smallCapsMap = char2CharMapOf( 'a' to 'ᴀ', 'b' to 'ʙ', 'c' to 'ᴄ', 'd' to 'ᴅ', 'e' to 'ᴇ', 'f' to 'ғ', 'g' to 'ɢ', 'h' to 'ʜ', 'i' to 'ɪ', 'j' to 'ᴊ', 'k' to 'ᴋ', 'l' to 'ʟ', @@ -12,12 +18,30 @@ private val smallCapsMap = char2CharMapOf( '5' to '5', '6' to '6', '7' to '7', '8' to '8', '9' to '9' ) - -fun String.toSmallCaps(): String { - val result = StringBuilder(length) - for (char in this) { - result.append(smallCapsMap.getOrDefault(char.lowercaseChar(), char)) +/** + * Converts this character sequence to small caps formatting. + * + * Small caps are Unicode characters that resemble uppercase letters but are approximately + * the height of lowercase letters. This function transforms lowercase letters (a-z) to their + * small caps equivalents while leaving uppercase letters, digits, and other characters unchanged. + * + * @return A new string with lowercase letters converted to small caps Unicode characters. + */ +fun CharSequence.toSmallCaps(): String { + val chars = CharArray(length) + for (i in indices) { + val c = this[i] + chars[i] = smallCapsMap.getOrDefault(c.lowercaseChar(), c) } + return String(chars) +} + - return result.toString() +@Deprecated( + message = "Use CharSequence overload", + level = DeprecationLevel.HIDDEN +) + +fun String.toSmallCaps(): String { + return (this as CharSequence).toSmallCaps() } \ No newline at end of file From 63276e3df07e94b41dae5c6c17c87608ffc69172 Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:44:14 +0100 Subject: [PATCH 06/32] refactor: enhance VoxelLineTracer with detailed documentation and usage examples --- .../surfapi/core/api/math/VoxelLineTracer.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/math/VoxelLineTracer.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/math/VoxelLineTracer.kt index b223db236..1695dcf7a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/math/VoxelLineTracer.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/math/VoxelLineTracer.kt @@ -4,7 +4,33 @@ import org.spongepowered.math.vector.Vector3d import kotlin.math.abs import kotlin.math.sign +/** + * Utility for tracing lines through a voxel grid using a 3D Bresenham-like algorithm. + * + * This tracer determines all voxel coordinates that a line intersects between two 3D points, + * returning them in traversal order from the start point to the end point. + */ object VoxelLineTracer { + + /** + * Traces a line between two 3D points and returns all voxel coordinates along the path. + * + * Uses a 3D integer-based line algorithm to efficiently determine which discrete voxel + * positions a continuous line passes through. The sequence includes both endpoints and + * all intermediate voxels in traversal order. + * + * @param p0 The starting point of the line. + * @param p1 The ending point of the line. + * @return A lazy sequence of voxel coordinates from [p0] to [p1]. + * + * Example: + * ```kotlin + * val start = Vector3d(0.0, 0.0, 0.0) + * val end = Vector3d(3.0, 2.0, 1.0) + * val voxels = VoxelLineTracer.trace(start, end).toList() + * // Returns: [(0,0,0), (1,0,0), (1,1,0), (2,1,1), (3,2,1)] + * ``` + */ fun trace(p0: Vector3d, p1: Vector3d): Sequence = sequence { var x = p0.x(); var y = p0.y(); From 2c4521cb0ba69a09fa07bbb8e0fd1c858ab6c09d Mon Sep 17 00:00:00 2001 From: twisti Date: Sat, 7 Feb 2026 23:49:22 +0100 Subject: [PATCH 07/32] refactor: replace TranslationRegistry with TranslationStore for improved message handling --- .../core/api/messages/bundle/SurfMessageBundle.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/bundle/SurfMessageBundle.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/bundle/SurfMessageBundle.kt index 67953e532..6584cdfd2 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/bundle/SurfMessageBundle.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/bundle/SurfMessageBundle.kt @@ -6,7 +6,7 @@ import net.kyori.adventure.key.Key import net.kyori.adventure.text.Component import net.kyori.adventure.text.TranslatableComponent import net.kyori.adventure.translation.GlobalTranslator -import net.kyori.adventure.translation.TranslationRegistry +import net.kyori.adventure.translation.TranslationStore import net.kyori.adventure.util.UTF8ResourceBundleControl import org.jetbrains.annotations.NonNls import java.net.URLClassLoader @@ -149,19 +149,19 @@ class SurfMessageBundle @JvmOverloads constructor( baseName: String, bundles: List, ) { - val registry = TranslationRegistry.create( + val store = TranslationStore.messageFormat( Key.key( "surf", "bundle-${baseName.substringAfterLast('.').lowercase()}" ) ) - registry.defaultLocale(Locale.getDefault()) + store.defaultLocale(Locale.getDefault()) for (bundle in bundles) { - registry.registerAll(bundle.locale, bundle, true) + store.registerAll(bundle.locale, bundle, true) } - GlobalTranslator.translator().addSource(registry) + GlobalTranslator.translator().addSource(store) } /** @@ -287,7 +287,6 @@ class SurfMessageBundle @JvmOverloads constructor( baseName, it, classLoader, - UTF8ResourceBundleControl.get() ) }.getOrNull() }.toCollection(mutableObjectListOf()) From 69c4e72782a91e2396741c0ca8ca306b70ed15a7 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:34:27 +0100 Subject: [PATCH 08/32] feat: add suspend pagination support with new builder and renderer interfaces --- .../api/surf-api-core-api.api | 172 +++++++++++++++++- .../pagination/InternalPaginationBridge.kt | 1 + .../api/messages/pagination/Pagination.kt | 15 ++ .../messages/pagination/PaginationBuilder.kt | 68 ++++++- .../PaginationClickEventProvider.kt | 29 +++ .../messages/pagination/PaginationRenderer.kt | 116 ++++++++++-- .../pagination/PaginationRowRenderer.kt | 16 ++ .../InternalPaginationBridgeImpl.kt | 8 +- .../pagination/PaginationBuilderImpl.kt | 3 +- .../messages/pagination/PaginationImpl.kt | 45 ++--- .../suspend/SuspendPaginationBuilderImpl.kt | 67 +++++++ .../suspend/SuspendPaginationImpl.kt | 96 ++++++++++ 12 files changed, 592 insertions(+), 44 deletions(-) create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationBuilderImpl.kt create mode 100644 surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationImpl.kt diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 293c7e3df..027734200 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -5,8 +5,10 @@ public abstract interface class dev/slne/surf/surfapi/core/api/SurfCoreApi { public abstract fun sendPlayerToServer (Ljava/util/UUID;Ljava/lang/String;)V } -public final class dev/slne/surf/surfapi/core/api/SurfCoreApi$Companion { +public final class dev/slne/surf/surfapi/core/api/SurfCoreApi$Companion : dev/slne/surf/surfapi/core/api/SurfCoreApi { public final fun getInstance ()Ldev/slne/surf/surfapi/core/api/SurfCoreApi; + public fun getPlayer (Ljava/util/UUID;)Ljava/lang/Object; + public fun sendPlayerToServer (Ljava/util/UUID;Ljava/lang/String;)V } public final class dev/slne/surf/surfapi/core/api/SurfCoreApiKt { @@ -160,8 +162,18 @@ public abstract interface class dev/slne/surf/surfapi/core/api/config/SurfConfig public abstract fun reloadSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; } -public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApi$Companion { +public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApi$Companion : dev/slne/surf/surfapi/core/api/config/SurfConfigApi { + public fun createDazzlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; + public fun createSpongeJsonConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; + public fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun createSpongeYmlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; + public fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun getDazzlConfig (Ljava/lang/Class;)Ljava/lang/Object; public final fun getInstance ()Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi; + public fun getSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; + public fun getSpongeConfigManagerForConfig (Ljava/lang/Class;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun reloadDazzlConfig (Ljava/lang/Class;)Ljava/lang/Object; + public fun reloadSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; } public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApiKt { @@ -184,6 +196,11 @@ public final class dev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfig public final fun create (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/DazzlConfConfigManager; } +public final class dev/slne/surf/surfapi/core/api/config/manager/DazzlConfDeprecationMessageHolder { + public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/config/manager/DazzlConfDeprecationMessageHolder; + public static final field MESSAGE Ljava/lang/String; +} + public final class dev/slne/surf/surfapi/core/api/config/manager/LoadConfigException : java/lang/RuntimeException { public static final field Companion Ldev/slne/surf/surfapi/core/api/config/manager/LoadConfigException$Companion; public fun (Ljava/lang/String;)V @@ -221,7 +238,7 @@ public final class dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigMan public final class dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers { public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers; - public final fun getDEFAULTS ()Ljava/util/List; + public final fun getDEFAULTS ()Ljava/util/concurrent/CopyOnWriteArrayList; } public final class dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers$ComponentSerializer : space/arim/dazzleconf/serialiser/ValueSerialiser { @@ -271,7 +288,8 @@ public final class dev/slne/surf/surfapi/core/api/extensions/Packet_eventsKt { } public final class dev/slne/surf/surfapi/core/api/font/Small_capsKt { - public static final fun toSmallCaps (Ljava/lang/String;)Ljava/lang/String; + public static final fun toSmallCaps (Ljava/lang/CharSequence;)Ljava/lang/String; + public static final synthetic fun toSmallCaps (Ljava/lang/String;)Ljava/lang/String; } public final class dev/slne/surf/surfapi/core/api/generated/BlockTypeKeys { @@ -7815,6 +7833,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/bundle/SurfMessageBun public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge { public static final field Companion Ldev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge$Companion; public abstract fun createPaginationBuilder ()Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder; + public abstract fun createPaginationBuilderSuspend ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder; } public final class dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge$Companion { @@ -7884,6 +7903,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/paginati public fun nextPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V public fun previousPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V public fun rowRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer;)V + public fun rowRendererSimple (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer$Simple;)V public abstract fun setClickEventProvider (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationClickEventProvider;)V public abstract fun setFirstPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;)V public abstract fun setIndent (I)V @@ -7910,6 +7930,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/pagination/Pagination public static fun nextPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V public static fun previousPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V public static fun rowRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder;Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer;)V + public static fun rowRendererSimple (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder;Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer$Simple;)V public static fun title (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder;Lkotlin/jvm/functions/Function1;)V } @@ -7960,6 +7981,149 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/paginati public abstract fun renderRow (Ljava/lang/Object;I)Ljava/util/Collection; } +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer$Simple : dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer { + public abstract fun render (Ljava/lang/Object;)Lnet/kyori/adventure/text/Component; + public fun renderRow (Ljava/lang/Object;I)Ljava/util/Collection; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer$Simple$DefaultImpls { + public static fun renderRow (Ldev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer$Simple;Ljava/lang/Object;I)Ljava/util/Collection; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination { + public static final field Companion Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination$Companion; + public abstract fun render (Ljava/util/Collection;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun render$default (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun renderComponent (Ljava/util/Collection;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun renderComponent$default (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination$Companion { + public final fun invoke (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination$DefaultImpls { + public static synthetic fun render$default (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static fun renderComponent (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun renderComponent$default (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;ILkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder { + public static final field Companion Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder$Companion; + public abstract fun build ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination; + public fun clickEventProvider (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider;)V + public fun firstPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public abstract fun getClickEventProvider ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider; + public abstract fun getFirstPageButton ()Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton; + public abstract fun getIndent ()I + public abstract fun getLastPageButton ()Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton; + public abstract fun getNextPageButton ()Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton; + public abstract fun getPreviousPageButton ()Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton; + public abstract fun getRenderer ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer; + public abstract fun getResultsPerPage ()I + public abstract fun getRowRenderer ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer; + public abstract fun getTitle ()Lnet/kyori/adventure/text/Component; + public abstract fun getWidth ()I + public fun lastPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public fun nextPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public fun previousPageButton (Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public fun rowRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;)V + public fun rowRendererSimple (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer$Simple;)V + public abstract fun setClickEventProvider (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider;)V + public abstract fun setFirstPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;)V + public abstract fun setIndent (I)V + public abstract fun setLastPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;)V + public abstract fun setNextPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;)V + public abstract fun setPreviousPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;)V + public abstract fun setRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;)V + public abstract fun setResultsPerPage (I)V + public abstract fun setRowRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;)V + public abstract fun setTitle (Lnet/kyori/adventure/text/Component;)V + public abstract fun setWidth (I)V + public fun title (Lkotlin/jvm/functions/Function1;)V +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder$Companion { + public final fun builder ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder; + public final fun invoke (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder$DefaultImpls { + public static fun clickEventProvider (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider;)V + public static fun firstPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public static fun lastPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public static fun nextPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public static fun previousPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/Style;Lnet/kyori/adventure/text/format/Style;)V + public static fun rowRenderer (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;)V + public static fun rowRendererSimple (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer$Simple;)V + public static fun title (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationBuilder;Lkotlin/jvm/functions/Function1;)V +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider { + public static final field Companion Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider$Companion; + public abstract fun getCallback (Lkotlinx/coroutines/CoroutineScope;ILdev/slne/surf/surfapi/core/api/messages/pagination/SuspendPagination;Ljava/util/Collection;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider$Companion { + public final fun default ()Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationClickEventProvider; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer { + public fun renderEmpty (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderFooter (Lkotlinx/coroutines/CoroutineScope;IIIILdev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderHeader (Lkotlinx/coroutines/CoroutineScope;IILnet/kyori/adventure/text/Component;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderNextPageButton (Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderPreviousPageButton (Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderRow (Lkotlinx/coroutines/CoroutineScope;IIIILjava/lang/Object;ILdev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderUnknownPage (Lkotlinx/coroutines/CoroutineScope;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer$DEFAULT : dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer { + public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer$DEFAULT; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun renderEmpty (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderFooter (Lkotlinx/coroutines/CoroutineScope;IIIILdev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderHeader (Lkotlinx/coroutines/CoroutineScope;IILnet/kyori/adventure/text/Component;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderNextPageButton (Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderPreviousPageButton (Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderRow (Lkotlinx/coroutines/CoroutineScope;IIIILjava/lang/Object;ILdev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderUnknownPage (Lkotlinx/coroutines/CoroutineScope;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer$DefaultImpls { + public static fun renderEmpty (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderFooter (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;IIIILdev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderHeader (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;IILnet/kyori/adventure/text/Component;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderNextPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderPreviousPageButton (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;Ldev/slne/surf/surfapi/core/api/messages/pagination/PageButton;Lnet/kyori/adventure/text/event/ClickEvent;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderRow (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;IIIILjava/lang/Object;ILdev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun renderUnknownPage (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRenderer;Lkotlinx/coroutines/CoroutineScope;IILkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer { + public abstract fun renderRow (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer$Simple : dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer { + public abstract fun render (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renderRow (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer$Simple$DefaultImpls { + public static fun renderRow (Ldev/slne/surf/surfapi/core/api/messages/pagination/SuspendPaginationRowRenderer$Simple;Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;ILkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder { + public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolder; + public final fun miniMessage ()Lnet/kyori/adventure/text/minimessage/MiniMessage; +} + +public final class dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHolderKt { + public static final fun getMiniMessage ()Lnet/kyori/adventure/text/minimessage/MiniMessage; +} + public final class dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag : java/lang/Iterable { public static final field Companion Ldev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag$Companion; public static final synthetic fun box-impl (Lnet/kyori/adventure/nbt/BinaryTag;)Ldev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag; diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt index b5ad7d07c..925de1edc 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/InternalPaginationBridge.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.shared.api.util.InternalSurfApi @InternalSurfApi interface InternalPaginationBridge { fun createPaginationBuilder(): PaginationBuilder + fun createPaginationBuilderSuspend(): SuspendPaginationBuilder companion object { val instance = requiredService() diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/Pagination.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/Pagination.kt index 906ee3495..ea455e1ce 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/Pagination.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/Pagination.kt @@ -7,6 +7,21 @@ import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.format.Style import kotlin.experimental.ExperimentalTypeInference +interface SuspendPagination { + suspend fun render(content: Collection, page: Int = 1): List + suspend fun renderComponent(content: Collection, page: Int = 1): Component { + return Component.join(JoinConfiguration.newlines(), render(content, page)) + } + + companion object { + @OptIn(ExperimentalTypeInference::class) + operator fun invoke(@BuilderInference block: SuspendPaginationBuilder.() -> Unit): SuspendPagination { + val builder = InternalPaginationBridge.instance.createPaginationBuilderSuspend() + builder.block() + return builder.build() + } + } +} interface Pagination { diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder.kt index 96d04fd9b..09aa604d4 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationBuilder.kt @@ -6,6 +6,70 @@ import net.kyori.adventure.text.format.Style import org.jetbrains.annotations.Range import kotlin.experimental.ExperimentalTypeInference +interface SuspendPaginationBuilder { + // required properties + var title: Component + var rowRenderer: SuspendPaginationRowRenderer + + // optional properties + var width: @Range(from = 3, to = Int.MAX_VALUE.toLong()) Int + var indent: @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int + var resultsPerPage: @Range(from = 1, to = Int.MAX_VALUE.toLong()) Int + var renderer: SuspendPaginationRenderer + var clickEventProvider: SuspendPaginationClickEventProvider + var firstPageButton: PageButton + var previousPageButton: PageButton + var nextPageButton: PageButton + var lastPageButton: PageButton + + fun title(block: SurfComponentBuilder.() -> Unit) { + title = SurfComponentBuilder(block) + } + + fun rowRenderer(renderer: SuspendPaginationRowRenderer) { + rowRenderer = renderer + } + + fun rowRendererSimple(renderer: SuspendPaginationRowRenderer.Simple) { + rowRenderer = renderer + } + + fun clickEventProvider(provider: SuspendPaginationClickEventProvider) { + clickEventProvider = provider + } + + fun firstPageButton(text: String, enabledStyle: Style, disabledStyle: Style) { + firstPageButton = PageButton(text, enabledStyle, disabledStyle) + } + + fun previousPageButton(text: String, enabledStyle: Style, disabledStyle: Style) { + previousPageButton = PageButton(text, enabledStyle, disabledStyle) + } + + fun nextPageButton(text: String, enabledStyle: Style, disabledStyle: Style) { + nextPageButton = PageButton(text, enabledStyle, disabledStyle) + } + + fun lastPageButton(text: String, enabledStyle: Style, disabledStyle: Style) { + lastPageButton = PageButton(text, enabledStyle, disabledStyle) + } + + fun build(): SuspendPagination + + companion object { + @OptIn(ExperimentalTypeInference::class) + operator fun invoke(@BuilderInference block: SuspendPaginationBuilder.() -> Unit): SuspendPagination { + val builder = InternalPaginationBridge.instance.createPaginationBuilderSuspend() + builder.block() + return builder.build() + } + + fun builder(): SuspendPaginationBuilder { + return InternalPaginationBridge.instance.createPaginationBuilderSuspend() + } + } +} + interface PaginationBuilder { // required properties var title: Component @@ -22,7 +86,6 @@ interface PaginationBuilder { var nextPageButton: PageButton var lastPageButton: PageButton - fun title(block: SurfComponentBuilder.() -> Unit) { title = SurfComponentBuilder(block) } @@ -31,6 +94,9 @@ interface PaginationBuilder { rowRenderer = renderer } + fun rowRendererSimple(renderer: PaginationRowRenderer.Simple) { + rowRenderer = renderer + } fun clickEventProvider(provider: PaginationClickEventProvider) { clickEventProvider = provider diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationClickEventProvider.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationClickEventProvider.kt index 92c36dede..35f85792d 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationClickEventProvider.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationClickEventProvider.kt @@ -1,5 +1,7 @@ package dev.slne.surf.surfapi.core.api.messages.pagination +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import net.kyori.adventure.text.event.ClickEvent fun interface PaginationClickEventProvider { @@ -21,4 +23,31 @@ fun interface PaginationClickEventProvider { return DEFAULT as PaginationClickEventProvider } } +} + +fun interface SuspendPaginationClickEventProvider { + suspend fun CoroutineScope.getCallback( + targetPage: Int, + pagination: SuspendPagination, + content: Collection + ): ClickEvent + + companion object { + private object DEFAULT : SuspendPaginationClickEventProvider { + override suspend fun CoroutineScope.getCallback( + targetPage: Int, + pagination: SuspendPagination, + content: Collection + ): ClickEvent = ClickEvent.callback { clicker -> + launch { + clicker.sendMessage(pagination.renderComponent(content, targetPage)) + } + } + } + + fun default(): SuspendPaginationClickEventProvider { + @Suppress("UNCHECKED_CAST") + return DEFAULT as SuspendPaginationClickEventProvider + } + } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRenderer.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRenderer.kt index 4763cd132..a53543388 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRenderer.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRenderer.kt @@ -5,20 +5,100 @@ import dev.slne.surf.surfapi.core.api.messages.DefaultFontInfo import dev.slne.surf.surfapi.core.api.messages.adventure.buildText import dev.slne.surf.surfapi.core.api.messages.adventure.plain import dev.slne.surf.surfapi.core.api.messages.adventure.text +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.TextDecoration import kotlin.math.roundToInt +interface SuspendPaginationRenderer { + suspend fun CoroutineScope.renderEmpty(): Component = PaginationRenderer.DEFAULT.renderEmpty() + suspend fun CoroutineScope.renderUnknownPage(page: Int, pages: Int): Component = + PaginationRenderer.DEFAULT.renderUnknownPage(page, pages) + + suspend fun CoroutineScope.renderHeader( + width: Int, + indent: Int, + title: Component, + page: Int, + pages: Int + ): Component = PaginationRenderer.DEFAULT.renderHeader(width, indent, title, page, pages) + + suspend fun CoroutineScope.renderRow( + width: Int, + indent: Int, + page: Int, + pages: Int, + value: T, + contentIndex: Int, + renderer: SuspendPaginationRowRenderer, + ): Collection = renderer.run { + renderRow(value, contentIndex) + .map { text(" ".repeat(indent)).append(it) } + } + + suspend fun CoroutineScope.renderFooter( + width: Int, + indent: Int, + page: Int, + pages: Int, + firstPage: PageButton, + previousPage: PageButton, + nextPage: PageButton, + lastPage: PageButton, + changePageEvent: suspend CoroutineScope.(Int) -> ClickEvent?, + ): Component = buildText { + if (page == 1 && pages == 1) { + append(renderFooterSingle(width)) + } else { + appendNewline() + + val nav = buildText { + val first = async { renderPreviousPageButton(firstPage, changePageEvent(1), page > 1) } + val prev = async { renderPreviousPageButton(previousPage, changePageEvent(page - 1), page > 1) } + val next = async { renderNextPageButton(nextPage, changePageEvent(page + 1), page < pages) } + val last = async { renderNextPageButton(lastPage, changePageEvent(pages), page < pages) } + + append(first.await()) + append(prev.await()) + append { + info(page) + spacer("/") + info(pages) + } + append(next.await()) + append(last.await()) + } + + append(renderFooterLine(width, nav)) + } + } + + suspend fun CoroutineScope.renderPreviousPageButton( + button: PageButton, + clickEvent: ClickEvent?, + enabled: Boolean, + ): Component = PaginationRenderer.DEFAULT.renderPreviousPageButton(button, clickEvent, enabled) + + suspend fun CoroutineScope.renderNextPageButton( + button: PageButton, + clickEvent: ClickEvent?, + enabled: Boolean, + ): Component = PaginationRenderer.DEFAULT.renderNextPageButton(button, clickEvent, enabled) + + data object DEFAULT : SuspendPaginationRenderer +} + interface PaginationRenderer { fun renderEmpty(): Component = buildText { - appendPrefix() + appendInfoPrefix() info("Es wurden keine Ergebnisse gefunden.") } fun renderUnknownPage(page: Int, pages: Int): Component = buildText { - appendPrefix() + appendErrorPrefix() error("Unbekannte Seite: ") variableValue(page) error(". Es gibt nur ") @@ -64,8 +144,7 @@ interface PaginationRenderer { changePageEvent: (Int) -> ClickEvent?, ): Component = buildText { if (page == 1 && pages == 1) { - appendNewline() - darkSpacer("*" + "-".repeat(width - 2) + "*", TextDecoration.STRIKETHROUGH) + append(renderFooterSingle(width)) } else { appendNewline() @@ -81,16 +160,7 @@ interface PaginationRenderer { append(renderNextPageButton(lastPage, changePageEvent(pages), page < pages)) } - val dashPx = DefaultFontInfo.MINUS.length + 1 - val navPx = DefaultFontInfo.pixelWidth(nav.plain()) - val linePx = (width - 2) * dashPx - val sidePx = linePx - navPx - val left = (sidePx / 2.0 / dashPx).roundToInt() - val right = ((sidePx - left * dashPx.toDouble()) / dashPx).roundToInt() - - darkSpacer("*" + "-".repeat(left), TextDecoration.STRIKETHROUGH) - append(nav) - darkSpacer("-".repeat(right) + "*", TextDecoration.STRIKETHROUGH) + append(renderFooterLine(width, nav)) } } @@ -123,4 +193,22 @@ interface PaginationRenderer { } data object DEFAULT : PaginationRenderer +} + +private fun renderFooterSingle(width: Int): Component = buildText { + appendNewline() + darkSpacer("*" + "-".repeat(width - 2) + "*", TextDecoration.STRIKETHROUGH) +} + +private fun renderFooterLine(width: Int, nav: Component): Component = buildText { + val dashPx = DefaultFontInfo.MINUS.length + 1 + val navPx = DefaultFontInfo.pixelWidth(nav.plain()) + val linePx = (width - 2) * dashPx + val sidePx = linePx - navPx + val left = (sidePx / 2.0 / dashPx).roundToInt() + val right = ((sidePx - left * dashPx.toDouble()) / dashPx).roundToInt() + + darkSpacer("*" + "-".repeat(left), TextDecoration.STRIKETHROUGH) + append(nav) + darkSpacer("-".repeat(right) + "*", TextDecoration.STRIKETHROUGH) } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer.kt index fa6e88a66..842436949 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/pagination/PaginationRowRenderer.kt @@ -1,7 +1,23 @@ package dev.slne.surf.surfapi.core.api.messages.pagination +import kotlinx.coroutines.CoroutineScope import net.kyori.adventure.text.Component fun interface PaginationRowRenderer { fun renderRow(value: T, index: Int): Collection + + fun interface Simple : PaginationRowRenderer { + fun render(value: T): Component + override fun renderRow(value: T, index: Int): Collection = listOf(render(value)) + } } + +fun interface SuspendPaginationRowRenderer { + suspend fun CoroutineScope.renderRow(value: T, index: Int): Collection + + fun interface Simple : SuspendPaginationRowRenderer { + suspend fun CoroutineScope.render(value: T): Component + override suspend fun CoroutineScope.renderRow(value: T, index: Int): Collection = + listOf(render(value)) + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/InternalPaginationBridgeImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/InternalPaginationBridgeImpl.kt index 594ee6eec..a65d653aa 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/InternalPaginationBridgeImpl.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/InternalPaginationBridgeImpl.kt @@ -3,10 +3,16 @@ package dev.slne.surf.surfapi.core.server.impl.messages.pagination import com.google.auto.service.AutoService import dev.slne.surf.surfapi.core.api.messages.pagination.InternalPaginationBridge import dev.slne.surf.surfapi.core.api.messages.pagination.PaginationBuilder +import dev.slne.surf.surfapi.core.api.messages.pagination.SuspendPaginationBuilder +import dev.slne.surf.surfapi.core.server.impl.messages.pagination.suspend.SuspendPaginationBuilderImpl @AutoService(InternalPaginationBridge::class) -class InternalPaginationBridgeImpl: InternalPaginationBridge { +class InternalPaginationBridgeImpl : InternalPaginationBridge { override fun createPaginationBuilder(): PaginationBuilder { return PaginationBuilderImpl() } + + override fun createPaginationBuilderSuspend(): SuspendPaginationBuilder { + return SuspendPaginationBuilderImpl() + } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationBuilderImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationBuilderImpl.kt index 0fe21636c..a232ab669 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationBuilderImpl.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationBuilderImpl.kt @@ -3,7 +3,7 @@ package dev.slne.surf.surfapi.core.server.impl.messages.pagination import dev.slne.surf.surfapi.core.api.messages.pagination.* import net.kyori.adventure.text.Component -class PaginationBuilderImpl : PaginationBuilder { +open class PaginationBuilderImpl : PaginationBuilder { override var width: Int = Pagination.DEFAULT_WIDTH set(value) { require(value >= 3) { "Width must be at least 3" } @@ -22,7 +22,6 @@ class PaginationBuilderImpl : PaginationBuilder { field = value } - override var renderer: PaginationRenderer = PaginationRenderer.DEFAULT private var _title: Component? = null diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationImpl.kt index 3e1776c04..4db5ae507 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationImpl.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/PaginationImpl.kt @@ -19,6 +19,7 @@ data class PaginationImpl( private val lastPageButton: PageButton, private val clickEventProvider: PaginationClickEventProvider, ) : Pagination { + override fun render( content: Collection, page: Int, @@ -67,30 +68,30 @@ data class PaginationImpl( } companion object { - private fun pages(pageSize: Int, count: Int): Int = (count + pageSize - 1) / pageSize - } -} + fun pages(pageSize: Int, count: Int): Int = (count + pageSize - 1) / pageSize -private fun forEachPageEntry( - content: Collection, - pageSize: Int, - page: Int, - consumer: (T, Int) -> Unit, -) { - val size = content.size - val start = pageSize * (page - 1) - val end = pageSize * page + inline fun forEachPageEntry( + content: Collection, + pageSize: Int, + page: Int, + consumer: (T, Int) -> Unit, + ) { + val size = content.size + val start = pageSize * (page - 1) + val end = pageSize * page - if (content is List && content is RandomAccess) { - for (i in start until end.coerceAtMost(size)) { - consumer(content[i], i) - } - } else { - val iterator = content.iterator() - // Skip previous pages - repeat(start) { iterator.next() } - for (i in start until end.coerceAtMost(size)) { - consumer(iterator.next(), i) + if (content is List && content is RandomAccess) { + for (i in start until end.coerceAtMost(size)) { + consumer(content[i], i) + } + } else { + val iterator = content.iterator() + // Skip previous pages + repeat(start) { iterator.next() } + for (i in start until end.coerceAtMost(size)) { + consumer(iterator.next(), i) + } + } } } } diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationBuilderImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationBuilderImpl.kt new file mode 100644 index 000000000..7a8ab64ef --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationBuilderImpl.kt @@ -0,0 +1,67 @@ +package dev.slne.surf.surfapi.core.server.impl.messages.pagination.suspend + +import dev.slne.surf.surfapi.core.api.messages.pagination.* +import net.kyori.adventure.text.Component + +class SuspendPaginationBuilderImpl : SuspendPaginationBuilder { + override var width: Int = Pagination.DEFAULT_WIDTH + set(value) { + require(value >= 3) { "Width must be at least 3" } + field = value + } + + override var indent: Int = Pagination.DEFAULT_INDENT + set(value) { + require(value >= 0) { "Indent must be at least 0" } + field = value + } + + override var resultsPerPage: Int = Pagination.DEFAULT_RESULTS_PER_PAGE + set(value) { + require(value > 0) { "Results per page must be greater than 0" } + field = value + } + + private var _title: Component? = null + override var title: Component + get() = _title ?: error("Title must be set before building") + set(value) { + _title = value + } + + override var renderer: SuspendPaginationRenderer = SuspendPaginationRenderer.DEFAULT + + private var _rowRenderer: SuspendPaginationRowRenderer? = null + override var rowRenderer: SuspendPaginationRowRenderer + get() = _rowRenderer ?: error("Row renderer must be set before building") + set(value) { + _rowRenderer = value + } + + override var clickEventProvider: SuspendPaginationClickEventProvider = + SuspendPaginationClickEventProvider.default() + + override var firstPageButton: PageButton = Pagination.DEFAULT_FIRST_PAGE_BUTTON + override var previousPageButton: PageButton = Pagination.DEFAULT_PREVIOUS_PAGE_BUTTON + override var nextPageButton: PageButton = Pagination.DEFAULT_NEXT_PAGE_BUTTON + override var lastPageButton: PageButton = Pagination.DEFAULT_LAST_PAGE_BUTTON + + override fun build(): SuspendPagination = SuspendPaginationImpl( + width = width, + indent = indent, + resultsPerPage = resultsPerPage, + renderer = renderer, + title = title, + rowRenderer = rowRenderer, + firstPageButton = firstPageButton, + previousPageButton = previousPageButton, + nextPageButton = nextPageButton, + lastPageButton = lastPageButton, + clickEventProvider = clickEventProvider + ) + + override fun toString(): String { + return "SuspendPaginationBuilderImpl(width=$width, indent=$indent, resultsPerPage=$resultsPerPage, _title=$_title, _rowRenderer=$_rowRenderer, renderer=$renderer, clickEventProvider=$clickEventProvider, firstPageButton=$firstPageButton, previousPageButton=$previousPageButton, nextPageButton=$nextPageButton, lastPageButton=$lastPageButton)" + } + +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationImpl.kt new file mode 100644 index 000000000..e9f55c665 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/impl/messages/pagination/suspend/SuspendPaginationImpl.kt @@ -0,0 +1,96 @@ +package dev.slne.surf.surfapi.core.server.impl.messages.pagination.suspend + +import dev.slne.surf.surfapi.core.api.messages.pagination.* +import dev.slne.surf.surfapi.core.api.util.freeze +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import dev.slne.surf.surfapi.core.api.util.objectListOf +import dev.slne.surf.surfapi.core.server.impl.messages.pagination.PaginationImpl +import dev.slne.surf.surfapi.core.server.impl.messages.pagination.PaginationImpl.Companion.pages +import kotlinx.coroutines.* +import net.kyori.adventure.text.Component + +data class SuspendPaginationImpl( + private val width: Int, + private val resultsPerPage: Int, + private val indent: Int, + private val renderer: SuspendPaginationRenderer, + private val title: Component, + private val rowRenderer: SuspendPaginationRowRenderer, + private val firstPageButton: PageButton, + private val previousPageButton: PageButton, + private val nextPageButton: PageButton, + private val lastPageButton: PageButton, + private val clickEventProvider: SuspendPaginationClickEventProvider, +) : SuspendPagination { + + companion object { + private val log = logger() + private val renderContext = Dispatchers.Default + + CoroutineName("SuspendPagination-Rendering") + + CoroutineExceptionHandler { context, throwable -> + log.atSevere() + .withCause(throwable) + .log("Failed to render pagination") + } + + } + + override suspend fun render( + content: Collection, + page: Int + ): List = withContext(renderContext) { + if (content.isEmpty()) { + return@withContext renderer.run { objectListOf(renderEmpty()) } + } + + val pages = pages(resultsPerPage, content.size) + + if (page !in 1..pages) { + return@withContext renderer.run { objectListOf(renderUnknownPage(page, pages)) } + } + + val results = mutableObjectListOf>>() + coroutineScope { + results.add(async { listOf(renderer.run { renderHeader(width, indent, title, page, pages) }) }) + + PaginationImpl.forEachPageEntry(content, resultsPerPage, page) { value, index -> + val renderedComponents = renderer.run { + async { + renderRow( + width, + indent, + page, + pages, + value, + index, + rowRenderer + ) + } + } + results.add(renderedComponents) + } + + results.add(async { + renderer.run { + listOf( + renderFooter( + width, + indent, + page, + pages, + firstPageButton, + previousPageButton, + nextPageButton, + lastPageButton + ) { page -> clickEventProvider.run { getCallback(page, this@SuspendPaginationImpl, content) } }) + } + }) + } + + val finalResults = mutableObjectListOf() + results.forEach { finalResults.addAll(it.await()) } + + finalResults.freeze() + } +} \ No newline at end of file From 68769bb3131ec9a71bf2e01a66e1c5a6fac5baaa Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:41:17 +0100 Subject: [PATCH 09/32] refactor: improve documentation and color descriptions in Colors.kt --- .../surf/surfapi/core/api/messages/Colors.kt | 116 ++++++++---------- 1 file changed, 51 insertions(+), 65 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt index 7a99f0e18..541496421 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt @@ -9,15 +9,11 @@ import net.kyori.adventure.text.format.TextColor import net.kyori.adventure.text.format.TextColor.color /** - * A class that defines all the colors used in the Surf system, ensuring a consistent visual style - * across all Surf plugins. - * This class provides predefined colors for various UI elements, including - * informational messages, warnings, errors, and formatting components. + * Defines the standardized color palette for the Surf system. * - * For reference, - * see [Simons dc post](https://discord.com/channels/1094422317783851108/1096084922499862658). - * - * @see [Simons dc post](https://discord.com/channels/1094422317783851108/1096084922499862658) + * This interface provides a consistent visual style across all Surf plugins through predefined colors + * for UI elements, messages, and formatting components. All colors are defined as static fields + * in the companion object. */ @Suppress("unused") interface Colors { @@ -25,96 +21,87 @@ interface Colors { // -------------------- Surf Colors -------------------- // /** - * The primary Surf color (#3b92d1). - * Although rarely used in the system, it can be utilized - * for elements like titles and subtitles. + * Primary brand color (#3b92d1). + * Use for prominent UI elements such as titles and branded components. */ @JvmField val PRIMARY: TextColor = color(0x3b92d1) /** - * The secondary Surf color (#5b5b5b), mainly used for elements such as subtitles. + * Secondary color (#5b5b5b). + * Use for less prominent elements such as subtitles and secondary text. */ @JvmField val SECONDARY: TextColor = color(0x5b5b5b) /** - * The info color (#40d1db). - * Used to convey neutral information to users that is not directly - * a result of their actions, - * except in cases such as delayed status updates or toggle messages. + * Informational message color (#97B3F7). + * Use for neutral system information, status updates, and toggle confirmations + * that are not direct responses to user actions. */ @JvmField val INFO: TextColor = color(0x97B3F7) /** - * The note color (#6EA6D9). - * Used for supplemental or side-note information that adds context, - * tips, or clarifications to a primary message. - * Prefer this for ancillary guidance rather than main content: - * use [INFO] for neutral system messages or status updates, and [PRIMARY] - * for branded elements such as titles or key headings. + * Note color (#6EA6D9). + * Use for supplemental information, tips, or clarifications that accompany primary messages. + * Prefer [INFO] for system messages and [PRIMARY] for branded headings. */ @JvmField val NOTE: TextColor = color(0x6EA6D9) /** - * The success color (#65ff64). - * Indicates a positive outcome of a user action and is always - * used in direct response to the user. + * Success message color (#65ff64). + * Use exclusively for positive outcomes in direct response to user actions. */ @JvmField val SUCCESS: TextColor = color(0x65ff64) /** - * The warning color (#f9c353). - * Used to caution users about potential issues, often serving - * as a precursor to an error message. + * Warning message color (#f9c353). + * Use to indicate potential issues or caution users about actions that may lead to errors. */ @JvmField val WARNING: TextColor = color(0xf9c353) /** - * The error (or danger) color (#ee3d51). - * Represents error messages directed at the user, - * often following a direct user action or warning of a critical issue. + * Error message color (#ee3d51). + * Use for error messages and critical issues resulting from user actions or system failures. */ @JvmField val ERROR: TextColor = color(0xee3d51) /** - * The variable key color (#3b92d1). - * Typically used for key-value pair representations, - * such as in lists (for example, "Key 1: Value"). + * Variable key color (#97B3F7). + * Use for keys in key-value pairs (e.g., "Name: John"). */ @JvmField val VARIABLE_KEY: TextColor = INFO /** - * The variable value color (#f9c353). - * Commonly used to highlight values in lists and - * chat messages (for example, "Your property 'PROPERTY' has been sold."). + * Variable value color (#f9c353). + * Use to highlight dynamic values in messages (e.g., "Your property 'PROPERTY' has been sold"). */ @JvmField val VARIABLE_VALUE: TextColor = WARNING /** - * The spacer color (GRAY). Used for visual separators such as "-", "...", and "/". + * Standard spacer color (GRAY). + * Use for visual separators such as "-", "...", and "/". */ @JvmField val SPACER: NamedTextColor = NamedTextColor.GRAY /** - * The dark spacer color (DARK_GRAY). - * Used for darker separators, such as those found in - * prefixes like ">>" or "|". + * Dark spacer color (DARK_GRAY). + * Use for darker separators such as ">>" or "|" in prefixes. */ @JvmField val DARK_SPACER: NamedTextColor = NamedTextColor.DARK_GRAY /** - * The default prefix color (#3b92d1). - * Applied to all prefixes for consistency across Surf plugins. + * Default prefix color (#3b92d1). + * Applied to all message prefixes for consistency across Surf plugins. */ @JvmField val PREFIX_COLOR: TextColor = PRIMARY @@ -122,8 +109,7 @@ interface Colors { // -------------------- Default Colors -------------------- // /** - * The default prefix used across all Surf plugins, ensuring a recognizable and uniform - * identifier in messages. + * Default message prefix used across all Surf plugins. */ @JvmField val PREFIX: Component = buildText { @@ -132,7 +118,7 @@ interface Colors { } /** - * The default info prefix used in informational messages. + * Prefix for informational messages. */ @JvmField val INFO_PREFIX: Component = buildText { @@ -143,7 +129,7 @@ interface Colors { } /** - * The default success prefix used in success messages. + * Prefix for success messages. */ @JvmField val SUCCESS_PREFIX: Component = buildText { @@ -154,7 +140,7 @@ interface Colors { } /** - * The default warning prefix used in warning messages. + * Prefix for warning messages. */ @JvmField val WARNING_PREFIX: Component = buildText { @@ -165,7 +151,7 @@ interface Colors { } /** - * The default error prefix used in error messages. + * Prefix for error messages. */ @JvmField val ERROR_PREFIX: Component = buildText { @@ -176,97 +162,97 @@ interface Colors { } /** - * Represents the color black. + * Minecraft black color. */ @JvmField val BLACK: NamedTextColor = NamedTextColor.BLACK /** - * Represents the color dark blue. + * Minecraft dark blue color. */ @JvmField val DARK_BLUE: NamedTextColor = NamedTextColor.DARK_BLUE /** - * Represents the color dark green. + * Minecraft dark green color. */ @JvmField val DARK_GREEN: NamedTextColor = NamedTextColor.DARK_GREEN /** - * Represents the color dark aqua. + * Minecraft dark aqua color. */ @JvmField val DARK_AQUA: NamedTextColor = NamedTextColor.DARK_AQUA /** - * Represents the color dark red. + * Minecraft dark red color. */ @JvmField val DARK_RED: NamedTextColor = NamedTextColor.DARK_RED /** - * Represents the color dark purple. + * Minecraft dark purple color. */ @JvmField val DARK_PURPLE: NamedTextColor = NamedTextColor.DARK_PURPLE /** - * Represents the color gold. + * Minecraft gold color. */ @JvmField val GOLD: NamedTextColor = NamedTextColor.GOLD /** - * Represents the color gray. + * Minecraft gray color. */ @JvmField val GRAY: NamedTextColor = NamedTextColor.GRAY /** - * Represents the color dark gray. + * Minecraft dark gray color. */ @JvmField val DARK_GRAY: NamedTextColor = NamedTextColor.DARK_GRAY /** - * Represents the color blue. + * Minecraft blue color. */ @JvmField val BLUE: NamedTextColor = NamedTextColor.BLUE /** - * Represents the color green. + * Minecraft green color. */ @JvmField val GREEN: NamedTextColor = NamedTextColor.GREEN /** - * Represents the color aqua. + * Minecraft aqua color. */ @JvmField val AQUA: NamedTextColor = NamedTextColor.AQUA /** - * Represents the color red. + * Minecraft red color. */ @JvmField val RED: NamedTextColor = NamedTextColor.RED /** - * Represents the color light purple. + * Minecraft light purple color. */ @JvmField val LIGHT_PURPLE: NamedTextColor = NamedTextColor.LIGHT_PURPLE /** - * Represents the color yellow. + * Minecraft yellow color. */ @JvmField val YELLOW: NamedTextColor = NamedTextColor.YELLOW /** - * Represents the color white. + * Minecraft white color. */ @JvmField val WHITE: NamedTextColor = NamedTextColor.WHITE From a96b9a6192eade7c2dfeb39eb52c7ea9fc78d51b Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:42:32 +0100 Subject: [PATCH 10/32] refactor: update warning color and adjust variable value color in Colors.kt --- .../dev/slne/surf/surfapi/core/api/messages/Colors.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt index 541496421..08629188a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt @@ -58,11 +58,11 @@ interface Colors { val SUCCESS: TextColor = color(0x65ff64) /** - * Warning message color (#f9c353). + * Warning message color (#ffa64d). * Use to indicate potential issues or caution users about actions that may lead to errors. */ @JvmField - val WARNING: TextColor = color(0xf9c353) + val WARNING: TextColor = color(0xffa64d) /** * Error message color (#ee3d51). @@ -83,7 +83,7 @@ interface Colors { * Use to highlight dynamic values in messages (e.g., "Your property 'PROPERTY' has been sold"). */ @JvmField - val VARIABLE_VALUE: TextColor = WARNING + val VARIABLE_VALUE: TextColor = color(0xf9c353) /** * Standard spacer color (GRAY). From e35199a735570540c7b558aca9e551468487a66c Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:45:07 +0100 Subject: [PATCH 11/32] refactor: update variable key color in Colors.kt --- .../kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt index 08629188a..6991dce5a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/Colors.kt @@ -72,11 +72,11 @@ interface Colors { val ERROR: TextColor = color(0xee3d51) /** - * Variable key color (#97B3F7). + * Variable key color (#6B9BD1). * Use for keys in key-value pairs (e.g., "Name: John"). */ @JvmField - val VARIABLE_KEY: TextColor = INFO + val VARIABLE_KEY: TextColor = color(0x6B9BD1) /** * Variable value color (#f9c353). From 3a3b9d7161b9bbec0da06794b97fcacb41ef22d8 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:47:45 +0100 Subject: [PATCH 12/32] refactor: rename MAP_SEPERATOR to MAP_SEPARATOR and update references --- .../core/api/messages/CommonComponents.kt | 16 ++++++++++++---- .../api/messages/builder/SurfComponentBuilder.kt | 3 +-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt index 193f5f2f6..d740b860f 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt @@ -6,7 +6,7 @@ import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.PRIMARY import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.SPACER import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.VARIABLE_KEY import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.VARIABLE_VALUE -import dev.slne.surf.surfapi.core.api.messages.CommonComponents.MAP_SEPERATOR +import dev.slne.surf.surfapi.core.api.messages.CommonComponents.MAP_SEPARATOR import dev.slne.surf.surfapi.core.api.messages.adventure.appendNewline import dev.slne.surf.surfapi.core.api.messages.adventure.appendText import dev.slne.surf.surfapi.core.api.messages.adventure.clickOpensUrl @@ -37,11 +37,19 @@ object CommonComponents { @JvmField val ELLIPSIS = text("...") + /** * A separator (`->`) used to visually separate key-value pairs in text components. */ @JvmField - val MAP_SEPERATOR = text(" -> ", SPACER) + val MAP_SEPARATOR = text(" -> ", SPACER) + + /** + * @deprecated Use [MAP_SEPARATOR] instead. + */ + @Deprecated("Use MAP_SEPARATOR instead", ReplaceWith("MAP_SEPARATOR")) + @JvmField + val MAP_SEPERATOR = MAP_SEPARATOR /** * A separator (`:`) used to visually format time-related messages. @@ -425,7 +433,7 @@ object CommonComponents { keyFormatter: (K) -> Component, valueFormatter: (V) -> Component, linePrefix: Component = PREFIX, - keyValueSeparator: Component = MAP_SEPERATOR, + keyValueSeparator: Component = MAP_SEPARATOR, ): Component { val separator = buildText0 { appendNewline() @@ -544,5 +552,5 @@ inline fun Map.joinToComponent( keyFormatter: (K) -> Component, valueFormatter: (V) -> Component, linePrefix: Component = PREFIX, - keyValueSeparator: Component = MAP_SEPERATOR, + keyValueSeparator: Component = CommonComponents.MAP_SEPARATOR, ) = CommonComponents.formatMap(this, keyFormatter, valueFormatter, linePrefix, keyValueSeparator) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder.kt index d9b27290c..6730fc90e 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder.kt @@ -7,7 +7,6 @@ import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.VARIABLE_VALUE import dev.slne.surf.surfapi.core.api.messages.CommonComponents import dev.slne.surf.surfapi.core.api.messages.CommonComponents.DISCONNECT_HEADER import dev.slne.surf.surfapi.core.api.messages.CommonComponents.DISCORD_LINK -import dev.slne.surf.surfapi.core.api.messages.CommonComponents.MAP_SEPERATOR import dev.slne.surf.surfapi.core.api.messages.CommonComponents.TIME_SEPARATOR import dev.slne.surf.surfapi.core.api.messages.NoLowercase import dev.slne.surf.surfapi.core.api.messages.joinToComponent @@ -128,7 +127,7 @@ interface SurfComponentBuilder : TextComponent.Builder, ComponentBuilderColors { keyFormatter: (K) -> Component, valueFormatter: (V) -> Component, linePrefix: Component = PREFIX, - keyValueSeparator: Component = MAP_SEPERATOR, + keyValueSeparator: Component = CommonComponents.MAP_SEPARATOR, ) = append(map.joinToComponent(keyFormatter, valueFormatter, linePrefix, keyValueSeparator)) fun appendTime( From 7b562622b3b252359c7567a13bbdfdd058741cc3 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 15:51:53 +0100 Subject: [PATCH 13/32] refactor: enhance formatTime function for improved readability and efficiency --- .../core/api/messages/CommonComponents.kt | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt index d740b860f..c03298e39 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt @@ -458,24 +458,14 @@ object CommonComponents { } /** - * Formats a duration into a human-readable time representation. + * Formats a duration into a human-readable time representation. Supports multiple time units from centuries to seconds. * * @param time The duration to format. * @param showSeconds Whether to include seconds in the output. - * @param shortForms Whether to use short forms for time units. - * @param separator The separator between time units. + * @param shortForms Whether to use short forms for time units (e.g., "d" instead of "Tag"). + * @param separator The separator component between time units. * @param timeColor The color for the time values. - * @return A formatted [Component] representing the time. - * - * **Example Usage:** - * ```kotlin - * val time = Duration.ofSeconds(123456) - * val formatted = formatTime(time, showSeconds = true, shortForms = true) - * ``` - * **Output Example:** - * ``` - * 1d:10h:17m:36s - * ``` + * @return A component representing the formatted time. */ fun formatTime( time: Duration, @@ -494,7 +484,7 @@ object CommonComponents { val formatter = Formatter(shortForms, timeColor) val centuries = time.inWholeDays / 365 / 100 - val decades = time.inWholeDays / 365 % 100 + val decades = time.inWholeDays / 365 % 100 / 10 val years = time.inWholeDays / 365 % 10 val days = time.inWholeDays % 365 val hours = time.inWholeHours % 24 @@ -502,30 +492,23 @@ object CommonComponents { val seconds = time.inWholeSeconds % 60 return buildText0 { - if (centuries > 0) append(formatter(centuries, "Jahrhundert", "Jh")) - if (decades > 0) { - append(separator) - append(formatter(decades, "Jahrzehnt", "Jz")) - } - if (years > 0) { - append(separator) - append(formatter(years, "Jahr", "J")) - } - if (days > 0) { - append(separator) - append(formatter(days, "Tag", "d")) - } - if (hours > 0) { - append(separator) - append(formatter(hours, "Stunde", "h")) - } - if (minutes > 0) { - append(separator) - append(formatter(minutes, "Minute", "m")) + var hasAddedComponent = false + fun addComponent(value: Long, longForm: String, shortForm: String) { + if (value > 0) { + if (hasAddedComponent) append(separator) + append(formatter(value, longForm, shortForm)) + hasAddedComponent = true + } } + + addComponent(centuries, "Jahrhundert", "Jh") + addComponent(decades, "Jahrzehnt", "Jz") + addComponent(years, "Jahr", "J") + addComponent(days, "Tag", "d") + addComponent(hours, "Stunde", "h") + addComponent(minutes, "Minute", "m") if (showSeconds) { - append(separator) - append(formatter(seconds, "Sekunde", "s")) + addComponent(seconds, "Sekunde", "s") } } } From 57200dbcd3aad7316260b8fd40d4932c72056c65 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 16:06:40 +0100 Subject: [PATCH 14/32] refactor: correct spelling of MAP_SEPARATOR and enhance documentation in CommonComponents.kt --- .../core/api/messages/CommonComponents.kt | 508 +++++++++++++----- 1 file changed, 370 insertions(+), 138 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt index c03298e39..3f67e5a1a 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt @@ -7,6 +7,7 @@ import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.SPACER import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.VARIABLE_KEY import dev.slne.surf.surfapi.core.api.messages.Colors.Companion.VARIABLE_VALUE import dev.slne.surf.surfapi.core.api.messages.CommonComponents.MAP_SEPARATOR +import dev.slne.surf.surfapi.core.api.messages.CommonComponents.MAP_SEPERATOR import dev.slne.surf.surfapi.core.api.messages.adventure.appendNewline import dev.slne.surf.surfapi.core.api.messages.adventure.appendText import dev.slne.surf.surfapi.core.api.messages.adventure.clickOpensUrl @@ -19,9 +20,9 @@ import net.kyori.adventure.text.format.TextColor import kotlin.time.Duration /** - * Builds a [TextComponent] using the provided [block] to configure the component. + * Builds a [TextComponent] using the provided configuration block. * - * @param block The configuration block for the text component. + * @param block Configuration block for the text component builder. * @return A built [TextComponent] instance. */ @PublishedApi @@ -29,10 +30,13 @@ internal inline fun buildText0(block: TextComponent.Builder.() -> Unit): TextCom return Component.text().apply(block).build() } +/** + * Provides common text components and formatting utilities for messages. + */ object CommonComponents { /** - * An ellipsis (`...`). + * An ellipsis component (`...`). */ @JvmField val ELLIPSIS = text("...") @@ -52,20 +56,19 @@ object CommonComponents { val MAP_SEPERATOR = MAP_SEPARATOR /** - * A separator (`:`) used to visually format time-related messages. + * A separator component (` : `) used to visually format time-related messages. */ @JvmField val TIME_SEPARATOR = text(" : ", SPACER) + /** + * An em dash component (`—`) used as a visual separator. + */ + @JvmField val EM_DASH = text("—", SPACER) /** * A clickable Discord link component (`discord.gg/castcrafter`). - * - * **Output Example:** - * ``` - * discord.gg/castcrafter (clickable) - * ``` */ @JvmField val DISCORD_LINK = buildText0 { @@ -75,12 +78,14 @@ object CommonComponents { } /** - * The standard header for disconnection messages. + * The standard header for disconnection messages displaying the server name. * - * **Output Example:** + * **Output:** * ``` * CASTCRAFTER * COMMUNITY SERVER + * + * * ``` */ @JvmField @@ -94,10 +99,10 @@ object CommonComponents { /** * A footer message indicating the user should try again later. * - * **Output Example:** + * **Output:** * ``` - * \ - * \ + * + * * Bitte versuche es später erneut. * ``` */ @@ -108,17 +113,17 @@ object CommonComponents { } /** - * A footer message indicating the user should try again later and seek support if the issue persists. + * A footer message indicating the user should try again later and contact support if the issue persists. * - * **Output Example:** + * **Output:** * ``` - * \ - * \ + * + * * Bitte versuche es später erneut. * Sollte das Problem weiterhin bestehen, * kontaktiere den Support in unserem Discord. - * \ - * \ + * + * * discord.gg/castcrafter (clickable) * ``` */ @@ -134,26 +139,27 @@ object CommonComponents { } /** - * Renders a structured message for when a player is kicked from the server. + * Renders a structured kick message with custom content and footer. * - * @param messageRenderer A block to render the main message. - * @param footerRenderer A block to render the footer message. - * @return The formatted disconnect message. + * @param messageRenderer Block to render the main message content. + * @param footerRenderer Block to render the footer content. + * @return The formatted disconnect message component. * * **Output Example:** * ``` * CASTCRAFTER * COMMUNITY SERVER - * + * + * * DU WURDEST VOM SERVER GEWORFEN - * \ - * \ - * \ - * [Custom message] - * \ - * \ - * \ - * [Footer message] + * + * + * + * [Custom message content] + * + * + * + * [Footer content] * ``` */ inline fun renderKickDisconnectMessage( @@ -162,27 +168,29 @@ object CommonComponents { ) = renderKickDisconnectMessage(Component.text(), messageRenderer, footerRenderer) /** - * Renders a structured message for when a player is kicked from the server. + * Renders a structured kick message with custom content and footer using a provided builder. * - * @param builder The builder to use for the message. - * @param messageRenderer A block to render the main message. - * @param footerRenderer A block to render the footer message. - * @return The formatted disconnect message. + * @param B The type of builder. + * @param builder The builder to use for constructing the message. + * @param messageRenderer Block to render the main message content. + * @param footerRenderer Block to render the footer content. + * @return The formatted disconnect message component. * * **Output Example:** * ``` * CASTCRAFTER * COMMUNITY SERVER - * + * + * * DU WURDEST VOM SERVER GEWORFEN - * \ - * \ - * \ - * [Custom message] - * \ - * \ - * \ - * [Footer message] + * + * + * + * [Custom message content] + * + * + * + * [Footer content] * ``` */ inline fun renderKickDisconnectMessage( @@ -203,11 +211,54 @@ object CommonComponents { } /** - * Renders a structured message for when a player is kicked from the server. + * Renders a structured kick message with an automatic issue or retry footer. + * + * @param messageRenderer Block to render the main message content. + * @param issue Whether to include an issue-related footer instead of a simple retry footer. + * @return The formatted disconnect message component. * - * @param messageRenderer A block to render the main message. - * @param issue Whether the message should include an issue-related footer. - * @return The formatted disconnect message. + * **Output Example (issue = true):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * DU WURDEST VOM SERVER GEWORFEN + * + * + * + * [Custom message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * Sollte das Problem weiterhin bestehen, + * kontaktiere den Support in unserem Discord. + * + * + * discord.gg/castcrafter (clickable) + * ``` + * + * **Output Example (issue = false):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * DU WURDEST VOM SERVER GEWORFEN + * + * + * + * [Custom message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * ``` */ inline fun renderKickDisconnectMessage( messageRenderer: TextComponent.Builder.() -> Unit, @@ -215,12 +266,56 @@ object CommonComponents { ) = renderKickDisconnectMessage(Component.text(), messageRenderer, issue) /** - * Renders a structured message for when a player is kicked from the server. + * Renders a structured kick message with an automatic issue or retry footer using a provided builder. + * + * @param B The type of builder. + * @param builder The builder to use for constructing the message. + * @param messageRenderer Block to render the main message content. + * @param issue Whether to include an issue-related footer instead of a simple retry footer. + * @return The formatted disconnect message component. + * + * **Output Example (issue = true):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * DU WURDEST VOM SERVER GEWORFEN + * + * + * + * [Custom message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * Sollte das Problem weiterhin bestehen, + * kontaktiere den Support in unserem Discord. + * + * + * discord.gg/castcrafter (clickable) + * ``` * - * @param builder The builder to use for the message. - * @param messageRenderer A block to render the main message. - * @param issue Whether the message should include an issue-related footer. - * @return The formatted disconnect message. + * **Output Example (issue = false):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * DU WURDEST VOM SERVER GEWORFEN + * + * + * + * [Custom message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * ``` */ inline fun renderKickDisconnectMessage( builder: B, @@ -232,27 +327,28 @@ object CommonComponents { } /** - * Renders a structured message for when a player is disconnected from the server. + * Renders a structured disconnection message with a specific reason and help suggestion. * - * @param disconnectReason The reason for the disconnection. - * @param suggestHelp A block to render the help message. - * @param footerRenderer A block to render the footer message. - * @return The formatted disconnect message. + * @param disconnectReason The reason for the disconnection (will be displayed in uppercase). + * @param suggestHelp Block to render the help message content. + * @param footerRenderer Block to render the footer content. + * @return The formatted disconnect message component. * * **Output Example:** * ``` * CASTCRAFTER * COMMUNITY SERVER - * - * DU WURDEST VOM SERVER GEWORFEN - * \ - * \ - * \ - * [Help message] - * \ - * \ - * \ - * [Footer message] + * + * + * VERBINDUNG VERLOREN + * + * + * + * [Help message content] + * + * + * + * [Footer content] * ``` */ inline fun renderDisconnectMessage( @@ -262,28 +358,30 @@ object CommonComponents { ) = renderDisconnectMessage(Component.text(), disconnectReason, suggestHelp, footerRenderer) /** - * Renders a structured message for when a player is disconnected from the server. + * Renders a structured disconnection message with a specific reason and help suggestion using a provided builder. * - * @param builder The builder to use for the message. - * @param disconnectReason The reason for the disconnection. - * @param suggestHelp A block to render the help message. - * @param footerRenderer A block to render the footer message. - * @return The formatted disconnect message. + * @param B The type of builder. + * @param builder The builder to use for constructing the message. + * @param disconnectReason The reason for the disconnection (will be displayed in uppercase). + * @param suggestHelp Block to render the help message content. + * @param footerRenderer Block to render the footer content. + * @return The formatted disconnect message component. * * **Output Example:** * ``` * CASTCRAFTER * COMMUNITY SERVER - * - * DU WURDEST VOM SERVER GEWORFEN - * \ - * \ - * \ - * [Help message] - * \ - * \ - * \ - * [Footer message] + * + * + * VERBINDUNG VERLOREN + * + * + * + * [Help message content] + * + * + * + * [Footer content] * ``` */ inline fun renderDisconnectMessage( @@ -305,12 +403,31 @@ object CommonComponents { } /** - * Renders a structured message for when a player is disconnected from the server. + * Renders a structured disconnection message with an automatic issue or retry footer. + * + * @param disconnectReason The reason for the disconnection (will be displayed in uppercase). + * @param suggestHelp Block to render the help message content. + * @param issue Whether to include an issue-related footer instead of a simple retry footer. + * @return The formatted disconnect message component. * - * @param disconnectReason The reason for the disconnection. - * @param suggestHelp A block to render the help message. - * @param issue Whether the message should include an issue-related footer. - * @return The formatted disconnect message. + * **Output Example (issue = false):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * VERBINDUNG VERLOREN + * + * + * + * [Help message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * ``` */ inline fun renderDisconnectMessage( disconnectReason: @NoLowercase String, @@ -319,13 +436,33 @@ object CommonComponents { ) = renderDisconnectMessage(Component.text(), disconnectReason, suggestHelp, issue) /** - * Renders a structured message for when a player is disconnected from the server. + * Renders a structured disconnection message with an automatic issue or retry footer using a provided builder. + * + * @param B The type of builder. + * @param builder The builder to use for constructing the message. + * @param disconnectReason The reason for the disconnection (will be displayed in uppercase). + * @param suggestHelp Block to render the help message content. + * @param issue Whether to include an issue-related footer instead of a simple retry footer. + * @return The formatted disconnect message component. * - * @param builder The builder to use for the message. - * @param disconnectReason The reason for the disconnection. - * @param suggestHelp A block to render the help message. - * @param issue Whether the message should include an issue-related footer. - * @return The formatted disconnect message. + * **Output Example (issue = false):** + * ``` + * CASTCRAFTER + * COMMUNITY SERVER + * + * + * VERBINDUNG VERLOREN + * + * + * + * [Help message content] + * + * + * + * + * + * Bitte versuche es später erneut. + * ``` */ inline fun renderDisconnectMessage( builder: B, @@ -338,19 +475,20 @@ object CommonComponents { } /** - * Formats a collection into a comma-separated list. + * Formats a collection into a comma-separated list component. * + * @param E The element type. * @param collection The collection to format. - * @param formatter A function to format each element. - * @return A [Component] representing the formatted collection. + * @param formatter Function to convert each element to a component. + * @return A component representing the formatted collection. * - * **Example Usage:** + * **Example:** * ```kotlin - * val list = listOf("Apple", "Banana", "Cherry") - * val formatted = formatCollection(list) { text(it) } + * val fruits = listOf("Apple", "Banana", "Cherry") + * val formatted = formatCollection(fruits) { text(it) } * ``` * - * **Output Example:** + * **Output:** * ``` * Apple, Banana, Cherry * ``` @@ -365,21 +503,23 @@ object CommonComponents { } /** - * Formats a collection into a structured text representation with each element on a new line. + * Formats a collection into a structured list with each element on a new line, prefixed with an em dash. * + * @param E The element type. * @param collection The collection to format. - * @param linePrefix The prefix for each line. - * @param formatter A function to format each element. - * @return A [Component] representing the formatted collection. + * @param linePrefix The prefix component for each line. + * @param formatter Function to convert each element to a component. + * @return A component representing the formatted collection. * - * **Example Usage:** + * **Example:** * ```kotlin - * val list = listOf("Apple", "Banana", "Cherry") - * val formatted = formatCollectionNewLine(list, PREFIX) { text(it) } + * val fruits = listOf("Apple", "Banana", "Cherry") + * val formatted = formatCollectionNewLine(fruits, PREFIX) { text(it) } * ``` * - * **Output Example:** + * **Output:** * ``` + * * >> Surf | — Apple * >> Surf | — Banana * >> Surf | — Cherry @@ -407,23 +547,26 @@ object CommonComponents { } /** - * Formats a map into a structured text representation. + * Formats a map into a structured list with each entry on a new line showing key-value pairs. * + * @param K The key type. + * @param V The value type. * @param map The map to format. - * @param keyFormatter A function to format the keys. - * @param valueFormatter A function to format the values. - * @param linePrefix The prefix for each line. - * @param keyValueSeparator The separator between keys and values. - * @return A formatted [Component]. + * @param keyFormatter Function to convert each key to a component. + * @param valueFormatter Function to convert each value to a component. + * @param linePrefix The prefix component for each line. + * @param keyValueSeparator The separator component between keys and values. + * @return A component representing the formatted map. * - * **Example Usage:** + * **Example:** * ```kotlin - * val data = mapOf("Name" to "Alice", "Age" to "25") - * val formatted = formatMap(data, { text(it) }, { text(it) }, PREFIX, MAP_KEY_VALUE_SEPARATOR) + * val userData = mapOf("Name" to "Alice", "Age" to "25") + * val formatted = formatMap(userData, { text(it) }, { text(it) }, PREFIX, MAP_SEPERATOR) * ``` * - * **Output Example:** + * **Output:** * ``` + * * >> Surf | — Name -> Alice * >> Surf | — Age -> 25 * ``` @@ -433,7 +576,7 @@ object CommonComponents { keyFormatter: (K) -> Component, valueFormatter: (V) -> Component, linePrefix: Component = PREFIX, - keyValueSeparator: Component = MAP_SEPARATOR, + keyValueSeparator: Component = MAP_SEPERATOR, ): Component { val separator = buildText0 { appendNewline() @@ -462,10 +605,32 @@ object CommonComponents { * * @param time The duration to format. * @param showSeconds Whether to include seconds in the output. - * @param shortForms Whether to use short forms for time units (e.g., "d" instead of "Tag"). + * @param shortForms Whether to use short forms for time units (e.g., "d" instead of "Tage"). * @param separator The separator component between time units. * @param timeColor The color for the time values. * @return A component representing the formatted time. + * + * **Example (short forms):** + * ```kotlin + * val duration = Duration.parse("P123DT6H7M8S") // 123 days, 6 hours, 7 minutes, 8 seconds + * val formatted = formatTime(duration, showSeconds = true, shortForms = true) + * ``` + * + * **Output:** + * ``` + * 123d : 6h : 7m : 8s + * ``` + * + * **Example (long forms):** + * ```kotlin + * val duration = Duration.parse("P45DT6H7M") + * val formatted = formatTime(duration, showSeconds = false, shortForms = false) + * ``` + * + * **Output:** + * ``` + * 45 Tage : 6 Stunden : 7 Minuten + * ``` */ fun formatTime( time: Duration, @@ -475,9 +640,14 @@ object CommonComponents { timeColor: TextColor = VARIABLE_VALUE, ): Component { data class Formatter(val shortForms: Boolean, val timeColor: TextColor) { - operator fun invoke(time: Long, longForm: String, shortForm: String) = buildText0 { + operator fun invoke(time: Long, singularForm: String, pluralForm: String, shortForm: String) = buildText0 { append(Component.text(time, timeColor)) - appendText(if (shortForms) shortForm else " $longForm", timeColor) + appendText( + if (shortForms) shortForm + else if (time == 1L) " $singularForm" + else " $pluralForm", + timeColor + ) } } @@ -493,35 +663,72 @@ object CommonComponents { return buildText0 { var hasAddedComponent = false - fun addComponent(value: Long, longForm: String, shortForm: String) { + + fun addComponent(value: Long, singularForm: String, pluralForm: String, shortForm: String) { if (value > 0) { if (hasAddedComponent) append(separator) - append(formatter(value, longForm, shortForm)) + append(formatter(value, singularForm, pluralForm, shortForm)) hasAddedComponent = true } } - addComponent(centuries, "Jahrhundert", "Jh") - addComponent(decades, "Jahrzehnt", "Jz") - addComponent(years, "Jahr", "J") - addComponent(days, "Tag", "d") - addComponent(hours, "Stunde", "h") - addComponent(minutes, "Minute", "m") + addComponent(centuries, "Jahrhundert", "Jahrhunderte", "Jh") + addComponent(decades, "Jahrzehnt", "Jahrzehnte", "Jz") + addComponent(years, "Jahr", "Jahre", "J") + addComponent(days, "Tag", "Tage", "d") + addComponent(hours, "Stunde", "Stunden", "h") + addComponent(minutes, "Minute", "Minuten", "m") if (showSeconds) { - addComponent(seconds, "Sekunde", "s") + addComponent(seconds, "Sekunde", "Sekunden", "s") } } } } /** + * Formats this collection into a comma-separated list component. + * + * @param E The element type. + * @param formatter Function to convert each element to a component. + * @return A component representing the formatted collection. * @see CommonComponents.formatCollection + * + * **Example:** + * ```kotlin + * val fruits = listOf("Apple", "Banana", "Cherry") + * val formatted = fruits.joinToComponent { text(it) } + * ``` + * + * **Output:** + * ``` + * Apple, Banana, Cherry + * ``` */ inline fun Iterable.joinToComponent(formatter: (E) -> Component) = CommonComponents.formatCollection(this, formatter) /** + * Formats this collection into a structured list with each element on a new line. + * + * @param E The element type. + * @param linePrefix The prefix component for each line. + * @param formatter Function to convert each element to a component. + * @return A component representing the formatted collection. * @see CommonComponents.formatCollectionNewLine + * + * **Example:** + * ```kotlin + * val tasks = listOf("Buy groceries", "Clean room", "Study") + * val formatted = tasks.joinToComponentNewLine(PREFIX) { text(it) } + * ``` + * + * **Output:** + * ``` + * + * >> Surf | — Buy groceries + * >> Surf | — Clean room + * >> Surf | — Study + * ``` */ inline fun Iterable.joinToComponentNewLine( linePrefix: Component = PREFIX, @@ -529,11 +736,36 @@ inline fun Iterable.joinToComponentNewLine( ) = CommonComponents.formatCollectionNewLine(this, linePrefix, formatter) /** + * Formats this map into a structured list with each entry on a new line showing key-value pairs. + * + * @param K The key type. + * @param V The value type. + * @param keyFormatter Function to convert each key to a component. + * @param valueFormatter Function to convert each value to a component. + * @param linePrefix The prefix component for each line. + * @param keyValueSeparator The separator component between keys and values. + * @return A component representing the formatted map. * @see CommonComponents.formatMap + * + * **Example:** + * ```kotlin + * val settings = mapOf("Volume" to "80%", "Quality" to "High") + * val formatted = settings.joinToComponent( + * keyFormatter = { text(it) }, + * valueFormatter = { text(it) } + * ) + * ``` + * + * **Output:** + * ``` + * + * >> Surf | — Volume -> 80% + * >> Surf | — Quality -> High + * ``` */ inline fun Map.joinToComponent( keyFormatter: (K) -> Component, valueFormatter: (V) -> Component, linePrefix: Component = PREFIX, - keyValueSeparator: Component = CommonComponents.MAP_SEPARATOR, -) = CommonComponents.formatMap(this, keyFormatter, valueFormatter, linePrefix, keyValueSeparator) \ No newline at end of file + keyValueSeparator: Component = MAP_SEPERATOR, +) = CommonComponents.formatMap(this, keyFormatter, valueFormatter, linePrefix, keyValueSeparator) From 29c669902387c58acdfbcbe4e517c8904cf21e87 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 16:07:33 +0100 Subject: [PATCH 15/32] refactor: suppress EnumEntryName warning in DefaultFontInfo.kt --- .../dev/slne/surf/surfapi/core/api/messages/DefaultFontInfo.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/DefaultFontInfo.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/DefaultFontInfo.kt index 98a08179a..2c071b0cb 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/DefaultFontInfo.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/DefaultFontInfo.kt @@ -3,6 +3,7 @@ package dev.slne.surf.surfapi.core.api.messages /** * @see SpigotMC Forum */ +@Suppress("EnumEntryName") enum class DefaultFontInfo(val character: Char, val length: Int) { A('A', 5), a('a', 5), From 5a59d8d9f47ba64d792c98e38d6ccdcdb7a8d612 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 16:10:33 +0100 Subject: [PATCH 16/32] refactor: enhance CollectionBinaryTag with additional methods and documentation --- .../core/api/nbt/CollectionBinaryTag.kt | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag.kt index c44040c36..236f59a59 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag.kt @@ -1,21 +1,46 @@ package dev.slne.surf.surfapi.core.api.nbt import net.kyori.adventure.nbt.* -import java.lang.Iterable import java.util.* import java.util.stream.Stream +/** + * A unified wrapper for collection-like NBT tags (ListBinaryTag, ByteArrayBinaryTag, + * IntArrayBinaryTag, and LongArrayBinaryTag). + * + * This inline value class provides a common interface for iterating and accessing + * collection-based binary tags without type checking at each usage site. + * + * @property tag The underlying collection binary tag + */ @JvmInline value class CollectionBinaryTag private constructor(val tag: BinaryTag) : Iterable { companion object { + /** + * Creates a CollectionBinaryTag from the given tag if it represents a collection type. + * + * @param tag The binary tag to wrap + * @return A CollectionBinaryTag if the tag is a collection type, null otherwise + */ fun from(tag: BinaryTag) = if (tag.isCollectionTag()) CollectionBinaryTag(tag) else null + + /** + * Creates a CollectionBinaryTag from the given tag, throwing if it's not a collection type. + * + * @param tag The binary tag to wrap + * @return A CollectionBinaryTag wrapping the tag + * @throws IllegalArgumentException if the tag is not a collection type + */ fun require(tag: BinaryTag): CollectionBinaryTag { require(tag.isCollectionTag()) { "Tag is not a collection tag" } return CollectionBinaryTag(tag) } } + /** + * Returns an iterator over the elements in this collection tag. + */ override fun iterator() = when (tag) { is ListBinaryTag -> tag.tagIterator() is ByteArrayBinaryTag -> tag.tagIterator() @@ -24,6 +49,9 @@ value class CollectionBinaryTag private constructor(val tag: BinaryTag) : Iterab else -> throw MatchException(null, null) } + /** + * Returns a spliterator over the elements in this collection tag. + */ fun tagSpliterator(): Spliterator = when (tag) { is ListBinaryTag -> tag.tagSpliterator() is ByteArrayBinaryTag -> tag.tagSpliterator() @@ -32,6 +60,9 @@ value class CollectionBinaryTag private constructor(val tag: BinaryTag) : Iterab else -> throw MatchException(null, null) } + /** + * Returns the number of elements in this collection tag. + */ fun size(): Int = when (tag) { is ListBinaryTag -> tag.size() is ByteArrayBinaryTag -> tag.size() @@ -40,6 +71,9 @@ value class CollectionBinaryTag private constructor(val tag: BinaryTag) : Iterab else -> throw MatchException(null, null) } + /** + * Returns a sequential stream over the elements in this collection tag. + */ fun stream(): Stream = when (tag) { is ListBinaryTag -> tag.tagStream() is ByteArrayBinaryTag -> tag.tagStream() @@ -48,6 +82,17 @@ value class CollectionBinaryTag private constructor(val tag: BinaryTag) : Iterab else -> throw MatchException(null, null) } } - +/** + * Attempts to wrap this BinaryTag as a CollectionBinaryTag. + * + * @return A CollectionBinaryTag if this tag is a collection type, null otherwise + */ fun BinaryTag.asCollectionOrNull() = CollectionBinaryTag.from(this) + +/** + * Wraps this BinaryTag as a CollectionBinaryTag. + * + * @return A CollectionBinaryTag wrapping this tag + * @throws IllegalArgumentException if this tag is not a collection type + */ fun BinaryTag.asCollection() = CollectionBinaryTag.require(this) \ No newline at end of file From ad6eeb7a32875850ea91cda085a3157937a77e84 Mon Sep 17 00:00:00 2001 From: twisti Date: Sun, 8 Feb 2026 16:15:12 +0100 Subject: [PATCH 17/32] refactor: enhance FastCompoundBinaryTag with additional methods and documentation --- .../core/api/nbt/FastCompoundBinaryTag.kt | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/FastCompoundBinaryTag.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/FastCompoundBinaryTag.kt index 01f66064f..c2ada9d94 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/FastCompoundBinaryTag.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/nbt/FastCompoundBinaryTag.kt @@ -5,16 +5,57 @@ import it.unimi.dsi.fastutil.objects.ObjectIterator import it.unimi.dsi.fastutil.objects.ObjectSet import net.kyori.adventure.nbt.BinaryTag import net.kyori.adventure.nbt.CompoundBinaryTag +import org.jetbrains.annotations.UnmodifiableView +/** + * A mutable, high-performance implementation of CompoundBinaryTag backed by fastutil collections. + * + * This interface extends CompoundBinaryTag with additional mutation capabilities and optimized + * iteration through fastutil's specialized collection types. + * + * **Important:** This implementation violates the immutability principle of CompoundBinaryTag. + * All operations (put, remove, etc.) mutate the tag directly, unlike the standard CompoundBinaryTag + * which is immutable and returns a new tag for each operation. + */ interface FastCompoundBinaryTag : CompoundBinaryTag { + + /** + * Removes all key-value mappings from this compound tag. + */ fun clear() - override fun keySet(): ObjectSet + /** + * Returns an optimized set view of the keys contained in this compound tag. + * + * @return An ObjectSet providing efficient key iteration + */ + override fun keySet(): @UnmodifiableView ObjectSet + + /** + * Returns an optimized iterator over the entries in this compound tag. + * + * @return An ObjectIterator for efficient entry traversal + */ override fun iterator(): ObjectIterator> } +/** + * Wraps this CompoundBinaryTag in a mutable FastCompoundBinaryTag for improved performance. + * + * The returned tag is mutable and all operations modify the tag directly, unlike the immutable + * CompoundBinaryTag interface. + * + * @param synchronize If true, wraps the underlying map with synchronization for thread-safe access + * @return A mutable FastCompoundBinaryTag backed by fastutil collections + */ fun CompoundBinaryTag.fast(synchronize: Boolean = false) = InternalNbtBridge.instance.wrapCompoundBinaryTag(this, synchronize) +/** + * Builds and wraps the result in a mutable FastCompoundBinaryTag. + * + * @param synchronize If true, wraps the underlying map with synchronization for thread-safe access + * @return A mutable FastCompoundBinaryTag backed by fastutil collections + */ fun CompoundBinaryTag.Builder.buildFast(synchronize: Boolean = false) = build().fast(synchronize) \ No newline at end of file From f1475e2ec1a1441abb95ac06b302d3f2a9cf369c Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 16:42:00 +0100 Subject: [PATCH 18/32] refactor: add utility functions for Audience UUID and permission handling --- .../messages/adventure/audience-extension.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/audience-extension.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/audience-extension.kt index c2f74988d..3bb846ba3 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/audience-extension.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/audience-extension.kt @@ -1,10 +1,15 @@ +@file:Suppress("NOTHING_TO_INLINE") + package dev.slne.surf.surfapi.core.api.messages.adventure import dev.slne.surf.surfapi.core.api.messages.builder.SurfComponentBuilder import net.kyori.adventure.audience.Audience +import net.kyori.adventure.identity.Identity import net.kyori.adventure.inventory.Book +import net.kyori.adventure.permission.PermissionChecker import net.kyori.adventure.sound.Sound import net.kyori.adventure.sound.Sound.Emitter +import net.kyori.adventure.util.TriState inline fun Audience.sendText(block: SurfComponentBuilder.() -> Unit) { sendMessage(SurfComponentBuilder(block)) @@ -33,4 +38,15 @@ inline fun Audience.playSound(useSelfEmitter: Boolean, block: @SoundDsl Sound.Bu inline fun Audience.showTitle(block: @TitleDsl TitleBuilder.() -> Unit) { showTitle(Title(block)) -} \ No newline at end of file +} + +inline fun Audience.uuidOrNull() = getPointer(Identity.UUID) +inline fun Audience.uuid() = uuidOrNull() ?: error("Audience does not have a UUID pointer") +inline fun Audience.nameOrNull() = getPointer(Identity.NAME) +inline fun Audience.name() = nameOrNull() ?: error("Audience does not have a name pointer") +inline fun Audience.displayNameOrNull() = getPointer(Identity.DISPLAY_NAME) +inline fun Audience.displayName() = displayNameOrNull() ?: error("Audience does not have a display name pointer") +inline fun Audience.testPermission(permission: String) = + getPointer(PermissionChecker.POINTER)?.value(permission) ?: TriState.NOT_SET + +inline fun Audience.hasPermission(permission: String) = getPointer(PermissionChecker.POINTER)?.test(permission) ?: false \ No newline at end of file From a659f1f9c72dc7e946160318d65fce359894678f Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 17:37:29 +0100 Subject: [PATCH 19/32] refactor: add click callback extension functions for Audience handling --- .../messages/adventure/callback-extension.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/callback-extension.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/callback-extension.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/callback-extension.kt new file mode 100644 index 000000000..678439178 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/callback-extension.kt @@ -0,0 +1,71 @@ +package dev.slne.surf.surfapi.core.api.messages.adventure + +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.BuildableComponent +import net.kyori.adventure.text.ComponentBuilder +import net.kyori.adventure.text.event.ClickCallback +import net.kyori.adventure.text.event.ClickEvent +import java.util.function.Consumer +import kotlin.experimental.ExperimentalTypeInference +import kotlin.time.Duration +import kotlin.time.toJavaDuration + +fun , B : ComponentBuilder> ComponentBuilder.clickCallback( + callback: ClickCallback, +) = clickEvent(ClickEvent.callback(callback)) + +fun , B : ComponentBuilder> ComponentBuilder.clickCallbackWithOptions( + builder: ClickCallbackWithOptionsBuilder.() -> Unit, +) = clickEvent(ClickCallbackWithOptionsBuilder(Audience::class.java).apply(builder).build()) + +inline fun , B : ComponentBuilder> ComponentBuilder.clickCallbackTyped( + callback: ClickCallback, +) = clickEvent(ClickEvent.callback(ClickCallback.widen(callback, T::class.java))) + +@OptIn(ExperimentalTypeInference::class) +inline fun , B : ComponentBuilder> ComponentBuilder.clickCallbackTypedWithOptions( + @ClickCallbackWithOptionsBuilderDsl @BuilderInference builder: ClickCallbackWithOptionsBuilder.() -> Unit +) = clickEvent(ClickCallbackWithOptionsBuilder(T::class.java).apply(builder).build()) + +fun ClickCallback.Options.Builder.lifetime(duration: Duration) = lifetime(duration.toJavaDuration()) + +@DslMarker +annotation class ClickCallbackWithOptionsBuilderDsl + +@ClickCallbackWithOptionsBuilderDsl +class ClickCallbackWithOptionsBuilder @PublishedApi internal constructor(private val type: Class) { + private var callback: ClickCallback? = null + private var permission: String? = null + private var permissionOtherwise: Consumer? = null + private var options: ClickCallback.Options = ClickCallback.Options.builder().build() + + fun requiresPermission(permission: String) { + this.permission = permission + } + + fun requiresPermissionOrElse( + permission: String, + @ClickCallbackWithOptionsBuilderDsl callback: Consumer + ) { + this.permission = permission + this.permissionOtherwise = callback + } + + fun callback(@ClickCallbackWithOptionsBuilderDsl callback: ClickCallback) { + this.callback = callback + } + + fun options(options: ClickCallback.Options.Builder.() -> Unit) { + this.options = ClickCallback.Options.builder(this.options).apply(options).build() + } + + @PublishedApi + internal fun build(): ClickEvent { + val callback = callback ?: return ClickEvent.callback { } + val callbackWithPermission = permission + ?.let { permission -> callback.requiringPermission(permission, permissionOtherwise) } + ?: callback + + return ClickEvent.callback(ClickCallback.widen(callbackWithPermission, type), options) + } +} \ No newline at end of file From ea2f89fa3e634a00278eeb7cc4213229a2531b5f Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 17:39:34 +0100 Subject: [PATCH 20/32] refactor: update title duration properties to use default values from Title.DEFAULT_TIMES --- .../surfapi/core/api/messages/adventure/title-extensions.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/title-extensions.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/title-extensions.kt index 730aaae79..5a4e95866 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/title-extensions.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/adventure/title-extensions.kt @@ -165,17 +165,17 @@ class TitleTimesBuilder { /** * The fade-in duration before the title fully appears. */ - internal var fadeIn: JavaDuration = Ticks.duration(10) + internal var fadeIn: JavaDuration = Title.DEFAULT_TIMES.fadeIn() /** * The duration for which the title remains visible. */ - internal var stay: JavaDuration = Ticks.duration(70) + internal var stay: JavaDuration = Title.DEFAULT_TIMES.stay() /** * The fade-out duration before the title disappears. */ - internal var fadeOut: JavaDuration = Ticks.duration(20) + internal var fadeOut: JavaDuration = Title.DEFAULT_TIMES.fadeOut() /** From dbfee428e155397aa2307ac324d9b5031f2f718f Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 21:30:14 +0100 Subject: [PATCH 21/32] refactor: integrate Apache Commons Math3 for enhanced random number generation and selection --- gradle/libs.versions.toml | 2 + .../surf-api-bukkit-server/build.gradle.kts | 1 + .../api/surf-api-core-api.api | 93 ++- .../surf-api-core-api/build.gradle.kts | 1 + .../api/random/Jdk2ApacheRandomGenerator.kt | 160 +++++ .../surfapi/core/api/random/RandomSelector.kt | 621 +++++++++++++++--- .../core/api/random/RandomSelectorImpl.kt | 183 ++++-- .../surf/surfapi/core/api/random/Weighted.kt | 153 ++++- .../surf-api-hytale-server/build.gradle.kts | 1 + surf-api-standalone/build.gradle.kts | 1 + .../surf-api-velocity-server/build.gradle.kts | 1 + 11 files changed, 1024 insertions(+), 193 deletions(-) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGenerator.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1cb7545a..0ba317bf5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ gson = "2.13.2" commons-lang3 = "3.20.0" commons-text = "1.15.0" commons-math4-core = "4.0-beta1" +commons-math3 = "3.6.1" plugin-yml-paper = "0.8.0" spongepowered-math = "2.0.1" fastutil = "8.5.18" @@ -139,6 +140,7 @@ gson = { module = "com.google.code.gson:gson", version.ref = "gson" } commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" } commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } commons-math4-core = { module = "org.apache.commons:commons-math4-core", version.ref = "commons-math4-core" } +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } spongepowered-math = { module = "org.spongepowered:math", version.ref = "spongepowered-math" } fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } diff --git a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts index 04b7d4278..c53b03a04 100644 --- a/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts +++ b/surf-api-bukkit/surf-api-bukkit-server/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { paperLibrary(libs.flogger) paperLibrary(libs.flogger.slf4j.backend) paperLibrary(libs.commons.math4.core) + paperLibrary(libs.commons.math3) paperLibrary(libs.aide.reflection) api(libs.mccoroutine.folia.api) api(libs.mccoroutine.folia.core) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 027734200..8c7165dd4 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -6504,8 +6504,10 @@ public final class dev/slne/surf/surfapi/core/api/messages/CommonComponents { public static final field DISCONNECT_HEADER Lnet/kyori/adventure/text/TextComponent; public static final field DISCORD_LINK Lnet/kyori/adventure/text/TextComponent; public static final field ELLIPSIS Lnet/kyori/adventure/text/TextComponent; + public static final field EM_DASH Lnet/kyori/adventure/text/TextComponent; public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/messages/CommonComponents; public static final field ISSUE_FOOTER Lnet/kyori/adventure/text/TextComponent; + public static final field MAP_SEPARATOR Lnet/kyori/adventure/text/TextComponent; public static final field MAP_SEPERATOR Lnet/kyori/adventure/text/TextComponent; public static final field RETRY_LATER_FOOTER Lnet/kyori/adventure/text/TextComponent; public static final field TIME_SEPARATOR Lnet/kyori/adventure/text/TextComponent; @@ -6516,7 +6518,6 @@ public final class dev/slne/surf/surfapi/core/api/messages/CommonComponents { public static synthetic fun formatMap$default (Ldev/slne/surf/surfapi/core/api/messages/CommonComponents;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Lnet/kyori/adventure/text/Component; public final fun formatTime-gRj5Bb8 (JZZLnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/format/TextColor;)Lnet/kyori/adventure/text/Component; public static synthetic fun formatTime-gRj5Bb8$default (Ldev/slne/surf/surfapi/core/api/messages/CommonComponents;JZZLnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/format/TextColor;ILjava/lang/Object;)Lnet/kyori/adventure/text/Component; - public final fun getEM_DASH ()Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Z)Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Lnet/kyori/adventure/text/TextComponent$Builder;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/text/TextComponent; @@ -6666,12 +6667,20 @@ public abstract interface annotation class dev/slne/surf/surfapi/core/api/messag } public final class dev/slne/surf/surfapi/core/api/messages/adventure/Audience_extensionKt { + public static final fun displayName (Lnet/kyori/adventure/audience/Audience;)Lnet/kyori/adventure/text/Component; + public static final fun displayNameOrNull (Lnet/kyori/adventure/audience/Audience;)Lnet/kyori/adventure/text/Component; + public static final fun hasPermission (Lnet/kyori/adventure/audience/Audience;Ljava/lang/String;)Z + public static final fun name (Lnet/kyori/adventure/audience/Audience;)Ljava/lang/String; + public static final fun nameOrNull (Lnet/kyori/adventure/audience/Audience;)Ljava/lang/String; public static final fun openBook (Lnet/kyori/adventure/audience/Audience;Lkotlin/jvm/functions/Function1;)V public static final fun playSound (Lnet/kyori/adventure/audience/Audience;Lkotlin/jvm/functions/Function1;)V public static final fun playSound (Lnet/kyori/adventure/audience/Audience;ZLkotlin/jvm/functions/Function1;)V public static final fun sendText (Lnet/kyori/adventure/audience/Audience;Lkotlin/jvm/functions/Function1;)V public static final fun showBossBar (Lnet/kyori/adventure/audience/Audience;Lkotlin/jvm/functions/Function1;)V public static final fun showTitle (Lnet/kyori/adventure/audience/Audience;Lkotlin/jvm/functions/Function1;)V + public static final fun testPermission (Lnet/kyori/adventure/audience/Audience;Ljava/lang/String;)Lnet/kyori/adventure/util/TriState; + public static final fun uuid (Lnet/kyori/adventure/audience/Audience;)Ljava/util/UUID; + public static final fun uuidOrNull (Lnet/kyori/adventure/audience/Audience;)Ljava/util/UUID; } public abstract interface annotation class dev/slne/surf/surfapi/core/api/messages/adventure/BookDsl : java/lang/annotation/Annotation { @@ -6710,6 +6719,24 @@ public final class dev/slne/surf/surfapi/core/api/messages/adventure/Bossbar_ext public static final fun bossBar (Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/bossbar/BossBar; } +public final class dev/slne/surf/surfapi/core/api/messages/adventure/Callback_extensionKt { + public static final fun clickCallback (Lnet/kyori/adventure/text/ComponentBuilder;Lnet/kyori/adventure/text/event/ClickCallback;)Lnet/kyori/adventure/text/ComponentBuilder; + public static final fun clickCallbackWithOptions (Lnet/kyori/adventure/text/ComponentBuilder;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/text/ComponentBuilder; + public static final fun lifetime-HG0u8IE (Lnet/kyori/adventure/text/event/ClickCallback$Options$Builder;J)Lnet/kyori/adventure/text/event/ClickCallback$Options$Builder; +} + +public final class dev/slne/surf/surfapi/core/api/messages/adventure/ClickCallbackWithOptionsBuilder { + public fun (Ljava/lang/Class;)V + public final fun build ()Lnet/kyori/adventure/text/event/ClickEvent; + public final fun callback (Lnet/kyori/adventure/text/event/ClickCallback;)V + public final fun options (Lkotlin/jvm/functions/Function1;)V + public final fun requiresPermission (Ljava/lang/String;)V + public final fun requiresPermissionOrElse (Ljava/lang/String;Ljava/util/function/Consumer;)V +} + +public abstract interface annotation class dev/slne/surf/surfapi/core/api/messages/adventure/ClickCallbackWithOptionsBuilderDsl : java/lang/annotation/Annotation { +} + public final class dev/slne/surf/surfapi/core/api/messages/adventure/Component_extensionKt { public static final fun appendNewPrefixedLine (Lnet/kyori/adventure/text/ComponentBuilder;I)V public static final fun appendNewline (Lnet/kyori/adventure/text/ComponentBuilder;I)V @@ -8124,7 +8151,7 @@ public final class dev/slne/surf/surfapi/core/api/minimessage/SurfMiniMessageHol public static final fun getMiniMessage ()Lnet/kyori/adventure/text/minimessage/MiniMessage; } -public final class dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag : java/lang/Iterable { +public final class dev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { public static final field Companion Ldev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag$Companion; public static final synthetic fun box-impl (Lnet/kyori/adventure/nbt/BinaryTag;)Ldev/slne/surf/surfapi/core/api/nbt/CollectionBinaryTag; public fun equals (Ljava/lang/Object;)Z @@ -8191,47 +8218,59 @@ public final class dev/slne/surf/surfapi/core/api/nbt/Nbt_extensionKt { public static final fun tagStream (Lnet/kyori/adventure/nbt/LongArrayBinaryTag;)Ljava/util/stream/Stream; } -public final class dev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum : java/lang/Enum, dev/slne/surf/surfapi/core/api/random/Weighted { - public static final field COMMON Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static final field Companion Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum$Companion; - public static final field EXTREMELY_RARE Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static final field RARE Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static final field UNCOMMON Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static final field VERY_RARE Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getWeight ()D - public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; - public static fun values ()[Ldev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum; +public final class dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGenerator : org/apache/commons/math3/random/RandomGenerator { + public fun (Ljava/util/random/RandomGenerator;)V + public fun nextBoolean ()Z + public fun nextBytes ([B)V + public fun nextDouble ()D + public fun nextFloat ()F + public fun nextGaussian ()D + public fun nextInt ()I + public fun nextInt (I)I + public fun nextLong ()J + public fun setSeed (I)V + public fun setSeed (J)V + public fun setSeed ([I)V } -public final class dev/slne/surf/surfapi/core/api/random/ExampleWeightedEnum$Companion { - public final fun getSelector ()Ldev/slne/surf/surfapi/core/api/random/RandomSelector; +public final class dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGeneratorKt { + public static final fun toApache (Ljava/util/random/RandomGenerator;)Lorg/apache/commons/math3/random/RandomGenerator; + public static final fun toApacheNullable (Ljava/util/random/RandomGenerator;)Lorg/apache/commons/math3/random/RandomGenerator; } public abstract interface class dev/slne/surf/surfapi/core/api/random/RandomSelector { public static final field Companion Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion; + public abstract fun flow ()Lkotlinx/coroutines/flow/Flow; public abstract fun flow (Ljava/util/random/RandomGenerator;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun flow$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public abstract fun flowOrNull (D)Lkotlinx/coroutines/flow/Flow; + public abstract fun pick ()Ljava/lang/Object; public abstract fun pick (Ljava/util/random/RandomGenerator;)Ljava/lang/Object; - public static synthetic fun pick$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun pickOrNull (D)Ljava/lang/Object; + public abstract fun sequence ()Lkotlin/sequences/Sequence; + public abstract fun sequenceOrNull (D)Lkotlin/sequences/Sequence; } public final class dev/slne/surf/surfapi/core/api/random/RandomSelector$Companion { + public final fun fromFlow (Lkotlinx/coroutines/flow/Flow;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun fromFlow (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun fromFlow (Lkotlinx/coroutines/flow/Flow;Lorg/apache/commons/math3/random/RandomGenerator;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun fromFlow$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion;Lkotlinx/coroutines/flow/Flow;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun fromInfinityFlow (Lkotlinx/coroutines/flow/Flow;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; public final fun fromInfinityFlow (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromInfinityFlow (Lkotlinx/coroutines/flow/Flow;Lorg/apache/commons/math3/random/RandomGenerator;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public static synthetic fun fromInfinityFlow$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion;Lkotlinx/coroutines/flow/Flow;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; public final fun fromIterable (Ljava/lang/Iterable;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromIterable (Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromIterable (Ljava/lang/Iterable;Lorg/apache/commons/math3/random/RandomGenerator;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public static synthetic fun fromIterable$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion;Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; public final fun fromWeightedIterable (Ljava/lang/Iterable;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromWeightedIterable (Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromWeightedIterable (Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; public final fun fromWeightedIterable (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; -} - -public final class dev/slne/surf/surfapi/core/api/random/RandomSelector$DefaultImpls { - public static synthetic fun flow$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun pick$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Ljava/lang/Object; -} - -public final class dev/slne/surf/surfapi/core/api/random/RandomSelectorKt { - public static final fun main ()V - public static synthetic fun main ([Ljava/lang/String;)V + public final fun fromWeightedIterable (Ljava/lang/Iterable;Lorg/apache/commons/math3/random/RandomGenerator;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public final fun fromWeightedIterable (Ljava/lang/Iterable;Lorg/apache/commons/math3/random/RandomGenerator;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public static synthetic fun fromWeightedIterable$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion;Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; + public static synthetic fun fromWeightedIterable$default (Ldev/slne/surf/surfapi/core/api/random/RandomSelector$Companion;Ljava/lang/Iterable;Ljava/util/random/RandomGenerator;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/random/RandomSelector; } public abstract interface class dev/slne/surf/surfapi/core/api/random/Weighted { diff --git a/surf-api-core/surf-api-core-api/build.gradle.kts b/surf-api-core/surf-api-core-api/build.gradle.kts index 9cfc75e1d..544a10a25 100644 --- a/surf-api-core/surf-api-core-api/build.gradle.kts +++ b/surf-api-core/surf-api-core-api/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(libs.configurate.kotlin) compileOnlyApi(libs.flogger) compileOnlyApi(libs.commons.math4.core) + compileOnlyApi(libs.commons.math3) compileOnlyApi(libs.aide.reflection) api(libs.glm) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGenerator.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGenerator.kt new file mode 100644 index 000000000..353621851 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Jdk2ApacheRandomGenerator.kt @@ -0,0 +1,160 @@ +package dev.slne.surf.surfapi.core.api.random + +import org.apache.commons.math3.random.RandomGenerator +import org.apache.commons.math3.random.RandomGeneratorFactory +import java.util.* + +/** + * Adapter class that bridges JDK's [java.util.random.RandomGenerator] to Apache Commons Math's + * [org.apache.commons.math3.random.RandomGenerator] interface. + * + * This adapter enables the use of modern Java random number generators (introduced in Java 17) + * with Apache Commons Math3 statistical distributions and utilities, which require the older + * Commons Math RandomGenerator interface. + * + * ## Supported Operations + * + * All randomization operations are delegated to the underlying JDK [RandomGenerator]: + * - Primitive random value generation (int, long, double, float, boolean) + * - Byte array filling + * - Gaussian distribution + * + * ## Seed Methods + * + * The [setSeed] methods are **no-ops** for most JDK RandomGenerator implementations because: + * - Most modern random generators don't support runtime reseeding + * - Reseeding would break thread-safety of many implementations + * - The only exception is [java.util.Random], which does support setSeed + * + * For reproducible random sequences, provide a seeded generator at construction time: + * ```kotlin + * val seeded = Random(42L).asJavaRandom() + * val adapter = Jdk2ApacheRandomGenerator(seeded) + * ``` + * + * ## Example Usage + * + * ```kotlin + * // Using Java's SplittableRandom + * val jdkRandom = SplittableRandom(42L) + * val apacheRandom = Jdk2ApacheRandomGenerator(jdkRandom) + * + * // Now usable with Apache Commons Math + * val distribution = EnumeratedDistribution(apacheRandom, probabilityMassFunction) + * val sample = distribution.sample() + * ``` + * + * ## Extension Functions + * + * For convenience, extension functions are provided: + * ```kotlin + * val jdkRandom: java.util.random.RandomGenerator = Random() + * val apacheRandom: RandomGenerator = jdkRandom.toApache() + * + * // Nullable variant + * val maybeRandom: java.util.random.RandomGenerator? = null + * val maybeApache: RandomGenerator? = maybeRandom.toApache() // Returns null + * ``` + * + * @property jdk The underlying JDK RandomGenerator instance to which all operations are delegated. + * @constructor Creates a new adapter wrapping the given JDK RandomGenerator. + * @see java.util.random.RandomGenerator + * @see org.apache.commons.math3.random.RandomGenerator + */ +class Jdk2ApacheRandomGenerator(private val jdk: java.util.random.RandomGenerator) : RandomGenerator { + + /** + * Attempts to set the random seed using an integer value. + * + * **Note:** This is a no-op for most JDK RandomGenerator implementations. + * Only [java.util.Random] supports runtime reseeding. For other generators, + * this method does nothing and the call is silently ignored. + * + * @param seed The integer seed value (converted to Long for Random). + */ + override fun setSeed(seed: Int) { + if (jdk is Random) { + jdk.setSeed(seed.toLong()) + } + } + + /** + * Attempts to set the random seed using an integer array. + * + * **Note:** This is a no-op for most JDK RandomGenerator implementations. + * Only [java.util.Random] supports runtime reseeding, and it will convert + * the array to a single Long value. + * + * @param seed The integer array seed value. + */ + override fun setSeed(seed: IntArray) { + if (jdk is Random) { + jdk.setSeed(RandomGeneratorFactory.convertToLong(seed)) + } + } + + /** + * Attempts to set the random seed using a long value. + * + * **Note:** This is a no-op for most JDK RandomGenerator implementations. + * Only [java.util.Random] supports runtime reseeding. + * + * @param seed The long seed value. + */ + override fun setSeed(seed: Long) { + if (jdk is Random) { + jdk.setSeed(seed) + } + } + + override fun nextBytes(bytes: ByteArray) { + jdk.nextBytes(bytes) + } + + override fun nextInt(): Int { + return jdk.nextInt() + } + + override fun nextInt(n: Int): Int { + return jdk.nextInt(n) + } + + override fun nextLong(): Long { + return jdk.nextLong() + } + + override fun nextBoolean(): Boolean { + return jdk.nextBoolean() + } + + override fun nextFloat(): Float { + return jdk.nextFloat() + } + + override fun nextDouble(): Double { + return jdk.nextDouble() + } + + override fun nextGaussian(): Double { + return jdk.nextGaussian() + } +} + +/** + * Converts a nullable JDK [java.util.random.RandomGenerator] to an Apache Commons Math + * [org.apache.commons.math3.random.RandomGenerator]. + * + * @receiver The JDK RandomGenerator to convert, or null. + * @return An Apache Commons Math RandomGenerator wrapping the JDK generator, or null if the receiver is null. + */ +@JvmName("toApacheNullable") +fun java.util.random.RandomGenerator?.toApache(): RandomGenerator? = this?.let(::Jdk2ApacheRandomGenerator) + +/** + * Converts a JDK [java.util.random.RandomGenerator] to an Apache Commons Math + * [org.apache.commons.math3.random.RandomGenerator]. + * + * @receiver The JDK RandomGenerator to convert. + * @return An Apache Commons Math RandomGenerator wrapping the JDK generator. + */ +fun java.util.random.RandomGenerator.toApache(): RandomGenerator = Jdk2ApacheRandomGenerator(this) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelector.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelector.kt index 817a28a73..819dd0058 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelector.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelector.kt @@ -1,156 +1,605 @@ package dev.slne.surf.surfapi.core.api.random -import dev.slne.surf.surfapi.core.api.util.random +import dev.slne.surf.surfapi.core.api.random.RandomSelector.Companion.fromInfinityFlow +import dev.slne.surf.surfapi.core.api.random.RandomSelector.Companion.fromWeightedIterable import kotlinx.coroutines.flow.Flow import java.util.random.RandomGenerator /** - * A generic interface for selecting random elements from a collection, supporting both uniform and weighted selection. + * A generic interface for selecting random elements from a collection with support for both uniform + * and weighted probability distributions. * - * The [RandomSelector] provides a flexible way to pick elements randomly, either using equal probabilities - * or custom weights, and supports various collection types including [Iterable] and [Flow]. + * [RandomSelector] provides an efficient and statistically correct implementation for random selection + * using Apache Commons Math3's [EnumeratedDistribution]. It supports various collection types including + * [Iterable] and [Flow], with optional custom [RandomGenerator] instances for reproducibility. * - * ### Example 1: Simple Random Selection - * ``` - * val items = listOf("Apple", "Banana", "Cherry") - * val selector = RandomSelector.fromIterable(items) + * ## Key Features + * - **Uniform Selection**: Equal probability for all elements + * - **Weighted Selection**: Custom probability distribution via weights + * - **Multiple Output Formats**: Single picks, infinite flows, or sequences + * - **Reproducibility**: Support for seeded random generators + * - **Type Safety**: Full Kotlin generics support * - * println(selector.pick()) // Prints a random item from the list - * ``` + * ## Usage Examples * - * ### Example 2: Weighted Random Selection + * ### Example 1: Simple Uniform Selection + * ```kotlin + * val fruits = listOf("Apple", "Banana", "Cherry") + * val selector = RandomSelector.fromIterable(fruits) + * + * val randomFruit = selector.pick() + * println(randomFruit) // Each fruit has 33.3% probability * ``` - * data class Item(val name: String, val weight: Double) - * val items = listOf( - * Item("Common", 1.0), - * Item("Uncommon", 0.5), - * Item("Rare", 0.1) - * ) * - * val selector = RandomSelector.fromWeightedIterable(items) { it.weight } + * ### Example 2: Weighted Selection with Custom Weights + * ```kotlin + * data class Item(val name: String, val rarity: Double) * - * println(selector.pick()) // Prints an item with probability proportional to its weight - * ``` + * val lootTable = listOf( + * Item("Common Sword", 1.0), + * Item("Rare Shield", 0.5), + * Item("Epic Helmet", 0.1) + * ) * - * ### Example 3: Weighted Random Selection with Enums + * val selector = RandomSelector.fromWeightedIterable(lootTable) { it.rarity } + * val drop = selector.pick() // Higher weight = higher probability * ``` + * + * ### Example 3: Weighted Selection with Enums + * ```kotlin * enum class Rarity(override val weight: Double) : Weighted { - * COMMON(1.0), - * UNCOMMON(0.5), - * RARE(0.1), - * VERY_RARE(0.01) + * COMMON(1.0), // 62.2% chance + * UNCOMMON(0.5), // 31.1% chance + * RARE(0.1), // 6.2% chance + * VERY_RARE(0.01) // 0.6% chance * * companion object { * val selector = RandomSelector.fromWeightedIterable(entries) * } * } * - * for (i in 1..10) { - * println(Rarity.selector.pick()) // Prints a random rarity based on its weight + * repeat(100) { + * println(Rarity.selector.pick()) * } * ``` * - * ### Example 4: Infinite Random Flow + * ### Example 4: Reproducible Results with Seeded Random + * ```kotlin + * val seed = 42L + * val random = Random(seed).asJavaRandom() + * val selector = RandomSelector.fromWeightedIterable(items, random) { it.weight } + * + * // Always produces the same sequence with the same seed + * val result1 = selector.pick() * ``` - * val items = listOf("A", "B", "C") - * val selector = RandomSelector.fromIterable(items) - * val randomFlow = selector.flow() * - * randomFlow.take(5).collect { println(it) } // Continuously emits random items + * ### Example 5: Infinite Random Stream + * ```kotlin + * val selector = RandomSelector.fromIterable(listOf("A", "B", "C")) + * + * // Using Flow (for coroutines) + * selector.flow().take(5).collect { println(it) } + * + * // Using Sequence (for synchronous code) + * selector.sequence().take(5).forEach { println(it) } * ``` * - * @param E The type of elements to be selected. + * ## Performance Characteristics + * - **Creation**: O(n) where n is the number of elements + * - **Selection**: O(log n) using binary search on cumulative weights + * - **Memory**: O(n) for storing elements and cumulative distribution + * + * ## Thread Safety + * Individual [RandomSelector] instances are NOT thread-safe. For concurrent access, either: + * - Create separate selectors per thread with different [RandomGenerator] instances + * - Use external synchronization + * - Create a new selector from the same source data + * + * @param E The type of elements to be selected. Can be any type. + * @see Weighted + * @see Weighter */ interface RandomSelector { /** - * Picks a single random element from the collection. + * Selects and returns a single random element based on the configured probability distribution. + * + * This method is deprecated in favor of specifying the [RandomGenerator] at selector creation time. + * The generator parameter here is only used for backward compatibility and creates a temporary + * distribution instance. + * + * @param randomGenerator The random number generator to use for this selection. + * @return A randomly selected element according to the probability distribution. + * @see pick() + */ + @Deprecated( + message = "Use pick() without parameters. RandomGenerator can be specified at creation time.", + replaceWith = ReplaceWith("pick()"), + level = DeprecationLevel.WARNING + ) + fun pick(randomGenerator: RandomGenerator): E + + /** + * Selects and returns a single random element based on the configured probability distribution. + * + * For uniform selectors, each element has equal probability. For weighted selectors, elements + * are selected with probability proportional to their weights. + * + * **Example:** + * ```kotlin + * val selector = RandomSelector.fromWeightedIterable(items) { it.weight } + * val selected = selector.pick() // Returns one element + * ``` + * + * @return A randomly selected element according to the probability distribution. + * @throws IllegalStateException if the selector is based on an infinite flow. + */ + fun pick(): E + + /** + * Selects and returns a random element, or null based on the success probability. + * + * This method adds an implicit "empty result" outcome to the probability distribution. + * The success rate represents the probability of selecting an actual element (not null). + * + * ## Probability + * - `P(element selected) = successRate` + * - `P(null returned) = 1 - successRate` + * + * ## Use Cases + * - **Fishing Systems**: Chance to catch something + * - **Loot Drops**: Probability enemy drops an item + * - **Random Encounters**: Event trigger chance + * - **Gacha/Gatcha**: Pull success rate + * + * ## Examples + * + * **Fishing with 40% success rate:** + * ```kotlin + * val fishSelector = RandomSelector.fromWeightedIterable(fishTypes) { it.weight } + * val caught = fishSelector.pickOrNull(successRate = 0.4) // 40% catch, 60% nothing + * + * when (caught) { + * null -> println("Nothing bites...") + * else -> println("You caught a $caught!") + * } + * ``` + * + * **Loot with 25% drop rate:** + * ```kotlin + * val lootSelector = RandomSelector.fromWeightedIterable(items) { it.rarity } + * val loot = lootSelector.pickOrNull(successRate = 0.25) // 25% drop rate + * + * if (loot != null) { + * player.inventory.add(loot) + * } + * ``` + * + * **50/50 chance:** + * ```kotlin + * val result = selector.pickOrNull(successRate = 0.5) + * ``` + * + * @param successRate The probability of selecting an element (between 0.0 and 1.0). + * 0.0 = always returns null, 1.0 = always returns an element. + * @return A randomly selected element, or null based on the success rate. + * @throws IllegalArgumentException if successRate is not in range [0.0, 1.0]. + * @throws IllegalStateException if the selector is based on an infinite flow. + */ + fun pickOrNull(successRate: Double): E? + + /** + * Creates an infinite [Flow] that continuously emits random elements. + * + * This method is deprecated in favor of specifying the [RandomGenerator] at selector creation time. + * + * @param randomGenerator The random number generator to use for selections. + * @return An infinite [Flow] of randomly selected elements. + * @see flow() + */ + @Deprecated( + message = "Use flow() without parameters. RandomGenerator can be specified at creation time.", + replaceWith = ReplaceWith("flow()"), + level = DeprecationLevel.WARNING + ) + fun flow(randomGenerator: RandomGenerator): Flow + + /** + * Creates an infinite [Flow] that continuously emits random elements. + * + * Each emitted element is selected independently according to the probability distribution. + * This flow never completes and will emit elements indefinitely until cancelled. + * + * **Use cases:** + * - Procedural generation in games (spawning enemies, loot drops) + * - Simulation systems requiring continuous random events + * - Testing with random data streams + * + * **Example:** + * ```kotlin + * val selector = RandomSelector.fromIterable(listOf("Red", "Green", "Blue")) + * + * selector.flow() + * .take(10) + * .collect { color -> + * println("Generated: $color") + * } + * ``` + * + * @return An infinite [Flow] of randomly selected elements. + */ + fun flow(): Flow + + /** + * Creates an infinite [Flow] that continuously emits random elements or null values. + * + * This is equivalent to repeatedly calling [pickOrNull] with the specified success rate. + * Each emission is independent, with the configured probability of being an actual element. + * + * **Example: Random Event Stream with Downtime** + * ```kotlin + * val eventSelector = RandomSelector.fromWeightedIterable(events) { it.frequency } + * + * eventSelector.flowOrNull(successRate = 0.3) // 30% events, 70% nothing + * .collect { event -> + * if (event != null) { + * println("Event triggered: $event") + * } else { + * println("No event this tick") + * } + * } + * ``` + * + * @param successRate The probability of emitting an element (between 0.0 and 1.0). + * @return An infinite [Flow] of randomly selected elements or nulls. + * @throws IllegalArgumentException if successRate is not in range [0.0, 1.0]. + */ + fun flowOrNull(successRate: Double): Flow + + /** + * Creates an infinite [Sequence] that lazily generates random elements. * - * @param randomGenerator The random number generator to use. Defaults to a shared instance. - * @return A randomly selected element. + * Unlike [flow], this is a synchronous operation suitable for non-coroutine contexts. + * The sequence is lazy and generates elements only when requested. It never terminates + * unless explicitly limited with operations like [Sequence.take]. + * + * **Performance Note:** Sequences are more efficient than flows for simple synchronous + * iteration as they avoid coroutine overhead. + * + * **Example:** + * ```kotlin + * val selector = RandomSelector.fromWeightedIterable(dice) { it.weight } + * + * val rolls = selector.sequence() + * .take(100) + * .groupingBy { it } + * .eachCount() + * + * println("Distribution: $rolls") + * ``` + * + * @return An infinite [Sequence] of randomly selected elements. + * @throws UnsupportedOperationException if the selector is based on an infinite flow. */ - fun pick(randomGenerator: RandomGenerator = random): E + fun sequence(): Sequence /** - * Creates an infinite [Flow] that emits random elements. + * Creates an infinite [Sequence] that lazily generates random elements or null values. + * + * This is the sequence equivalent of [flowOrNull], useful for synchronous code that + * needs nullable random results. + * + * **Example: Simulating Fishing Attempts** + * ```kotlin + * val fishSelector = RandomSelector.fromWeightedIterable(Fish.entries) + * + * val attempts = fishSelector.sequenceOrNull(successRate = 0.3) // 30% catch rate + * .take(50) + * .toList() * - * @param randomGenerator The random number generator to use. Defaults to a shared instance. - * @return A [Flow] of randomly selected elements. + * val caught = attempts.filterNotNull() + * println("Caught ${caught.size} fish in 50 attempts (expected ~15)") + * ``` + * + * @param successRate The probability of generating an element (between 0.0 and 1.0). + * @return An infinite [Sequence] of randomly selected elements or nulls. + * @throws IllegalArgumentException if successRate is not in range [0.0, 1.0]. + * @throws UnsupportedOperationException if the selector is based on an infinite flow. */ - fun flow(randomGenerator: RandomGenerator = random): Flow + fun sequenceOrNull(successRate: Double): Sequence companion object { /** - * Creates a [RandomSelector] from an [Iterable], using uniform selection probabilities. + * Creates a [RandomSelector] with uniform probability distribution from an [Iterable]. + * + * All elements have equal probability of being selected (1/n where n is the element count). + * + * **Example:** + * ```kotlin + * val cards = listOf("Ace", "King", "Queen", "Jack") + * val selector = RandomSelector.fromIterable(cards) + * val card = selector.pick() // Each card has 25% probability + * ``` + * + * @param E The type of elements in the collection. + * @param iterable The source collection of elements. Must not be empty. + * @param randomGenerator Optional custom random generator for reproducible results. + * If null, uses a default instance. + * @return A [RandomSelector] with uniform selection probability. + * @throws IllegalArgumentException if the iterable is empty. + */ + @JvmOverloads + fun fromIterable( + iterable: Iterable, + randomGenerator: RandomGenerator? = null + ): RandomSelector = RandomSelectorImpl.fromIterable(iterable, randomGenerator.toApache()) + + /** + * Creates a [RandomSelector] with uniform probability distribution from an [Iterable]. * - * @param iterable The iterable collection of elements. - * @return A [RandomSelector] that selects elements uniformly at random. + * This overload accepts an Apache Commons Math [org.apache.commons.math3.random.RandomGenerator] + * directly for advanced use cases or interoperability with existing Apache Commons code. + * + * @param E The type of elements in the collection. + * @param iterable The source collection of elements. Must not be empty. + * @param randomGenerator Apache Commons Math random generator instance. + * @return A [RandomSelector] with uniform selection probability. + * @throws IllegalArgumentException if the iterable is empty. */ - fun fromIterable(iterable: Iterable): RandomSelector = - RandomSelectorImpl.fromIterable(iterable) + fun fromIterable( + iterable: Iterable, + randomGenerator: org.apache.commons.math3.random.RandomGenerator + ): RandomSelector = RandomSelectorImpl.fromIterable(iterable, randomGenerator) /** - * Creates a [RandomSelector] from a weighted [Iterable]. + * Creates a [RandomSelector] with custom weighted probability distribution from an [Iterable]. + * + * Elements are selected with probability proportional to their weights. For example, an element + * with weight 2.0 is twice as likely to be selected as an element with weight 1.0. + * + * **Weight Requirements:** + * - All weights must be positive (> 0) + * - All weights must be finite (not NaN or Infinity) + * - Weights are relative; they don't need to sum to 1.0 * - * @param iterable The iterable collection of elements. - * @param weighter A function that determines the weight of each element. - * @return A [RandomSelector] that selects elements based on their weights. + * **Example:** + * ```kotlin + * data class Monster(val name: String, val spawnRate: Double) + * + * val monsters = listOf( + * Monster("Goblin", 10.0), // 76.9% spawn chance + * Monster("Orc", 2.0), // 15.4% spawn chance + * Monster("Dragon", 1.0) // 7.7% spawn chance + * ) + * + * val selector = RandomSelector.fromWeightedIterable(monsters) { it.spawnRate } + * val spawned = selector.pick() + * ``` + * + * @param E The type of elements in the collection. + * @param iterable The source collection of elements. Must not be empty. + * @param randomGenerator Optional custom random generator for reproducible results. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the iterable is empty or any weight is invalid. */ + @JvmOverloads fun fromWeightedIterable( iterable: Iterable, + randomGenerator: RandomGenerator? = null, weighter: Weighter, - ): RandomSelector = - RandomSelectorImpl.fromWeightedIterable(iterable, weighter) + ): RandomSelector = RandomSelectorImpl.fromWeightedIterable(iterable, weighter, randomGenerator.toApache()) /** - * Creates a [RandomSelector] from a weighted [Iterable] of elements that implement [Weighted]. + * Creates a [RandomSelector] with custom weighted probability distribution from an [Iterable]. * - * @param iterable The iterable collection of elements. - * @return A [RandomSelector] that selects elements based on their weights. + * This overload accepts an Apache Commons Math [org.apache.commons.math3.random.RandomGenerator] + * directly for advanced use cases. + * + * @param E The type of elements in the collection. + * @param iterable The source collection of elements. Must not be empty. + * @param randomGenerator Apache Commons Math random generator instance. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the iterable is empty or any weight is invalid. */ - fun fromWeightedIterable(iterable: Iterable): RandomSelector = - RandomSelectorImpl.fromWeightedIterable(iterable) { it.weight } + fun fromWeightedIterable( + iterable: Iterable, + randomGenerator: org.apache.commons.math3.random.RandomGenerator, + weighter: Weighter, + ): RandomSelector = RandomSelectorImpl.fromWeightedIterable(iterable, weighter, randomGenerator) /** - * Creates a [RandomSelector] from a weighted **finite** [Flow]. + * Creates a [RandomSelector] from an [Iterable] of elements implementing [Weighted]. * - * @param flow The **finite** flow of elements. - * @param weighter A function that determines the weight of each element. - * @return A [RandomSelector] that selects elements based on their weights. + * This is a convenience method for types that already have a weight property. The [Weighted.weight] + * property is used automatically without needing to specify a weighter function. + * + * **Example:** + * ```kotlin + * enum class ItemRarity(override val weight: Double) : Weighted { + * COMMON(100.0), + * UNCOMMON(20.0), + * RARE(5.0), + * LEGENDARY(1.0) + * } + * + * val selector = RandomSelector.fromWeightedIterable(ItemRarity.entries) + * ``` + * + * @param E The type of elements, which must implement [Weighted]. + * @param iterable The source collection of weighted elements. Must not be empty. + * @param randomGenerator Optional custom random generator for reproducible results. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the iterable is empty or any weight is invalid. */ - suspend fun fromFlow(flow: Flow, weighter: Weighter): RandomSelector = - RandomSelectorImpl.fromFlow(flow, weighter) + @JvmOverloads + fun fromWeightedIterable( + iterable: Iterable, + randomGenerator: RandomGenerator? = null + ): RandomSelector = + RandomSelectorImpl.fromWeightedIterable(iterable, { it.weight }, randomGenerator.toApache()) /** - * Creates a [RandomSelector] from an infinite weighted [Flow]. + * Creates a [RandomSelector] from an [Iterable] of elements implementing [Weighted]. * - * ##### Note - * The returned [RandomSelector] does not support [pick] operation. Use [flow] to consume elements. + * This overload accepts an Apache Commons Math [org.apache.commons.math3.random.RandomGenerator] + * directly. * - * @param flow The flow of elements. - * @param weighter A function that determines the weight of each element. - * @return A [RandomSelector] that emits elements based on their weights. + * @param E The type of elements, which must implement [Weighted]. + * @param iterable The source collection of weighted elements. Must not be empty. + * @param randomGenerator Apache Commons Math random generator instance. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the iterable is empty or any weight is invalid. */ - fun fromInfinityFlow(flow: Flow, weighter: Weighter): RandomSelector = - RandomSelectorImpl.fromInfinityFlow(flow, weighter) - } -} + fun fromWeightedIterable( + iterable: Iterable, + randomGenerator: org.apache.commons.math3.random.RandomGenerator + ): RandomSelector = + RandomSelectorImpl.fromWeightedIterable(iterable, { it.weight }, randomGenerator) + + /** + * Creates a [RandomSelector] from a **finite** weighted [Flow]. + * + * This suspending function collects all elements from the flow into memory before creating + * the selector. The flow **must** be finite; infinite flows will cause this function to + * suspend indefinitely. + * + * **Use cases:** + * - Creating selectors from database queries + * - Building selectors from asynchronous API responses + * - Processing streamed data that needs random selection + * + * **Warning:** All elements are collected into memory. For large datasets, consider using + * [fromWeightedIterable] with a pre-materialized collection instead. + * + * **Example:** + * ```kotlin + * suspend fun loadItemsFromDatabase(): Flow = ... + * + * val selector = RandomSelector.fromFlow( + * flow = loadItemsFromDatabase(), + * weighter = { it.dropChance } + * ) + * ``` + * + * @param E The type of elements in the flow. + * @param flow The finite source flow of elements. Must emit at least one element. + * @param randomGenerator Optional custom random generator for reproducible results. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the flow is empty or any weight is invalid. + */ + @JvmOverloads + suspend fun fromFlow( + flow: Flow, + randomGenerator: RandomGenerator? = null, + weighter: Weighter + ): RandomSelector = + RandomSelectorImpl.fromFlow(flow, weighter, randomGenerator.toApache()) -enum class ExampleWeightedEnum(override val weight: Double) : Weighted { - COMMON(1.0), - UNCOMMON(0.5), - RARE(0.1), - VERY_RARE(0.01), - EXTREMELY_RARE(0.001); + /** + * Creates a [RandomSelector] from a **finite** weighted [Flow]. + * + * This overload accepts an Apache Commons Math [org.apache.commons.math3.random.RandomGenerator] + * directly. + * + * @param E The type of elements in the flow. + * @param flow The finite source flow of elements. Must emit at least one element. + * @param randomGenerator Apache Commons Math random generator instance. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] with weighted selection probability. + * @throws IllegalArgumentException if the flow is empty or any weight is invalid. + */ + suspend fun fromFlow( + flow: Flow, + randomGenerator: org.apache.commons.math3.random.RandomGenerator, + weighter: Weighter + ): RandomSelector = + RandomSelectorImpl.fromFlow(flow, weighter, randomGenerator) - companion object { - val selector = RandomSelector.fromWeightedIterable(entries) - } -} + /** + * Creates a [RandomSelector] from an infinite weighted [Flow]. + * + * **DEPRECATED:** This method cannot provide statistically correct weighted random selection + * on infinite streams. The underlying reservoir sampling algorithm causes systematic bias where + * earlier elements become exponentially less likely to be selected over time, regardless of + * their weights. + * + * ## Why This is Problematic + * + * Consider an infinite stream with weights [A=1.0, B=1.0, C=1.0, ...]: + * - After 1000 elements: Element A has ~0.1% selection probability (not 33.3%) + * - After 10000 elements: Element A has ~0.01% selection probability + * - This violates the fundamental property of weighted random selection + * + * ## Recommended Alternatives + * + * 1. **Finite Collection**: Collect elements into a list first + * ```kotlin + * val items = infiniteFlow.take(1000).toList() + * val selector = RandomSelector.fromWeightedIterable(items) { it.weight } + * ``` + * + * 2. **Direct Flow Consumption**: Process the flow without attempting random selection + * ```kotlin + * infiniteFlow.collect { element -> + * // Process each element as it arrives + * } + * ``` + * + * 3. **Windowed Selection**: Select from recent elements only + * ```kotlin + * infiniteFlow + * .chunked(100) + * .map { chunk -> RandomSelector.fromIterable(chunk).pick() } + * .collect { selected -> /* use selected element */ } + * ``` + * + * @param E The type of elements in the flow. + * @param flow The infinite source flow of elements. + * @param randomGenerator Optional custom random generator. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] that uses biased reservoir sampling (NOT statistically correct). + */ + @Deprecated( + message = "Cannot provide unbiased weighted random selection on infinite streams. " + + "Reservoir sampling causes earlier elements to become increasingly unlikely over time. " + + "Use fromIterable() with a finite collection or consume the flow directly.", + level = DeprecationLevel.WARNING + ) + @JvmOverloads + fun fromInfinityFlow( + flow: Flow, + randomGenerator: RandomGenerator? = null, + weighter: Weighter + ): RandomSelector = + RandomSelectorImpl.fromInfinityFlow(flow, weighter, randomGenerator.toApache()) -fun main() { - for (i in 1..10) { - println(ExampleWeightedEnum.selector.pick()) + /** + * Creates a [RandomSelector] from an infinite weighted [Flow]. + * + * **DEPRECATED:** See the other [fromInfinityFlow] overload for detailed explanation of why + * this method is problematic and recommended alternatives. + * + * @param E The type of elements in the flow. + * @param flow The infinite source flow of elements. + * @param randomGenerator Apache Commons Math random generator instance. + * @param weighter A function that extracts or calculates the weight for each element. + * @return A [RandomSelector] that uses biased reservoir sampling (NOT statistically correct). + */ + @Deprecated( + message = "Cannot provide unbiased weighted random selection on infinite streams. " + + "Reservoir sampling causes earlier elements to become increasingly unlikely over time. " + + "Use fromIterable() with a finite collection or consume the flow directly.", + level = DeprecationLevel.WARNING + ) + fun fromInfinityFlow( + flow: Flow, + randomGenerator: org.apache.commons.math3.random.RandomGenerator, + weighter: Weighter + ): RandomSelector = + RandomSelectorImpl.fromInfinityFlow(flow, weighter, randomGenerator) } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelectorImpl.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelectorImpl.kt index 7139adb31..f8f59d102 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelectorImpl.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/RandomSelectorImpl.kt @@ -1,116 +1,191 @@ package dev.slne.surf.surfapi.core.api.random -import dev.slne.surf.surfapi.core.api.util.collectionSizeOrDefault -import dev.slne.surf.surfapi.core.api.util.mutableDoubleListOf import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf -import dev.slne.surf.surfapi.core.api.util.toObjectList -import it.unimi.dsi.fastutil.doubles.DoubleList -import it.unimi.dsi.fastutil.objects.ObjectList +import it.unimi.dsi.fastutil.objects.Object2DoubleMap import kotlinx.coroutines.flow.Flow +import org.apache.commons.math3.distribution.EnumeratedDistribution +import org.apache.commons.math3.random.Well19937c import java.util.random.RandomGenerator internal class RandomSelectorImpl( - private val cumulativeWeights: DoubleList, - private val elements: ObjectList, + randomGenerator: org.apache.commons.math3.random.RandomGenerator?, + elements: List> ) : RandomSelector { + private val distribution: EnumeratedDistribution + private val random = randomGenerator ?: Well19937c() + init { require(elements.isNotEmpty()) { "RandomSelector must have at least one element." } + + val pmf = elements.map { org.apache.commons.math3.util.Pair.create(it.key, it.doubleValue) } + distribution = EnumeratedDistribution(random, pmf) } + @Suppress("OVERRIDE_DEPRECATION") override fun pick(randomGenerator: RandomGenerator): E { - val totalWeight = cumulativeWeights.getDouble(cumulativeWeights.size - 1) - val r = randomGenerator.nextDouble(totalWeight) - - // lower_bound search for first cumulative >= r - var lo = 0 - var hi = cumulativeWeights.size - 1 - while (lo < hi) { - val mid = (lo + hi) ushr 1 - if (r <= cumulativeWeights.getDouble(mid)) { - hi = mid - } else { - lo = mid + 1 - } + // Create temporary distribution with provided generator for backward compatibility + val tempDistribution = EnumeratedDistribution( + Jdk2ApacheRandomGenerator(randomGenerator), + distribution.pmf + ) + return tempDistribution.sample() + } + + override fun pick(): E { + return distribution.sample() + } + + override fun pickOrNull(successRate: Double): E? { + require(successRate in 0.0..1.0) { + "Success rate must be between 0.0 and 1.0, got $successRate." + } + + return if (random.nextDouble() < successRate) { + pick() + } else { + null } - return elements[lo] } + @Suppress("OVERRIDE_DEPRECATION") override fun flow(randomGenerator: RandomGenerator): Flow = kotlinx.coroutines.flow.flow { + // Create temporary distribution with provided generator for backward compatibility + val tempDistribution = EnumeratedDistribution( + Jdk2ApacheRandomGenerator(randomGenerator), + distribution.pmf + ) while (true) { - emit(pick(randomGenerator)) + emit(tempDistribution.sample()) + } + } + + override fun flow(): Flow = kotlinx.coroutines.flow.flow { + while (true) { + emit(pick()) + } + } + + override fun flowOrNull(successRate: Double): Flow = kotlinx.coroutines.flow.flow { + require(successRate in 0.0..1.0) { + "Success rate must be between 0.0 and 1.0, got $successRate." + } + + while (true) { + emit(pickOrNull(successRate)) + } + } + + override fun sequence(): Sequence = generateSequence { + pick() + } + + override fun sequenceOrNull(successRate: Double): Sequence { + require(successRate in 0.0..1.0) { + "Success rate must be between 0.0 and 1.0, got $successRate." + } + + return generateSequence { + pickOrNull(successRate) } } companion object { - fun fromIterable(iterable: Iterable): RandomSelectorImpl { - val elements = iterable.toObjectList() - val cumulativeWeights = - DoubleList.of(*DoubleArray(elements.size) { (it + 1).toDouble() }) - return RandomSelectorImpl(cumulativeWeights, elements) + fun fromIterable( + iterable: Iterable, + randomGenerator: org.apache.commons.math3.random.RandomGenerator? + ): RandomSelectorImpl { + val elements = iterable.map { Object2DoubleMap.entry(it, 1.0) } + return RandomSelectorImpl(randomGenerator, elements) } fun fromWeightedIterable( iterable: Iterable, weighter: Weighter, + randomGenerator: org.apache.commons.math3.random.RandomGenerator? ): RandomSelectorImpl { - val expectedSize = iterable.collectionSizeOrDefault(10) - val elements = mutableObjectListOf(expectedSize) - val cumulativeWeights = mutableDoubleListOf(expectedSize) - var cumulativeWeight = 0.0 - - for (element in iterable) { + val elements = iterable.map { element -> val weight = weighter(element) - require(weight > 0) { "Weight must be greater than 0." } - cumulativeWeight += weight - cumulativeWeights.add(cumulativeWeight) - elements.add(element) + require(weight > 0) { "Weight must be greater than 0, got $weight for element $element." } + require(weight.isFinite()) { "Weight must be finite, got $weight for element $element." } + Object2DoubleMap.entry(element, weight) } - return RandomSelectorImpl(cumulativeWeights, elements) + return RandomSelectorImpl(randomGenerator, elements) } - suspend fun fromFlow(flow: Flow, weighter: Weighter): RandomSelectorImpl { - val elements = mutableObjectListOf() - val cumulativeWeights = mutableDoubleListOf() - var cumulativeWeight = 0.0 + suspend fun fromFlow( + flow: Flow, + weighter: Weighter, + randomGenerator: org.apache.commons.math3.random.RandomGenerator? + ): RandomSelectorImpl { + val elements = mutableObjectListOf>() flow.collect { element -> val weight = weighter(element) - require(weight > 0) { "Weight must be greater than 0." } - cumulativeWeight += weight - cumulativeWeights.add(cumulativeWeight) - elements.add(element) + require(weight > 0) { "Weight must be greater than 0, got $weight for element $element." } + require(weight.isFinite()) { "Weight must be finite, got $weight for element $element." } + elements.add(Object2DoubleMap.entry(element, weight)) } - return RandomSelectorImpl(cumulativeWeights, elements) + return RandomSelectorImpl(randomGenerator, elements) } - fun fromInfinityFlow(flow: Flow, weighter: Weighter): RandomSelector { - return FlowRandomSelectorImpl(flow, weighter) + fun fromInfinityFlow( + flow: Flow, + weighter: Weighter, + randomGenerator: org.apache.commons.math3.random.RandomGenerator? + ): RandomSelector { + return FlowRandomSelectorImpl(flow, randomGenerator ?: Well19937c(), weighter) } } } internal class FlowRandomSelectorImpl( private val flow: Flow, + private val randomGenerator: org.apache.commons.math3.random.RandomGenerator, private val weighter: Weighter, ) : RandomSelector { + @Suppress("OVERRIDE_DEPRECATION") override fun pick(randomGenerator: RandomGenerator): E { - throw UnsupportedOperationException("FlowRandomSelector does not support pick operation.") + throw UnsupportedOperationException("FlowRandomSelector does not support pick operation. Use flow() instead.") } - override fun flow(randomGenerator: RandomGenerator): Flow = kotlinx.coroutines.flow.flow { + override fun pick(): E { + throw UnsupportedOperationException("FlowRandomSelector does not support pick operation. Use flow() instead.") + } + + override fun pickOrNull(successRate: Double): E? { + throw UnsupportedOperationException("FlowRandomSelector does not support pickOrNull operation.") + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun flow(randomGenerator: RandomGenerator): Flow = createFlow(randomGenerator.toApache()) + override fun flow(): Flow = createFlow(randomGenerator) + override fun flowOrNull(successRate: Double): Flow = throw UnsupportedOperationException("FlowRandomSelector does not support flowOrNull operation.") + + override fun sequence(): Sequence { + throw UnsupportedOperationException("FlowRandomSelector does not support sequence operation.") + } + + override fun sequenceOrNull(successRate: Double): Sequence { + throw UnsupportedOperationException("FlowRandomSelector does not support sequenceOrNull operation.") + } + + private fun createFlow( + generator: org.apache.commons.math3.random.RandomGenerator + ): Flow = kotlinx.coroutines.flow.flow { var selectedElement: E? = null var totalWeight = 0.0 flow.collect { element -> val weight = weighter(element) - require(weight > 0) { "Weight must be greater than 0." } + require(weight > 0.0) { "Weight must be greater than 0, got $weight." } + require(weight.isFinite()) { "Weight must be finite, got $weight." } totalWeight += weight - val probability = weight / totalWeight + val threshold = weight / totalWeight - if (randomGenerator.nextDouble() < probability) { + if (generator.nextDouble() < threshold) { selectedElement = element } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Weighted.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Weighted.kt index 5fb89e9fb..fe3fe6c0e 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Weighted.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/random/Weighted.kt @@ -1,55 +1,156 @@ package dev.slne.surf.surfapi.core.api.random /** - * A type alias for a function that computes the weight of an object for weighted random selection. + * A type alias for functions that compute the weight of an element for weighted random selection. * - * The [Weighter] is a function that takes an element of type [E] and returns a [Double] - * representing its weight. It is used in cases where elements do not implement [Weighted] - * directly, allowing custom weight computation. + * The [Weighter] function takes an element and returns a positive [Double] representing its + * relative weight in the probability distribution. Higher weights increase selection probability. * - * ### Example - * ``` - * data class Item(val name: String, val importance: Double) + * ## Weight Requirements + * - Must return a value greater than 0.0 + * - Must return a finite value (not NaN or Infinity) + * - Weights are relative; they don't need to sum to 1.0 + * + * ## Example Usage * - * val items = listOf( - * Item("Basic", 1.0), - * Item("Advanced", 2.0), - * Item("Premium", 5.0) + * ```kotlin + * data class QuestReward(val item: String, val rarity: Int) + * + * val rewards = listOf( + * QuestReward("Gold Coins", rarity = 10), + * QuestReward("Magic Potion", rarity = 5), + * QuestReward("Legendary Sword", rarity = 1) * ) * - * val selector = RandomSelector.fromWeightedIterable(items) { it.importance } + * // Weighter that converts rarity to probability weight + * val weighter: Weighter = { reward -> + * 1.0 / reward.rarity // Rarer items have lower weight + * } + * + * val selector = RandomSelector.fromWeightedIterable(rewards, weighter = weighter) + * val reward = selector.pick() + * ``` + * + * ## Common Weighter Patterns * - * println(selector.pick()) // Outputs an item based on its importance + * **Direct Property Access:** + * ```kotlin + * val selector = RandomSelector.fromWeightedIterable(items) { it.weight } * ``` * - * @param E The type of the object for which the weight is being computed. + * **Computed Weight:** + * ```kotlin + * val selector = RandomSelector.fromWeightedIterable(players) { player -> + * player.level * player.experienceMultiplier + * } + * ``` + * + * **Inverse Probability:** + * ```kotlin + * val selector = RandomSelector.fromWeightedIterable(tasks) { task -> + * 1.0 / task.priority // Lower priority number = higher selection chance + * } + * ``` + * + * @param E The type of the element being weighted. + * @return A positive, finite Double representing the element's weight. + * @see Weighted + * @see RandomSelector.fromWeightedIterable */ typealias Weighter = (E) -> Double /** - * Represents an object that has an associated weight used for weighted random selection. + * Interface for objects that have an intrinsic weight for weighted random selection. + * + * Classes implementing [Weighted] can be used directly with [RandomSelector.fromWeightedIterable] + * without needing to provide a separate [Weighter] function. This is particularly useful for + * enums, sealed classes, and domain objects where weight is a natural property. * - * Classes implementing this interface can be used directly with methods like - * [RandomSelector.fromWeightedIterable], where the weight of the object determines its likelihood - * of being selected. + * ## Implementation Guidelines * - * ### Example + * The [weight] property: + * - Must always return a positive value (> 0.0) + * - Must always return a finite value (not NaN or Infinity) + * - Should be immutable for consistent selection probabilities + * - Represents relative probability (doesn't need to sum to 1.0) + * + * ## Example: Enum with Weights + * + * ```kotlin + * enum class LootRarity(override val weight: Double) : Weighted { + * COMMON(100.0), // 79.4% drop chance + * UNCOMMON(20.0), // 15.9% drop chance + * RARE(5.0), // 4.0% drop chance + * LEGENDARY(1.0); // 0.8% drop chance + * + * companion object { + * // Reusable selector for the enum + * val selector = RandomSelector.fromWeightedIterable(entries) + * } + * } + * + * fun dropLoot(): LootRarity = LootRarity.selector.pick() * ``` - * enum class Rarity(override val weight: Double) : Weighted { - * COMMON(1.0), - * UNCOMMON(0.5), - * RARE(0.1) + * + * ## Example: Data Class with Dynamic Weight + * + * ```kotlin + * data class Enemy( + * val name: String, + * val level: Int, + * val spawnMultiplier: Double + * ) : Weighted { + * override val weight: Double + * get() = level * spawnMultiplier * } * - * val selector = RandomSelector.fromWeightedIterable(Rarity.entries) + * val enemies = listOf( + * Enemy("Goblin", level = 1, spawnMultiplier = 2.0), + * Enemy("Orc", level = 5, spawnMultiplier = 1.0), + * Enemy("Dragon", level = 20, spawnMultiplier = 0.1) + * ) * - * println(selector.pick()) // Outputs a random Rarity based on the weights + * val selector = RandomSelector.fromWeightedIterable(enemies) * ``` + * + * ## Example: Sealed Class Hierarchy + * + * ```kotlin + * sealed class RandomEvent(override val weight: Double) : Weighted { + * data object SunnyDay : RandomEvent(10.0) + * data object RainyDay : RandomEvent(5.0) + * data object Thunderstorm : RandomEvent(2.0) + * data object Earthquake : RandomEvent(0.1) + * + * companion object { + * val allEvents = listOf(SunnyDay, RainyDay, Thunderstorm, Earthquake) + * val selector = RandomSelector.fromWeightedIterable(allEvents) + * } + * } + * ``` + * + * @see Weighter + * @see RandomSelector.fromWeightedIterable */ interface Weighted { /** - * The weight of the object. Higher values increase the likelihood of being selected. + * The weight of this object for weighted random selection. + * + * Higher values increase the probability of this element being selected. + * The weight is relative to other elements in the same selection pool. + * + * **Requirements:** + * - Must be > 0.0 + * - Must be finite (not NaN or Infinity) + * - Should remain constant for predictable selection behavior + * + * **Probability Calculation:** + * ``` + * P(element) = element.weight / sum(all weights) + * ``` + * + * @return A positive, finite Double representing the selection weight. */ val weight: Double } \ No newline at end of file diff --git a/surf-api-hytale/surf-api-hytale-server/build.gradle.kts b/surf-api-hytale/surf-api-hytale-server/build.gradle.kts index c1f2f7375..c7280869a 100644 --- a/surf-api-hytale/surf-api-hytale-server/build.gradle.kts +++ b/surf-api-hytale/surf-api-hytale-server/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(libs.configurate.jackson) api(libs.flogger) api(libs.commons.math4.core) + api(libs.commons.math3) implementation(libs.packetevents.netty.common) runtimeOnly(libs.flogger.slf4j.backend) } diff --git a/surf-api-standalone/build.gradle.kts b/surf-api-standalone/build.gradle.kts index 9826e57f2..57f9bcee4 100644 --- a/surf-api-standalone/build.gradle.kts +++ b/surf-api-standalone/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(libs.configurate.jackson) api(libs.flogger) api(libs.commons.math4.core) + api(libs.commons.math3) implementation(libs.packetevents.netty.common) runtimeOnly(libs.flogger.slf4j.backend) } diff --git a/surf-api-velocity/surf-api-velocity-server/build.gradle.kts b/surf-api-velocity/surf-api-velocity-server/build.gradle.kts index 407523758..2ce68a617 100644 --- a/surf-api-velocity/surf-api-velocity-server/build.gradle.kts +++ b/surf-api-velocity/surf-api-velocity-server/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(libs.fastutil) api(libs.flogger) api(libs.commons.math4.core) + api(libs.commons.math3) api(libs.aide.reflection) runtimeOnly(libs.flogger.slf4j.backend) kapt(libs.velocity.api) From a02587fd7d9ab33d594e19b8d948cbcb2416a366 Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 22:29:34 +0100 Subject: [PATCH 22/32] refactor: introduce ProxyCreationException and ProxyInvocationException for improved error handling --- .../core/api/reflection/SurfReflection.kt | 84 +++-- .../api/reflection/reflection-annotations.kt | 65 +++- .../reflection/SurfInvocationHandlerJava.java | 296 ++++++++++++++---- .../reflection/ProxyCreationException.java | 16 + .../reflection/ProxyInvocationException.java | 16 + 5 files changed, 390 insertions(+), 87 deletions(-) create mode 100644 surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyCreationException.java create mode 100644 surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyInvocationException.java diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/SurfReflection.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/SurfReflection.kt index 9c2da7cb2..45bb750af 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/SurfReflection.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/SurfReflection.kt @@ -3,54 +3,86 @@ package dev.slne.surf.surfapi.core.api.reflection import dev.slne.surf.surfapi.core.api.util.requiredService /** - * Represents a high-level interface for creating dynamic proxies for classes. - * The SurfReflection interface enables creation of proxy instances for given - * classes at runtime, typically for facilitating reflective operations or interception. + * Provides a high-level interface for creating dynamic proxies that enable reflective access to classes. + * + * SurfReflection allows you to define an interface annotated with [SurfProxy] and its methods with + * reflection annotations ([Field], [Constructor], [Static], etc.) to access private or internal + * members of a target class at runtime without direct reflection calls. + * + * Example usage: + * ```kotlin + * @SurfProxy(qualifiedName = "com.example.internal.HiddenClass") + * interface HiddenClassProxy { + * // Access private field + * @Field(name = "secretValue", type = Field.Type.GETTER) + * fun getSecret(instance: Any): String + * + * // Call private static method + * @Static + * fun staticMethod(param: String): Int + * + * // Invoke constructor + * @Constructor + * fun create(arg: String): Any + * } + * + * // Create proxy and use it + * val proxy = surfReflection.createProxy() + * val instance = proxy.create("test") + * val secret = proxy.getSecret(instance) + * val result = proxy.staticMethod("value") + * ``` */ interface SurfReflection { /** * Creates a dynamic proxy instance for the specified class using the provided class loader. - * This function is typically used for creating runtime proxies to enable reflective operations - * or to intercept method calls on the provided class type. * - * @param T The type of the class for which the proxy is created. - * @param clazz The `Class` object representing the class for which the proxy needs to be created. - * @param classLoader The `ClassLoader` to be used for defining the proxy class. - * @return A dynamic proxy instance of the specified class type `T`. - * @throws IllegalArgumentException If the provided class or class loader is invalid, or if a proxy cannot be created. + * The class must be an interface annotated with [SurfProxy]. The proxy will intercept method + * calls and translate them to reflective operations on the target class specified in [SurfProxy]. + * + * @param T The proxy interface type, must be annotated with [SurfProxy] + * @param clazz The interface class object for which the proxy is created + * @param classLoader The ClassLoader to use for loading the target class and defining the proxy + * @return A dynamic proxy instance implementing the specified interface + * @throws IllegalArgumentException If the class is not an interface, lacks [SurfProxy] annotation, + * or if the target class specified in [SurfProxy] cannot be found */ - fun createProxy(clazz: Class, classLoader: ClassLoader): T + fun createProxy(clazz: Class, classLoader: ClassLoader): T /** - * Creates a dynamic proxy instance for the specified class using its class loader. - * This method simplifies the creation of runtime proxies by leveraging the provided class. + * Creates a dynamic proxy instance using the class's own ClassLoader. * - * @param T The type of the class for which the proxy is created. - * @param clazz The `Class` object representing the class for which the proxy needs to be created. - * @return A dynamic proxy instance of the specified class type `T`. - * @throws IllegalArgumentException If the provided class or class loader is invalid, or if a proxy cannot be created. + * This is a convenience method that delegates to [createProxy] with the class's own ClassLoader. + * + * @param T The proxy interface type, must be annotated with [SurfProxy] + * @param clazz The interface class object for which the proxy is created + * @return A dynamic proxy instance implementing the specified interface + * @throws IllegalArgumentException If the class is not an interface, lacks [SurfProxy] annotation, + * or if the target class cannot be found or accessed */ - fun createProxy(clazz: Class): T = createProxy(clazz, clazz.getClassLoader()) + fun createProxy(clazz: Class): T = createProxy(clazz, clazz.classLoader) - companion object { + companion object : SurfReflection by surfReflection { /** * The singleton instance of the SurfReflection interface. */ @JvmStatic - val instance = requiredService() + val instance = surfReflection } } /** * The singleton instance of the SurfReflection interface. */ -val surfReflection get() = SurfReflection.instance +val surfReflection = requiredService() /** - * Creates a dynamic proxy instance for the specified class type using its class loader. - * This method provides a concise inline approach by leveraging the reified type parameter. + * Creates a dynamic proxy for the reified type parameter. + * + * This inline extension function provides type-safe proxy creation without explicitly passing the class. * - * @return A dynamic proxy instance of the specified class type `T`. - * @throws IllegalArgumentException If the proxy cannot be created for the provided class type `T`. + * @param T The proxy interface type, must be annotated with [SurfProxy] + * @return A dynamic proxy instance of type T + * @throws IllegalArgumentException If T is not properly configured for proxy creation */ -inline fun SurfReflection.createProxy(): T = createProxy(T::class.java) \ No newline at end of file +inline fun SurfReflection.createProxy(): T = createProxy(T::class.java) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/reflection-annotations.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/reflection-annotations.kt index c528ae493..fd543095c 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/reflection-annotations.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/reflection/reflection-annotations.kt @@ -4,7 +4,20 @@ import java.lang.annotation.Inherited import kotlin.reflect.KClass /** - * Marks a method in a [SurfProxy] as a constructor. + * Marks a proxy method as a constructor invocation for the target class. + * + * When applied to a method in a [SurfProxy] interface, this annotation indicates that calling + * the method should invoke a constructor of the target class. The method's parameters must match + * the constructor's parameter types. + * + * Example: + * ```kotlin + * @SurfProxy(qualifiedName = "com.example.TargetClass") + * interface TargetProxy { + * @Constructor + * fun create(name: String, value: Int): Any + * } + * ``` */ @Retention(AnnotationRetention.RUNTIME) @Target( @@ -15,7 +28,15 @@ import kotlin.reflect.KClass annotation class Constructor /** - * Marks a method in a [SurfProxy] as a Field. + * Marks a proxy method as a field getter or setter for the target class. + * + * Use this annotation to access fields (including private ones) on the target class. + * For instance fields, the first parameter must be the instance object. + * For setters, the last parameter is the value to set. + * + * @param name The field name in the target class. If empty, uses the method name + * @param type Whether this method should get ([Type.GETTER]) or set ([Type.SETTER]) the field value + * @param overrideFinal When true, allows setting final fields using reflection. Use with caution */ @Retention(AnnotationRetention.RUNTIME) @Target( @@ -35,8 +56,12 @@ annotation class Field( } /** - * Overrides the reflection name of a method or field. This annotation overrides anything previously - * set in other annotations. + * Overrides the name of the target method or field. + * + * This annotation takes precedence over names specified in other annotations like [Field] or [Static]. + * Use it when the proxy method name differs from the target member name. + * + * @param value The actual name of the method or field in the target class */ @Retention(AnnotationRetention.RUNTIME) @Target( @@ -47,7 +72,12 @@ annotation class Field( annotation class Name(val value: String) /** - * Marks a method in a [SurfProxy] as a static method. + * Marks a proxy method as invoking a static method or accessing a static field in the target class. + * + * Static methods do not require an instance parameter. When used with [Field], this accesses + * a static field instead of an instance field. + * + * @param name The static member name in the target class. If empty, uses the method name */ @Retention(AnnotationRetention.RUNTIME) @Target( @@ -57,6 +87,31 @@ annotation class Name(val value: String) ) annotation class Static(val name: String = "") +/** + * Designates an interface as a reflection proxy for a target class. + * + * Apply this annotation to an interface to enable proxy creation via [SurfReflection]. + * Specify the target class either by [value] or [qualifiedName], but not both. + * + * Methods in the annotated interface can use [Field], [Constructor], [Static], and [Name] + * annotations to define reflective operations on the target class. + * + * @param value The target class to proxy. Use `Unit::class` if providing [qualifiedName] instead + * @param qualifiedName The fully qualified name of the target class as a string. Use this when + * the target class is not accessible at compile time or is in a different module + * + * Example: + * ```kotlin + * @SurfProxy(qualifiedName = "com.example.HiddenClass") + * interface HiddenClassProxy { + * @Constructor + * fun newInstance(): Any + * + * @Field(name = "value", type = Field.Type.GETTER) + * fun getValue(instance: Any): String + * } + * ``` + */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Inherited diff --git a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java index 8cdb75189..5e2e6e833 100644 --- a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java +++ b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/SurfInvocationHandlerJava.java @@ -4,21 +4,18 @@ import dev.slne.surf.surfapi.core.api.reflection.Name; import dev.slne.surf.surfapi.core.api.reflection.Static; import dev.slne.surf.surfapi.core.api.util.SurfUtil; +import dev.slne.surf.surfapi.core.server.impl.reflection.reflection.ProxyCreationException; +import dev.slne.surf.surfapi.core.server.impl.reflection.reflection.ProxyInvocationException; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.reflect.FieldUtils; -import org.apache.commons.lang3.reflect.MethodUtils; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; +import java.lang.invoke.VarHandle; +import java.lang.reflect.*; import java.util.Arrays; import java.util.Map; import java.util.Objects; @@ -34,7 +31,7 @@ public final class SurfInvocationHandlerJava implements InvocationHandler { private final Class proxyClass; private final Class proxiedClass; - private final Object2ObjectMap cache; + private final Object2ObjectMap cache; private final Map defaultCache = new ConcurrentHashMap<>(); public SurfInvocationHandlerJava(Class proxyClass, Class proxiedClass) { @@ -52,13 +49,13 @@ public SurfInvocationHandlerJava(Class proxyClass, Class proxiedClass) { } else if (isHashCodeMethod(method)) { return System.identityHashCode(proxy); } else if (isToStringMethod(method)) { - return ToStringBuilder.reflectionToString(proxy); + return "SurfProxy[" + proxiedClass.getName() + "@" + Integer.toHexString(System.identityHashCode(proxy)) + "]"; } else if (method.isDefault()) { return invokeDefault(proxy, method, args); } else { final Invokable invokable = cache.get(method); if (invokable == null) { - throw new IllegalStateException("No handler cached for " + method.getName()); + throw new ProxyInvocationException("No handler found for method '" + method.getName() + "' in proxy for class " + proxiedClass.getName()); } else { return invokable.invoke(args != null ? args : EMPTY_ARGS); } @@ -99,32 +96,22 @@ private Invokable createInvokable(final Method method) { final var constructorAnnotation = method.getDeclaredAnnotation( dev.slne.surf.surfapi.core.api.reflection.Constructor.class); final var nameAnnotation = method.getDeclaredAnnotation(Name.class); - final var privateLookup = sneaky(() -> MethodHandles.privateLookupIn(proxiedClass, LOOKUP)); - if (fieldAnnotation != null) { - final String fieldName = getMethodName(method, nameAnnotation, fieldAnnotation, - staticAnnotation, constructorAnnotation); - final Field field = sneaky(() -> findField(proxiedClass, fieldName)); - final boolean isGetter = fieldAnnotation.type() == Type.GETTER; - final MethodHandle handleGetter = - isGetter ? sneaky(() -> privateLookup.unreflectGetter(field)) : null; - final MethodHandle handleSetter = !isGetter && !fieldAnnotation.overrideFinal() - ? sneaky(() -> privateLookup.unreflectSetter(field)) : null; - - if (isGetter) { - checkParamCount(method, staticAnnotation != null ? 0 : 1); - final boolean hasParams = staticAnnotation == null; // instance parameter - return new HandleInvokable(normalizeMethodHandleType(handleGetter), hasParams); - } else { - if (fieldAnnotation.overrideFinal()) { - checkParamCount(method, staticAnnotation != null ? 1 : 2); - return new ReflectionSetterInvokable(field, staticAnnotation != null); - } + validateAnnotationCombination(method, fieldAnnotation, staticAnnotation, constructorAnnotation); - checkParamCount(method, staticAnnotation != null ? 1 : 2); - final boolean hasParams = true; // setter always has params - return new HandleInvokable(normalizeMethodHandleType(handleSetter), hasParams); - } + final MethodHandles.Lookup privateLookup; + try { + privateLookup = MethodHandles.privateLookupIn(proxiedClass, LOOKUP); + } catch (IllegalAccessException e) { + throw new ProxyCreationException( + "Cannot access class " + proxiedClass.getName() + + ". Module may not be open for reflection. " + + "Consider adding 'opens " + proxiedClass.getPackageName() + + ";' to your module-info.java", e); + } + + if (fieldAnnotation != null) { + return createFieldInvokable(method, fieldAnnotation, staticAnnotation, nameAnnotation, privateLookup); } if (constructorAnnotation != null) { @@ -135,8 +122,9 @@ private Invokable createInvokable(final Method method) { } if (staticAnnotation == null && method.getParameterCount() == 0) { - throw new IllegalStateException( - "Instance method '" + method.getName() + "' must have a receiver parameter"); + throw new ProxyCreationException( + "Instance method '" + method.getName() + "' must have at least one parameter " + + "(the instance object). Add @Static if this should be a static method call."); } final Method target = sneaky( @@ -146,6 +134,69 @@ private Invokable createInvokable(final Method method) { return new HandleInvokable(normalizeMethodHandleType(handle), hasParams); } + private Invokable createFieldInvokable( + final Method method, + final dev.slne.surf.surfapi.core.api.reflection.Field fieldAnnotation, + final @Nullable Static staticAnnotation, + final @Nullable Name nameAnnotation, + final MethodHandles.Lookup privateLookup + ) { + final String fieldName = getMethodName(method, nameAnnotation, fieldAnnotation, + staticAnnotation, null); + final Field field = sneaky(() -> findField(proxiedClass, fieldName)); + final boolean isGetter = fieldAnnotation.type() == Type.GETTER; + + if (isGetter) { + final java.lang.invoke.MethodHandle handleGetter = sneaky(() -> privateLookup.unreflectGetter(field)); + final boolean hasParams = staticAnnotation == null; + return new HandleInvokable(normalizeMethodHandleType(handleGetter), hasParams); + } else { + if (fieldAnnotation.overrideFinal()) { + return new VarHandleSetterInvokable(field, staticAnnotation != null, privateLookup); + } + + final java.lang.invoke.MethodHandle handleSetter = sneaky(() -> privateLookup.unreflectSetter(field)); + return new HandleInvokable(normalizeMethodHandleType(handleSetter), true); + } + } + + private static void validateAnnotationCombination( + final Method method, + final dev.slne.surf.surfapi.core.api.reflection.@Nullable Field fieldAnnotation, + final @Nullable Static staticAnnotation, + final dev.slne.surf.surfapi.core.api.reflection.@Nullable Constructor constructorAnnotation + ) { + int annotationCount = 0; + if (fieldAnnotation != null) annotationCount++; + if (constructorAnnotation != null) annotationCount++; + + if (annotationCount > 1) { + throw new ProxyCreationException( + "Method '" + method.getName() + "' has multiple incompatible annotations. " + + "Use only one of: @Field, @Constructor"); + } + + if (constructorAnnotation != null && staticAnnotation != null) { + throw new ProxyCreationException( + "Method '" + method.getName() + "' cannot have both @Constructor and @Static"); + } + + if (fieldAnnotation != null) { + final boolean isStatic = staticAnnotation != null; + final boolean isGetter = fieldAnnotation.type() == Type.GETTER; + final int paramCount = method.getParameterCount(); + final int expectedParams = isStatic ? (isGetter ? 0 : 1) : (isGetter ? 1 : 2); + + if (paramCount != expectedParams) { + throw new ProxyCreationException( + "Method '" + method.getName() + "' has invalid parameter count. " + + "Expected " + expectedParams + " parameters for " + + (isStatic ? "static " : "instance ") + + (isGetter ? "getter" : "setter") + ", found " + paramCount); + } + } + } + private static MethodHandle normalizeMethodHandleType(final MethodHandle handle) { if (handle.type().parameterCount() == 0) { return handle.asType(MethodType.methodType(Object.class)); @@ -157,10 +208,23 @@ private static MethodHandle normalizeMethodHandleType(final MethodHandle handle) private static Field findField(final Class clazz, final String name) throws NoSuchFieldException { - final Field field = FieldUtils.getField(clazz, name, true); + Field field = null; + Class current = clazz; + + while (current != null && field == null) { + try { + field = current.getDeclaredField(name); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + if (field == null) { - throw new NoSuchFieldException(name); + throw new NoSuchFieldException("Field '" + name + "' not found in class " + + clazz.getName() + " or its superclasses"); } + + field.setAccessible(true); return field; } @@ -181,32 +245,50 @@ private static Method findMethod( final Class[] paramTypes = Arrays.copyOfRange(original.getParameterTypes(), paramOffset, original.getParameterCount()); final String methodName = getMethodName(original, nameAnnotation, null, staticAnnotation, null); - Method method = MethodUtils.getMatchingMethod(clazz, methodName, paramTypes); + Method method = findMethodExact(clazz, methodName, paramTypes); if (method == null && isAllObjectParams(paramTypes)) { - method = findMethodByNameAndParamCount(clazz, methodName, paramTypes.length); + method = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> m.getName().equals(methodName)) + .filter(m -> m.getParameterCount() == paramTypes.length) + .findFirst() + .orElse(null); } if (method != null) { method.setAccessible(true); + return method; } - if (method == null) { - throw new NoSuchMethodException( - "Method '" + methodName + "' with params " + Arrays.toString(paramTypes)); - } - - return method; + final String availableMethods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> m.getName().equals(methodName)) + .map(m -> methodName + "(" + + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")") + .collect(Collectors.joining(", ")); + + throw new NoSuchMethodException( + "Method '" + methodName + "' with parameters [" + + Arrays.stream(paramTypes).map(Class::getSimpleName).collect(Collectors.joining(", ")) + + "] not found in class " + clazz.getName() + + (availableMethods.isEmpty() ? "" : ". Available methods with same name: " + availableMethods)); } - private static @Nullable Method findMethodByNameAndParamCount( - final Class clazz, - final String methodName, - final int paramCount - ) { - for (Method method : clazz.getDeclaredMethods()) { - if (method.getName().equals(methodName) && method.getParameterCount() == paramCount) { - return method; + private static @Nullable Method findMethodExact(final Class clazz, final String name, + final Class[] paramTypes) { + Class current = clazz; + while (current != null) { + try { + return current.getDeclaredMethod(name, paramTypes); + } catch (NoSuchMethodException e) { + for (Method method : current.getDeclaredMethods()) { + if (method.getName().equals(name) && + Arrays.equals(method.getParameterTypes(), paramTypes)) { + return method; + } + } + current = current.getSuperclass(); } } return null; @@ -228,7 +310,6 @@ private static String getMethodName( final @Nullable Static staticAnnotation, final dev.slne.surf.surfapi.core.api.reflection.@Nullable Constructor constructorAnnotation ) { - // when block converted to if-else structure if (nameAnnotation != null && !nameAnnotation.value().isBlank()) { return nameAnnotation.value(); } else if (fieldAnnotation != null && !fieldAnnotation.name().isBlank()) { @@ -267,7 +348,7 @@ private static boolean isEqualsHashOrToStringMethod(final Method method) { return isEqualsMethod(method) || isHashCodeMethod(method) || isToStringMethod(method); } - private sealed interface Invokable permits HandleInvokable, ReflectionSetterInvokable { + private sealed interface Invokable permits HandleInvokable, ReflectionSetterInvokable, VarHandleSetterInvokable { @Nullable Object invoke(final Object[] args) throws Throwable; @@ -299,6 +380,109 @@ private record ReflectionSetterInvokable(Field field, boolean isStatic) implemen } } + private record VarHandleSetterInvokable(Field field, boolean isStatic, + MethodHandles.Lookup lookup) implements Invokable { + @Override + public @Nullable Object invoke(Object[] args) throws ProxyInvocationException { + if (Modifier.isFinal(field.getModifiers())) { + return setFieldViaUnsafe(args); + } + + try { + VarHandle varHandle = isStatic + ? lookup.findStaticVarHandle(field.getDeclaringClass(), field.getName(), field.getType()) + : lookup.findVarHandle(field.getDeclaringClass(), field.getName(), field.getType()); + + if (isStatic) { + varHandle.set(args[0]); + } else { + varHandle.set(args[0], args[1]); + } + return null; + } catch (Exception e) { + try { + return setFieldViaUnsafe(args); + } catch (Exception e2) { + // If we reach this point, it means we failed to set the final field using both VarHandle and Unsafe. + String moduleName = field.getDeclaringClass().getModule().getName(); + String errorMsg = buildFinalFieldErrorMessage(moduleName); + throw new ProxyInvocationException(errorMsg, e2); + } + } + } + + @SuppressWarnings("removal") + private @Nullable Object setFieldViaUnsafe(Object[] args) throws ProxyInvocationException { + sun.misc.Unsafe unsafe = SurfUtil.getUnsafe(); + + long offset = isStatic + ? unsafe.staticFieldOffset(field) + : unsafe.objectFieldOffset(field); + + Object value = isStatic ? args[0] : args[1]; + Object target = isStatic ? unsafe.staticFieldBase(field) : args[0]; + + Class type = field.getType(); + if (type == int.class) { + unsafe.putInt(target, offset, (Integer) value); + } else if (type == long.class) { + unsafe.putLong(target, offset, (Long) value); + } else if (type == boolean.class) { + unsafe.putBoolean(target, offset, (Boolean) value); + } else if (type == byte.class) { + unsafe.putByte(target, offset, (Byte) value); + } else if (type == short.class) { + unsafe.putShort(target, offset, (Short) value); + } else if (type == char.class) { + unsafe.putChar(target, offset, (Character) value); + } else if (type == float.class) { + unsafe.putFloat(target, offset, (Float) value); + } else if (type == double.class) { + unsafe.putDouble(target, offset, (Double) value); + } else { + unsafe.putObject(target, offset, value); + } + + return null; + } + + private String buildFinalFieldErrorMessage(@Nullable String moduleName) { + String javaVersion = System.getProperty("java.version"); + String fieldInfo = (isStatic ? "static " : "") + "final field '" + + field.getName() + "' in class " + + field.getDeclaringClass().getName(); + + StringBuilder sb = new StringBuilder(); + sb.append("Cannot set ").append(fieldInfo).append(".\n\n"); + sb.append("Java ").append(javaVersion).append(" enforces final field immutability (JEP 500).\n\n"); + sb.append("SHORT-TERM SOLUTIONS:\n"); + sb.append("1. Allow final field mutation temporarily:\n"); + sb.append(" --illegal-final-field-mutation=allow\n\n"); + sb.append("2. Get warnings but allow mutation (current default in JDK 26):\n"); + sb.append(" --illegal-final-field-mutation=warn\n\n"); + sb.append("RECOMMENDED SOLUTION:\n"); + sb.append("Enable final field mutation for your module:\n"); + + if (moduleName != null && !moduleName.isEmpty()) { + sb.append(" --enable-final-field-mutation=").append(moduleName).append("\n\n"); + } else { + sb.append(" --enable-final-field-mutation=ALL-UNNAMED\n\n"); + } + + sb.append("Add this to:\n"); + sb.append("- Command line: java --enable-final-field-mutation=... -jar app.jar\n"); + sb.append("- Environment: export JDK_JAVA_OPTIONS=\"--enable-final-field-mutation=...\"\n"); + sb.append("- JAR Manifest: Enable-Final-Field-Mutation: ALL-UNNAMED\n\n"); + + sb.append("IMPORTANT: This capability will be further restricted in future Java releases.\n"); + sb.append("Consider refactoring to avoid final field mutation where possible.\n"); + sb.append("See: https://openjdk.org/jeps/500"); + + return sb.toString(); + } + } + + private static T sneaky(final ExceptionalSupplier supplier) { try { return supplier.get(); diff --git a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyCreationException.java b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyCreationException.java new file mode 100644 index 000000000..807eec686 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyCreationException.java @@ -0,0 +1,16 @@ +package dev.slne.surf.surfapi.core.server.impl.reflection.reflection; + +import java.io.Serial; + +public class ProxyCreationException extends RuntimeException { + @Serial + private static final long serialVersionUID = 5706973123233542233L; + + public ProxyCreationException(String message) { + super(message); + } + + public ProxyCreationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyInvocationException.java b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyInvocationException.java new file mode 100644 index 000000000..cb9ea9d00 --- /dev/null +++ b/surf-api-core/surf-api-core-server/src/main/java/dev/slne/surf/surfapi/core/server/impl/reflection/reflection/ProxyInvocationException.java @@ -0,0 +1,16 @@ +package dev.slne.surf.surfapi.core.server.impl.reflection.reflection; + +import java.io.Serial; + +public class ProxyInvocationException extends RuntimeException { + @Serial + private static final long serialVersionUID = -3335067571221950470L; + + public ProxyInvocationException(String message) { + super(message); + } + + public ProxyInvocationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file From 0330ce928099788e218c075e3aa3e623711ba4fa Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 22:58:34 +0100 Subject: [PATCH 23/32] refactor: implement custom cache expiry policy for player lookups and enhance error handling --- .../core/api/service/PlayerLookupService.kt | 290 ++++++++++++------ 1 file changed, 192 insertions(+), 98 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/service/PlayerLookupService.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/service/PlayerLookupService.kt index b3492ffec..711a9d192 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/service/PlayerLookupService.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/service/PlayerLookupService.kt @@ -1,13 +1,15 @@ package dev.slne.surf.surfapi.core.api.service import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Expiry import com.sksamuel.aedile.core.asLoadingCache -import com.sksamuel.aedile.core.expireAfterWrite +import dev.slne.surf.surfapi.core.api.util.logger import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* +import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -28,6 +30,8 @@ import kotlin.time.Duration.Companion.minutes */ object PlayerLookupService { + private val log = logger() + /** HTTP client configured for JSON requests and responses. */ private val client = HttpClient(OkHttp) { install(ContentNegotiation) { @@ -39,48 +43,77 @@ object PlayerLookupService { } } + /** + * Sealed interface representing the result of a player lookup. + * This allows caching of non-null values including failures. + */ + sealed interface LookupResult { + /** Successfully found player data. */ + data class Found(val value: T) : LookupResult + + /** Player does not exist. */ + data object NotFound : LookupResult + + /** API rate limit or temporary failure. */ + data class RateLimited(val error: String) : LookupResult + + data class Failed(val error: Throwable) : LookupResult + } + + /** + * Custom expiry policy that uses different TTLs based on result type: + * - Found: 15 minutes (successful lookups) + * - NotFound: 5 minutes (non-existent players, can be rechecked sooner) + * - RateLimited: 1 minute (temporary errors, retry soon) + */ + private class LookupExpiry : Expiry> { + override fun expireAfterCreate( + key: K, + value: LookupResult, + currentTime: Long + ): Long = when (value) { + is LookupResult.Found -> 15.minutes.inWholeNanoseconds + is LookupResult.NotFound -> 5.minutes.inWholeNanoseconds + is LookupResult.RateLimited, is LookupResult.Failed -> 1.minutes.inWholeNanoseconds + } + + override fun expireAfterUpdate( + key: K, + value: LookupResult, + currentTime: Long, + currentDuration: Long + ): Long = expireAfterCreate(key, value, currentTime) + + override fun expireAfterRead( + key: K, + value: LookupResult, + currentTime: Long, + currentDuration: Long + ): Long = currentDuration + } + /** * Cache mapping usernames to UUIDs. - * Cached entries expire after 15 minutes. + * Uses custom expiry based on result type. */ private val nameToUuid = Caffeine.newBuilder() - .expireAfterWrite(15.minutes) - .asLoadingCache { name -> - try { - MojangApi.getUuid(name) - } catch (_: Exception) { - try { - MinecraftServicesApi.getUuid(name) - } catch (_: Exception) { - try { - MinetoolsApi.getUuid(name) - } catch (_: Exception) { - null - } - } - } + .expireAfter(LookupExpiry()) + .recordStats() + .maximumSize(10_000) + .asLoadingCache> { name -> + lookupUuid(name) } /** * Cache mapping UUIDs to usernames. - * Cached entries expire after 15 minutes. + * Uses custom expiry based on result type. */ private val uuidToName = Caffeine.newBuilder() - .expireAfterWrite(15.minutes) - .asLoadingCache { uuid -> - try { - MojangApi.getUsername(uuid) - } catch (_: Exception) { - try { - MinecraftServicesApi.getUsername(uuid) - } catch (_: Exception) { - try { - MinetoolsApi.getUsername(uuid) - } catch (_: Exception) { - null - } - } - } + .expireAfter(LookupExpiry()) + .recordStats() // Enables cache statistics + .maximumSize(10_000) + .asLoadingCache> { uuid -> + lookupUsername(uuid) } /** @@ -88,96 +121,157 @@ object PlayerLookupService { * @param uuid UUID of the player. * @return Username associated with the UUID or null if not found. */ - suspend fun getUsername(uuid: UUID): String? = uuidToName.get(uuid) + suspend fun getUsername(uuid: UUID): String? = when (val result = uuidToName.get(uuid)) { + is LookupResult.Found -> result.value + is LookupResult.NotFound, is LookupResult.RateLimited, is LookupResult.Failed -> null + } /** * Retrieves the UUID corresponding to the provided username. * @param username Minecraft username. * @return UUID associated with the username or null if not found. */ - suspend fun getUuid(username: String): UUID? = nameToUuid.get(username) + suspend fun getUuid(username: String): UUID? = when (val result = nameToUuid.get(username)) { + is LookupResult.Found -> result.value + is LookupResult.NotFound, is LookupResult.RateLimited, is LookupResult.Failed -> null + } + + fun getCacheStats(): CacheStats { + val nameStats = nameToUuid.underlying().synchronous().stats() + val uuidStats = uuidToName.underlying().synchronous().stats() + + return CacheStats( + nameToUuidHitRate = nameStats.hitRate(), + nameToUuidSize = nameToUuid.underlying().synchronous().estimatedSize(), + uuidToNameHitRate = uuidStats.hitRate(), + uuidToNameSize = uuidToName.underlying().synchronous().estimatedSize() + ) + } + + data class CacheStats( + val nameToUuidHitRate: Double, + val nameToUuidSize: Long, + val uuidToNameHitRate: Double, + val uuidToNameSize: Long + ) + + private suspend fun lookupUuid(name: String): LookupResult { + try { + val (mojangStatus, mojangUuid) = MojangApi.getUuid(name) + if (mojangUuid != null && mojangStatus.isSuccess()) { + return LookupResult.Found(mojangUuid) + } + + if (mojangStatus == HttpStatusCode.NotFound) return LookupResult.NotFound // No need to check further if Mojang says it doesn't exist + + val (minecraftStatus, minecraftUuid) = MinecraftServicesApi.getUuid(name) + if (minecraftUuid != null && minecraftStatus.isSuccess()) { + return LookupResult.Found(minecraftUuid) + } + + val (mineToolsStatus, mineToolUuid) = MinetoolsApi.getUuid(name) + if (mineToolUuid != null && mineToolsStatus.isSuccess()) { + return LookupResult.Found(mineToolUuid) + } + + return handleLookupError(mineToolsStatus) + } catch (e: Throwable) { + log.atWarning() + .withCause(e) + .log("Failed to lookup UUID for username $name") + return LookupResult.Failed(e) + } + } + + private suspend fun lookupUsername(uuid: UUID): LookupResult { + try { + val (mojangStatus, mojangName) = MojangApi.getUsername(uuid) + if (mojangName != null && mojangStatus.isSuccess()) { + return LookupResult.Found(mojangName) + } + + if (mojangStatus == HttpStatusCode.NotFound) return LookupResult.NotFound // No need to check further if Mojang says it doesn't exist + + val (minecraftStatus, mcName) = MinecraftServicesApi.getUsername(uuid) + if (mcName != null && minecraftStatus.isSuccess()) { + return LookupResult.Found(mcName) + } + + val (mineToolsStatus, mtName) = MinetoolsApi.getUsername(uuid) + if (mtName != null && mineToolsStatus.isSuccess()) { + return LookupResult.Found(mtName) + } + + return handleLookupError(mineToolsStatus) + } catch (e: Throwable) { + log.atWarning() + .withCause(e) + .log("Failed to lookup username for UUID $uuid") + return LookupResult.Failed(e) + } + } + + private fun handleLookupError(status: HttpStatusCode): LookupResult { + return when (status) { + HttpStatusCode.NotFound -> LookupResult.NotFound + HttpStatusCode.TooManyRequests -> LookupResult.RateLimited("Rate limit exceeded") + HttpStatusCode.ServiceUnavailable -> LookupResult.RateLimited("Service unavailable") + HttpStatusCode.GatewayTimeout -> LookupResult.RateLimited("Timeout") + else -> LookupResult.RateLimited("API error: ${status.value}") + } + } - /** - * API interaction with Mojang's official endpoints. - */ private object MojangApi { private const val BASE_URL = "https://api.mojang.com" - /** - * Fetches username from Mojang using UUID. - * @param uuid Player UUID. - * @return Username retrieved from Mojang. - */ - suspend fun getUsername(uuid: UUID): String { - return client.get("$BASE_URL/user/profile/${UUIDSerializer.fromUUID(uuid)}") - .body() - .name + suspend fun getUsername(uuid: UUID): Pair { + val response = client.get("$BASE_URL/user/profile/${UUIDSerializer.fromUUID(uuid)}") + val status = response.status + val name = runCatching { response.body().name }.getOrNull() + return status to name } - /** - * Fetches UUID from Mojang using username. - * @param username Player username. - * @return UUID retrieved from Mojang. - */ - suspend fun getUuid(username: String): UUID { - return client.get("$BASE_URL/users/profiles/minecraft/$username") - .body() - .id + suspend fun getUuid(username: String): Pair { + val response = client.get("$BASE_URL/users/profiles/minecraft/$username") + val status = response.status + val uuid = runCatching { response.body().id }.getOrNull() + return status to uuid } } private object MinecraftServicesApi { private const val BASE_URL = "https://api.minecraftservices.com" - /** - * Fetches username from Minecraft Services using UUID. - * @param uuid Player UUID. - * @return Username retrieved from Minecraft Services. - */ - suspend fun getUsername(uuid: UUID): String { - return client.get("$BASE_URL/minecraft/profile/lookup/${UUIDSerializer.fromUUID(uuid)}") - .body() - .name + suspend fun getUsername(uuid: UUID): Pair { + val response = client.get("$BASE_URL/minecraft/profile/lookup/${UUIDSerializer.fromUUID(uuid)}") + val status = response.status + val name = runCatching { response.body().name }.getOrNull() + return status to name } - /** - * Fetches UUID from Minecraft Services using username. - * @param username Player username. - * @return UUID retrieved from Minecraft Services. - */ - suspend fun getUuid(username: String): UUID { - return client.get("$BASE_URL/minecraft/profile/lookup/name/$username") - .body() - .id + suspend fun getUuid(username: String): Pair { + val response = client.get("$BASE_URL/minecraft/profile/lookup/name/$username") + val status = response.status + val uuid = runCatching { response.body().id }.getOrNull() + return status to uuid } } - /** - * API interaction with Minetools as a fallback. - */ private object MinetoolsApi { private const val BASE_URL = "https://api.minetools.eu" - /** - * Fetches username from Minetools using UUID. - * @param uuid Player UUID. - * @return Username retrieved from Minetools. - */ - suspend fun getUsername(uuid: UUID): String { - return client.get("$BASE_URL/uuid/${UUIDSerializer.fromUUID(uuid)}") - .body() - .name + suspend fun getUsername(uuid: UUID): Pair { + val response = client.get("$BASE_URL/uuid/${UUIDSerializer.fromUUID(uuid)}") + val status = response.status + val name = runCatching { response.body().name }.getOrNull() + return status to name } - /** - * Fetches UUID from Minetools using username. - * @param username Player username. - * @return UUID retrieved from Minetools. - */ - suspend fun getUuid(username: String): UUID { - return client.get("$BASE_URL/uuid/$username") - .body() - .id + suspend fun getUuid(username: String): Pair { + val response = client.get("$BASE_URL/uuid/$username") + val status = response.status + val uuid = runCatching { response.body().id }.getOrNull() + return status to uuid } } @@ -214,6 +308,8 @@ private object UUIDSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + private val uuidFormatRegex = "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})".toRegex() + override fun serialize(encoder: Encoder, value: UUID) { encoder.encodeString(fromUUID(value)) } @@ -230,9 +326,7 @@ private object UUIDSerializer : KSerializer { /** Converts simplified UUID string back to standard UUID format. */ fun fromString(input: String): UUID { return UUID.fromString( - input.replaceFirst( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})".toRegex(), "$1-$2-$3-$4-$5" - ) + input.replaceFirst(uuidFormatRegex, "$1-$2-$3-$4-$5") ) } } \ No newline at end of file From f73bc12acf7c224e3902cb83f346c6c9bb636005 Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 23:00:11 +0100 Subject: [PATCH 24/32] refactor: mark BlockStateFactory and BlockStateFactoryImpl as deprecated --- .../surf/surfapi/core/api/util/blockstate/BlockStateFactory.kt | 1 + .../surfapi/core/api/util/blockstate/BlockStateFactoryImpl.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactory.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactory.kt index 8363de52c..f3a47226f 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactory.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactory.kt @@ -8,6 +8,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +@Deprecated("Not longer maintained.") interface BlockStateFactory { @Suppress("unused") interface Builder { diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactoryImpl.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactoryImpl.kt index b39301172..6e481f9c1 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactoryImpl.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/blockstate/BlockStateFactoryImpl.kt @@ -4,6 +4,7 @@ import com.github.retrooper.packetevents.protocol.world.BlockFace import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState import com.github.retrooper.packetevents.protocol.world.states.enums.* +@Deprecated("Not longer maintained.") class BlockStateFactoryImpl { internal data class BuilderImpl(val blockState: WrappedBlockState) : BlockStateFactory.Builder { override fun age(): Int { From 4e7837c70ec769c3f3190d33059eba5cba5c2fc1 Mon Sep 17 00:00:00 2001 From: twisti Date: Mon, 9 Feb 2026 23:07:55 +0100 Subject: [PATCH 25/32] refactor: enhance PlayerLookupService with cache statistics and result handling --- .../api/surf-api-core-api.api | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 8c7165dd4..135d3ec25 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -8314,7 +8314,9 @@ public abstract interface class dev/slne/surf/surfapi/core/api/reflection/SurfRe public static fun getInstance ()Ldev/slne/surf/surfapi/core/api/reflection/SurfReflection; } -public final class dev/slne/surf/surfapi/core/api/reflection/SurfReflection$Companion { +public final class dev/slne/surf/surfapi/core/api/reflection/SurfReflection$Companion : dev/slne/surf/surfapi/core/api/reflection/SurfReflection { + public fun createProxy (Ljava/lang/Class;)Ljava/lang/Object; + public fun createProxy (Ljava/lang/Class;Ljava/lang/ClassLoader;)Ljava/lang/Object; public final fun getInstance ()Ldev/slne/surf/surfapi/core/api/reflection/SurfReflection; } @@ -9337,10 +9339,71 @@ public final class dev/slne/surf/surfapi/core/api/serializer/spongepowered/math/ public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService { public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService; + public final fun getCacheStats ()Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$CacheStats; public final fun getUsername (Ljava/util/UUID;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getUuid (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$CacheStats { + public fun (DJDJ)V + public final fun component1 ()D + public final fun component2 ()J + public final fun component3 ()D + public final fun component4 ()J + public final fun copy (DJDJ)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$CacheStats; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$CacheStats;DJDJILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$CacheStats; + public fun equals (Ljava/lang/Object;)Z + public final fun getNameToUuidHitRate ()D + public final fun getNameToUuidSize ()J + public final fun getUuidToNameHitRate ()D + public final fun getUuidToNameSize ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult { +} + +public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Failed : dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult { + public fun (Ljava/lang/Throwable;)V + public final fun component1 ()Ljava/lang/Throwable; + public final fun copy (Ljava/lang/Throwable;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Failed; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Failed;Ljava/lang/Throwable;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Failed; + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Found : dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult { + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public final fun copy (Ljava/lang/Object;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Found; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Found;Ljava/lang/Object;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$Found; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$NotFound : dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult { + public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$NotFound; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$RateLimited : dev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult { + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$RateLimited; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$RateLimited;Ljava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/service/PlayerLookupService$LookupResult$RateLimited; + public fun equals (Ljava/lang/Object;)Z + public final fun getError ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class dev/slne/surf/surfapi/core/api/util/ByEnum { public abstract fun value ()Ljava/lang/Object; } From 1d93a52c963324ae97c7e9f366887bec92c9ca1a Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 19:17:42 +0100 Subject: [PATCH 26/32] refactor: mark ItemStackFactory, LocationFactory, and ParticleFactory as deprecated with replacement suggestions --- .../surfapi/core/api/util/ItemStackFactory.kt | 15 +++- .../surfapi/core/api/util/LocationFactory.kt | 23 +++--- .../surfapi/core/api/util/ParticleFactory.kt | 80 +++++++++++++++++-- 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ItemStackFactory.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ItemStackFactory.kt index 7d613fd22..663136ace 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ItemStackFactory.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ItemStackFactory.kt @@ -7,8 +7,20 @@ import org.jetbrains.annotations.ApiStatus import java.util.function.Consumer @ApiStatus.NonExtendable +@Deprecated("Not longer maintained.") interface ItemStackFactory { companion object { + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "ItemStack.builder()\n" + + ".type(material)\n" + + ".amount(amount)\n" + + ".nbt(NBTCompound().also { nbtConsumer.accept(it) })\n" + + ".build()", + "java.util.function.Consumer" + ) + ) @JvmOverloads fun of( material: ItemType, amount: Int = 1, nbtConsumer: Consumer = Consumer { } @@ -16,7 +28,8 @@ interface ItemStackFactory { val nbt = NBTCompound() nbtConsumer.accept(nbt) - return ItemStack.builder().type(material).amount(amount).nbt(nbt).build() + return ItemStack.builder().type(material).amount(amount).nbt(NBTCompound().also { nbtConsumer.accept(it) }) + .build() } } } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/LocationFactory.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/LocationFactory.kt index 6d83f501c..fa6cee089 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/LocationFactory.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/LocationFactory.kt @@ -1,36 +1,31 @@ package dev.slne.surf.surfapi.core.api.util import com.github.retrooper.packetevents.protocol.world.Location -import org.jetbrains.annotations.ApiStatus -import org.jetbrains.annotations.Contract import org.spongepowered.math.GenericMath +@Deprecated("Not longer maintained.") interface LocationFactory { companion object { @JvmStatic + @Deprecated("Not longer maintained.", ReplaceWith("locationA.position.distanceSquared(locationB.position)")) fun distanceSquared(locationA: Location, locationB: Location): Double { - return (square(locationA.x - locationB.x) + square(locationA.y - locationB.y) + square( - locationA.z - locationB.z - )) + return locationA.position.distanceSquared(locationB.position) } @JvmStatic + @Deprecated("Not longer maintained.", ReplaceWith("locationA.position.distance(locationB.position)")) fun distance(locationA: Location, locationB: Location): Double { - return GenericMath.sqrt(distanceSquared(locationA, locationB)) - } - - @ApiStatus.Internal - @Contract(pure = true) - private fun square(value: Double): Double { - return value * value + return GenericMath.sqrt(locationA.position.distanceSquared(locationB.position)) } } } +@Deprecated("Not longer maintained.", ReplaceWith("this.position.distanceSquared(location.position)")) infix fun Location.distanceSquared(location: Location): Double { - return LocationFactory.distanceSquared(this, location) + return this.position.distanceSquared(location.position) } +@Deprecated("Not longer maintained.", ReplaceWith("this.position.distance(location.position)")) infix fun Location.distance(location: Location): Double { - return LocationFactory.distance(this, location) + return this.position.distance(location.position) } diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ParticleFactory.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ParticleFactory.kt index 04b7b2cb2..d17471464 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ParticleFactory.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/ParticleFactory.kt @@ -10,30 +10,54 @@ import net.kyori.adventure.text.format.TextColor import org.jetbrains.annotations.ApiStatus @ApiStatus.NonExtendable +@Deprecated("Not longer maintained.") interface ParticleFactory { companion object { @JvmStatic + @Deprecated( + "Not longer maintained.", + ReplaceWith("Particle(type)", "com.github.retrooper.packetevents.protocol.particle.Particle") + ) fun of(type: ParticleType<*>): Particle<*> { return Particle(type) } + @Deprecated( + "Not longer maintained.", + ReplaceWith("Particle(type, data)", "com.github.retrooper.packetevents.protocol.particle.Particle") + ) fun of(type: ParticleType, data: D): Particle { return Particle(type, data) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleBlockStateData(blockState))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleBlockStateData.ParticleBlockStateData" + ) + ) fun of( type: ParticleType, blockState: WrappedBlockState ): Particle { - return of(type, ParticleBlockStateData(blockState)) + return Particle(type, ParticleBlockStateData(blockState)) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleDustColorTransitionData(scale, start.red().toFloat(), start.green().toFloat(), start.blue().toFloat(), end.red().toFloat(), end.green().toFloat(), end.blue().toFloat()))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleDustColorTransitionData", + ) + ) fun of( type: ParticleType, scale: Float, start: TextColor, end: TextColor ): Particle { - - return of( + return Particle( type, ParticleDustColorTransitionData( scale, start.red().toFloat(), start.green().toFloat(), start.blue().toFloat(), @@ -42,36 +66,76 @@ interface ParticleFactory { ) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleItemStackData(itemStack))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleItemStackData" + ) + ) fun of( type: ParticleType, itemStack: ItemStack ): Particle { - return of(type, ParticleItemStackData(itemStack)) + return Particle(type, ParticleItemStackData(itemStack)) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleSculkChargeData(roll))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleSculkChargeData" + ) + ) fun of( type: ParticleType, roll: Float ): Particle { - return of(type, ParticleSculkChargeData(roll)) + return Particle(type, ParticleSculkChargeData(roll)) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleShriekData(delay))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleShriekData" + ) + ) fun of(type: ParticleType, delay: Int): Particle { - return of(type, ParticleShriekData(delay)) + return Particle(type, ParticleShriekData(delay)) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleVibrationData(startingPosition, blockPosition, ticks))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleVibrationData" + ) + ) fun of( type: ParticleType, startingPosition: Vector3i, blockPosition: Vector3i, ticks: Int ): Particle { - return of(type, ParticleVibrationData(startingPosition, blockPosition, ticks)) + return Particle(type, ParticleVibrationData(startingPosition, blockPosition, ticks)) } + @Deprecated( + "Not longer maintained.", + ReplaceWith( + "Particle(type, ParticleVibrationData(startingPosition, entityId, ticks))", + "com.github.retrooper.packetevents.protocol.particle.Particle", + "com.github.retrooper.packetevents.protocol.particle.data.ParticleVibrationData" + ) + ) fun of( type: ParticleType, startingPosition: Vector3i, entityId: Int, ticks: Int ): Particle { - return of(type, ParticleVibrationData(startingPosition, entityId, ticks)) + return Particle(type, ParticleVibrationData(startingPosition, entityId, ticks)) } } } From 485c61e154722ed86eb9d7779953a3d234a702d7 Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 19:21:34 +0100 Subject: [PATCH 27/32] refactor: enhance date and reflection utilities with additional formatting and annotation retrieval functions --- .../dev/slne/surf/surfapi/core/api/util/date-util.kt | 11 +++++++++++ .../surf/surfapi/core/api/util/reflection-util.kt | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/date-util.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/date-util.kt index f875ff8ad..6fa19ebfd 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/date-util.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/date-util.kt @@ -3,6 +3,17 @@ package dev.slne.surf.surfapi.core.api.util import java.time.LocalDateTime import java.time.format.DateTimeFormatter +/** + * A date-time formatter that formats dates and times in the pattern "dd.MM.yyyy HH:mm". + * + * For example: "10.02.2026 19:30" + */ val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") + +/** + * Returns the current date and time formatted as "dd.MM.yyyy HH:mm". + * + * This property retrieves the current system time each time it is accessed. + */ val currentDateTimeFormatted: String get() = LocalDateTime.now().format(dateTimeFormatter) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/reflection-util.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/reflection-util.kt index 00cf7bc22..7b9aa481e 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/reflection-util.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/reflection-util.kt @@ -2,5 +2,16 @@ package dev.slne.surf.surfapi.core.api.util import java.lang.reflect.AccessibleObject +/** + * Returns the annotation of type [A] that is directly present on this accessible object, + * or null if no such annotation is present. + * + * This function provides a Kotlin-idiomatic way to retrieve annotations from Java reflection + * objects without explicitly passing the annotation class. It uses reified type parameters + * to infer the annotation type at compile time. + * + * @param A the type of annotation to query for + * @return this element's annotation for the specified type if directly present, null otherwise + */ inline fun AccessibleObject.findAnnotation(): A? = getDeclaredAnnotation(A::class.java) \ No newline at end of file From 2ee469c4d42e510e19d08895be50fe834cbb4b8c Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 19:27:04 +0100 Subject: [PATCH 28/32] refactor: update requiredService function to throw ServiceConfigurationError for unavailable services --- .../slne/surf/surfapi/core/api/util/service-util.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/service-util.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/service-util.kt index 8135236d1..6f5ecb77d 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/service-util.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/service-util.kt @@ -1,13 +1,16 @@ package dev.slne.surf.surfapi.core.api.util import net.kyori.adventure.util.Services +import java.util.* /** - * Retrieves a required service of the specified type using `Services.serviceWithFallback`. - * If the service is not available, it throws an `Error`. + * Retrieves a required service of type [T] using the Adventure service provider mechanism. * - * @return The service instance of the specified type `T` if available. - * @throws Error if the service of type `T` is not available. + * This function uses `Services.serviceWithFallback` to locate a service implementation. + * If no service provider is registered for the specified type, an exception is thrown. + * + * @return the service instance of type [T] + * @throws ServiceConfigurationError if the service of type [T] is not available */ inline fun requiredService(): T = Services.serviceWithFallback(T::class.java) - .orElseThrow { Error("Service ${T::class.java.name} not available") } \ No newline at end of file + .orElseThrow { ServiceConfigurationError("Service ${T::class.java.name} not available") } \ No newline at end of file From 9968e4e0ed1b1a29276f999a50e20ea6aee254c9 Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 19:45:32 +0100 Subject: [PATCH 29/32] refactor: enhance SurfTypeParameterMatcher with improved documentation and thread-safe caching --- .../core/api/util/SurfTypeParameterMatcher.kt | 169 +++++++++++++++--- 1 file changed, 141 insertions(+), 28 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher.kt index 5a0f8900e..d6fa03708 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher.kt @@ -1,63 +1,151 @@ package dev.slne.surf.surfapi.core.api.util -import it.unimi.dsi.fastutil.objects.Object2ObjectMap -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import dev.slne.surf.surfapi.core.api.util.SurfTypeParameterMatcher.Companion.find +import dev.slne.surf.surfapi.core.api.util.SurfTypeParameterMatcher.Companion.get import java.lang.reflect.Array import java.lang.reflect.GenericArrayType import java.lang.reflect.ParameterizedType -import java.util.* +import java.util.concurrent.ConcurrentHashMap /** - * A utility class for matching type parameters of generic types at runtime. + * A utility class for matching type parameters of generic types at runtime using reflection. + * + * This class resolves and matches generic type parameters from parameterized superclasses or interfaces, + * enabling runtime type checking of generic types. It provides caching mechanisms to optimize repeated + * lookups and is fully thread-safe. + * + * ## Example Usage + * + * ```kotlin + * // Define a generic handler interface + * interface MessageHandler { + * fun handle(message: T) + * } + * + * // Implement the handler with a concrete type + * class StringMessageHandler : MessageHandler { + * override fun handle(message: String) { + * println("Handling: $message") + * } + * } + * + * // Use the type parameter matcher + * val handler = StringMessageHandler() + * val matcher = SurfTypeParameterMatcher.find(handler, MessageHandler::class.java, "T") + * + * println(matcher.match("Hello")) // true - String matches + * println(matcher.match(123)) // false - Int doesn't match + * ``` + * + * ## Thread Safety + * + * All caching operations are thread-safe. Multiple threads can concurrently call [get] and [find] + * without external synchronization. */ abstract class SurfTypeParameterMatcher { /** - * Determines whether the provided object matches the criteria defined by the matcher. + * Determines whether the provided object matches the expected type parameter. * - * @param any The object to be checked. - * @return `true` if the object matches the criteria, `false` otherwise. + * @param any The object to be checked against the type parameter. + * @return `true` if the object is an instance of the matched type, `false` otherwise. */ abstract fun match(any: Any): Boolean companion object { /** - * Cache for storing matchers based on their parameter types. + * Thread-safe cache for storing matchers based on parameterized types and type parameter names. + * The outer map is concurrent, and each inner map is synchronized during creation. */ - private val getCache = IdentityHashMap, SurfTypeParameterMatcher>() + private val findCache = ConcurrentHashMap, ConcurrentHashMap>() /** - * Cache for storing matchers based on their parameterized superclass and type parameter names. + * A no-operation matcher that always returns `true`, used for [Object] type parameters. */ - private val findCache = - IdentityHashMap, Object2ObjectMap>() + private val noop = object : SurfTypeParameterMatcher() { + override fun match(any: Any): Boolean = true + } /** - * A no-operation matcher that always returns `true`. + * Thread-safe cache for storing matchers based on parameter types. + * Uses [ClassValue] to ensure that the cache is automatically cleaned up when classes are unloaded, + * preventing memory leaks. */ - private val noop = object : SurfTypeParameterMatcher() { - override fun match(any: Any): Boolean = true + private val getCache = object : ClassValue() { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + override fun computeValue(type: Class<*>): SurfTypeParameterMatcher { + if (type == Object::class.java) return noop + return ReflectiveMatcher(type) + } } /** * Retrieves a [SurfTypeParameterMatcher] for the given parameter type. * - * @param parameterType The class representing the type parameter. + * Results are cached for performance. For `Object` type, returns a no-op matcher + * that always matches any object. + * + * ## Example + * + * ```kotlin + * val stringMatcher = SurfTypeParameterMatcher[String::class.java] + * println(stringMatcher.match("test")) // true + * println(stringMatcher.match(42)) // false + * + * val objectMatcher = SurfTypeParameterMatcher[Object::class.java] + * println(objectMatcher.match("anything")) // true (always matches) + * ``` + * + * @param parameterType The class representing the type parameter to match. * @return A [SurfTypeParameterMatcher] for the provided type. */ - operator fun get(parameterType: Class<*>): SurfTypeParameterMatcher = - getCache.computeIfAbsent(parameterType) { - if (parameterType == Object::class.java) noop else ReflectiveMatcher(parameterType) - } + operator fun get(parameterType: Class<*>): SurfTypeParameterMatcher = getCache.get(parameterType) /** - * Finds and retrieves a [SurfTypeParameterMatcher] for a specified type parameter name of - * a parameterized superclass or interface. + * Finds and retrieves a [SurfTypeParameterMatcher] for a specified type parameter name + * from a parameterized superclass or interface. + * + * This method analyzes the object's class hierarchy to resolve the actual runtime type + * of the specified generic type parameter. Results are cached per class and type parameter name. + * + * ## Example + * + * ```kotlin + * // Generic repository interface + * interface Repository { + * fun findById(id: ID): T? + * } + * + * // Concrete implementation + * class UserRepository : Repository { + * override fun findById(id: Long): User? = null + * } + * + * val repo = UserRepository() + * + * // Match the entity type parameter + * val entityMatcher = SurfTypeParameterMatcher.find( + * repo, + * Repository::class.java, + * "T" + * ) + * println(entityMatcher.match(User())) // true + * + * // Match the ID type parameter + * val idMatcher = SurfTypeParameterMatcher.find( + * repo, + * Repository::class.java, + * "ID" + * ) + * println(idMatcher.match(123L)) // true + * println(idMatcher.match("not-a-long")) // false + * ``` * * @param any The object whose type parameters are being analyzed. * @param parametrizedType The class representing the parameterized superclass or interface. - * @param typeParamName The name of the type parameter to resolve. + * @param typeParamName The name of the type parameter to resolve (e.g., "T", "E", "K"). * @return A [SurfTypeParameterMatcher] for the resolved type parameter. + * @throws IllegalStateException If the type parameter cannot be resolved from the class hierarchy. */ fun find( any: Any, @@ -65,8 +153,8 @@ abstract class SurfTypeParameterMatcher { typeParamName: String ): SurfTypeParameterMatcher { val thisClass = any.javaClass - val map = findCache.computeIfAbsent(thisClass) { Object2ObjectOpenHashMap() } - return map.computeIfAbsent(typeParamName) { + val innerMap = findCache.computeIfAbsent(thisClass) { ConcurrentHashMap() } + return innerMap.computeIfAbsent(typeParamName) { get(find0(any, parametrizedType, typeParamName)) } } @@ -93,6 +181,14 @@ abstract class SurfTypeParameterMatcher { return result ?: fail(thisClass, typeParamName) } + /** + * Attempts to resolve the type parameter by traversing the superclass hierarchy. + * + * @param currentClass The class to start traversal from. + * @param parametrizedType The target parameterized type. + * @param typeParamName The name of the type parameter. + * @return The resolved class or `null` if not found in the superclass hierarchy. + */ private fun resolveTypeFromSuperclass( currentClass: Class<*>, parametrizedType: Class<*>, @@ -112,6 +208,14 @@ abstract class SurfTypeParameterMatcher { return null } + /** + * Attempts to resolve the type parameter by examining implemented interfaces. + * + * @param currentClass The class whose interfaces are being examined. + * @param parametrizedType The target parameterized type. + * @param typeParamName The name of the type parameter. + * @return The resolved class or `null` if not found in the interfaces. + */ private fun resolveTypeFromInterfaces( currentClass: Class<*>, parametrizedType: Class<*>, @@ -129,6 +233,14 @@ abstract class SurfTypeParameterMatcher { return null } + /** + * Extracts the actual type argument from a parameterized type. + * + * @param parameterizedType The parameterized type containing type arguments. + * @param parametrizedType The base parameterized class/interface. + * @param typeParamName The name of the type parameter to extract. + * @return The resolved class or `null` if extraction fails. + */ private fun resolveTypeFromGenericInfo( parameterizedType: ParameterizedType?, parametrizedType: Class<*>, @@ -156,7 +268,7 @@ abstract class SurfTypeParameterMatcher { * * @param type The class being analyzed. * @param typeParamName The name of the type parameter. - * @throws IllegalStateException Always thrown with a message indicating the unresolved parameter. + * @throws IllegalStateException Always thrown with a descriptive error message. */ private fun fail(type: Class<*>, typeParamName: String): Nothing { throw IllegalStateException( @@ -165,9 +277,10 @@ abstract class SurfTypeParameterMatcher { } /** - * A [SurfTypeParameterMatcher] implementation that matches objects based on their runtime type. + * A [SurfTypeParameterMatcher] implementation that matches objects based on their runtime type + * using [Class.isInstance]. * - * @param type The class representing the type to match. + * @param type The class representing the type to match against. */ private class ReflectiveMatcher(private val type: Class<*>) : SurfTypeParameterMatcher() { override fun match(any: Any) = type.isInstance(any) From 5ba1658a87afd5906a4a8513e64be3f52ae73fbf Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 20:01:45 +0100 Subject: [PATCH 30/32] refactor: improve documentation and structure of utility functions in util.kt --- .../slne/surf/surfapi/core/api/util/util.kt | 409 ++++++++++++++---- 1 file changed, 334 insertions(+), 75 deletions(-) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/util.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/util.kt index 91e43b355..7754d0cdf 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/util.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/util/util.kt @@ -1,5 +1,6 @@ @file:OptIn(ExperimentalContracts::class) @file:JvmName("SurfUtil") +@file:Suppress("removal", "DEPRECATION") package dev.slne.surf.surfapi.core.api.util @@ -31,14 +32,10 @@ import kotlin.reflect.KProperty import kotlin.streams.asSequence /** - * A lazily-initialized instance of `SecureRandom` providing a secure source of randomness. + * Lazily initialized secure random instance. * - * This variable attempts to obtain a strong `SecureRandom` instance using `SecureRandom.getInstanceStrong()`. - * If this operation fails (e.g., due to system or environment limitations), it logs the error and falls back - * to a default `SecureRandom` instance. - * - * This ensures that a `SecureRandom` instance is always available, prioritizing strong cryptographic security - * when possible, while maintaining resilience to initialization failures. + * Attempts to use a strong instance via [SecureRandom.getInstanceStrong], falling back to the default + * implementation if unavailable. Initialization failures are logged but do not prevent fallback. */ val random: SecureRandom by lazy { try { @@ -52,24 +49,18 @@ val random: SecureRandom by lazy { } /** - * Provides a `FluentLogger` instance associated with the enclosing class. - * - * This method is intended to simplify access to `FluentLogger` for logging - * purposes, automatically associating the logger with the class that calls it. + * Returns a [FluentLogger] for the enclosing class. * - * @return A `FluentLogger` instance for the enclosing class. + * This function is caller-sensitive and must be inlined to correctly identify the enclosing class. */ @Suppress("NOTHING_TO_INLINE") // Caller sensitive inline fun logger(): FluentLogger = FluentLogger.forEnclosingClass() /** - * Executes the provided logging operation if the specified condition evaluates to true. - * - * This function allows for conditional logging using a specified `LoggingApi`. - * The logging operation will only be performed if the `condition` lambda returns true. + * Executes the logging operation only if the condition evaluates to true. * - * @param condition A lambda that provides a condition to evaluate. The logging operation will be executed only if this condition returns true. - * @param logOperation The logging operation to perform if the condition is satisfied. This is an extension function on the logging API. + * @param condition Lambda returning true if logging should occur. + * @param logOperation Logging operation to execute conditionally. */ inline fun > LoggingApi.logIf( condition: () -> Boolean, @@ -85,71 +76,32 @@ inline fun > LoggingApi.logIf( } /** - * A singleton instance of `StackWalker` configured with the `Option.RETAIN_CLASS_REFERENCE` option. - * - * This instance is used for retrieving information about the current call stack, - * including details such as the declaring class of a caller. The - * `RETAIN_CLASS_REFERENCE` option ensures that the `Class` objects of stack frames - * are retained, making it possible to perform operations requiring class meta-information. - * - * Typical usage of this instance involves walking the stack to identify or introspect - * the caller classes or methods with precise class retention capabilities. This is - * particularly beneficial in scenarios where well-defined caller verification or context - * inference is needed. + * Stack walker instance configured to retain class references for caller inspection. */ private val STACK_WALK_INSTANCE: StackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) /** - * Retrieves the class of the method's immediate caller. + * Returns the class of the immediate caller. * - * This utility function uses a `StackWalker` instance to walk the call stack - * and retrieve the class that invoked the current method. - * The `depth` parameter determines how far back in the call stack the search - * proceeds, relative to the default offset used internally. - * - * This function is useful for tasks such as debugging, logging, or implementing - * caller-sensitive behaviors. - * - * Note: Invoking this function may have performance overhead depending on the - * depth of the stack and the number of elements traversed. - * - * @return The `Class` object of the caller, or null if no caller is found in the - * stack at the specified depth. + * @return The caller's class, or null if unavailable. */ fun getCallerClass() = getCallerClass(0) /** - * Retrieves the class of a caller at a specific depth in the call stack. - * - * This utility function uses a `StackWalker` instance to walk the call stack - * and retrieve the class that invoked the current method at a specified depth. - * The `depth` parameter determines how many levels to move up in the stack - * to locate the desired caller. + * Returns the class of the caller at the specified stack depth. * - * This function is commonly used for tasks such as debugging, logging, or implementing - * caller-sensitive behaviors. - * - * Note: The default offset of 3 accounts for the internal implementation of the stack walker. - * - * @param depth The number of levels to walk up the call stack to locate the desired caller. - * A value of 0 corresponds to the immediate caller, while higher values - * move further up the stack. - * @return The `Class` object of the caller at the specified depth, or null if no class - * is found at that depth in the call stack. + * @param depth Number of additional stack frames to skip (0 for immediate caller). + * @return The caller's class at the specified depth, or null if unavailable. */ fun getCallerClass(depth: Int) = STACK_WALK_INSTANCE.walk { it.asSequence().drop(3 + depth).firstOrNull()?.declaringClass } /** - * Checks if the immediate caller class matches the expected class. + * Verifies that the immediate caller is the expected class. * - * This function verifies that the class of the immediate caller matches the provided - * expected class. If the caller class does not match, an `IllegalStateException` is thrown. - * - * @param expected The `Class` object representing the expected caller class. This is the class - * that the function expects to be the direct invoker of the current method. - * @throws IllegalStateException If the class of the immediate caller does not match the expected class. + * @param expected The expected caller class. + * @throws IllegalStateException if the caller does not match. */ fun checkCallerClass(expected: Class<*>) { if (getCallerClass(1) != expected) { @@ -158,21 +110,22 @@ fun checkCallerClass(expected: Class<*>) { } /** - * Verifies that the current method is instantiated by `java.util.ServiceLoader`. - * - * This function checks the call stack to ensure that the class instantiating - * an object is `java.util.ServiceLoader`. It prevents direct instantiation of - * the object outside of the expected context (i.e., via `ServiceLoader`). + * Verifies that instantiation occurs via [ServiceLoader]. * - * If the instantiation is not performed by `ServiceLoader`, an `IllegalStateException` - * is thrown with an appropriate error message, enforcing proper usage. + * Prevents direct instantiation by ensuring the caller is from the ServiceLoader class hierarchy. * - * @throws IllegalStateException If the instantiation is not done by `java.util.ServiceLoader`. + * @throws IllegalStateException if not instantiated by ServiceLoader. */ fun checkInstantiationByServiceLoader() { check(getCallerClass(1)?.name?.startsWith("java.util.ServiceLoader") == true) { "Cannot instantiate instance directly" } } +/** + * Direct access to the JVM's Unsafe API. + * + * This provides low-level memory operations that bypass Java's type safety and access control. + * Use only when absolutely necessary, as misuse can cause JVM crashes. + */ val unsafe = try { val unsafeField = Unsafe::class.java.getDeclaredField("theUnsafe") unsafeField.isAccessible = true @@ -181,125 +134,363 @@ val unsafe = try { throw RuntimeException(e) } +/** + * Modifies a static final object field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Any?) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putObject(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final object field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Any?) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putObject(instance, fieldOffset, value) } } +/** + * Modifies a static final int field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Int) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putInt(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final int field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Int) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putInt(instance, fieldOffset, value) } } +/** + * Modifies a static final long field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Long) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putLong(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final long field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Long) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putLong(instance, fieldOffset, value) } } +/** + * Modifies a static final boolean field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Boolean) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putBoolean(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final boolean field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Boolean) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putBoolean(instance, fieldOffset, value) } } +/** + * Modifies a static final byte field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Byte) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putByte(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final byte field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Byte) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putByte(instance, fieldOffset, value) } } +/** + * Modifies a static final short field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Short) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putShort(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final short field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Short) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putShort(instance, fieldOffset, value) } } +/** + * Modifies a static final float field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Float) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putFloat(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final float field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Float) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putFloat(instance, fieldOffset, value) } } +/** + * Modifies a static final double field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Double) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putDouble(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final double field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Double) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putDouble(instance, fieldOffset, value) } } +/** + * Modifies a static final char field. + * + * @param field The static final field to modify. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setStaticFinalField(field: Field, value: Char) { processStaticFinalField(field) { unsafe, fieldBase, fieldOffset -> unsafe.putChar(fieldBase, fieldOffset, value) } } +/** + * Modifies an instance final char field. + * + * @param field The final field to modify. + * @param instance The object containing the field. + * @param value The new value. + */ +@Deprecated( + message = "Uses internal Unsafe API which bypasses Java's security and type safety. " + + "This can cause JVM instability and is not guaranteed to work in future Java versions. " + + "Consider using VarHandles or redesigning to avoid modifying final fields.", + level = DeprecationLevel.WARNING +) fun setFinalField(field: Field, instance: Any, value: Char) { processFinalField(field) { unsafe, fieldOffset -> unsafe.putChar(instance, fieldOffset, value) } } +/** + * Internal helper for modifying static final fields using Unsafe. + */ private fun processStaticFinalField(field: Field, putOperation: (Unsafe, Any, Long) -> Unit) { val fieldBase = unsafe.staticFieldBase(field) val fieldOffset = unsafe.staticFieldOffset(field) putOperation(unsafe, fieldBase, fieldOffset) } +/** + * Internal helper for modifying instance final fields using Unsafe. + */ private fun processFinalField(field: Field, putOperation: (Unsafe, Long) -> Unit) { val fieldOffset = unsafe.objectFieldOffset(field) putOperation(unsafe, fieldOffset) } +/** + * Creates an unmodifiable map from enum constants to their string IDs. + * + * @param enumClass The enum class. + * @param idMapper Function mapping each enum constant to its string ID. + * @return Unmodifiable map from string IDs to enum constants. + */ fun > byStringIdMap( enumClass: Class, idMapper: (T) -> String, @@ -309,7 +500,13 @@ fun > byStringIdMap( ) ) - +/** + * Creates an unmodifiable map from enum constants to their integer IDs. + * + * @param enumClass The enum class. + * @param idMapper Function mapping each enum constant to its integer ID. + * @return Unmodifiable map from integer IDs to enum constants. + */ fun > byIdMap( enumClass: Class, idMapper: ToIntFunction, @@ -317,6 +514,13 @@ fun > byIdMap( return byIdMap(idMapper, enumClass.enumConstants) } +/** + * Creates an unmodifiable map from values to their integer IDs. + * + * @param idMapper Function mapping each value to its integer ID. + * @param values Array of values to map. + * @return Unmodifiable map from integer IDs to values. + */ fun byIdMap( idMapper: ToIntFunction, values: Array, @@ -328,6 +532,13 @@ fun byIdMap( ) } +/** + * Creates an unmodifiable map from values to their integer IDs. + * + * @param idMapper Function mapping each value to its integer ID. + * @param values Array of values to map. + * @return Unmodifiable map from integer IDs to values. + */ fun byIdMap( idMapper: (T) -> Int, values: Array, @@ -339,6 +550,13 @@ fun byIdMap( ) } +/** + * Creates an unmodifiable map from values to their byte IDs. + * + * @param values Array of values to map. + * @param idMapper Function mapping each value to its byte ID. + * @return Unmodifiable map from byte IDs to values. + */ fun byByteIdMap( values: Array, idMapper: (T) -> Byte, @@ -350,6 +568,12 @@ fun byByteIdMap( ) } +/** + * Creates an unmodifiable map from enum constants to computed values. + * + * @param valueMapper Function mapping each enum constant to its associated value. + * @return Unmodifiable map from enum constants to their values. + */ inline fun > byEnumMap( valueMapper: (T) -> Any, ): Object2ObjectMap { @@ -360,14 +584,25 @@ inline fun > byEnumMap( ) } +/** + * Functional interface for mapping a value to a byte. + */ fun interface ToByteFunction { fun applyAsByte(value: T): Byte } +/** + * Interface for types that can be identified by an enum value. + */ fun interface ByEnum { fun value(): T } +/** + * Converts a sequence to an enumeration. + * + * @return An enumeration that iterates over the sequence elements. + */ fun Sequence.toEnumeration(): Enumeration { val iterator = iterator() return object : Enumeration { @@ -376,17 +611,41 @@ fun Sequence.toEnumeration(): Enumeration { } } +/** + * Returns the size of this iterable if it is a collection, otherwise returns the default value. + * + * @param default The value to return if this is not a collection. + * @return The collection size, or the default value. + */ fun Iterable.collectionSizeOrDefault(default: Int) = if (this is Collection<*>) this.size else default +/** + * Delegation operator for [WeakReference] allowing property-style access. + * + * @return The referenced object, or null if collected. + */ operator fun WeakReference.getValue(thisRef: Any?, property: KProperty<*>): T? { return get() } +/** + * Delegation operator for [SoftReference] allowing property-style access. + * + * @return The referenced object, or null if collected. + */ operator fun SoftReference.getValue(thisRef: Any?, property: KProperty<*>): T? { return get() } +/** + * Maps each element asynchronously using coroutines. + * + * All transformations run concurrently, and this function suspends until all complete. + * + * @param transform Suspending transformation function applied to each element. + * @return List of transformed elements in the original order. + */ suspend inline fun Iterable.mapAsync(crossinline transform: suspend (E) -> R): List { return coroutineScope { map { element -> From 4f816ed12235a0978db275b6e4c563f447e746ce Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 20:05:08 +0100 Subject: [PATCH 31/32] chore(abi): dump abi --- .../api/surf-api-core-api.api | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 135d3ec25..be9cd3324 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -6482,6 +6482,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/Colors { public static final field LIGHT_PURPLE Lnet/kyori/adventure/text/format/NamedTextColor; public static final field NOTE Lnet/kyori/adventure/text/format/TextColor; public static final field PREFIX Lnet/kyori/adventure/text/Component; + public static final field PREFIX_CHARACTER C public static final field PREFIX_COLOR Lnet/kyori/adventure/text/format/TextColor; public static final field PRIMARY Lnet/kyori/adventure/text/format/TextColor; public static final field RED Lnet/kyori/adventure/text/format/NamedTextColor; @@ -6881,9 +6882,13 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/ComponentBuil public static fun appendErrorPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendInfoPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewErrorPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewErrorPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewInfoPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewInfoPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewSuccessPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewSuccessPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewWarningPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewWarningPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendSuccessPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendWarningPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun aqua (Ldev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;C[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7093,12 +7098,16 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/SurfComponent public static fun appendMap (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static synthetic fun appendMap$default (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewErrorPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewErrorPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewInfoPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewInfoPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewSuccessPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewSuccessPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewWarningPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewWarningPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewline (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewlineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7248,6 +7257,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/DarkSp public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor : dev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColor { public fun appendErrorPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun appendNewErrorPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public fun appendNewErrorPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun error (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;C[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun error (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun error (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/String;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7257,6 +7267,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor$DefaultImpls { public static fun appendErrorPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewErrorPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewErrorPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;CLnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7270,6 +7281,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/ErrorC public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor : dev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColor { public fun appendInfoPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun appendNewInfoPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public fun appendNewInfoPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun info (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;C[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun info (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun info (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/String;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7279,6 +7291,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor$DefaultImpls { public static fun appendInfoPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewInfoPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewInfoPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;CLnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/InfoComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/String;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7345,6 +7358,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/Spacer public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor : dev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColor { public fun appendNewSuccessPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public fun appendNewSuccessPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun appendSuccessPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun success (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;C[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun success (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7354,6 +7368,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor$DefaultImpls { public static fun appendNewSuccessPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewSuccessPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendSuccessPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;CLnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SuccessComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7372,9 +7387,13 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/SurfCo public static fun appendErrorPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendInfoPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendNewErrorPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewErrorPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewInfoPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewInfoPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewSuccessPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewSuccessPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendNewWarningPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewWarningPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendSuccessPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun appendWarningPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/SurfComponentBuilderColors;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;CLnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7461,6 +7480,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/Variab public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor : dev/slne/surf/surfapi/core/api/messages/builder/ComponentBuilderColor { public fun appendNewWarningPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public fun appendNewWarningPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun appendWarningPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun warning (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;C[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public fun warning (Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; @@ -7470,6 +7490,7 @@ public abstract interface class dev/slne/surf/surfapi/core/api/messages/builder/ public final class dev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor$DefaultImpls { public static fun appendNewWarningPrefixedLine (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; + public static fun appendNewWarningPrefixedLineAsync (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun appendWarningPrefix (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;CLnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; public static fun coloredComponent (Ldev/slne/surf/surfapi/core/api/messages/builder/colors/WarningComponentBuilderColor;Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder;Ljava/lang/Number;Lnet/kyori/adventure/text/format/TextColor;[Lnet/kyori/adventure/text/format/TextDecoration;)Ldev/slne/surf/surfapi/core/api/messages/builder/SurfComponentBuilder; From 824ad362299e4fc6e74ce11e9c6ad6f4b30181a4 Mon Sep 17 00:00:00 2001 From: twisti Date: Tue, 10 Feb 2026 20:08:48 +0100 Subject: [PATCH 32/32] refactor: deprecate getEM_DASH function for binary compatibility and update version to 1.21.11-2.59.0 --- gradle.properties | 2 +- surf-api-core/surf-api-core-api/api/surf-api-core-api.api | 1 + .../slne/surf/surfapi/core/api/messages/CommonComponents.kt | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0b3c9747d..00508fc71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.58.0 +version=1.21.11-2.59.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index be9cd3324..cb4a6d693 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -6519,6 +6519,7 @@ public final class dev/slne/surf/surfapi/core/api/messages/CommonComponents { public static synthetic fun formatMap$default (Ldev/slne/surf/surfapi/core/api/messages/CommonComponents;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Lnet/kyori/adventure/text/Component; public final fun formatTime-gRj5Bb8 (JZZLnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/format/TextColor;)Lnet/kyori/adventure/text/Component; public static synthetic fun formatTime-gRj5Bb8$default (Ldev/slne/surf/surfapi/core/api/messages/CommonComponents;JZZLnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/format/TextColor;ILjava/lang/Object;)Lnet/kyori/adventure/text/Component; + public final synthetic fun getEM_DASH ()Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Z)Lnet/kyori/adventure/text/TextComponent; public final fun renderDisconnectMessage (Lnet/kyori/adventure/text/TextComponent$Builder;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lnet/kyori/adventure/text/TextComponent; diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt index 3f67e5a1a..49cbfaeea 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/messages/CommonComponents.kt @@ -67,6 +67,10 @@ object CommonComponents { @JvmField val EM_DASH = text("—", SPACER) + @Suppress("FunctionName") + @Deprecated("Binary compatibility", ReplaceWith("EM_DASH"), DeprecationLevel.HIDDEN) + fun getEM_DASH() = EM_DASH + /** * A clickable Discord link component (`discord.gg/castcrafter`). */