Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.2
group=dev.slne.surf.api
version=3.30.0
version=3.31.0
relocationPrefix=dev.slne.surf.api.libs
snapshot=false
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ kotlinxCoroutines = "1.11.0"
kotlinx-serialization = "1.11.0"

# Packet Events
packetevents = "2.12.2"
packetevents-plugin = "2.12.2"
packetevents = "2.13.0"
packetevents-plugin = "2.13.0"

# Command API
commandapi = "11.2.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.slne.surf.api.core.server.impl

import dev.slne.surf.api.core.SurfApiCore
import net.kyori.adventure.audience.Audience
import org.apache.commons.lang3.builder.ToStringBuilder

/**
Expand All @@ -9,7 +10,13 @@ import org.apache.commons.lang3.builder.ToStringBuilder
*/
abstract class SurfApiCoreImpl protected constructor() : SurfApiCore {

abstract fun isPlayer(audience: Audience): Boolean

override fun toString(): String {
return ToStringBuilder.reflectionToString(this)
}

companion object {
fun get() = SurfApiCore.instance as SurfApiCoreImpl
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package dev.slne.surf.api.core.server.impl.actionbar

import com.google.auto.service.AutoService
import dev.slne.surf.api.core.SurfApiCore
import dev.slne.surf.api.core.actionbar.ActionBarFinishReason
import dev.slne.surf.api.core.actionbar.ActionBarService
import dev.slne.surf.api.core.messages.adventure.nameOrNull
import dev.slne.surf.api.core.messages.adventure.uuidOrNull
import dev.slne.surf.api.core.server.impl.SurfApiCoreImpl
import dev.slne.surf.api.core.util.logger
import kotlinx.coroutines.*
import net.kyori.adventure.audience.Audience
import net.kyori.adventure.text.Component
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration
import kotlin.time.TimeSource

@AutoService(ActionBarService::class)
class ActionBarServiceImpl : ActionBarService {

companion object {
private val log = logger()
}

override fun sendActionBar(
scope: CoroutineScope,
audience: Audience,
duration: Duration,
interval: Duration,
fadeOut: Boolean,
autoCancelPlayer: Boolean,
computation: () -> Component,
afterTick: (() -> Unit)?,
onFinish: ((ActionBarFinishReason) -> Unit)?
): Job {
require(duration.isPositive()) { "duration must be positive" }
require(interval.isPositive()) { "interval must be positive" }

val uuid = audience.uuidOrNull()
val audienceName = audience.nameOrNull()
?: uuid?.toString()
?: "#Unknown"

val autoCancelUuid = if (
autoCancelPlayer &&
uuid != null &&
SurfApiCoreImpl.get().isPlayer(audience)
) {
uuid
} else {
null
}

fun shouldAutoCancel(): Boolean {
return autoCancelUuid != null && SurfApiCore.getPlayer(autoCancelUuid) == null
}

val finishReason = AtomicReference(ActionBarFinishReason.CANCELLED)

val job = scope.launch {
val end = TimeSource.Monotonic.markNow() + duration

while (true) {
ensureActive()

if (shouldAutoCancel()) {
finishReason.set(ActionBarFinishReason.AUTO_CANCELLED)
break
}

if (end.hasPassedNow()) {
finishReason.set(ActionBarFinishReason.COMPLETED)
break
}

val tickStart = TimeSource.Monotonic.markNow()
val component = try {
computation()
} catch (t: Throwable) {
log.atWarning()
.withCause(t)
.atMostEvery(900, TimeUnit.MILLISECONDS)
.log("Failed to compute actionbar for $audienceName")
null
}

if (component != null) {
audience.sendActionBar(component)
}

try {
afterTick?.invoke()
} catch (t: Throwable) {
log.atWarning()
.withCause(t)
.atMostEvery(900, TimeUnit.MILLISECONDS)
.log("Failed to invoke afterTick for $audienceName")
}

val remainingDelay = interval - tickStart.elapsedNow()
if (remainingDelay.isPositive()) {
delay(remainingDelay)
}
}

ensureActive()

if (!fadeOut) {
audience.sendActionBar(Component.empty())
}
}

job.invokeOnCompletion { throwable ->
val reason = when (throwable) {
null -> finishReason.get()
is CancellationException -> ActionBarFinishReason.CANCELLED
else -> ActionBarFinishReason.FAILED
}

try {
onFinish?.invoke(reason)
} catch (throwable: Throwable) {
log.atWarning()
.withCause(throwable)
.log("Failed to invoke onFinish for $audienceName")
}
}

return job
}
}
32 changes: 32 additions & 0 deletions surf-api-core/surf-api-core/api/surf-api-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ public final class dev/slne/surf/api/core/SurfApiCore$Companion : dev/slne/surf/
public fun sendPlayerToServer (Ljava/util/UUID;Ljava/lang/String;)V
}

public final class dev/slne/surf/api/core/actionbar/ActionBarFinishReason : java/lang/Enum {
public static final field AUTO_CANCELLED Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
public static final field CANCELLED Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
public static final field COMPLETED Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
public static final field FAILED Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
public static fun values ()[Ldev/slne/surf/api/core/actionbar/ActionBarFinishReason;
}

public abstract interface class dev/slne/surf/api/core/actionbar/ActionBarService {
public static final field Companion Ldev/slne/surf/api/core/actionbar/ActionBarService$Companion;
public abstract fun sendActionBar-FragotA (Lkotlinx/coroutines/CoroutineScope;Lnet/kyori/adventure/audience/Audience;JJZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;
public static synthetic fun sendActionBar-FragotA$default (Ldev/slne/surf/api/core/actionbar/ActionBarService;Lkotlinx/coroutines/CoroutineScope;Lnet/kyori/adventure/audience/Audience;JJZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
}

public final class dev/slne/surf/api/core/actionbar/ActionBarService$Companion : dev/slne/surf/api/core/actionbar/ActionBarService {
public final fun getInstance ()Ldev/slne/surf/api/core/actionbar/ActionBarService;
public fun sendActionBar-FragotA (Lkotlinx/coroutines/CoroutineScope;Lnet/kyori/adventure/audience/Audience;JJZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;
}

public final class dev/slne/surf/api/core/actionbar/ActionBarService$DefaultImpls {
public static synthetic fun sendActionBar-FragotA$default (Ldev/slne/surf/api/core/actionbar/ActionBarService;Lkotlinx/coroutines/CoroutineScope;Lnet/kyori/adventure/audience/Audience;JJZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
}

public final class dev/slne/surf/api/core/actionbar/ActionBarServiceKt {
public static final fun sendActionBar-DIOjvaQ (Lnet/kyori/adventure/audience/Audience;Lkotlinx/coroutines/CoroutineScope;JJZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;
public static synthetic fun sendActionBar-DIOjvaQ$default (Lnet/kyori/adventure/audience/Audience;Lkotlinx/coroutines/CoroutineScope;JJZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
public static final fun sendActionBar-FragotA (Lnet/kyori/adventure/audience/Audience;Lkotlinx/coroutines/CoroutineScope;JJZZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/Job;
public static synthetic fun sendActionBar-FragotA$default (Lnet/kyori/adventure/audience/Audience;Lkotlinx/coroutines/CoroutineScope;JJZZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
}

public final class dev/slne/surf/api/core/algorithms/ConvexHull2D {
public static final field INSTANCE Ldev/slne/surf/api/core/algorithms/ConvexHull2D;
public final fun ccw (Lorg/spongepowered/math/vector/Vectord;Lorg/spongepowered/math/vector/Vectord;Lorg/spongepowered/math/vector/Vectord;)Z
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.slne.surf.api.core.actionbar

/**
* Describes why a repeating action bar task finished.
*/
enum class ActionBarFinishReason {
/**
* The configured duration elapsed successfully.
*/
COMPLETED,

/**
* The task stopped because the target player is no longer online.
*/
AUTO_CANCELLED,

/**
* The returned job was cancelled before completion.
*/
CANCELLED,

/**
* The task failed with an unexpected exception.
*/
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package dev.slne.surf.api.core.actionbar

import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder
import dev.slne.surf.api.core.util.requiredService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import net.kyori.adventure.audience.Audience
import net.kyori.adventure.text.Component
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

interface ActionBarService {

/**
* Sends an action bar to the given [audience] repeatedly.
*
* The [computation] callback is invoked once per tick to build the component that should be
* sent for the current update. If [computation] throws, the exception is logged and the current
* tick is skipped.
*
* The returned [Job] can be cancelled to stop the action bar early. When the task finishes,
* [onFinish] is invoked with an [ActionBarFinishReason] describing why it ended.
*
* If [fadeOut] is `false`, an empty action bar is sent after the task completes normally or is
* auto-cancelled. If [fadeOut] is `true`, the last sent action bar is allowed to disappear
* naturally on the client.
*
* If [autoCancelPlayer] is `true` and the [audience] is a player, the task automatically stops
* once that player is no longer online.
*
* @param scope The coroutine scope used to launch the repeating action bar task.
* @param audience The audience that should receive the action bar.
* @param duration The total duration for which the action bar should be updated.
* @param interval The delay between two action bar updates.
* @param fadeOut Whether the last action bar should fade out naturally instead of being cleared.
* @param autoCancelPlayer Whether to stop automatically when a player audience goes offline.
* @param computation Computes the component to send for the current tick.
* @param afterTick Optional callback invoked after each tick, regardless of whether a component was sent.
* @param onFinish Optional callback invoked when the action bar task finishes.
*
* @return The launched action bar job.
*
* @throws IllegalArgumentException If [duration] or [interval] is not positive.
*/
fun sendActionBar(
scope: CoroutineScope,
audience: Audience,
duration: Duration,
interval: Duration = 1.seconds,
fadeOut: Boolean = false,
autoCancelPlayer: Boolean = true,
computation: () -> Component,
afterTick: (() -> Unit)? = null,
onFinish: ((ActionBarFinishReason) -> Unit)? = null
): Job

companion object : ActionBarService by INSTANCE {
val instance get() = INSTANCE
}
}

private val INSTANCE = requiredService<ActionBarService>()

/**
* Sends a repeatedly updated action bar to this [Audience].
*
* This overload supports [afterTick] and [onFinish] callbacks. Because [textComputation] is not
* the last parameter, callers usually need to pass it as a named argument when using callbacks.
*
* Example:
* ```kotlin
* player.sendActionBar(
* scope = scope,
* duration = 5.seconds,
* textComputation = {
* info("Loading...")
* },
* onFinish = { reason ->
* // handle finish reason
* }
* )
* ```
*
* @param scope The coroutine scope used to launch the repeating action bar task.
* @param duration The total duration for which the action bar should be updated.
* @param interval The delay between two action bar updates.
* @param fadeOut Whether the last action bar should fade out naturally instead of being cleared.
* @param autoCancelPlayer Whether to stop automatically when this audience is an offline player.
* @param textComputation Builds the action bar component for the current tick.
* @param afterTick Optional callback invoked after each tick.
* @param onFinish Optional callback invoked when the action bar task finishes.
*
* @return The launched action bar job.
*/
fun Audience.sendActionBar(
scope: CoroutineScope,
duration: Duration,
interval: Duration = 1.seconds,
fadeOut: Boolean = false,
autoCancelPlayer: Boolean = true,
textComputation: SurfComponentBuilder.() -> Unit,
afterTick: (() -> Unit)? = null,
onFinish: ((ActionBarFinishReason) -> Unit)? = null
) = ActionBarService.instance.sendActionBar(
scope,
this,
duration,
interval,
fadeOut,
autoCancelPlayer,
{ SurfComponentBuilder(textComputation) },
afterTick,
onFinish
)

/**
* Sends a repeatedly updated action bar to this [Audience].
*
* This overload keeps [textComputation] as the trailing lambda, making simple action bars concise:
*
* ```kotlin
* player.sendActionBar(scope, 5.seconds) {
* info("Loading...")
* }
* ```
*
* Use the overload with [afterTick] and [onFinish] parameters when callbacks are required.
*
* @param scope The coroutine scope used to launch the repeating action bar task.
* @param duration The total duration for which the action bar should be updated.
* @param interval The delay between two action bar updates.
* @param fadeOut Whether the last action bar should fade out naturally instead of being cleared.
* @param autoCancelPlayer Whether to stop automatically when this audience is an offline player.
* @param textComputation Builds the action bar component for the current tick.
*
* @return The launched action bar job.
*/
fun Audience.sendActionBar(
scope: CoroutineScope,
duration: Duration,
interval: Duration = 1.seconds,
fadeOut: Boolean = false,
autoCancelPlayer: Boolean = true,
textComputation: SurfComponentBuilder.() -> Unit
) = ActionBarService.instance.sendActionBar(
scope,
this,
duration,
interval,
fadeOut,
autoCancelPlayer,
{ SurfComponentBuilder(textComputation) },
null,
null
)
Loading