From ba08ba7f29a6d81cb2686e5f2584fbc2134f9821 Mon Sep 17 00:00:00 2001 From: twisti-dev <76837088+twisti-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:53:51 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat(scoreboard):=20add=20viewe?= =?UTF-8?q?r=20line=20functionality=20to=20scoreboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement viewer line that displays player health in the scoreboard - introduce new utility functions for building dynamic and static lines - refactor scoreboard definition to support viewer-specific components --- .../scoreboard/CreateScoreboard.java | 8 +- .../server/impl/scoreboard/ScoreboardLine.kt | 32 +++ .../SurfAutoUpdatablePlayerScoreboardImpl.kt | 25 +-- .../SurfAutoUpdatableScoreboardImpl.kt | 47 ++-- .../scoreboard/SurfScoreboardBuilderImpl.kt | 59 +++-- .../impl/scoreboard/SurfScoreboardImpl.kt | 208 +++++++++++++++--- .../surf-api-paper/api/surf-api-paper.api | 20 ++ .../api/paper/scoreboard/SurfScoreboard.kt | 2 + .../paper/scoreboard/SurfScoreboardBuilder.kt | 84 +++++++ .../paper/scoreboard/scoreboard-extensions.kt | 27 +++ 10 files changed, 408 insertions(+), 104 deletions(-) create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/ScoreboardLine.kt create mode 100644 surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/scoreboard-extensions.kt diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/CreateScoreboard.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/CreateScoreboard.java index 4956bda04..132e3bfa6 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/CreateScoreboard.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/CreateScoreboard.java @@ -5,14 +5,13 @@ import dev.slne.surf.api.core.messages.Colors; import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatablePlayerScoreboard; import dev.slne.surf.api.paper.scoreboard.SurfScoreboardBuilder; -import net.kyori.adventure.text.Component; -import net.minecraft.util.Util; -import org.jetbrains.annotations.Contract; - import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.minecraft.util.Util; +import org.jetbrains.annotations.Contract; public class CreateScoreboard extends CommandAPICommand { @@ -40,6 +39,7 @@ public CreateScoreboard(String commandName) { } })) .addUpdatableLine(() -> Component.text("Updatable Line: " + UUID.randomUUID())) + .addViewerLine(player -> Component.text("Viewer health: " + player.getHealth())) .buildAutoUpdatablePlayer(); scoreboard.enable(); diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/ScoreboardLine.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/ScoreboardLine.kt new file mode 100644 index 000000000..463eec65c --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/ScoreboardLine.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.paper.server.impl.scoreboard + +import net.kyori.adventure.text.Component +import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent +import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation +import org.bukkit.entity.Player +import java.util.function.Function + +sealed interface ScoreboardLine { + class Shared(val component: SidebarComponent) : ScoreboardLine + class Viewer(val factory: Function) : ScoreboardLine +} + +/** + * Holds the value of a mutable line source as of the last [refresh]. + */ +class LineSnapshot(private val source: () -> T) { + lateinit var value: T + private set + + fun refresh() { + value = source() + } +} + +class ScoreboardDefinition( + val title: Component, + val maxLines: Int, + val lines: List, + val snapshots: List>, + val animations: List> +) diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatablePlayerScoreboardImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatablePlayerScoreboardImpl.kt index 378cbc514..dbdeee29d 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatablePlayerScoreboardImpl.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatablePlayerScoreboardImpl.kt @@ -2,34 +2,23 @@ package dev.slne.surf.api.paper.server.impl.scoreboard import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatablePlayerScoreboard import dev.slne.surf.api.paper.util.forEachPlayer -import net.kyori.adventure.text.Component import net.kyori.adventure.text.logger.slf4j.ComponentLogger -import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent -import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation import org.bukkit.entity.Player class SurfAutoUpdatablePlayerScoreboardImpl( - title: Component, - maxLines: Int, - sidebarComponent: SidebarComponent, - animations: List> -) : SurfAutoUpdatableScoreboardImpl( - title, maxLines, sidebarComponent, animations -), SurfAutoUpdatablePlayerScoreboard { + definition: ScoreboardDefinition +) : SurfAutoUpdatableScoreboardImpl(definition), SurfAutoUpdatablePlayerScoreboard { override fun addViewer(viewer: Player) { ComponentLogger.logger() .warn("You are not allowed to add viewers to this scoreboard. This Scoreboard automatically adds viewers.") } override fun removeViewer(viewer: Player) { - ComponentLogger.logger().warn( - "You are not allowed to remove viewers from this scoreboard. This Scoreboard automatically removes viewers." - ) + ComponentLogger.logger() + .warn("You are not allowed to remove viewers from this scoreboard. This Scoreboard automatically removes viewers.") } - override fun update() { - super.update() - val scoreboard = scoreboard ?: return - forEachPlayer { scoreboard.addPlayer(it) } + override fun onUpdate() { + forEachPlayer { addViewerInternal(it) } } -} \ 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/impl/scoreboard/SurfAutoUpdatableScoreboardImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatableScoreboardImpl.kt index 93122b6f8..ebcf48f98 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatableScoreboardImpl.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfAutoUpdatableScoreboardImpl.kt @@ -1,39 +1,42 @@ package dev.slne.surf.api.paper.server.impl.scoreboard -import com.github.shynixn.mccoroutine.folia.launch -import com.github.shynixn.mccoroutine.folia.ticks +import com.github.shynixn.mccoroutine.folia.scope +import dev.slne.surf.api.core.util.runAtFixedRate import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatableScoreboard import dev.slne.surf.api.paper.server.plugin +import io.papermc.paper.util.Tick import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import net.kyori.adventure.text.Component -import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent -import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation +import kotlin.concurrent.withLock +import kotlin.time.toKotlinDuration open class SurfAutoUpdatableScoreboardImpl( - title: Component, - maxLines: Int, - sidebarComponent: SidebarComponent, - animations: List> -) : SurfScoreboardImpl(title, maxLines, sidebarComponent, animations), SurfAutoUpdatableScoreboard { + definition: ScoreboardDefinition +) : SurfScoreboardImpl(definition), SurfAutoUpdatableScoreboard { private var updater: Job? = null override fun enable() { - super.enable() - - this.updater = launchUpdater() + lock.withLock { + super.enable() + updater = launchUpdater() + } } override fun disable() { - super.disable() + lock.withLock { + super.disable() + updater?.cancel() + updater = null + } + } - updater!!.cancel() + private fun launchUpdater(): Job = plugin.scope.runAtFixedRate( + fiveTicks, + taskName = "SurfAutoUpdatableScoreboardUpdater" + ) { + updateIfEnabled() } - private fun launchUpdater() = plugin.launch { - while (true) { - update() - delay(5.ticks) - } + companion object { + private val fiveTicks = Tick.of(5).toKotlinDuration() } -} \ 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/impl/scoreboard/SurfScoreboardBuilderImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardBuilderImpl.kt index 76a9396bc..eb9af852e 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardBuilderImpl.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardBuilderImpl.kt @@ -9,10 +9,13 @@ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent import net.megavex.scoreboardlibrary.api.sidebar.component.animation.CollectionSidebarAnimation import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation +import org.bukkit.entity.Player +import java.util.function.Function import java.util.function.Supplier class SurfScoreboardBuilderImpl(private val title: Component) : SurfScoreboardBuilder { - private val sidebarComponentBuilder = SidebarComponent.builder() + private val lines = mutableObjectListOf() + private val snapshots = mutableObjectListOf>() private val animations = mutableObjectListOf>() private var maxLines = SurfScoreboardBuilder.DEFAULT_MAX_LINES @@ -21,52 +24,48 @@ class SurfScoreboardBuilderImpl(private val title: Component) : SurfScoreboardBu this.maxLines = maxLines } - override fun addLine(line: Component) = apply { - sidebarComponentBuilder.addStaticLine(line) - } + override fun addLine(line: Component) = addShared(SidebarComponent.staticLine(line)) override fun addUpdatableLine(line: Supplier) = apply { - sidebarComponentBuilder.addDynamicLine(line) + val snapshot = LineSnapshot(line::get).also { snapshots.add(it) } + addShared { drawable -> drawable.drawLine(snapshot.value) } } - override fun addAnimatedLine(animation: SidebarAnimation) = apply { - sidebarComponentBuilder.addAnimatedComponent(animation) + override fun addViewerComponent(component: Function) = apply { + lines.add(ScoreboardLine.Viewer(component)) } + override fun addAnimatedLine(animation: SidebarAnimation) = + addShared(SidebarComponent.animatedComponent(animation)) + override fun addAnimatedLine(frames: MutableList) = apply { check(frames.isNotEmpty()) { "frames cannot be empty" } + addAnimation(CollectionSidebarAnimation(frames)) + } - val animation = CollectionSidebarAnimation(frames) - sidebarComponentBuilder.addAnimatedLine(animation) + override fun addGradientLine(text: Component, start: TextColor, end: TextColor) = + addAnimation(createGradientAnimation(text, start.asHexString(), end.asHexString())) + + private fun addAnimation(animation: SidebarAnimation) = apply { animations.add(animation) + addShared(SidebarComponent.animatedLine(animation)) } - override fun addGradientLine(text: Component, start: TextColor, end: TextColor) = apply { - val gradient = createGradientAnimation(text, start.asHexString(), end.asHexString()) - sidebarComponentBuilder.addAnimatedLine(gradient) - animations.add(gradient) + private fun addShared(component: SidebarComponent) = apply { + lines.add(ScoreboardLine.Shared(component)) } - override fun build() = SurfScoreboardImpl( - title, maxLines, sidebarComponentBuilder.build(), animations - ) - - - override fun buildAutoUpdatable() = SurfAutoUpdatableScoreboardImpl( - title, - maxLines, - sidebarComponentBuilder.build(), - animations - ) - - - override fun buildAutoUpdatablePlayer() = SurfAutoUpdatablePlayerScoreboardImpl( + private fun definition() = ScoreboardDefinition( title, maxLines, - sidebarComponentBuilder.build(), - animations + lines.toList(), + snapshots.toList(), + animations.toList(), ) + override fun build() = SurfScoreboardImpl(definition()) + override fun buildAutoUpdatable() = SurfAutoUpdatableScoreboardImpl(definition()) + override fun buildAutoUpdatablePlayer() = SurfAutoUpdatablePlayerScoreboardImpl(definition()) companion object { private fun createGradientAnimation( @@ -95,4 +94,4 @@ class SurfScoreboardBuilderImpl(private val title: Component) : SurfScoreboardBu return CollectionSidebarAnimation(frames) } } -} \ 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/impl/scoreboard/SurfScoreboardImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardImpl.kt index 727f3c5d7..1ba4a9da3 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardImpl.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardImpl.kt @@ -1,64 +1,212 @@ package dev.slne.surf.api.paper.server.impl.scoreboard +import dev.slne.surf.api.core.util.mutableObject2ObjectMapOf +import dev.slne.surf.api.paper.extensions.server import dev.slne.surf.api.paper.scoreboard.SurfScoreboard import dev.slne.surf.api.paper.scoreboard.SurfScoreboardApi +import dev.slne.surf.api.paper.server.plugin +import io.papermc.paper.threadedregions.scheduler.ScheduledTask import net.kyori.adventure.text.Component import net.megavex.scoreboardlibrary.api.sidebar.Sidebar import net.megavex.scoreboardlibrary.api.sidebar.component.ComponentSidebarLayout import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent import net.megavex.scoreboardlibrary.api.sidebar.component.animation.FramedSidebarAnimation -import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation import org.bukkit.entity.Player +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import java.util.function.Consumer +import kotlin.concurrent.withLock -open class SurfScoreboardImpl( - protected val title: Component, - protected val maxLines: Int, - protected val sidebarComponent: SidebarComponent, - protected val animations: List> -) : SurfScoreboard { - protected var scoreboard: Sidebar? = null - protected var sidebarLayout: ComponentSidebarLayout? = null - protected var enabled: Boolean = false +open class SurfScoreboardImpl(definition: ScoreboardDefinition) : SurfScoreboard { + protected val lock = ReentrantLock() + + private val maxLines = definition.maxLines + private val lines = definition.lines + private val snapshots = definition.snapshots + private val animations = definition.animations + private val titleComponent = SidebarComponent.staticLine(definition.title) + + /** Layout of the single shared sidebar, or `null` if the scoreboard contains viewer lines. */ + private val sharedLayout = if (lines.all { it is ScoreboardLine.Shared }) { + layoutOf(lines.map { (it as ScoreboardLine.Shared).component }) + } else { + null + } + private val perViewer = sharedLayout == null + + private var enabled: Boolean = false + private var sharedSidebar: Sidebar? = null + private val viewerBoards = mutableObject2ObjectMapOf() override fun addViewer(viewer: Player) { - check(enabled) { "Scoreboard is not enabled. Did you forget to call enable()?" } + lock.withLock { + checkEnabled() + addViewerInternal(viewer) + } + } + + /** Adds [viewer] without the public API checks of subclasses. */ + protected fun addViewerInternal(viewer: Player) { + assert(lock.isHeldByCurrentThread) { "addViewerInternal must be called with lock held" } + + if (!perViewer) { + sharedSidebar!!.addPlayer(viewer) + return + } - scoreboard!!.addPlayer(viewer) + val existing = viewerBoards[viewer.uniqueId] + if (existing != null) { + if (existing.viewer === viewer) return + existing.sidebar.close() + } + + val board = ViewerBoard(viewer, createSidebar()) + viewerBoards[viewer.uniqueId] = board + scheduleRender(board) } override fun removeViewer(viewer: Player) { - check(enabled) { "Scoreboard is not enabled. Did you forget to call enable()?" } + lock.withLock { + checkEnabled() - scoreboard!!.removePlayer(viewer) + if (perViewer) { + viewerBoards.remove(viewer.uniqueId)?.sidebar?.close() + } else { + sharedSidebar!!.removePlayer(viewer) + } + } } override fun enable() { - check(!enabled) { "Scoreboard is already enabled" } + lock.withLock { + check(!enabled) { "Scoreboard is already enabled" } - scoreboard = SurfScoreboardApi.scoreboardLibrary().createSidebar(maxLines) - sidebarLayout = ComponentSidebarLayout( - SidebarComponent.staticLine(title), - sidebarComponent - ).also { it.apply(scoreboard!!) } + snapshots.forEach { it.refresh() } + if (sharedLayout != null) { + sharedSidebar = createSidebar().also { sharedLayout.apply(it) } + } - enabled = true + enabled = true + } } override fun disable() { - check(enabled) { "Scoreboard is not enabled. Did you forget to call enable()?" } + lock.withLock { + checkEnabled() - scoreboard!!.close() - animations.forEach { (it as? FramedSidebarAnimation)?.switchFrame(0) } + sharedSidebar?.close() + sharedSidebar = null + viewerBoards.values.forEach { it.sidebar.close() } + viewerBoards.clear() + animations.forEach { (it as? FramedSidebarAnimation)?.switchFrame(0) } - this.scoreboard = null - sidebarLayout = null - enabled = false + enabled = false + } } override fun update() { + lock.withLock { + checkEnabled() + + removeDisconnectedViewers() + animations.forEach { it.nextFrame() } + snapshots.forEach { it.refresh() } + + if (sharedLayout != null) { + sharedLayout.apply(sharedSidebar!!) + } else { + viewerBoards.values.forEach(::scheduleRender) + } + + onUpdate() + } + } + + /** Runs [update] if the scoreboard is enabled; does nothing otherwise. */ + protected fun updateIfEnabled() { + lock.withLock { + if (enabled) update() + } + } + + /** Called at the end of every [update] while [lock] is held. */ + protected open fun onUpdate() {} + + private fun removeDisconnectedViewers() { + assert(lock.isHeldByCurrentThread) { "removeDisconnectedViewers must be called with lock held" } + + if (perViewer) { + viewerBoards.values.removeIf { board -> + if (board.viewer.isConnected) return@removeIf false + board.sidebar.close() + true + } + } else { + val sidebar = sharedSidebar!! + sidebar.players().filterNot { it.isConnected }.forEach(sidebar::removePlayer) + } + } + + private fun scheduleRender(board: ViewerBoard) { + if (!board.renderPending.compareAndSet(false, true)) return + + if (board.viewer.scheduler.run(plugin, board.renderTask, board.retiredTask) == null) { + board.renderPending.set(false) + } + } + + private fun renderViewer(board: ViewerBoard) { + assert(server.isOwnedByCurrentRegion(board.viewer)) { "renderViewer must be called on the viewer's entity scheduler thread" } + + board.renderPending.set(false) + + val components = arrayOfNulls(lines.size) + lines.forEachIndexed { index, line -> + if (line is ScoreboardLine.Viewer) { + components[index] = line.factory.apply(board.viewer) + } + } + + lock.withLock { + if (!enabled || viewerBoards[board.viewer.uniqueId] !== board) return + + board.viewerComponents = components + board.layout.apply(board.sidebar) + board.sidebar.addPlayer(board.viewer) + } + } + + private fun layoutOf(components: List) = ComponentSidebarLayout( + titleComponent, + SidebarComponent.builder().apply { components.forEach(::addComponent) }.build() + ) + + private fun createSidebar() = SurfScoreboardApi.scoreboardLibrary().createSidebar(maxLines) + + private fun checkEnabled() { check(enabled) { "Scoreboard is not enabled. Did you forget to call enable()?" } + } + + private inner class ViewerBoard(val viewer: Player, val sidebar: Sidebar) { + val renderPending = AtomicBoolean() + val renderTask = Consumer { renderViewer(this) } + val retiredTask = Runnable { renderPending.set(false) } + + /** Components of the viewer lines from the last render, indexed like [lines]. */ + var viewerComponents = arrayOfNulls(lines.size) + + val layout = layoutOf(lines.mapIndexed { index, line -> + when (line) { + is ScoreboardLine.Shared -> line.component + is ScoreboardLine.Viewer -> SidebarComponent { drawable -> + viewerComponents[index]?.draw(drawable) + } + } + }) - animations.forEach { it.nextFrame() } - sidebarLayout!!.apply(scoreboard!!) + override fun toString(): String { + return "ViewerBoard(viewer=$viewer, sidebar=$sidebar, renderPending=$renderPending)" + } } -} \ No newline at end of file +} diff --git a/surf-api-paper/surf-api-paper/api/surf-api-paper.api b/surf-api-paper/surf-api-paper/api/surf-api-paper.api index dd0050131..21df4224c 100644 --- a/surf-api-paper/surf-api-paper/api/surf-api-paper.api +++ b/surf-api-paper/surf-api-paper/api/surf-api-paper.api @@ -3028,6 +3028,13 @@ public final class dev/slne/surf/api/paper/region/TickThreadGuard$Companion : de public final fun getINSTANCE ()Ldev/slne/surf/api/paper/region/TickThreadGuard; } +public final class dev/slne/surf/api/paper/scoreboard/Scoreboard_extensionsKt { + public static final fun buildDynamicLine (Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder;Lkotlin/jvm/functions/Function1;)Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder; + public static final fun buildSidebarComponent (Lkotlin/jvm/functions/Function1;)Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent; + public static final fun buildStaticLine (Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder;Lkotlin/jvm/functions/Function1;)Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder; + public static final fun buildStaticLine (Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder;Lnet/megavex/scoreboardlibrary/api/objective/ScoreFormat;Lkotlin/jvm/functions/Function1;)Lnet/megavex/scoreboardlibrary/api/sidebar/component/SidebarComponent$Builder; +} + public abstract interface class dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard : dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard { public abstract fun addViewer (Lorg/bukkit/entity/Player;)V public abstract fun removeViewer (Lorg/bukkit/entity/Player;)V @@ -3067,9 +3074,16 @@ public abstract interface class dev/slne/surf/api/paper/scoreboard/SurfScoreboar public abstract fun addLine (Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public fun addLineSeparator ()Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public abstract fun addUpdatableLine (Ljava/util/function/Supplier;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public abstract fun addViewerComponent (Ljava/util/function/Function;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public fun addViewerLine (Ljava/util/function/Function;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public abstract fun build ()Ldev/slne/surf/api/paper/scoreboard/SurfScoreboard; public abstract fun buildAutoUpdatable ()Ldev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard; public abstract fun buildAutoUpdatablePlayer ()Ldev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard; + public fun buildGradientLine (Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public fun buildLine (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public fun buildUpdatableLine (Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public fun buildViewerComponent (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public fun buildViewerLine (Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public static fun builder (Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public abstract fun maxLines (I)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; } @@ -3082,6 +3096,12 @@ public final class dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder$Comp public final class dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder$DefaultImpls { public static fun addEmptyLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; public static fun addLineSeparator (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun addViewerLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Ljava/util/function/Function;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun buildGradientLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun buildLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun buildUpdatableLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun buildViewerComponent (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; + public static fun buildViewerLine (Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder;Lkotlin/jvm/functions/Function2;)Ldev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder; } public final class dev/slne/surf/api/paper/serializer/bukkit/blockdata/BlockDataSerializer : kotlinx/serialization/KSerializer { diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboard.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboard.kt index 9dc1063e1..c89983383 100644 --- a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboard.kt +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboard.kt @@ -5,6 +5,8 @@ import org.bukkit.entity.Player /** * The SurfScoreboard interface represents a scoreboard in a surf game. This scoreboard can be * enabled, disabled, and updated. Viewers can be added and removed from the scoreboard. + * + * All methods are thread-safe. Viewers that disconnect are removed on the next [update]. */ interface SurfScoreboard { /** diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder.kt index dea628e92..d8dacd057 100644 --- a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder.kt +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardBuilder.kt @@ -1,11 +1,14 @@ package dev.slne.surf.api.paper.scoreboard import dev.slne.surf.api.core.messages.Colors +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent import net.megavex.scoreboardlibrary.api.sidebar.component.animation.SidebarAnimation +import org.bukkit.entity.Player import org.jetbrains.annotations.Range +import java.util.function.Function import java.util.function.Supplier /** @@ -30,6 +33,15 @@ interface SurfScoreboardBuilder { */ fun addLine(line: Component): SurfScoreboardBuilder + /** + * Adds a static line built with [SurfComponentBuilder]. + * + * @see addLine + */ + fun buildLine(line: SurfComponentBuilder.() -> Unit): SurfScoreboardBuilder { + return addLine(SurfComponentBuilder(line)) + } + /** * Adds an empty line to the SurfScoreboardBuilder. * @@ -49,6 +61,65 @@ interface SurfScoreboardBuilder { */ fun addUpdatableLine(line: Supplier): SurfScoreboardBuilder + /** + * Adds an updatable line whose content is rebuilt with [SurfComponentBuilder] on every update. + * + * @see addUpdatableLine + */ + fun buildUpdatableLine(line: SurfComponentBuilder.() -> Unit): SurfScoreboardBuilder { + return addUpdatableLine { SurfComponentBuilder(line) } + } + + /** + * Adds a line that is rendered separately for every viewer. + * + * [line] is invoked on the viewer's entity scheduler thread once per update, so it may safely + * access the viewer's state. A scoreboard containing viewer lines maintains one sidebar per + * viewer; shared lines are still evaluated only once per update. + * + * @param line renders the line for the given viewer + * @return the SurfScoreboardBuilder instance + */ + fun addViewerLine(line: Function): SurfScoreboardBuilder { + return addViewerComponent { viewer -> SidebarComponent.staticLine(line.apply(viewer)) } + } + + /** + * Adds a viewer line whose content is built with [SurfComponentBuilder] for every viewer. + * + * @see addViewerLine + */ + fun buildViewerLine(line: SurfComponentBuilder.(viewer: Player) -> Unit): SurfScoreboardBuilder { + return addViewerLine { viewer -> SurfComponentBuilder { line(viewer) } } + } + + /** + * Adds a component that is created separately for every viewer and may draw a variable number + * of lines. + * + * [component] is invoked on the viewer's entity scheduler thread once per update, like + * [addViewerLine]. The returned component is drawn immediately afterwards on the same thread + * while the scoreboard is locked, so it should only draw precomputed lines. + * + * @param component creates the component for the given viewer + * @return the SurfScoreboardBuilder instance + */ + fun addViewerComponent(component: Function): SurfScoreboardBuilder + + /** + * Adds a viewer component whose lines are added to a [SidebarComponent.Builder] for every viewer. + * + * Lines may be added conditionally, so the number of lines can differ between viewers and updates. + * [buildStaticLine] builds a line with [SurfComponentBuilder]. + * + * @see addViewerComponent + */ + fun buildViewerComponent( + component: SidebarComponent.Builder.(viewer: Player) -> Unit + ): SurfScoreboardBuilder { + return addViewerComponent { viewer -> buildSidebarComponent { component(viewer) } } + } + /** * Adds an animated line to the SurfScoreboardBuilder. * @@ -84,6 +155,19 @@ interface SurfScoreboardBuilder { end: TextColor ): SurfScoreboardBuilder + /** + * Adds a gradient line whose text is built with [SurfComponentBuilder]. + * + * @see addGradientLine + */ + fun buildGradientLine( + start: TextColor, + end: TextColor, + text: SurfComponentBuilder.() -> Unit + ): SurfScoreboardBuilder { + return addGradientLine(SurfComponentBuilder(text), start, end) + } + /** * Adds a line separator to the SurfScoreboardBuilder. This method adds a gradient line separator * composed of '-' characters to the SurfScoreboardBuilder. The default colors for the gradient diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/scoreboard-extensions.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/scoreboard-extensions.kt new file mode 100644 index 000000000..74fac1def --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/scoreboard-extensions.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.paper.scoreboard + +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import net.megavex.scoreboardlibrary.api.objective.ScoreFormat +import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent + +/** Builds a [SidebarComponent] from the lines added in [block]. */ +inline fun buildSidebarComponent(block: SidebarComponent.Builder.() -> Unit): SidebarComponent = + SidebarComponent.builder().apply(block).build() + +/** Adds a static line built with [SurfComponentBuilder]. */ +inline fun SidebarComponent.Builder.buildStaticLine( + line: SurfComponentBuilder.() -> Unit +): SidebarComponent.Builder = addStaticLine(SurfComponentBuilder(line)) + +/** + * Adds a static line built with [SurfComponentBuilder] and a custom score format. + */ +inline fun SidebarComponent.Builder.buildStaticLine( + scoreFormat: ScoreFormat, + line: SurfComponentBuilder.() -> Unit +): SidebarComponent.Builder = addStaticLine(SurfComponentBuilder(line), scoreFormat) + +/** Adds a line that is rebuilt with [SurfComponentBuilder] every time the component is drawn. */ +fun SidebarComponent.Builder.buildDynamicLine( + line: SurfComponentBuilder.() -> Unit +): SidebarComponent.Builder = addDynamicLine { SurfComponentBuilder(line) } From 78c37ca9c444b67ce03c2083013f358e11954041 Mon Sep 17 00:00:00 2001 From: twisti-dev <76837088+twisti-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:54:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=A7=20chore:=20update=20version=20?= =?UTF-8?q?to=203.45.0=20in=20gradle.properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5da771f94..7156012db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.2 group=dev.slne.surf.api -version=3.44.0 +version=3.45.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false