Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
74e183f
✨ feat(config): add new configuration types for numeric and boolean v…
twisti-dev May 17, 2026
63bee17
```
twisti-dev May 17, 2026
3c9eed6
🔧 chore(abi): update api dump
twisti-dev May 17, 2026
d239a3f
✨ feat(config): add serializers for various configuration types and c…
twisti-dev May 17, 2026
637d9cf
✨ feat(config): add ConfigDuration type and update duration constraints
twisti-dev May 17, 2026
eb80024
✨ feat(config): add new validation annotations for configuration values
twisti-dev May 17, 2026
d3bf30c
🔧 chore(abi): update api dump
twisti-dev May 17, 2026
75a8ce0
🔧 chore: update version to 3.13.0 in gradle.properties
twisti-dev May 17, 2026
c6fcd2d
🔧 chore: downgrade version from 3.13.0 to 3.12.0 in gradle.properties
twisti-dev May 17, 2026
495bbd9
✨ feat(config): add new configuration types and update serializers
twisti-dev May 17, 2026
2e45982
♻️ refactor(serializers): simplify key serialization logic in Registr…
twisti-dev May 17, 2026
b05371c
Potential fix for pull request finding
twisti-dev May 17, 2026
c50eafb
fix: address review feedback - SerializationException, AnnotatedType,…
Copilot May 17, 2026
52f43db
fix: use PrimitiveToPrimitive for all non-parameterized fastutil prim…
Copilot May 17, 2026
b52a1d9
🐛 fix(ExistingFile): handle potential exception in string to Path con…
twisti-dev May 17, 2026
41f99b8
✨ feat(serializers): add fastutil Int2IntMap for integer key-value pairs
twisti-dev May 17, 2026
48c45d7
🔧 chore(abi): update api dump
twisti-dev May 17, 2026
5de7d75
🐛 fix(PathSerializer): handle InvalidPathException during path deseri…
twisti-dev May 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -14,6 +16,7 @@ abstract class CoreInstance {

@MustBeInvokedByOverriders
open suspend fun onLoad() {
bootstrapping.set(false)
}

@MustBeInvokedByOverriders
Expand All @@ -30,4 +33,6 @@ abstract class CoreInstance {
PlayerSkinFetcher
Colors
}

fun isBootstrapping(): Boolean = bootstrapping.get()
}
382 changes: 377 additions & 5 deletions surf-api-core/surf-api-core/api/surf-api-core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<Contains, String?> {
override fun make(data: Contains, type: Type): Constraint<String?> = { value ->
if (value != null && !value.contains(data.value)) {
throw SerializationException("String must contain '${data.value}'")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Directory, Any?> {
override fun make(data: Directory, type: Type): Constraint<Any?> = Constraint { value ->
val path = value.asPathOrNull() ?: return@Constraint
if (!path.exists() || !path.isDirectory()) {
throw SerializationException("Path must point to an existing directory: $path")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<DisallowValues, Any?> {
override fun make(data: DisallowValues, type: Type): Constraint<Any?> = { value ->
if (value != null && data.values.any { it.equals(value.toString(), ignoreCase = true) }) {
throw SerializationException("Value '$value' is not allowed")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<EndsWith, String?> {
override fun make(data: EndsWith, type: Type): Constraint<String?> = { value ->
if (value != null && !value.endsWith(data.suffix)) {
throw SerializationException("String must end with '${data.suffix}'")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ExistingFile, Any?> {
override fun make(data: ExistingFile, type: Type): Constraint<Any?> = 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
Comment thread
twisti-dev marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -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<MaxDuration, ConfigDuration?> {
override fun make(data: MaxDuration, type: Type): Constraint<ConfigDuration?> = { value ->
if (value != null && value.value.inWholeSeconds > data.seconds) {
throw SerializationException("Duration is too long: ${value.value}, expected <= ${data.seconds}s")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MaxLength, String?> {
override fun make(data: MaxLength, type: Type): Constraint<String?> = { value ->
if (value != null && value.length > data.max) {
throw SerializationException("String is too long: ${value.length}, expected <= ${data.max}")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MaxNumber, Number?> {
override fun make(data: MaxNumber, type: Type): Constraint<Number?> = { number ->
if (number != null && number.toDouble() > data.max) {
throw SerializationException(type, "Number is too big: $number, expected <= ${data.max}")
}
Comment thread
twisti-dev marked this conversation as resolved.
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MaxSize, Any?> {
override fun make(data: MaxSize, type: Type): Constraint<Any?> = { value ->
val size = value.configSizeOrNull()
if (size != null && size > data.max) {
throw SerializationException("Collection size is too large: $size, expected <= ${data.max}")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<MinDuration, ConfigDuration?> {
override fun make(data: MinDuration, type: Type): Constraint<ConfigDuration?> = { value ->
if (value != null && value.value.inWholeSeconds < data.seconds) {
throw SerializationException("Duration is too short: ${value.value}, expected >= ${data.seconds}s")
}
}
}
}
}
Loading