diff --git a/gradle.properties b/gradle.properties index de8d5e8f9..f1e646c9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.2 group=dev.slne.surf.api -version=3.11.2 +version=3.12.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt index 71faad8aa..9d02aa633 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt @@ -4,8 +4,10 @@ import dev.slne.surf.api.core.messages.Colors import dev.slne.surf.api.core.server.listener.CoreListenerManager import dev.slne.surf.api.core.server.util.PlayerSkinFetcher import org.jetbrains.annotations.MustBeInvokedByOverriders +import java.util.concurrent.atomic.AtomicBoolean abstract class CoreInstance { + private val bootstrapping = AtomicBoolean(true) @MustBeInvokedByOverriders open suspend fun bootstrap() { @@ -14,6 +16,7 @@ abstract class CoreInstance { @MustBeInvokedByOverriders open suspend fun onLoad() { + bootstrapping.set(false) } @MustBeInvokedByOverriders @@ -30,4 +33,6 @@ abstract class CoreInstance { PlayerSkinFetcher Colors } + + fun isBootstrapping(): Boolean = bootstrapping.get() } \ No newline at end of file diff --git a/surf-api-core/surf-api-core/api/surf-api-core.api b/surf-api-core/surf-api-core/api/surf-api-core.api index e2c721426..ded822807 100644 --- a/surf-api-core/surf-api-core/api/surf-api-core.api +++ b/surf-api-core/surf-api-core/api/surf-api-core.api @@ -207,6 +207,182 @@ public final class dev/slne/surf/api/core/config/SurfConfigApiKt { public abstract interface annotation class dev/slne/surf/api/core/config/YamlConfigFileNamePattern : java/lang/annotation/Annotation { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Contains : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Contains$Companion; + public abstract fun value ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/Contains$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Directory : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Directory$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/Directory$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/DisallowValues : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/DisallowValues$Companion; + public abstract fun values ()[Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/DisallowValues$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/EndsWith : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/EndsWith$Companion; + public abstract fun suffix ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/EndsWith$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/ExistingFile : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/ExistingFile$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/ExistingFile$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxDuration : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxDuration$Companion; + public abstract fun seconds ()J +} + +public final class dev/slne/surf/api/core/config/constraints/MaxDuration$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxLength : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxLength$Companion; + public abstract fun max ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MaxLength$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxNumber$Companion; + public abstract fun max ()D +} + +public final class dev/slne/surf/api/core/config/constraints/MaxNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxSize : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxSize$Companion; + public abstract fun max ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MaxSize$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinDuration : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinDuration$Companion; + public abstract fun seconds ()J +} + +public final class dev/slne/surf/api/core/config/constraints/MinDuration$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinLength : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinLength$Companion; + public abstract fun min ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MinLength$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinNumber$Companion; + public abstract fun min ()D +} + +public final class dev/slne/surf/api/core/config/constraints/MinNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinSize : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinSize$Companion; + public abstract fun min ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MinSize$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Namespace : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Namespace$Companion; + public abstract fun namespace ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/Namespace$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NegativeNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NegativeNumber$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NegativeNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NoDuplicates : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NoDuplicates$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NoDuplicates$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NotBlank : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NotBlank$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NotBlank$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NotEmpty : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NotEmpty$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NotEmpty$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/PositiveNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/PositiveNumber$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/PositiveNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Range : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Range$Companion; + public abstract fun max ()D + public abstract fun min ()D +} + +public final class dev/slne/surf/api/core/config/constraints/Range$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/StartsWith : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/StartsWith$Companion; + public abstract fun prefix ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/StartsWith$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Trimmed : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Trimmed$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/Trimmed$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/WritablePath : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/WritablePath$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/WritablePath$Companion { +} + public final class dev/slne/surf/api/core/config/manager/LoadConfigException : java/lang/RuntimeException { public static final field Companion Ldev/slne/surf/api/core/config/manager/LoadConfigException$Companion; public fun (Ljava/lang/String;)V @@ -287,17 +463,17 @@ public abstract class dev/slne/surf/api/core/config/serializer/SpongeConfigSeria public final fun getClassSerializers ()Lit/unimi/dsi/fastutil/objects/Object2ObjectMap; public final fun getTypeTokenSerializers ()Lit/unimi/dsi/fastutil/objects/Object2ObjectMap; public final fun registerClassSerializer (Ljava/lang/Class;Lorg/spongepowered/configurate/serialize/TypeSerializer;)V + protected fun registerDefaults (Lorg/spongepowered/configurate/serialize/TypeSerializerCollection$Builder;)V public final fun registerTypeTokenSerializer (Lio/leangen/geantyref/TypeToken;Lorg/spongepowered/configurate/serialize/TypeSerializer;)V public final fun unregisterSerializer (Ljava/lang/Class;)V public final fun unregisterTypeTokenSerializer (Lio/leangen/geantyref/TypeToken;)V } -public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$ComponentSerializer : org/spongepowered/configurate/serialize/TypeSerializer { +public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$ComponentSerializer : org/spongepowered/configurate/serialize/ScalarSerializer$Annotated { public fun ()V - public synthetic fun deserialize (Ljava/lang/reflect/Type;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/lang/Object; - public fun deserialize (Ljava/lang/reflect/Type;Lorg/spongepowered/configurate/ConfigurationNode;)Lnet/kyori/adventure/text/Component; - public synthetic fun serialize (Ljava/lang/reflect/Type;Ljava/lang/Object;Lorg/spongepowered/configurate/ConfigurationNode;)V - public fun serialize (Ljava/lang/reflect/Type;Lnet/kyori/adventure/text/Component;Lorg/spongepowered/configurate/ConfigurationNode;)V + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ljava/lang/Object; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Lnet/kyori/adventure/text/Component; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Ljava/util/function/Predicate;)Ljava/lang/Object; } public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$LinkedListSerializer : org/spongepowered/configurate/serialize/AbstractListChildSerializer { @@ -316,6 +492,202 @@ public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializ public static final fun getSurfSpongeConfigSerializers ()Ldev/slne/surf/api/core/config/serializer/SpongeConfigSerializers; } +public abstract class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer : org/spongepowered/configurate/serialize/TypeSerializer$Annotated { + public fun (Lkotlin/jvm/functions/Function1;)V + protected abstract fun createBaseMapType (Ljava/lang/reflect/AnnotatedParameterizedType;)Ljava/lang/reflect/Type; + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/lang/Object; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/util/Map; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Lorg/spongepowered/configurate/ConfigurationNode;)V + public fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/util/Map;Lorg/spongepowered/configurate/ConfigurationNode;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$PrimitiveToPrimitive : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;Ljava/lang/reflect/Type;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$PrimitiveToSomething : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$SomethingToPrimitive : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$SomethingToSomething : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;)V +} + +public abstract interface annotation class dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions : java/lang/annotation/Annotation { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/core/config/type/BooleanOrDefault { + public static final field Companion Ldev/slne/surf/api/core/config/type/BooleanOrDefault$Companion; + public static final field FALSE Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public static final field TRUE Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public synthetic fun (Ljava/lang/Boolean;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Boolean; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/Boolean; + public fun hashCode ()I + public final fun or (Z)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/BooleanOrDefault$Companion { +} + +public final class dev/slne/surf/api/core/config/type/ConfigDuration { + public static final field Companion Ldev/slne/surf/api/core/config/type/ConfigDuration$Companion; + public synthetic fun (JLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun asDuration-UwyO8pc ()J + public final fun component1-UwyO8pc ()J + public final fun component2 ()Ljava/lang/String; + public final fun copy-VtjQ1oo (JLjava/lang/String;)Ldev/slne/surf/api/core/config/type/ConfigDuration; + public static synthetic fun copy-VtjQ1oo$default (Ldev/slne/surf/api/core/config/type/ConfigDuration;JLjava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/ConfigDuration; + public fun equals (Ljava/lang/Object;)Z + public final fun getRawValue ()Ljava/lang/String; + public final fun getValue-UwyO8pc ()J + public fun hashCode ()I + public final fun inWholeSeconds ()J + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/ConfigDuration$Companion { + public final fun format-LRDsOJo (J)Ljava/lang/String; + public final fun parse (Ljava/lang/String;)Ldev/slne/surf/api/core/config/type/ConfigDuration; +} + +public final class dev/slne/surf/api/core/config/type/DurationOrDisabled { + public static final field Companion Ldev/slne/surf/api/core/config/type/DurationOrDisabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/DurationOrDisabled; + public synthetic fun (Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-FghU774 ()Lkotlin/time/Duration; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue-FghU774 ()Lkotlin/time/Duration; + public fun hashCode ()I + public final fun isDisabled ()Z + public final fun or-wmV0flA (J)J + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/DurationOrDisabled$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty : java/lang/annotation/Annotation { +} + +public abstract interface class dev/slne/surf/api/core/config/type/number/DoubleOr { + public fun doubleValue ()D + public abstract fun getValue ()Ljava/lang/Double; + public abstract fun or (D)D +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Default : dev/slne/surf/api/core/config/type/number/DoubleOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default$Companion; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public fun (Ljava/lang/Double;)V + public final fun component1 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/Double;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default;Ljava/lang/Double;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public fun doubleValue ()D + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Double; + public fun hashCode ()I + public fun or (D)D + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Default$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$DefaultImpls { + public static fun doubleValue (Ldev/slne/surf/api/core/config/type/number/DoubleOr;)D +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled : dev/slne/surf/api/core/config/type/number/DoubleOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public fun (Ljava/lang/Double;)V + public final fun component1 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/Double;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled;Ljava/lang/Double;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public fun doubleValue ()D + public final fun enabled ()Z + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Double; + public fun hashCode ()I + public fun or (D)D + public final fun test (Lkotlin/jvm/functions/Function1;)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Serializer : org/spongepowered/configurate/serialize/ScalarSerializer$Annotated { + public static final field INSTANCE Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Serializer; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Ljava/util/function/Predicate;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/api/core/config/type/number/IntOr { + public abstract fun getValue ()Ljava/lang/Integer; + public fun intValue ()I + public fun isDefined ()Z + public abstract fun or (I)I +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Default : dev/slne/surf/api/core/config/type/number/IntOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/IntOr$Default$Companion; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public fun (Ljava/lang/Integer;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;)Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/IntOr$Default;Ljava/lang/Integer;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Integer; + public fun hashCode ()I + public fun intValue ()I + public fun isDefined ()Z + public fun or (I)I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Default$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$DefaultImpls { + public static fun intValue (Ldev/slne/surf/api/core/config/type/number/IntOr;)I + public static fun isDefined (Ldev/slne/surf/api/core/config/type/number/IntOr;)Z +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Disabled : dev/slne/surf/api/core/config/type/number/IntOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public fun (Ljava/lang/Integer;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;)Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled;Ljava/lang/Integer;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public final fun enabled ()Z + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Integer; + public fun hashCode ()I + public fun intValue ()I + public fun isDefined ()Z + public fun or (I)I + public final fun test (Lkotlin/jvm/functions/Function1;)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Disabled$Companion { +} + public abstract class dev/slne/surf/api/core/event/SurfAsyncEvent : dev/slne/surf/api/core/event/SurfEvent { public fun ()V public final fun call (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt new file mode 100644 index 000000000..b1f571f8f --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.api.core.config.constraints + +internal fun Any?.configSizeOrNull(): Int? = when (this) { + null -> null + is Collection<*> -> size + is Map<*, *> -> size + is Array<*> -> size + is CharSequence -> length + else -> null +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt new file mode 100644 index 000000000..46b770659 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a string config value to contain a specific substring. + * + * This annotation ensures that the annotated string contains the specified substring. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property value The substring that must be present in the string value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Contains(val value: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Contains, type: Type): Constraint = { value -> + if (value != null && !value.contains(data.value)) { + throw SerializationException("String must contain '${data.value}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt new file mode 100644 index 000000000..183880ded --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt @@ -0,0 +1,43 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.io.path.exists +import kotlin.io.path.isDirectory + +/** + * Ensures that a configuration value represents a valid, existing directory. + * + * This annotation validates that the annotated value points to a directory that exists on the filesystem. + * If the value does not exist, or if it does not represent a directory, validation will fail with a + * `SerializationException`. + * + * This constraint supports different types of input, such as `Path`, `File`, and `String`, converting + * them to a `Path` internally using the helper method `asPathOrNull`. + * + * Constraints: + * - The value must point to an existing directory. + * - The value must be convertible to a `Path` object. + * + * Intended for use on fields in configuration classes. + * + * Validation failure will throw a `SerializationException` with a descriptive message indicating the + * problem with the directory path. + * + * Associated factory implementation provides the actual validation logic. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Directory { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Directory, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (!path.exists() || !path.isDirectory()) { + throw SerializationException("Path must point to an existing directory: $path") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt new file mode 100644 index 000000000..740e6ad88 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Prevents specific values from being assigned to the annotated field. + * + * This annotation is used to define a set of disallowed string values for a field. If the annotated field's + * value matches any of the specified disallowed values (case-insensitive), a `SerializationException` is thrown. + * + * @property values The array of string values that are disallowed. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class DisallowValues(vararg val values: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: DisallowValues, type: Type): Constraint = { value -> + if (value != null && data.values.any { it.equals(value.toString(), ignoreCase = true) }) { + throw SerializationException("Value '$value' is not allowed") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt new file mode 100644 index 000000000..c013118f1 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a string config value ends with a specified suffix. + * + * This annotation ensures that the annotated string ends with the provided suffix. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property suffix The suffix that the string value must end with. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class EndsWith(val suffix: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: EndsWith, type: Type): Constraint = { value -> + if (value != null && !value.endsWith(data.suffix)) { + throw SerializationException("String must end with '${data.suffix}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt new file mode 100644 index 000000000..047598548 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt @@ -0,0 +1,52 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.io.File +import java.lang.reflect.Type +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile + +/** + * Requires that the annotated field refers to an existing file. + * + * This annotation ensures that the value of the annotated field corresponds to + * a valid file path that exists and is a regular file. If the validation fails, + * a `SerializationException` is thrown with a descriptive error message. + * + * The value can be: + * - A `Path` object. + * - A `File` object. + * - A `String` representing the file path. + * + * If the value is not convertible to a file path or the file does not exist + * or is not a regular file, the validation fails. + * + * This annotation is typically used for configuration values to ensure + * that specified file paths are valid at runtime. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class ExistingFile { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: ExistingFile, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (!path.exists() || !path.isRegularFile()) { + throw SerializationException("Path must point to an existing file: $path") + } + } + } + } +} + +internal fun Any?.asPathOrNull(): Path? { + return when (this) { + null -> null + is Path -> this + is File -> toPath() + is String -> runCatching { Path.of(this) }.getOrNull() + else -> null + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt new file mode 100644 index 000000000..1bd481223 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.core.config.constraints + +import dev.slne.surf.api.core.config.type.ConfigDuration +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Annotation for constraining a `ConfigDuration`'s value to a maximum duration. + * + * This annotation ensures that the duration value of the annotated field does not exceed + * the specified number of seconds. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property seconds The maximum duration in seconds that the annotated field can have. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxDuration(val seconds: Long) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxDuration, type: Type): Constraint = { value -> + if (value != null && value.value.inWholeSeconds > data.seconds) { + throw SerializationException("Duration is too long: ${value.value}, expected <= ${data.seconds}s") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt new file mode 100644 index 000000000..38f979dee --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a string config value does not exceed a specified maximum length. + * + * This annotation is used to ensure that the length of the annotated string + * is less than or equal to the specified maximum value. If the validation fails, + * a `SerializationException` is thrown with a descriptive error message. + * + * @property max The maximum allowed length for the string value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxLength(val max: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxLength, type: Type): Constraint = { value -> + if (value != null && value.length > data.max) { + throw SerializationException("String is too long: ${value.length}, expected <= ${data.max}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt new file mode 100644 index 000000000..cbe07763f --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt @@ -0,0 +1,39 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is less than or equal to [max]. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class RateLimitConfig( + * @field:MaxNumber(100.0) + * val percentage: Double = 50.0 + * ) + * ``` + * + * @property max The inclusive maximum value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxNumber(val max: Double) { + companion object { + + /** + * Configurate constraint factory for [MaxNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: MaxNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() > data.max) { + throw SerializationException(type, "Number is too big: $number, expected <= ${data.max}") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt new file mode 100644 index 000000000..8edf9080c --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Specifies a maximum size constraint for collections, maps, arrays, or strings. + * + * This annotation enforces that the size or length of the annotated value does not exceed + * the specified maximum. If the validation fails, a `SerializationException` is thrown + * with a descriptive error message. + * + * @property max The maximum allowed size or length for the annotated value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxSize(val max: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxSize, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size > data.max) { + throw SerializationException("Collection size is too large: $size, expected <= ${data.max}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt new file mode 100644 index 000000000..857959695 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.core.config.constraints + +import dev.slne.surf.api.core.config.type.ConfigDuration +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Specifies a minimum duration constraint for configuration values annotated with this annotation. + * + * This annotation ensures that durations provided in the configuration meet or exceed + * the specified minimum value in seconds. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property seconds The minimum allowed duration in seconds. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinDuration(val seconds: Long) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinDuration, type: Type): Constraint = { value -> + if (value != null && value.value.inWholeSeconds < data.seconds) { + throw SerializationException("Duration is too short: ${value.value}, expected >= ${data.seconds}s") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt new file mode 100644 index 000000000..81f5985bd --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a string configuration value meets a minimum length requirement. + * + * The string is considered valid if its length is greater than or equal to the specified [min] value. + * Null values are ignored, making this annotation compatible with nullable string fields. + * + * @property min The inclusive minimum number of characters required for the string. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinLength(val min: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinLength, type: Type): Constraint = { value -> + if (value != null && value.length < data.min) { + throw SerializationException("String is too short: ${value.length}, expected >= ${data.min}") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt new file mode 100644 index 000000000..1d6edca4b --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is greater than or equal to [min]. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class EconomyConfig( + * @field:MinNumber(0.0) + * val startingBalance: Double = 100.0 + * ) + * ``` + * + * @property min The inclusive minimum value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinNumber(val min: Double) { + companion object { + + /** + * Configurate constraint factory for [MinNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: MinNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() < data.min) { + throw SerializationException(type, "Number is too small: $number, expected >= ${data.min}") + } + } + } + } +} + diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt new file mode 100644 index 000000000..823d7b71d --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Specifies a minimum size constraint for collections, maps, arrays, or strings. + * + * This annotation enforces that the size or length of the annotated value is at least + * the specified minimum. If the validation fails, a `SerializationException` is thrown + * with a descriptive error message. + * + * @property min The minimum required size or length for the annotated value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinSize(val min: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinSize, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size < data.min) { + throw SerializationException("Collection size is too small: $size, expected >= ${data.min}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt new file mode 100644 index 000000000..250b17918 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.core.config.constraints + +import net.kyori.adventure.key.Key +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a key value conforms to a specific namespace. + * + * This annotation validates that the `namespace` of a key matches the expected namespace + * defined in the `Namespace` annotation. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property namespace The required namespace for the key value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Namespace(val namespace: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Namespace, type: Type): Constraint = { value -> + if (value != null && value.namespace() != data.namespace) { + throw SerializationException("Key must use namespace '${data.namespace}', got '${value.namespace()}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt new file mode 100644 index 000000000..f28f51da0 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt @@ -0,0 +1,25 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a numeric config value is strictly negative. + * + * This annotation validates that the annotated numeric value is less than zero. If the + * validation fails, a `SerializationException` is thrown with a descriptive error message. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NegativeNumber { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NegativeNumber, type: Type): Constraint = { value -> + if (value != null && value.toDouble() >= 0.0) { + throw SerializationException("Number must be negative: $value, expected < 0") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt new file mode 100644 index 000000000..23022e3b5 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a collection or array does not contain duplicate elements. + * + * This annotation validates that the annotated value, if it is a collection or array, + * contains only unique elements. If duplicates are found, a `SerializationException` + * is thrown with a descriptive error message. + * + * Supported types: + * - `Iterable`: Validates all elements in the iterable. + * - `Array`: Validates all elements in the array. + * + * Values that are `null` or of unsupported types are ignored during validation. + * + * Exceptions: + * - `SerializationException`: Thrown if the collection or array contains duplicate elements. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NoDuplicates { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NoDuplicates, type: Type): Constraint = Constraint { value -> + val elements = when (value) { + null -> return@Constraint + is Iterable<*> -> value.toList() + is Array<*> -> value.toList() + else -> return@Constraint + } + + if (elements.size != elements.toSet().size) { + throw SerializationException("Collection must not contain duplicate values") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt new file mode 100644 index 000000000..b862e595c --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a string config value to contain at least one non-whitespace character. + * + * Usage: + * ```kotlin + * @field:NotBlank + * val name: String = "default" + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NotBlank { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NotBlank, type: Type): Constraint = { value -> + if (value != null && value.isBlank()) { + throw SerializationException("String must not be blank") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt new file mode 100644 index 000000000..b939aeee4 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a collection, map, array, or string config value is not empty. + * + * This annotation enforces that the size or length of the annotated value is greater + * than 0. If the validation fails, a `SerializationException` is thrown with a + * descriptive error message. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NotEmpty { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NotEmpty, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size == 0) { + throw SerializationException("Value must not be empty") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt new file mode 100644 index 000000000..cad0081e0 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is strictly positive. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class ServerConfig( + * @field:PositiveNumber + * val maxPlayers: Int = 100 + * ) + * ``` + * + * Values less than or equal to zero are rejected. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class PositiveNumber { + companion object { + /** + * Configurate constraint factory for [PositiveNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: PositiveNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() <= 0) { + throw SerializationException("Number is not positive: $number, expected > 0") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt new file mode 100644 index 000000000..26ca037c5 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt @@ -0,0 +1,31 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a numeric config value to be inside an inclusive range. + * + * Usage: + * ```kotlin + * @field:Range(min = 0.0, max = 1.0) + * val chance: Double = 0.5 + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Range(val min: Double, val max: Double) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Range, type: Type): Constraint = { value -> + if (value != null) { + val double = value.toDouble() + if (double < data.min || double > data.max) { + throw SerializationException("Number is out of range: $value, expected ${data.min}..${data.max}") + } + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt new file mode 100644 index 000000000..255a28ab3 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a string config value starts with a specified prefix. + * + * This annotation ensures that the annotated string begins with the provided prefix. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property prefix The prefix that the string value must start with. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class StartsWith(val prefix: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: StartsWith, type: Type): Constraint = { value -> + if (value != null && !value.startsWith(data.prefix)) { + throw SerializationException("String must start with '${data.prefix}'") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt new file mode 100644 index 000000000..90e28d8eb --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a string config value to have no leading or trailing whitespace. + * + * This validates only; it does not mutate the loaded value. + * + * Usage: + * ```kotlin + * @field:Trimmed + * val id: String = "example" + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Trimmed { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Trimmed, type: Type): Constraint = { value -> + if (value != null && value != value.trim()) { + throw SerializationException("String must not have leading or trailing whitespace") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt new file mode 100644 index 000000000..a41fb48b6 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.io.path.exists +import kotlin.io.path.isWritable + +/** + * Validates that a configuration value represents a writable path when the path exists. + * + * This annotation ensures that the annotated value refers to a file system path + * that is writable if it already exists. If the validation fails for an existing path, + * a `SerializationException` will be thrown with a descriptive error message. + * + * Supported value types include: + * - `Path`: Directly represents a file system path. + * - `File`: Converted to a `Path` for validation. + * - `String`: Parsed as a file system path. + * + * Validation checks: + * - If the path exists, it must be writable. + * - If the value cannot be converted to a valid path, it is ignored during validation. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class WritablePath { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: WritablePath, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (path.exists() && !path.isWritable()) { + throw SerializationException("Path must be writable: $path") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt new file mode 100644 index 000000000..392d24c0b --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt @@ -0,0 +1,47 @@ +package dev.slne.surf.api.core.config.serializer + +import io.leangen.geantyref.GenericTypeReflector +import io.leangen.geantyref.TypeToken +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.util.EnumLookup +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Configurate scalar serializer for enum values. + * + * Enum constants are read using Configurate's enum lookup logic. If no direct match is found, + * underscores are converted to dashes and lookup is attempted again. + */ +internal object EnumValueSerializer : ScalarSerializer.Annotated>(object : TypeToken>() {}) { + + /** + * Resolves a serialized value to an enum constant of the requested enum type. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Enum<*> { + val constant = obj.toString() + val typeClass = GenericTypeReflector.erase(type.type).asSubclass(Enum::class.java) + + var foundEnum = EnumLookup.lookupEnum(typeClass, constant) + if (foundEnum == null) { + foundEnum = EnumLookup.lookupEnum(typeClass, constant.replace("_", "-")) + } + if (foundEnum == null) { + val joinedEnumOptions = typeClass.enumConstants.joinToString(limit = 10) + throw SerializationException( + type.type, + "Invalid enum constant '$constant' for ${typeClass.simpleName}, expected one of: [$joinedEnumOptions]" + ) + } + + return foundEnum + } + + /** + * Serializes an enum value using its constant name. + */ + override fun serialize(type: AnnotatedType, item: Enum<*>, typeSupported: Predicate>): Any { + return item.name + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt new file mode 100644 index 000000000..3407c871d --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import java.io.File +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object FileSerializer : ScalarSerializer.Annotated(File::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): File { + return File(obj.toString()) + } + + override fun serialize(type: AnnotatedType, item: File, typeSupported: Predicate>): Any { + return item.path + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt new file mode 100644 index 000000000..d60cac238 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.core.config.serializer + +import net.kyori.adventure.key.InvalidKeyException +import net.kyori.adventure.key.Key +import org.spongepowered.configurate.serialize.ScalarSerializer.Annotated +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Configurate scalar serializer for Adventure [Key] values. + * + * Values are read from and written to their canonical string representation. + */ +internal object KeySerializer : Annotated(Key::class.java) { + + /** + * Parses a string value into an Adventure [Key]. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Key { + try { + return Key.key(obj.toString()) + } catch (e: InvalidKeyException) { + throw SerializationException(Key::class.java, e) + } + } + + /** + * Serializes [item] as its canonical string representation. + */ + override fun serialize(type: AnnotatedType, item: Key, typeSupported: Predicate>): Any { + return item.asString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt new file mode 100644 index 000000000..fae49a4e1 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.util.function.Predicate + +internal object PathSerializer : ScalarSerializer.Annotated(Path::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Path { + try { + return Path.of(obj.toString()) + } catch (e: InvalidPathException) { + throw SerializationException(Path::class.java, "$obj($type) is not a valid path", e) + } + } + + override fun serialize(type: AnnotatedType, item: Path, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt new file mode 100644 index 000000000..13559dc41 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +internal object PatternSerializer : ScalarSerializer.Annotated(Pattern::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Pattern { + return try { + Pattern.compile(obj.toString()) + } catch (e: PatternSyntaxException) { + throw SerializationException(Pattern::class.java, "$obj($type) is not a valid pattern", e) + } + } + + override fun serialize(type: AnnotatedType, item: Pattern, typeSupported: Predicate>): Any { + return item.pattern() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt new file mode 100644 index 000000000..9811fb997 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import java.util.regex.PatternSyntaxException + +internal object RegexSerializer : ScalarSerializer.Annotated(Regex::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Regex { + return try { + obj.toString().toRegex() + } catch (e: PatternSyntaxException) { + throw SerializationException(Regex::class.java, "$obj($type) is not a valid regex", e) + } + } + + override fun serialize(type: AnnotatedType, item: Regex, typeSupported: Predicate>): Any { + return item.pattern + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt index 8dfb04f8a..e36053d5f 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt @@ -1,23 +1,33 @@ package dev.slne.surf.api.core.config.serializer +import dev.slne.surf.api.core.config.constraints.* +import dev.slne.surf.api.core.config.serializer.collection.map.FastutilMapSerializer +import dev.slne.surf.api.core.config.serializer.collection.map.MapSerializer +import dev.slne.surf.api.core.config.type.BooleanOrDefault +import dev.slne.surf.api.core.config.type.ConfigDuration +import dev.slne.surf.api.core.config.type.DurationOrDisabled +import dev.slne.surf.api.core.config.type.number.DoubleOr +import dev.slne.surf.api.core.config.type.number.IntOr import dev.slne.surf.api.core.minimessage.SurfMiniMessageHolder import dev.slne.surf.api.core.util.freeze import dev.slne.surf.api.core.util.mutableObject2ObjectMapOf import dev.slne.surf.api.core.util.requiredService import io.leangen.geantyref.TypeToken +import it.unimi.dsi.fastutil.ints.* +import it.unimi.dsi.fastutil.longs.* +import it.unimi.dsi.fastutil.objects.* import net.kyori.adventure.text.Component -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.serialize.AbstractListChildSerializer -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import org.spongepowered.configurate.serialize.TypeSerializerCollection +import org.jetbrains.annotations.MustBeInvokedByOverriders +import org.spongepowered.configurate.kotlin.dataClassFieldDiscoverer +import org.spongepowered.configurate.kotlin.extensions.addConstraint +import org.spongepowered.configurate.objectmapping.ObjectMapper +import org.spongepowered.configurate.serialize.* import org.spongepowered.configurate.util.CheckedConsumer import java.lang.reflect.AnnotatedParameterizedType import java.lang.reflect.AnnotatedType -import java.lang.reflect.Type import java.util.* import java.util.function.Consumer +import java.util.function.Predicate val surfSpongeConfigSerializers = requiredService() @@ -32,7 +42,6 @@ abstract class SpongeConfigSerializers { val classSerializers get() = _classSerializers.freeze() init { - registerClassSerializer(Component::class.java, ComponentSerializer()) registerTypeTokenSerializer(LinkedListSerializer.TYPE, LinkedListSerializer()) } @@ -56,6 +65,10 @@ abstract class SpongeConfigSerializers { _typeTokenSerializers.remove(typeToken) } + @MustBeInvokedByOverriders + protected open fun registerDefaults(builder: TypeSerializerCollection.Builder) { + } + /** * Registers custom serializers with the provided builder. */ @@ -69,25 +82,124 @@ abstract class SpongeConfigSerializers { builder.register(type as TypeToken, serializer as TypeSerializer) } - builder.registerAnnotatedObjects(objectMapperFactory()) + builder.register(ComponentSerializer()) + builder.register(EnumValueSerializer) + builder.register(KeySerializer) + builder.register(ConfigDuration.Serializer) + builder.register(BooleanOrDefault.Serializer) + builder.register(DurationOrDisabled.Serializer) + builder.register(IntOr.Default.Serializer) + builder.register(IntOr.Disabled.Serializer) + builder.register(DoubleOr.Default.Serializer) + builder.register(DoubleOr.Disabled.Serializer) + builder.register(UuidSerializer) + builder.register(RegexSerializer) + builder.register(PatternSerializer) + builder.register(UriSerializer) + builder.register(UrlSerializer) + builder.register(PathSerializer) + builder.register(FileSerializer) + builder.register(TextColorSerializer) + + //region fastutil maps + // @formatter:off + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2BooleanOpenHashMap(it as Map<*, Boolean>) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ByteOpenHashMap(it as Map<*, Byte>) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2CharOpenHashMap(it as Map<*, Char>) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2DoubleOpenHashMap(it as Map<*, Double>) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2FloatOpenHashMap(it as Map<*, Float>) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2IntOpenHashMap(it as Map<*, Int>) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2LongOpenHashMap(it as Map<*, Long>) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ShortOpenHashMap(it as Map<*, Short>) }, java.lang.Short.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Reference2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2BooleanOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2ByteOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Byte.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2CharOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Character.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2DoubleOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Double.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2FloatOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Float.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2IntOpenHashMap(it as Map) }, Integer.TYPE, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2LongOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ObjectOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ReferenceOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2ShortOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Short.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2BooleanOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2ByteOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Byte.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2CharOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Character.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2DoubleOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Double.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2FloatOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Float.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2IntOpenHashMap(it as Map) }, java.lang.Long.TYPE, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ObjectOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ReferenceOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2ShortOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Short.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2BooleanOpenHashMap(it as Map<*, Boolean>) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ByteOpenHashMap(it as Map<*, Byte>) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2CharOpenHashMap(it as Map<*, Char>) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2DoubleOpenHashMap(it as Map<*, Double>) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2FloatOpenHashMap(it as Map<*, Float>) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2IntOpenHashMap(it as Map<*, Int>) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2LongOpenHashMap(it as Map<*, Long>) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ReferenceOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ShortOpenHashMap(it as Map<*, Short>) }, java.lang.Short.TYPE)) + // @formatter:on + //endregion + + // register after fastutil specific serializers have been registered + builder.register(MapSerializer.TYPE, MapSerializer(false)) + + builder.registerAnnotatedObjects( + ObjectMapper.factoryBuilder() + .addDiscoverer(dataClassFieldDiscoverer()) + .addConstraint(PositiveNumber.Companion.Factory) + .addConstraint(NegativeNumber.Companion.Factory) + .addConstraint(MinNumber.Companion.Factory) + .addConstraint(MaxNumber.Companion.Factory) + .addConstraint(NotBlank.Companion.Factory) + .addConstraint(Trimmed.Companion.Factory) + .addConstraint(MaxLength.Companion.Factory) + .addConstraint(MinLength.Companion.Factory) + .addConstraint(StartsWith.Companion.Factory) + .addConstraint(EndsWith.Companion.Factory) + .addConstraint(Contains.Companion.Factory) + .addConstraint(Range.Companion.Factory) + .addConstraint(MinSize.Companion.Factory) + .addConstraint(MaxSize.Companion.Factory) + .addConstraint(NotEmpty.Companion.Factory) + .addConstraint(NoDuplicates.Companion.Factory) + .addConstraint(MinDuration.Companion.Factory) + .addConstraint(MaxDuration.Companion.Factory) + .addConstraint(DisallowValues.Companion.Factory) + .addConstraint(Namespace.Companion.Factory) + .addConstraint(ExistingFile.Companion.Factory) + .addConstraint(Directory.Companion.Factory) + .addConstraint(WritablePath.Companion.Factory) + .build() + ) + + registerDefaults(builder) } /** - * Serializer for [Component] objects in Sponge configurations. + * Configurate scalar serializer for Adventure [Component] values. + * + * Components are serialized as MiniMessage strings and deserialized through the shared + * Surf MiniMessage instance. */ - class ComponentSerializer : TypeSerializer { - override fun deserialize(type: Type?, node: ConfigurationNode): Component { - val message = node.string ?: return Component.empty() + class ComponentSerializer : ScalarSerializer.Annotated(Component::class.java) { - return SurfMiniMessageHolder.miniMessage().deserialize(message) + /** + * Deserializes a MiniMessage string into a [Component]. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Component { + return SurfMiniMessageHolder.miniMessage().deserialize(obj.toString()) } - override fun serialize(type: Type?, obj: Component?, node: ConfigurationNode) { - if (obj == null) { - return - } - - node.set(SurfMiniMessageHolder.miniMessage().serialize(obj)) + /** + * Serializes [item] into a MiniMessage string. + */ + override fun serialize(type: AnnotatedType, item: Component, typeSupported: Predicate>): Any { + return SurfMiniMessageHolder.miniMessage().serialize(item) } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt new file mode 100644 index 000000000..9b056b15b --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.core.config.serializer + +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object TextColorSerializer : ScalarSerializer.Annotated(TextColor::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): TextColor { + val input = obj.toString().trim() + + NamedTextColor.NAMES.value(input)?.let { return it } + TextColor.fromHexString(input)?.let { return it } + + val rgb = input.split(',', ';').map { it.trim() } + if (rgb.size == 3) { + val red = rgb[0].toIntOrNull() + val green = rgb[1].toIntOrNull() + val blue = rgb[2].toIntOrNull() + + if (red != null && green != null && blue != null) { + return TextColor.color(red, green, blue) + } + } + + throw SerializationException(TextColor::class.java, "$obj($type) is not a valid text color") + } + + override fun serialize(type: AnnotatedType, item: TextColor, typeSupported: Predicate>): Any { + return NamedTextColor.namedColor(item.value())?.toString() ?: item.asHexString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt new file mode 100644 index 000000000..f9b134180 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.net.URI +import java.net.URISyntaxException +import java.util.function.Predicate + +internal object UriSerializer : ScalarSerializer.Annotated(URI::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): URI { + return try { + URI(obj.toString()) + } catch (e: URISyntaxException) { + throw SerializationException(URI::class.java, "$obj($type) is not a valid URI", e) + } + } + + override fun serialize(type: AnnotatedType, item: URI, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt new file mode 100644 index 000000000..53e5bdcf8 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.net.MalformedURLException +import java.net.URL +import java.util.function.Predicate + +@Suppress("DEPRECATION") +internal object UrlSerializer : ScalarSerializer.Annotated(URL::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): URL { + return try { + URL(obj.toString()) + } catch (e: MalformedURLException) { + throw SerializationException(URL::class.java, "$obj($type) is not a valid URL", e) + } + } + + override fun serialize(type: AnnotatedType, item: URL, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt new file mode 100644 index 000000000..b50d07867 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.* +import java.util.function.Predicate + +internal object UuidSerializer : ScalarSerializer.Annotated(UUID::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): UUID { + return try { + UUID.fromString(obj.toString()) + } catch (e: IllegalArgumentException) { + throw SerializationException(UUID::class.java, "$obj($type) is not a valid UUID", e) + } + } + + override fun serialize(type: AnnotatedType, item: UUID, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt new file mode 100644 index 000000000..a97cdda5a --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt @@ -0,0 +1,191 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +import io.leangen.geantyref.GenericTypeReflector +import io.leangen.geantyref.TypeFactory +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.AnnotatedParameterizedType +import java.lang.reflect.AnnotatedType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +/** + * Serializer bridge for fastutil map implementations. + * + * Configurate does not need to know how to construct each fastutil map directly. Instead, + * this serializer maps the fastutil type to a regular [Map] type with boxed primitive + * key and value types, lets Configurate deserialize that regular map, and then creates + * the requested fastutil implementation via [factory]. + * + * Usage: + * ```kotlin + * builder.register( + * object : TypeToken>() {}, + * FastutilMapSerializer.SomethingToPrimitive>( + * { Object2IntOpenHashMap(it as Map) }, + * Integer.TYPE + * ) + * ) + * ``` + * + * @param M The concrete fastutil map type. + * @param factory Creates the concrete fastutil map from the deserialized regular map. + */ +abstract class FastutilMapSerializer>( + private val factory: (Map) -> M +) : TypeSerializer.Annotated { + + /** + * Deserializes the node as a regular boxed [Map] and converts it into the target + * fastutil map implementation. + */ + override fun deserialize(type: AnnotatedType, node: ConfigurationNode): M { + val mapType = createAnnotatedMapType(type) + val map = node.get(mapType) as? Map ?: emptyMap() + + return factory(map) + } + + /** + * Serializes the fastutil map through a regular boxed [Map] type. + * + * Empty maps are written as `null`. + */ + override fun serialize(type: AnnotatedType, obj: M?, node: ConfigurationNode) { + if (obj.isNullOrEmpty()) { + node.raw(null) + return + } + + node.set(createAnnotatedMapType(type), obj) + } + + + private fun createAnnotatedMapType(type: AnnotatedType): AnnotatedType { + val paramType = type as? AnnotatedParameterizedType + val baseType = createBaseMapType(paramType) + return if (paramType != null) { + GenericTypeReflector.annotate(baseType, paramType.annotations) + } else { + GenericTypeReflector.annotate(baseType) + } + } + + /** + * Creates the regular boxed [Map] type that Configurate should use internally. + * + * @param type The parameterized type, or `null` for non-generic fastutil maps + * (e.g. `Int2IntMap`) where both types are known primitives. + */ + protected abstract fun createBaseMapType(type: AnnotatedParameterizedType?): Type + + /** + * Serializer variant for maps with a regular object-like key and a primitive value. + * + * Example fastutil types: + * - `Object2IntMap` + * - `Reference2LongMap` + */ + class SomethingToPrimitive>( + factory: (Map) -> M, + private val primitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "SomethingToPrimitive requires a parameterized type with a key type argument" + } + return TypeFactory.parameterizedClass( + Map::class.java, + (type.type as ParameterizedType).actualTypeArguments[0], + GenericTypeReflector.box(primitiveType) + ) + } + } + + /** + * Serializer variant for maps with a primitive key and a regular object-like value. + * + * Example fastutil types: + * - `Int2ObjectMap` + * - `Long2ReferenceMap` + */ + class PrimitiveToSomething>( + factory: (Map) -> M, + private val primitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "PrimitiveToSomething requires a parameterized type with a value type argument" + } + return TypeFactory.parameterizedClass( + Map::class.java, + GenericTypeReflector.box(primitiveType), + (type.type as ParameterizedType).actualTypeArguments[0] + ) + } + } + + /** + * Serializer variant for maps where both key and value are represented by type arguments. + * + * Example fastutil types: + * - `Object2ObjectMap` + * - `Reference2ObjectMap` + */ + class SomethingToSomething>( + factory: (Map) -> M, + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "SomethingToSomething requires a parameterized type with key and value type arguments" + } + val typeArgs = (type.type as ParameterizedType).actualTypeArguments + return TypeFactory.parameterizedClass( + Map::class.java, + typeArgs[0], + typeArgs[1] + ) + } + } + + /** + * Serializer variant for maps where both key and value are fixed primitive types. + * + * Use this for non-generic fastutil maps such as `Int2IntMap`, `Long2LongMap`, + * `Int2LongMap`, etc., which have no type parameters. + * + * Example fastutil types: + * - `Int2IntMap` + * - `Long2LongMap` + * - `Int2LongMap` + */ + class PrimitiveToPrimitive>( + factory: (Map) -> M, + private val keyPrimitiveType: Type, + private val valuePrimitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + return TypeFactory.parameterizedClass( + Map::class.java, + GenericTypeReflector.box(keyPrimitiveType), + GenericTypeReflector.box(valuePrimitiveType) + ) + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt new file mode 100644 index 000000000..be94d34e7 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt @@ -0,0 +1,257 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +import io.leangen.geantyref.TypeToken +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import org.spongepowered.configurate.BasicConfigurationNode +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.ConfigurationOptions +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import org.spongepowered.configurate.serialize.TypeSerializerCollection +import java.lang.reflect.AnnotatedParameterizedType +import java.lang.reflect.AnnotatedType + +/** + * Fault-tolerant Configurate serializer for maps. + * + * Invalid individual entries are logged and skipped instead of failing the entire map. + * If [ThrowExceptions] is present on the map type, this serializer delegates to Configurate's + * default map serializer and preserves the default exception behavior. + * + * When [clearInvalids] is enabled, keys that are present in the existing node but not written + * by the current serialized map are removed during serialization. + * + * @param clearInvalids Whether stale keys should be removed from the configuration node + * during serialization. + */ +internal class MapSerializer( + private val clearInvalids: Boolean +) : TypeSerializer.Annotated> { + companion object { + private val logger = ComponentLogger.logger() + + /** + * Type token used to register this serializer for all map types. + */ + val TYPE = object : TypeToken>() {} + } + private val fallback = requireNotNull(TypeSerializerCollection.defaults().get(TYPE)) { + "Could not find default Map serializer" + } + + /** + * Deserializes a map while skipping entries whose key or value cannot be deserialized. + * + * If [ThrowExceptions] is present on [type], the default Configurate map serializer is used. + */ + override fun deserialize(type: AnnotatedType, node: ConfigurationNode): Map<*, *> { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + return fallback.deserialize(type, node) + } + + val map = linkedMapOf() + val rawType = type.type + + if (!node.isMap) { + return map + } + + if (type !is AnnotatedParameterizedType) { + throw SerializationException(rawType, "Raw types are not supported for collections") + } + + val args = type.annotatedActualTypeArguments + if (args.size != 2) { + throw SerializationException(rawType, "Map expected two type arguments!") + } + + val keyType = args[0] + val valueType = args[1] + + val keySerializer = node.options().serializers().get(keyType) + ?: throw SerializationException(rawType, "No type serializer available for key type $keyType") + + val valueSerializer = node.options().serializers().get(valueType) + ?: throw SerializationException(rawType, "No type serializer available for value type $valueType") + + val writeKeyBack = keyType.isAnnotationPresent(WriteKeyBack::class.java) + val keyNode = BasicConfigurationNode.root(node.options()) + val keysToClear = mutableSetOf() + + for ((rawKey, valueNode) in node.childrenMap()) { + val deserializedKey = deserializePart( + keyType, + keySerializer, + "key", + keyNode.set(rawKey), + node.path() + ) + + val deserializedValue = deserializePart( + valueType, + valueSerializer, + "value", + valueNode, + valueNode.path() + ) + + if (deserializedKey == null || deserializedValue == null) { + continue + } + + if (writeKeyBack) { + val shouldKeep = serializePart( + keyType, + keySerializer, + deserializedKey, + "key", + keyNode, + node.path() + ) + + val writtenKey = requireNotNull(keyNode.raw()) { "Key must not be null!" } + + if (shouldKeep && rawKey != writtenKey) { + keysToClear += rawKey + } + } + + map[deserializedKey] = deserializedValue + } + + + if (writeKeyBack) { + for (keyToClear in keysToClear) { + node.node(keyToClear).raw(null) + } + } + + return map + } + + /** + * Serializes a map into [node]. + * + * If [clearInvalids] is enabled, existing child nodes that were not visited while writing + * the current map are removed. + */ + override fun serialize(type: AnnotatedType, obj: Map<*, *>?, node: ConfigurationNode) { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + fallback.serialize(type, obj, node) + return + } + + val rawType = type.type + + if (type !is AnnotatedParameterizedType) { + throw SerializationException(rawType, "Raw types are not supported for collections") + } + + val args = type.annotatedActualTypeArguments + if (args.size != 2) { + throw SerializationException(rawType, "Map expected two type arguments!") + } + + val keyType = args[0] + val valueType = args[1] + + val keySerializer = node.options().serializers().get(keyType) + ?: throw SerializationException(rawType, "No type serializer available for key type $keyType") + + val valueSerializer = node.options().serializers().get(valueType) + ?: throw SerializationException(rawType, "No type serializer available for value type $valueType") + + if (obj.isNullOrEmpty()) { + node.set(emptyMap()) + return + } + + val unvisitedKeys = if (node.empty()) { + node.raw(emptyMap()) + mutableSetOf() + } else { + node.childrenMap().keys.toMutableSet() + } + + val keyNode = BasicConfigurationNode.root(node.options()) + + for ((key, value) in obj) { + if (!serializePart(keyType, keySerializer, key, "key", keyNode, node.path())) { + continue + } + + val keyObj = requireNotNull(keyNode.raw()) { "Key must not be null!" } + val child = node.node(keyObj) + + serializePart(valueType, valueSerializer, value, "value", child, child.path()) + + unvisitedKeys -= keyObj + } + + if (clearInvalids) { + for (unusedChild in unvisitedKeys) { + node.removeChild(unusedChild) + } + } + } + + /** + * Returns an empty linked map for missing map values. + */ + override fun emptyValue(type: AnnotatedType, options: ConfigurationOptions): Map<*, *> { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + return fallback.emptyValue(type, options) ?: linkedMapOf() + } + + return linkedMapOf() + } + + private fun deserializePart( + type: AnnotatedType, + serializer: TypeSerializer<*>, + mapPart: String, + node: ConfigurationNode, + path: Any + ): Any? { + return try { + serializer.deserialize(type, node) + } catch (e: SerializationException) { + e.initPath(node::path) + logger.error( + "Could not deserialize {} {} into {} at {}: {}", + mapPart, + node.raw(), + type, + path, + e.rawMessage() + ) + null + } + } + + @Suppress("UNCHECKED_CAST") + private fun serializePart( + type: AnnotatedType, + serializer: TypeSerializer<*>, + obj: Any?, + mapPart: String, + node: ConfigurationNode, + path: Any + ): Boolean { + return try { + (serializer as TypeSerializer).serialize(type, obj, node) + true + } catch (e: SerializationException) { + e.initPath(node::path) + logger.error( + "Could not serialize {} {} from {} at {}: {}", + mapPart, + obj, + type, + path, + e.rawMessage() + ) + false + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt new file mode 100644 index 000000000..cbda94859 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +/** + * Forces [MapSerializer] to use Configurate's default map serializer behavior. + * + * Without this annotation, [MapSerializer] logs and skips individual invalid map entries. + * With this annotation, serialization and deserialization errors are thrown normally. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val strictMap: @ThrowExceptions Map = emptyMap() + * ) + * ``` + */ +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +annotation class ThrowExceptions \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt new file mode 100644 index 000000000..3ab019c84 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +/** + * Writes normalized map keys back to the configuration after deserialization. + * + * This is useful for key types whose serializer accepts multiple input formats but serializes + * back into one canonical format. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val values: Map<@WriteKeyBack Key, Int> = emptyMap() + * ) + * ``` + * + * If the key serializer reads `minecraft:STONE` but serializes it as `minecraft:stone`, + * the old key is removed and the normalized key is written back. + */ +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +annotation class WriteKeyBack \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt new file mode 100644 index 000000000..75bfb8c22 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt @@ -0,0 +1,89 @@ +package dev.slne.surf.api.core.config.type + +import org.apache.commons.lang3.BooleanUtils +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents a boolean configuration value that can either be explicitly set to `true` or `false`, + * or defer to a caller-provided default value. + * + * The serialized configuration values are: + * - `true` + * - `false` + * - `default` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class FeatureConfig( + * val enabled: BooleanOrDefault = BooleanOrDefault.USE_DEFAULT + * ) + * + * val enabled = config.enabled or globalDefaultEnabled + * ``` + */ +@ConsistentCopyVisibility +data class BooleanOrDefault private constructor(val value: Boolean?) { + + /** + * Returns the configured boolean value, or [other] if this value is configured as `default`. + */ + infix fun or(other: Boolean) = value ?: other + + companion object { + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = BooleanOrDefault(null) + + /** + * Represents an explicitly enabled configuration value. + */ + @JvmField + val TRUE = BooleanOrDefault(true) + + /** + * Represents an explicitly disabled configuration value. + */ + @JvmField + val FALSE = BooleanOrDefault(false) + } + + /** + * Configurate serializer for [BooleanOrDefault]. + */ + internal object Serializer : ScalarSerializer.Annotated(BooleanOrDefault::class.java) { + private const val DEFAULT_VALUE = "default" + + override fun deserialize(type: AnnotatedType, obj: Any): BooleanOrDefault { + if (obj is String) { + if (obj.equals(DEFAULT_VALUE, ignoreCase = true)) { + return USE_DEFAULT + } + + try { + return BooleanOrDefault(BooleanUtils.toBoolean(obj.lowercase(), "true", "false")) + } catch (e: IllegalArgumentException) { + throw SerializationException( + BooleanOrDefault::class.java, + "$obj($type) is not a boolean or '$DEFAULT_VALUE'", + e + ) + } + } else if (obj is Boolean) { + return BooleanOrDefault(obj) + } + + throw SerializationException(BooleanOrDefault::class.java, "$obj($type) is not a boolean or '$DEFAULT_VALUE'") + } + + override fun serialize(type: AnnotatedType, item: BooleanOrDefault, typeSupported: Predicate>): Any { + return item.value ?: DEFAULT_VALUE + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt new file mode 100644 index 000000000..233334847 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt @@ -0,0 +1,84 @@ +package dev.slne.surf.api.core.config.type + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.math.BigDecimal +import java.util.function.Predicate +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * Encapsulates a configuration value representing a duration, with functionality for parsing, + * formatting, and serialization. + * + * @property value The parsed duration value as a [Duration]. + * @property rawValue The original raw input that was used to construct this [ConfigDuration]. + * This is stored for serialization purposes. + */ +data class ConfigDuration( + val value: Duration, + val rawValue: String = format(value) +) { + fun asDuration(): Duration = value + fun inWholeSeconds(): Long = value.inWholeSeconds + + companion object { + private val DURATION_PATTERN = + Regex("""^\s*(-?\d+(?:\.\d+)?)\s*([dhms])\s*$""", RegexOption.IGNORE_CASE) + + fun parse(input: String): ConfigDuration { + val cleaned = input.trim() + val match = DURATION_PATTERN.matchEntire(cleaned) + ?: throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + + val amount = match.groupValues[1].toDoubleOrNull() + ?: throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + + val unit = when (match.groupValues[2].lowercase()) { + "d" -> DurationUnit.DAYS + "h" -> DurationUnit.HOURS + "m" -> DurationUnit.MINUTES + "s" -> DurationUnit.SECONDS + else -> throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + } + + return ConfigDuration(amount.toDuration(unit), cleaned) + } + + fun format(duration: Duration): String { + if (duration.isInfinite()) { + throw SerializationException( + ConfigDuration::class.java, + "$duration is infinite and cannot be serialized" + ) + } + + return when (duration) { + duration.inWholeDays.toDuration(DurationUnit.DAYS) -> "${duration.inWholeDays}d" + duration.inWholeHours.toDuration(DurationUnit.HOURS) -> "${duration.inWholeHours}h" + duration.inWholeMinutes.toDuration(DurationUnit.MINUTES) -> "${duration.inWholeMinutes}m" + else -> "${duration.toDouble(DurationUnit.SECONDS).formatNumber()}s" + } + } + } + + internal object Serializer : ScalarSerializer.Annotated(ConfigDuration::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): ConfigDuration { + return parse(obj.toString()) + } + + public override fun serialize( + type: AnnotatedType, + item: ConfigDuration, + typeSupported: Predicate> + ): Any { + return item.rawValue + } + } +} + +private fun Double.formatNumber(): String { + return BigDecimal.valueOf(this).stripTrailingZeros().toPlainString() +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt new file mode 100644 index 000000000..bc1c9c145 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt @@ -0,0 +1,94 @@ +package dev.slne.surf.api.core.config.type + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import kotlin.time.Duration + +/** + * Represents a duration configuration value that can either contain a Kotlin [Duration], + * or be explicitly disabled. + * + * The serialized configuration values are: + * - duration strings such as `10s`, `5m`, `2h`, or `1d` + * - `disabled` + * + * Usage: + * ```kotlin + * import kotlin.time.Duration.Companion.seconds + * + * @ConfigSerializable + * data class TimeoutConfig( + * val timeout: DurationOrDisabled = DurationOrDisabled.DISABLED + * ) + * + * val timeout = config.timeout or 30.seconds + * + * if (config.timeout.isDisabled()) { + * // Disable timeout handling + * } + * ``` + */ +@ConsistentCopyVisibility +data class DurationOrDisabled private constructor(val value: Duration?) { + + /** + * Returns the configured duration, or [other] if this value is disabled. + */ + infix fun or(other: Duration) = value ?: other + + /** + * Returns `true` if this value is configured as `disabled`. + */ + fun isDisabled() = value == null + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = DurationOrDisabled(null) + } + + /** + * Configurate serializer for [DurationOrDisabled]. + */ + internal object Serializer : ScalarSerializer.Annotated(DurationOrDisabled::class.java) { + + override fun deserialize(type: AnnotatedType, obj: Any): DurationOrDisabled { + if (obj is String) { + if (obj.equals(DISABLED_VALUE, ignoreCase = true)) { + return DISABLED + } + + return try { + DurationOrDisabled( + ConfigDuration.Serializer.deserialize(type, obj).asDuration() + ) + } catch (e: Exception) { + throw SerializationException( + DurationOrDisabled::class.java, + "$obj($type) is not a duration or '$DISABLED_VALUE'", + e + ) + } + } + + throw SerializationException( + DurationOrDisabled::class.java, + "$obj($type) is not a duration or '$DISABLED_VALUE'" + ) + } + + override fun serialize( + type: AnnotatedType, + item: DurationOrDisabled, + typeSupported: Predicate> + ): Any { + return item.value?.let { ConfigDuration.Serializer.serialize(type, ConfigDuration(it), typeSupported) } ?: DISABLED_VALUE + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt new file mode 100644 index 000000000..282bffb45 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.type.number + +/** + * Marks an optional numeric config value so that negative values are treated as the empty state. + * + * This is mainly useful for `IntOr` and `DoubleOr` values where old or user-provided + * negative values should behave like `default` or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class ViewConfig( + * @field:BelowZeroToEmpty + * val viewDistance: IntOr.Default = IntOr.Default.USE_DEFAULT + * ) + * ``` + * + * In this example, `view-distance: -1` is interpreted as `default`. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class BelowZeroToEmpty \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt new file mode 100644 index 000000000..e8896063d --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt @@ -0,0 +1,197 @@ +package dev.slne.surf.api.core.config.type.number + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents an optional double configuration value. + * + * Implementations decide what the empty state means: + * - [Default] uses `default` + * - [Disabled] uses `disabled` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class SpawnConfig( + * val multiplier: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT, + * val customChance: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED + * ) + * + * val multiplier = config.multiplier or 1.0 + * + * if (config.customChance.test { it > 0.0 }) { + * val chance = config.customChance.doubleValue() + * } + * ``` + */ +interface DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is empty. + */ + infix fun or(fallback: Double): Double + + /** + * The configured double value, or `null` if this value is empty. + */ + val value: Double? + + /** + * Returns the configured double value. + * + * @throws NullPointerException if this value is empty. + */ + fun doubleValue(): Double = value!! + + /** + * Double value that can be configured as either a concrete number or `default`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val multiplier: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT + * ) + * + * val multiplier = config.multiplier or 1.0 + * ``` + */ + data class Default( + override val value: Double? + ) : DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is `default`. + */ + override fun or(fallback: Double) = value ?: fallback + + companion object { + private const val DEFAULT_VALUE = "default" + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = Default(null) + } + + /** + * Configurate serializer for [Default]. + */ + internal object Serializer : ScalarSerializer.Annotated(Default::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Default { + val value = parseDouble(type, obj, DEFAULT_VALUE) ?: return USE_DEFAULT + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return USE_DEFAULT + } + + return Default(value) + } + + override fun serialize( + type: AnnotatedType, + item: Default, + typeSupported: Predicate> + ): Any { + return item.value ?: DEFAULT_VALUE + } + } + } + + /** + * Double value that can be configured as either a concrete number or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val threshold: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED + * ) + * + * if (config.threshold.enabled()) { + * val threshold = config.threshold.doubleValue() + * } + * ``` + */ + data class Disabled( + override val value: Double? + ) : DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is `disabled`. + */ + override fun or(fallback: Double) = value ?: fallback + + /** + * Returns `true` if this value contains an explicit double. + */ + fun enabled(): Boolean = value != null + + /** + * Returns `true` if this value is enabled and its double value matches [predicate]. + */ + fun test(predicate: (Double) -> Boolean): Boolean { + return value?.let(predicate) ?: false + } + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = Disabled(null) + } + + /** + * Configurate serializer for [Disabled]. + */ + object Serializer : ScalarSerializer.Annotated(Disabled::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Disabled { + val value = parseDouble(type, obj, DISABLED_VALUE) ?: return DISABLED + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return DISABLED + } + + return Disabled(value) + } + + override fun serialize( + type: AnnotatedType, + item: Disabled, + typeSupported: Predicate> + ): Any { + return item.value ?: DISABLED_VALUE + } + } + } +} + +private fun parseDouble(type: AnnotatedType, obj: Any, emptyValue: String): Double? { + return when (obj) { + is String -> { + if (obj.equals(emptyValue, ignoreCase = true)) { + null + } else { + obj.toDoubleOrNull() + ?: throw SerializationException( + Double::class.java, + "$obj($type) is not a double or '$emptyValue'" + ) + } + } + + is Number -> obj.toDouble() + + else -> throw SerializationException( + Double::class.java, + "$obj($type) is not a double or '$emptyValue'" + ) + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt new file mode 100644 index 000000000..2be56d520 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt @@ -0,0 +1,199 @@ +package dev.slne.surf.api.core.config.type.number + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents an optional integer configuration value. + * + * Implementations decide what the empty state means: + * - [Default] uses `default` + * - [Disabled] uses `disabled` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class RetryConfig( + * val maxRetries: IntOr.Default = IntOr.Default.USE_DEFAULT, + * val queueLimit: IntOr.Disabled = IntOr.Disabled.DISABLED + * ) + * + * val retries = config.maxRetries or 3 + * + * if (config.queueLimit.enabled()) { + * val limit = config.queueLimit.intValue() + * } + * ``` + */ +interface IntOr { + + /** + * Returns the configured integer value, or [fallback] if this value is empty. + */ + infix fun or(fallback: Int): Int + + /** + * The configured integer value, or `null` if this value is empty. + */ + val value: Int? + + /** + * Returns `true` if this value contains an explicit integer. + */ + fun isDefined(): Boolean = value != null + + /** + * Returns the configured integer value. + * + * @throws NullPointerException if this value is empty. + */ + fun intValue(): Int = value!! + + /** + * Integer value that can be configured as either a concrete number or `default`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val amount: IntOr.Default = IntOr.Default.USE_DEFAULT + * ) + * + * val amount = config.amount or 10 + * ``` + */ + data class Default(override val value: Int?) : IntOr { + + /** + * Returns the configured integer value, or [fallback] if this value is `default`. + */ + override fun or(fallback: Int) = value ?: fallback + + companion object { + private const val DEFAULT_VALUE = "default" + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = Default(null) + } + + /** + * Configurate serializer for [Default]. + */ + internal object Serializer : ScalarSerializer.Annotated(Default::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Default { + val value = parseInt(type, obj, DEFAULT_VALUE) ?: return USE_DEFAULT + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return USE_DEFAULT + } + + return Default(value) + } + + override fun serialize( + type: AnnotatedType, + item: Default, + typeSupported: Predicate> + ): Any { + return item.value ?: DEFAULT_VALUE + } + } + } + + /** + * Integer value that can be configured as either a concrete number or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val limit: IntOr.Disabled = IntOr.Disabled.DISABLED + * ) + * + * if (config.limit.enabled()) { + * val limit = config.limit.intValue() + * } + * ``` + */ + data class Disabled( + override val value: Int? + ) : IntOr { + /** + * Returns the configured integer value, or [fallback] if this value is `disabled`. + */ + override fun or(fallback: Int) = value ?: fallback + + /** + * Returns `true` if this value contains an explicit integer. + */ + fun enabled(): Boolean = value != null + + /** + * Returns `true` if this value is enabled and its integer value matches [predicate]. + */ + inline fun test(predicate: (Int) -> Boolean): Boolean { + return value?.let(predicate) ?: false + } + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = Disabled(null) + } + + /** + * Configurate serializer for [Disabled]. + */ + internal object Serializer : ScalarSerializer.Annotated(Disabled::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Disabled { + val value = parseInt(type, obj, DISABLED_VALUE) ?: return DISABLED + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return DISABLED + } + + return Disabled(value) + } + + override fun serialize( + type: AnnotatedType, + item: Disabled, + typeSupported: Predicate> + ): Any { + return item.value ?: DISABLED_VALUE + } + } + } +} + +private fun parseInt(type: AnnotatedType, obj: Any, emptyValue: String): Int? { + return when (obj) { + is String -> { + if (obj.equals(emptyValue, ignoreCase = true)) { + null + } else { + obj.toIntOrNull() + ?: throw SerializationException( + Int::class.java, + "$obj($type) is not an int or '$emptyValue'" + ) + } + } + + is Number -> obj.toInt() + + else -> throw SerializationException( + Int::class.java, + "$obj($type) is not an int or '$emptyValue'" + ) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java index d61c79347..c4dcc9208 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java @@ -1,6 +1,6 @@ package dev.slne.surf.api.paper.test.command; -import dev.jorel.commandapi.*; +import dev.jorel.commandapi.CommandAPICommand; import dev.slne.surf.api.paper.test.command.subcommands.*; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.*; @@ -32,7 +32,8 @@ public SurfApiTestCommand() { new SortInvCommand("sortInv"), new SignedMessageArgumentTest("signedmessage"), new BlockPdcContainerTest("blockpdc"), - new OfflineInventoryEditTest("editOfflineInventory") + new OfflineInventoryEditTest("editOfflineInventory"), + new ModernSerializerTestConfigCommand("modernSerializerTestConfig") ); } } diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java index c645947a0..be64f2c52 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java @@ -1,5 +1,6 @@ package dev.slne.surf.api.paper.test.config; +import dev.slne.surf.api.core.config.type.BooleanOrDefault; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Matches; @@ -13,6 +14,8 @@ public class TestConfig2 { @Setting("connection") public ConnectionConfig connectionConfig = new ConnectionConfig(); + public BooleanOrDefault testBooleanOrDefault = BooleanOrDefault.USE_DEFAULT; + @ConfigSerializable public static class ConnectionConfig { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt index c36793593..2e7064a77 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt @@ -5,14 +5,12 @@ import com.destroystokyo.paper.event.server.ServerTickStartEvent import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.jorel.commandapi.CommandAPI import dev.slne.surf.api.core.component.surfComponentApi -import dev.slne.surf.api.core.config.SurfConfigApi import dev.slne.surf.api.paper.event.listen import dev.slne.surf.api.paper.inventory.framework.register import dev.slne.surf.api.paper.nms.NmsUseWithCaution import dev.slne.surf.api.paper.packet.listener.SurfPaperPacketListenerApi import dev.slne.surf.api.paper.test.command.SurfApiTestCommand import dev.slne.surf.api.paper.test.command.subcommands.reflection.Reflection -import dev.slne.surf.api.paper.test.config.TestConfig2 import dev.slne.surf.api.paper.test.listener.ChatListener import dev.slne.surf.surfapi.bukkit.test.command.dialog.dialogTestCommand import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInventoryView @@ -34,7 +32,7 @@ class PaperPluginMain : SuspendingJavaPlugin() { TestInventoryView.register() testInventoryViewDsl.register() - SurfConfigApi.createSpongeYmlConfig(TestConfig2::class.java, dataPath, "test-config-2.yml") +// SurfConfigApi.createSpongeYmlConfig(TestConfig2::class.java, dataPath, "test-config-2.yml") } override suspend fun onEnableAsync() { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt new file mode 100644 index 000000000..0eb14e801 --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt @@ -0,0 +1,17 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.surfapi.bukkit.test.config.ModernSerializerTestConfig + +class ModernSerializerTestConfigCommand(name: String): CommandAPICommand(name) { + + init { + subcommand("reload") { + anyExecutor { _, _ -> + ModernSerializerTestConfig.reloadFromFile() + } + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt new file mode 100644 index 000000000..fe511dd72 --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt @@ -0,0 +1,185 @@ +package dev.slne.surf.surfapi.bukkit.test.config + +import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.config.constraints.* +import dev.slne.surf.api.core.config.serializer.collection.map.ThrowExceptions +import dev.slne.surf.api.core.config.serializer.collection.map.WriteKeyBack +import dev.slne.surf.api.core.config.type.BooleanOrDefault +import dev.slne.surf.api.core.config.type.ConfigDuration +import dev.slne.surf.api.core.config.type.DurationOrDisabled +import dev.slne.surf.api.core.config.type.number.BelowZeroToEmpty +import dev.slne.surf.api.core.config.type.number.DoubleOr +import dev.slne.surf.api.core.config.type.number.IntOr +import dev.slne.surf.surfapi.bukkit.test.plugin +import it.unimi.dsi.fastutil.ints.Int2IntMap +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap +import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor +import org.bukkit.GameRule +import org.bukkit.GameRules +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ItemType +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.io.File +import java.net.URI +import java.net.URL +import java.nio.file.Path +import java.util.* +import java.util.regex.Pattern +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@ConfigSerializable +data class ModernSerializerTestConfig( + // scalar serializers + val component: Component = Component.text("Hello "), + val key: Key = Key.key("minecraft:stone"), + val duration: ConfigDuration = ConfigDuration(30.seconds), + val uuid: UUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), + val regex: Regex = "^[a-z0-9_-]+$".toRegex(), + val pattern: Pattern = Pattern.compile("^[A-Z_]+$"), + val uri: URI = URI("https://example.com/api"), + val url: URL = URI("https://example.com").toURL(), + val path: Path = Path.of("plugins/Surf/test.yml"), + val file: File = File("plugins/Surf/test.yml"), + val textColor: TextColor = TextColor.color(0x55FFFF), + + // wrapper types + val booleanOrDefault: BooleanOrDefault = BooleanOrDefault.USE_DEFAULT, + val durationOrDisabled: DurationOrDisabled = DurationOrDisabled.DISABLED, + val intOrDefault: IntOr.Default = IntOr.Default.USE_DEFAULT, + val intOrDisabled: IntOr.Disabled = IntOr.Disabled.DISABLED, + val doubleOrDefault: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT, + val doubleOrDisabled: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED, + + @BelowZeroToEmpty + val belowZeroIntDefault: IntOr.Default = IntOr.Default.USE_DEFAULT, + + @BelowZeroToEmpty + val belowZeroDoubleDisabled: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED, + + // numeric constraints + @PositiveNumber + val positiveNumber: Int = 1, + + @NegativeNumber + val negativeNumber: Int = -1, + + @MinNumber(10.0) + val minNumber: Int = 10, + + @MaxNumber(100.0) + val maxNumber: Int = 100, + + @Range(min = 0.0, max = 1.0) + val chance: Double = 0.5, + + @NotBlank + val notBlankString: String = "hello", + + @Trimmed + val trimmedString: String = "hello", + + @MinLength(3) + val minLengthString: String = "abc", + + @MaxLength(16) + val maxLengthString: String = "short-text", + + @StartsWith("surf:") + val startsWithString: String = "surf:test", + + @EndsWith(".yml") + val endsWithString: String = "config.yml", + + @Contains("surf") + val containsString: String = "hello-surf-api", + + // collection constraints + @NotEmpty + val notEmptyList: List = listOf("one"), + + @MinSize(1) + val minSizeList: List = listOf("one"), + + @MaxSize(3) + val maxSizeList: List = listOf("one", "two"), + + @NoDuplicates + val noDuplicatesList: List = listOf("one", "two"), + + // duration constraints + @MinDuration(seconds = 5) + val minDuration: ConfigDuration = ConfigDuration(10.seconds), + + @MaxDuration(seconds = 300) + val maxDuration: ConfigDuration = ConfigDuration(5.minutes), + + // key / enum-ish constraints + @Namespace("minecraft") + val namespacedKey: Key = Key.key("minecraft:dirt"), + + @DisallowValues("DISABLED", "NONE") + val mode: TestMode = TestMode.ENABLED, + + // path constraints + @ExistingFile + val existingFile: Path = Path.of("plugins/SurfPaperPluginTest/test.yml"), + + @Directory + val directory: Path = Path.of("plugins/Surf"), + + @WritablePath + val writablePath: Path = Path.of("plugins/SurfPaperPluginTest/test.yml"), + + // map serializers + val normalMap: Map = mapOf( + "one" to 1, + "two" to 2, + ), + + val strictMap: @ThrowExceptions Map = mapOf( + "strict" to 1, + ), + + val keyWriteBackMap: Map<@WriteKeyBack Key, Int> = mapOf( + Key.key("minecraft:stone") to 1, + ), + + // fastutil map serializer + val fastutilMap: Int2ObjectMap = Int2ObjectOpenHashMap().apply { + put(1, "one") + put(2, "two") + }, + + val fastUtilInt2Int: Int2IntMap = Int2IntOpenHashMap().apply { + put(1, 1) + put(2, 2) + }, + + // paper + val stack: ItemStack = ItemType.BOOK.createItemStack(), + + val location: Location = Location(plugin.server.worlds.first(), 0.0, 0.0, 0.0), + + val itemType: ItemType = ItemType.STONE, + val material: Material = Material.STONE, + val selectedGameRules: List> = listOf(GameRules.ADVANCE_TIME, GameRules.MAX_BLOCK_MODIFICATIONS), +) { + companion object : SpongeYmlConfigClass( + ModernSerializerTestConfig::class.java, + plugin.dataPath, + "modern-serializer-test-config.yml" + ) +} + +enum class TestMode { + ENABLED, + DISABLED, + NONE +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt index 1091e13d1..da5333818 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt @@ -1,6 +1,7 @@ package dev.slne.surf.surfapi.bukkit.test.config import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.config.type.BooleanOrDefault import dev.slne.surf.surfapi.bukkit.test.plugin import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.objectmapping.meta.Comment @@ -10,7 +11,8 @@ data class ModernTestConfig( @Comment("This is a modern config!") var message: String = "Hello from Modern Config!", var number: Int = 42, - var enabled: Boolean = true + var enabled: Boolean = true, + var booleanOrDefault: BooleanOrDefault = BooleanOrDefault.TRUE, ) { companion object : SpongeYmlConfigClass( ModernTestConfig::class.java, @@ -20,7 +22,13 @@ data class ModernTestConfig( fun randomise() = edit { message = "Random Message ${Math.random()}" number = (Math.random() * 100).toInt() + // enabled = Math.random() > 0.5 } } -} \ No newline at end of file +} + +@ConfigSerializable +data class ModernTestConfig2( + val map: Map = emptyMap() +) \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt new file mode 100644 index 000000000..7218ef029 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import io.leangen.geantyref.GenericTypeReflector +import org.bukkit.configuration.serialization.ConfigurationSerializable +import org.bukkit.configuration.serialization.ConfigurationSerialization +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.Type + +object BukkitConfigurationSerializableSerializer : TypeSerializer { + @Suppress("UNCHECKED_CAST") + override fun deserialize(type: Type, node: ConfigurationNode): ConfigurationSerializable { + val clazz = GenericTypeReflector.erase(type) as Class + val args = node.childrenMap().mapValues { it.value.raw() }.mapKeys { it.key.toString() } + + try { + return ConfigurationSerialization.deserializeObject(args, clazz) + ?: throw SerializationException( + clazz, + "Could not deserialize ${clazz.name} via ConfigurationSerializable" + ) + } catch (e: Throwable) { + if (e is SerializationException) throw e + throw SerializationException(clazz, "Could not deserialize ${clazz.name} via ConfigurationSerializable", e) + } + } + + + @Suppress("OverrideOnly") + override fun serialize(type: Type, obj: ConfigurationSerializable?, node: ConfigurationNode) { + if (obj == null) { + node.raw(null) + return + } + + val map = obj.serialize() + map.forEach { (key, value) -> + node.node(key).set(value) + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt new file mode 100644 index 000000000..478cfa9d6 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt @@ -0,0 +1,29 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.inventory.ItemStack +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import kotlin.io.encoding.Base64 + +object ItemStackSerializer : ScalarSerializer.Annotated(ItemStack::class.java) { + + override fun deserialize(type: AnnotatedType, obj: Any): ItemStack { + val base64 = obj.toString() + return try { + val bytes = Base64.decode(base64) + ItemStack.deserializeBytes(bytes) + } catch (e: IllegalArgumentException) { + throw SerializationException(type.type, "Invalid Base64-encoded ItemStack: ${e.message}", e) + } catch (e: Exception) { + throw SerializationException(type.type, "Failed to deserialize ItemStack: ${e.message}", e) + } + } + + override fun serialize(type: AnnotatedType, item: ItemStack?, typeSupported: Predicate>): Any? { + if (item == null) return null + val serialized = item.serializeAsBytes() + return Base64.encode(serialized) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt new file mode 100644 index 000000000..4be6c448e --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.Material +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object MaterialSerializer : ScalarSerializer.Annotated(Material::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Material { + return Material.matchMaterial(obj.toString()) ?: throw SerializationException( + Material::class.java, + "$obj($type) is not a valid material" + ) + } + + override fun serialize(type: AnnotatedType, item: Material, typeSupported: Predicate>): Any { + return item.key.toString() + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt new file mode 100644 index 000000000..541dd5fb2 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt @@ -0,0 +1,18 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.NamespacedKey +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object NamespacedKeySerializer : ScalarSerializer.Annotated(NamespacedKey::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): NamespacedKey { + return NamespacedKey.fromString(obj.toString()) + ?: throw SerializationException(NamespacedKey::class.java, "$obj($type) is not a valid NamespacedKey") + } + + override fun serialize(type: AnnotatedType, item: NamespacedKey, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt index 64df59d53..b39d07e25 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt @@ -2,45 +2,93 @@ package dev.slne.surf.api.paper.server.config.serializers import com.google.auto.service.AutoService import dev.slne.surf.api.core.config.serializer.SpongeConfigSerializers -import org.bukkit.inventory.ItemStack -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import java.lang.reflect.Type -import java.util.* +import dev.slne.surf.api.paper.server.PaperInstance +import dev.slne.surf.api.paper.server.config.serializers.registry.RegistryValueSerializer +import io.leangen.geantyref.TypeToken +import io.papermc.paper.datacomponent.DataComponentType +import io.papermc.paper.dialog.Dialog +import io.papermc.paper.entity.poi.PoiType +import io.papermc.paper.registry.RegistryKey +import org.bukkit.* +import org.bukkit.attribute.Attribute +import org.bukkit.block.Biome +import org.bukkit.block.BlockType +import org.bukkit.block.banner.PatternType +import org.bukkit.configuration.serialization.ConfigurationSerializable +import org.bukkit.damage.DamageType +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.* +import org.bukkit.generator.structure.Structure +import org.bukkit.generator.structure.StructureType +import org.bukkit.inventory.ItemType +import org.bukkit.inventory.MenuType +import org.bukkit.inventory.meta.trim.TrimMaterial +import org.bukkit.inventory.meta.trim.TrimPattern +import org.bukkit.map.MapCursor +import org.bukkit.potion.PotionEffectType +import org.spongepowered.configurate.serialize.TypeSerializerCollection +@Suppress("UnstableApiUsage") @AutoService(SpongeConfigSerializers::class) class PaperSpongeConfigSerializers : SpongeConfigSerializers() { - init { - registerClassSerializer(ItemStackSerializer) - } - - object ItemStackSerializer : TypeSerializer { - override fun deserialize( - type: Type, - node: ConfigurationNode - ): ItemStack { - val itemStackBase64 = node.string - ?: throw SerializationException("Expected a Base64 string for ItemStack deserialization") - val decoded = Base64.getDecoder().decode(itemStackBase64) - return ItemStack.deserializeBytes(decoded) - } + override fun registerDefaults(builder: TypeSerializerCollection.Builder) { + super.registerDefaults(builder) - override fun serialize( - type: Type, - obj: ItemStack?, - node: ConfigurationNode - ) { - if (obj == null) { - node.raw(null) - return - } + builder.register(NamespacedKeySerializer) + builder.register(MaterialSerializer) - val serialized = obj.serializeAsBytes() - val encoded = Base64.getEncoder().encodeToString(serialized) + if (!PaperInstance.isBootstrapping()) { + builder.register(ItemStackSerializer) - node.set(encoded) + //region Registry serializer + //@formatter:off + builder.register(RegistryValueSerializer(GameEvent::class.java, RegistryKey.GAME_EVENT, true)) + builder.register(RegistryValueSerializer(StructureType::class.java, RegistryKey.STRUCTURE_TYPE, true)) + builder.register(RegistryValueSerializer(PotionEffectType::class.java, RegistryKey.MOB_EFFECT, true)) + builder.register(RegistryValueSerializer(BlockType::class.java, RegistryKey.BLOCK, true)) + builder.register(RegistryValueSerializer(ItemType::class.java, RegistryKey.ITEM, true)) + builder.register(RegistryValueSerializer(Villager.Profession::class.java, RegistryKey.VILLAGER_PROFESSION, true)) + builder.register(RegistryValueSerializer(PoiType::class.java, RegistryKey.POINT_OF_INTEREST_TYPE, true)) + builder.register(RegistryValueSerializer(Villager.Type::class.java, RegistryKey.VILLAGER_TYPE, true)) + builder.register(RegistryValueSerializer(MapCursor.Type::class.java, RegistryKey.MAP_DECORATION_TYPE, true)) + builder.register(RegistryValueSerializer(MenuType::class.java, RegistryKey.MENU, true)) + builder.register(RegistryValueSerializer(Attribute::class.java, RegistryKey.ATTRIBUTE, true)) + builder.register(RegistryValueSerializer(Fluid::class.java, RegistryKey.FLUID, true)) + builder.register(RegistryValueSerializer(Sound::class.java, RegistryKey.SOUND_EVENT, true)) + builder.register(RegistryValueSerializer(DataComponentType::class.java, RegistryKey.DATA_COMPONENT_TYPE, true)) + builder.register(RegistryValueSerializer(object : TypeToken>() {}, RegistryKey.GAME_RULE, true)) + builder.register(RegistryValueSerializer(Biome::class.java, RegistryKey.BIOME, true)) + builder.register(RegistryValueSerializer(Structure::class.java, RegistryKey.STRUCTURE, true)) + builder.register(RegistryValueSerializer(TrimMaterial::class.java, RegistryKey.TRIM_MATERIAL, true)) + builder.register(RegistryValueSerializer(TrimPattern::class.java, RegistryKey.TRIM_PATTERN, true)) + builder.register(RegistryValueSerializer(DamageType::class.java, RegistryKey.DAMAGE_TYPE, true)) + builder.register(RegistryValueSerializer(Wolf.Variant::class.java, RegistryKey.WOLF_VARIANT, true)) + builder.register(RegistryValueSerializer(Wolf.SoundVariant::class.java, RegistryKey.WOLF_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Enchantment::class.java, RegistryKey.ENCHANTMENT, true)) + builder.register(RegistryValueSerializer(JukeboxSong::class.java, RegistryKey.JUKEBOX_SONG, true)) + builder.register(RegistryValueSerializer(PatternType::class.java, RegistryKey.BANNER_PATTERN, true)) + builder.register(RegistryValueSerializer(Art::class.java, RegistryKey.PAINTING_VARIANT, true)) + builder.register(RegistryValueSerializer(MusicInstrument::class.java, RegistryKey.INSTRUMENT, true)) + builder.register(RegistryValueSerializer(Cat.Type::class.java, RegistryKey.CAT_VARIANT, true)) + builder.register(RegistryValueSerializer(Cat.SoundVariant::class.java, RegistryKey.CAT_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Frog.Variant::class.java, RegistryKey.FROG_VARIANT, true)) + builder.register(RegistryValueSerializer(Chicken.Variant::class.java, RegistryKey.CHICKEN_VARIANT, true)) + builder.register(RegistryValueSerializer(Chicken.SoundVariant::class.java, RegistryKey.CHICKEN_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Cow.Variant::class.java, RegistryKey.COW_VARIANT, true)) + builder.register(RegistryValueSerializer(Cow.SoundVariant::class.java, RegistryKey.COW_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Pig.Variant::class.java, RegistryKey.PIG_VARIANT, true)) + builder.register(RegistryValueSerializer(Pig.SoundVariant::class.java, RegistryKey.PIG_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(ZombieNautilus.Variant::class.java, RegistryKey.ZOMBIE_NAUTILUS_VARIANT, true)) + builder.register(RegistryValueSerializer(Dialog::class.java, RegistryKey.DIALOG, true)) + //@formatter:on + //endregion } + + // register last to let other serializer override this + builder.register( + { type -> type is Class<*> && ConfigurationSerializable::class.java.isAssignableFrom(type) }, + BukkitConfigurationSerializableSerializer + ) } } \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt new file mode 100644 index 000000000..3130bafb6 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt @@ -0,0 +1,66 @@ +package dev.slne.surf.api.paper.server.config.serializers.registry + +import io.leangen.geantyref.TypeToken +import io.papermc.paper.registry.RegistryAccess +import io.papermc.paper.registry.RegistryKey +import net.kyori.adventure.key.InvalidKeyException +import net.kyori.adventure.key.Key +import org.bukkit.Keyed +import org.bukkit.Registry +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +abstract class RegistryEntrySerializer : ScalarSerializer.Annotated { + + protected val registryKey: RegistryKey + private val omitMinecraftNamespace: Boolean + + protected constructor( + type: TypeToken, + registryKey: RegistryKey, + omitMinecraftNamespace: Boolean + ) : super(type) { + this.registryKey = registryKey + this.omitMinecraftNamespace = omitMinecraftNamespace + } + + protected constructor( + type: Class, + registryKey: RegistryKey, + omitMinecraftNamespace: Boolean + ) : super(type) { + this.registryKey = registryKey + this.omitMinecraftNamespace = omitMinecraftNamespace + } + + protected fun registry(): Registry { + return RegistryAccess.registryAccess().getRegistry(registryKey) + } + + protected abstract fun convertFromResourceKey(key: Key): T + + override fun deserialize(type: AnnotatedType?, obj: Any): T? { + return convertFromResourceKey(deserializeKey(obj)) + } + + protected abstract fun convertToResourceKey(value: T): Key + + override fun serialize(type: AnnotatedType, item: T, typeSupported: Predicate>): Any { + val key = this.convertToResourceKey(item) + return if (this.omitMinecraftNamespace && key.namespace() == Key.MINECRAFT_NAMESPACE) { + key.value() + } else { + key.asString() + } + } + + private fun deserializeKey(input: Any): Key { + return try { + Key.key(input.toString()) + } catch (e: InvalidKeyException) { + throw SerializationException(Key::class.java, "Could not create a key from input", e) + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt new file mode 100644 index 000000000..bd0cbb424 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.paper.server.config.serializers.registry + +import io.leangen.geantyref.TypeToken +import io.papermc.paper.registry.RegistryKey +import net.kyori.adventure.key.Key +import org.bukkit.Keyed +import org.spongepowered.configurate.serialize.SerializationException + +class RegistryValueSerializer : RegistryEntrySerializer { + constructor(type: TypeToken, registryKey: RegistryKey, omitMinecraftNamespace: Boolean) : super( + type, + registryKey, + omitMinecraftNamespace + ) + + constructor(type: Class, registryKey: RegistryKey, omitMinecraftNamespace: Boolean) : super( + type, + registryKey, + omitMinecraftNamespace + ) + + override fun convertFromResourceKey(key: Key): T { + val value = registry().get(key) + ?: throw SerializationException("Missing value in $registryKey with string key: ${key.asString()}") + + return value + } + + override fun convertToResourceKey(value: T): Key { + return registry().getKeyOrThrow(value) + } +} \ No newline at end of file