diff --git a/gradle.properties b/gradle.properties index a7322216f..62f4d65c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.3 group=dev.slne.surf.api -version=3.45.0 +version=3.46.0 relocationPrefix=dev.slne.surf.api.libs -snapshot=true +snapshot=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc6a1495b..611187935 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,11 +9,12 @@ kotlinxCoroutines = "1.11.0" kotlinx-serialization = "1.11.0" # Packet Events -packetevents = "2.13.0" -packetevents-plugin = "2.13.0" +packetevents = "2.14.0" +packetevents-plugin = "2.14.0" # Command API commandapi = "12.0.0" +commandapiplugin = "12.1.0" # LuckPerms luckperms = "5.5" diff --git a/surf-api-gradle-plugin/build.gradle.kts b/surf-api-gradle-plugin/build.gradle.kts index 79aa8714e..66414abca 100644 --- a/surf-api-gradle-plugin/build.gradle.kts +++ b/surf-api-gradle-plugin/build.gradle.kts @@ -121,7 +121,7 @@ val generateConstants = tasks.register("generateConstants") { inputs.property("libs.canvas.api", libs.canvas.api.get().toString()) inputs.property("libs.velocity.api", libs.velocity.api.get().toString()) inputs.property("libs.auto.service.annotations", libs.auto.service.annotations.get().toString()) - inputs.property("libs.versions.commandapi", libs.versions.commandapi.get()) + inputs.property("libs.versions.commandapiplugin", libs.versions.commandapiplugin.get()) inputs.property("libs.versions.placeholder.api", libs.versions.placeholder.api.get()) inputs.property("libs.versions.luckperms", libs.versions.luckperms.get()) inputs.property( @@ -163,7 +163,7 @@ val generateConstants = tasks.register("generateConstants") { | const val MINECRAFT_VERSION = "$mcVersion" | const val SURF_API_VERSION = "+" | - | const val COMMAND_API_VERSION = "${libs.versions.commandapi.get()}" + | const val COMMAND_API_VERSION = "${libs.versions.commandapiplugin.get()}" | const val PLACEHOLDER_API_VERSION = "${libs.versions.placeholder.api.get()}" | const val LUCKPERMS_VERSION = "${libs.versions.luckperms.get()}" | const val PACKETEVENTS_VERSION = "${libs.versions.packetevents.plugin.get()}" diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt index 238e6f10a..d74f4dd94 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/NmsProvider.kt @@ -10,11 +10,13 @@ import dev.slne.surf.api.paper.nms.bridges.packets.entity.SurfPaperNmsSpawnPacke import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerChatPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerToastPackets +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard import dev.slne.surf.api.paper.packet.listener.listener.PacketListener import dev.slne.surf.api.paper.region.TickThreadGuard import dev.slne.surf.api.shared.internal.nms.NmsProviderConfig import dev.slne.surf.api.shared.internal.nms.NmsProviderMeta import dev.slne.surf.api.shared.internal.nms.NmsVersion +import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.io.File import java.net.URI @@ -81,6 +83,13 @@ interface NmsProvider { */ fun createGlowingApi(): SurfGlowingApi + // ==================== Scoreboard ==================== // + + /** + * Creates a packet-based sidebar for [player]; nothing is sent before its first update. + */ + fun createPlayerScoreboard(player: Player): PlayerNmsScoreboard + // ==================== Packet Listeners ==================== // fun createChannelInjector(): AbstractChannelInjector<*> diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/scoreboard/PlayerNmsScoreboard.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/scoreboard/PlayerNmsScoreboard.kt new file mode 100644 index 000000000..9e5e78f9b --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-common/src/main/kotlin/dev/slne/surf/api/paper/nms/common/scoreboard/PlayerNmsScoreboard.kt @@ -0,0 +1,108 @@ +/* + * This file contains code derived from FastBoard, licensed under the MIT License. + * + * Copyright (c) 2019-2026 MrMicky + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package dev.slne.surf.api.paper.nms.common.scoreboard + +import dev.slne.surf.api.paper.nms.bridges.packets.PacketOperation +import dev.slne.surf.api.paper.sidebar.SidebarLine +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.max + +/** + * Packet-based sidebar objective of a single player. + * + * Not thread-safe: the owner serializes all calls. Nothing is sent before the first [update], + * which creates and displays the objective; after [delete], further calls do nothing. + */ +abstract class PlayerNmsScoreboard(protected val player: Player) { + + val id = "sb-" + NEXT_ID.getAndIncrement() + + private var shown = false + private var deleted = false + private var title: Component = Component.empty() + private var lines: List = emptyList() + + /** + * Shows [title] and [lines], sending only what changed since the previous call as one bundle. + * + * [lines] is retained and must neither be modified afterwards nor exceed [MAX_LINES] entries. + */ + fun update(title: Component, lines: List) { + if (deleted) return + + val operation = PacketOperation.start() + if (!shown) { + shown = true + operation.add(createObjective(title)) + } else if (this.title != title) { + operation.add(updateObjective(title)) + } + this.title = title + + val oldLines = this.lines + this.lines = lines + for (index in 0 until max(oldLines.size, lines.size)) { + val line = lines.getOrNull(index) + if (line == null) { + operation.add(resetScore(ENTRIES[index])) + } else if (line != oldLines.getOrNull(index)) { + operation.add(setScore(ENTRIES[index], MAX_LINES - index, line)) + } + } + + send(operation) + } + + /** Removes the objective from the client if it was shown. */ + fun delete() { + if (deleted) return + + deleted = true + if (shown) send(removeObjective()) + } + + protected abstract fun createObjective(title: Component): PacketOperation + protected abstract fun updateObjective(title: Component): PacketOperation + protected abstract fun removeObjective(): PacketOperation + protected abstract fun setScore(owner: String, score: Int, line: SidebarLine): PacketOperation + protected abstract fun resetScore(owner: String): PacketOperation + + private fun send(operation: PacketOperation) { + if (!operation.isEmpty() && player.isConnected) { + operation.execute(player) + } + } + + companion object { + /** + * Maximum number of sidebar entries the client renders. + */ + const val MAX_LINES = 15 + + private val NEXT_ID = AtomicInteger() + private val ENTRIES = List(MAX_LINES) { "ยง" + it.toString(16) } + } +} diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt index 8ec6d6365..5940421c2 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/V1_21_11NmsProvider.kt @@ -10,6 +10,7 @@ import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerChat import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerToastPackets import dev.slne.surf.api.paper.nms.common.* +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard import dev.slne.surf.api.paper.packet.listener.listener.PacketListener import dev.slne.surf.api.paper.region.TickThreadGuard import dev.slne.surf.api.paper.server.nms.v1_21_11.bridges.* @@ -28,8 +29,10 @@ import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.lore.V1_21_11PacketLor import dev.slne.surf.api.paper.server.nms.v1_21_11.packet.lore.V1_21_11PacketLoreRegistry import dev.slne.surf.api.paper.server.nms.v1_21_11.reflection.V1_21_11Reflection import dev.slne.surf.api.paper.server.nms.v1_21_11.region.V1_21_11TickThreadGuard +import dev.slne.surf.api.paper.server.nms.v1_21_11.scoreboard.V1_21_11PlayerNmsScoreboardImpl import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion +import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.util.* @@ -90,6 +93,7 @@ class V1_21_11NmsProvider(override val plugin: JavaPlugin) : NmsProvider { V1_21_11GlowingLifecycleHandler() override fun createGlowingApi(): SurfGlowingApi = V1_21_11SurfGlowingApiImpl + override fun createPlayerScoreboard(player: Player): PlayerNmsScoreboard = V1_21_11PlayerNmsScoreboardImpl(player) override fun createChannelInjector(): AbstractChannelInjector<*> = V1_21_11ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V1_21_11PacketListenerApiImpl() diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/bridges/packets/V1_21_11PacketOperationImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/bridges/packets/V1_21_11PacketOperationImpl.kt index 9e00e2afd..e6ee6945f 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/bridges/packets/V1_21_11PacketOperationImpl.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/bridges/packets/V1_21_11PacketOperationImpl.kt @@ -98,6 +98,14 @@ class V1_21_11PacketOperationImpl : PacketOperation { return V1_21_11PacketOperationImpl(operation) } + @JvmStatic + fun multi(operation: (player: Player, packets: LinkedList>) -> Unit): V1_21_11PacketOperationImpl { + return V1_21_11PacketOperationImpl { player, packets -> + operation(player, packets) + packets + } + } + fun simple(packetSupplier: (Player) -> Packet): V1_21_11PacketOperationImpl { return V1_21_11PacketOperationImpl { player, packets -> packets.add(packetSupplier(player)) diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/scoreboard/V1_21_11PlayerNmsScoreboardImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/scoreboard/V1_21_11PlayerNmsScoreboardImpl.kt new file mode 100644 index 000000000..e7a63b579 --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v1-21-11/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v1_21_11/scoreboard/V1_21_11PlayerNmsScoreboardImpl.kt @@ -0,0 +1,85 @@ +package dev.slne.surf.api.paper.server.nms.v1_21_11.scoreboard + +import dev.slne.surf.api.paper.nms.bridges.packets.PacketOperation +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.server.nms.v1_21_11.bridges.packets.V1_21_11PacketOperationImpl +import dev.slne.surf.api.paper.server.nms.v1_21_11.extensions.toNms +import dev.slne.surf.api.paper.sidebar.SidebarLine +import net.kyori.adventure.text.Component +import net.minecraft.network.chat.numbers.BlankFormat +import net.minecraft.network.chat.numbers.FixedFormat +import net.minecraft.network.protocol.game.ClientboundResetScorePacket +import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetScorePacket +import net.minecraft.world.scores.DisplaySlot +import net.minecraft.world.scores.Objective +import net.minecraft.world.scores.Scoreboard +import net.minecraft.world.scores.criteria.ObjectiveCriteria +import org.bukkit.entity.Player +import java.util.* + +@Suppress("ClassName") +class V1_21_11PlayerNmsScoreboardImpl(player: Player) : PlayerNmsScoreboard(player) { + + override fun createObjective(title: Component): PacketOperation { + val objective = objective(title) + + return V1_21_11PacketOperationImpl.multi { _, packets -> + packets.add( + ClientboundSetObjectivePacket( + objective, + ClientboundSetObjectivePacket.METHOD_ADD + ) + ) + packets.add(ClientboundSetDisplayObjectivePacket(DisplaySlot.SIDEBAR, objective)) + } + } + + override fun updateObjective(title: Component): PacketOperation { + return V1_21_11PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(title), + ClientboundSetObjectivePacket.METHOD_CHANGE + ) + } + } + + override fun removeObjective(): PacketOperation = V1_21_11PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(Component.empty()), + ClientboundSetObjectivePacket.METHOD_REMOVE + ) + } + + override fun setScore(owner: String, score: Int, line: SidebarLine): PacketOperation { + return V1_21_11PacketOperationImpl.simple { + val format = line.score?.let { FixedFormat(it.toNms()) } ?: BlankFormat.INSTANCE + ClientboundSetScorePacket( + owner, + id, + score, + Optional.of(line.text.toNms()), + Optional.of(format) + ) + } + } + + override fun resetScore(owner: String): PacketOperation = V1_21_11PacketOperationImpl.simple { + ClientboundResetScorePacket(owner, id) + } + + private fun objective(title: Component) = Objective( + scoreboard, + id, + ObjectiveCriteria.DUMMY, + title.toNms(), + ObjectiveCriteria.RenderType.INTEGER, + false, + null + ) + + companion object { + private val scoreboard = Scoreboard() + } +} diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt index 63ecc4647..5c3d4881c 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/V26_1NmsProvider.kt @@ -10,6 +10,7 @@ import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerChat import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerToastPackets import dev.slne.surf.api.paper.nms.common.* +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard import dev.slne.surf.api.paper.packet.listener.listener.PacketListener import dev.slne.surf.api.paper.region.TickThreadGuard import dev.slne.surf.api.paper.server.nms.v26_1.bridges.* @@ -28,8 +29,10 @@ import dev.slne.surf.api.paper.server.nms.v26_1.packet.lore.V26_1PacketLoreListe import dev.slne.surf.api.paper.server.nms.v26_1.packet.lore.V26_1PacketLoreRegistry import dev.slne.surf.api.paper.server.nms.v26_1.reflection.V26_1Reflection import dev.slne.surf.api.paper.server.nms.v26_1.region.V26_1TickThreadGuard +import dev.slne.surf.api.paper.server.nms.v26_1.scoreboard.V26_1PlayerNmsScoreboardImpl import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion +import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.util.* @@ -68,6 +71,7 @@ class V26_1NmsProvider(override val plugin: JavaPlugin) : NmsProvider { override fun createPacketLoreRegistry(): PacketLoreRegistry = V26_1PacketLoreRegistry() override fun createGlowingLifecycleHandler(): GlowingLifecycleHandler = V26_1GlowingLifecycleHandler() override fun createGlowingApi(): SurfGlowingApi = V26_1SurfGlowingApiImpl + override fun createPlayerScoreboard(player: Player): PlayerNmsScoreboard = V26_1PlayerNmsScoreboardImpl(player) override fun createChannelInjector(): AbstractChannelInjector<*> = V26_1ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V26_1PacketListenerApiImpl() diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/bridges/packets/V26_1PacketOperationImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/bridges/packets/V26_1PacketOperationImpl.kt index 61a5752ae..708090c90 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/bridges/packets/V26_1PacketOperationImpl.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/bridges/packets/V26_1PacketOperationImpl.kt @@ -99,6 +99,14 @@ class V26_1PacketOperationImpl : PacketOperation { return V26_1PacketOperationImpl(operation) } + @JvmStatic + fun multi(operation: (player: Player, packets: LinkedList>) -> Unit): V26_1PacketOperationImpl { + return V26_1PacketOperationImpl { player, packets -> + operation(player, packets) + packets + } + } + fun simple(packetSupplier: (Player) -> Packet): V26_1PacketOperationImpl { return V26_1PacketOperationImpl { player, packets -> packets.add(packetSupplier(player)) diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/scoreboard/V26_1PlayerNmsScoreboardImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/scoreboard/V26_1PlayerNmsScoreboardImpl.kt new file mode 100644 index 000000000..3d90f2f0c --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-1/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_1/scoreboard/V26_1PlayerNmsScoreboardImpl.kt @@ -0,0 +1,84 @@ +package dev.slne.surf.api.paper.server.nms.v26_1.scoreboard + +import dev.slne.surf.api.paper.nms.bridges.packets.PacketOperation +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.server.nms.v26_1.bridges.packets.V26_1PacketOperationImpl +import dev.slne.surf.api.paper.server.nms.v26_1.extensions.toNms +import dev.slne.surf.api.paper.sidebar.SidebarLine +import net.kyori.adventure.text.Component +import net.minecraft.network.chat.numbers.BlankFormat +import net.minecraft.network.chat.numbers.FixedFormat +import net.minecraft.network.protocol.game.ClientboundResetScorePacket +import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetScorePacket +import net.minecraft.world.scores.DisplaySlot +import net.minecraft.world.scores.Objective +import net.minecraft.world.scores.Scoreboard +import net.minecraft.world.scores.criteria.ObjectiveCriteria +import org.bukkit.entity.Player +import java.util.* + +@Suppress("ClassName") +class V26_1PlayerNmsScoreboardImpl(player: Player) : PlayerNmsScoreboard(player) { + + override fun createObjective(title: Component): PacketOperation { + val objective = objective(title) + + return V26_1PacketOperationImpl.multi { _, packets -> + packets.add( + ClientboundSetObjectivePacket( + objective, + ClientboundSetObjectivePacket.METHOD_ADD + ) + ) + packets.add(ClientboundSetDisplayObjectivePacket(DisplaySlot.SIDEBAR, objective)) + } + } + + override fun updateObjective(title: Component): PacketOperation { + return V26_1PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(title), + ClientboundSetObjectivePacket.METHOD_CHANGE + ) + } + } + + override fun removeObjective(): PacketOperation = V26_1PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(Component.empty()), + ClientboundSetObjectivePacket.METHOD_REMOVE + ) + } + + override fun setScore(owner: String, score: Int, line: SidebarLine): PacketOperation = + V26_1PacketOperationImpl.simple { + val format = line.score?.let { FixedFormat(it.toNms()) } ?: BlankFormat.INSTANCE + ClientboundSetScorePacket( + owner, + id, + score, + Optional.of(line.text.toNms()), + Optional.of(format) + ) + } + + override fun resetScore(owner: String): PacketOperation = V26_1PacketOperationImpl.simple { + ClientboundResetScorePacket(owner, id) + } + + private fun objective(title: Component) = Objective( + scoreboard, + id, + ObjectiveCriteria.DUMMY, + title.toNms(), + ObjectiveCriteria.RenderType.INTEGER, + false, + null + ) + + companion object { + private val scoreboard = Scoreboard() + } +} diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/V26_2NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/V26_2NmsProvider.kt index 887502e79..92e2b855e 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/V26_2NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/V26_2NmsProvider.kt @@ -10,6 +10,7 @@ import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerChat import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerToastPackets import dev.slne.surf.api.paper.nms.common.* +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard import dev.slne.surf.api.paper.packet.listener.listener.PacketListener import dev.slne.surf.api.paper.region.TickThreadGuard import dev.slne.surf.api.paper.server.nms.v26_2.bridges.* @@ -28,8 +29,10 @@ import dev.slne.surf.api.paper.server.nms.v26_2.packet.lore.V26_2PacketLoreListe import dev.slne.surf.api.paper.server.nms.v26_2.packet.lore.V26_2PacketLoreRegistry import dev.slne.surf.api.paper.server.nms.v26_2.reflection.V26_2Reflection import dev.slne.surf.api.paper.server.nms.v26_2.region.V26_2TickThreadGuard +import dev.slne.surf.api.paper.server.nms.v26_2.scoreboard.V26_2PlayerNmsScoreboardImpl import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion +import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.util.* @@ -68,6 +71,7 @@ class V26_2NmsProvider(override val plugin: JavaPlugin) : NmsProvider { override fun createPacketLoreRegistry(): PacketLoreRegistry = V26_2PacketLoreRegistry() override fun createGlowingLifecycleHandler(): GlowingLifecycleHandler = V26_2GlowingLifecycleHandler() override fun createGlowingApi(): SurfGlowingApi = V26_2SurfGlowingApiImpl + override fun createPlayerScoreboard(player: Player): PlayerNmsScoreboard = V26_2PlayerNmsScoreboardImpl(player) override fun createChannelInjector(): AbstractChannelInjector<*> = V26_2ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V26_2PacketListenerApiImpl() diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/bridges/packets/V26_2PacketOperationImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/bridges/packets/V26_2PacketOperationImpl.kt index bd1af40e6..4720ab380 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/bridges/packets/V26_2PacketOperationImpl.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/bridges/packets/V26_2PacketOperationImpl.kt @@ -99,6 +99,14 @@ class V26_2PacketOperationImpl : PacketOperation { return V26_2PacketOperationImpl(operation) } + @JvmStatic + fun multi(operation: (player: Player, packets: LinkedList>) -> Unit): V26_2PacketOperationImpl { + return V26_2PacketOperationImpl { player, packets -> + operation(player, packets) + packets + } + } + fun simple(packetSupplier: (Player) -> Packet): V26_2PacketOperationImpl { return V26_2PacketOperationImpl { player, packets -> packets.add(packetSupplier(player)) diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/scoreboard/V26_2PlayerNmsScoreboardImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/scoreboard/V26_2PlayerNmsScoreboardImpl.kt new file mode 100644 index 000000000..82d38d8b0 --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-2/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_2/scoreboard/V26_2PlayerNmsScoreboardImpl.kt @@ -0,0 +1,83 @@ +package dev.slne.surf.api.paper.server.nms.v26_2.scoreboard + +import dev.slne.surf.api.paper.nms.bridges.packets.PacketOperation +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.server.nms.v26_2.bridges.packets.V26_2PacketOperationImpl +import dev.slne.surf.api.paper.server.nms.v26_2.extensions.toNms +import dev.slne.surf.api.paper.sidebar.SidebarLine +import net.kyori.adventure.text.Component +import net.minecraft.network.chat.numbers.BlankFormat +import net.minecraft.network.chat.numbers.FixedFormat +import net.minecraft.network.protocol.game.ClientboundResetScorePacket +import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetScorePacket +import net.minecraft.world.scores.DisplaySlot +import net.minecraft.world.scores.Objective +import net.minecraft.world.scores.Scoreboard +import net.minecraft.world.scores.criteria.ObjectiveCriteria +import org.bukkit.entity.Player +import java.util.* + +@Suppress("ClassName") +class V26_2PlayerNmsScoreboardImpl(player: Player) : PlayerNmsScoreboard(player) { + + override fun createObjective(title: Component): PacketOperation { + val objective = objective(title) + + return V26_2PacketOperationImpl.multi { _, packets -> + packets.add( + ClientboundSetObjectivePacket( + objective, + ClientboundSetObjectivePacket.METHOD_ADD + ) + ) + packets.add(ClientboundSetDisplayObjectivePacket(DisplaySlot.SIDEBAR, objective)) + } + } + + override fun updateObjective(title: Component): PacketOperation = + V26_2PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(title), + ClientboundSetObjectivePacket.METHOD_CHANGE + ) + } + + override fun removeObjective(): PacketOperation = V26_2PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(Component.empty()), + ClientboundSetObjectivePacket.METHOD_REMOVE + ) + } + + override fun setScore(owner: String, score: Int, line: SidebarLine): PacketOperation = + V26_2PacketOperationImpl.simple { + val format = line.score?.let { FixedFormat(it.toNms()) } ?: BlankFormat.INSTANCE + ClientboundSetScorePacket( + owner, + id, + score, + Optional.of(line.text.toNms()), + Optional.of(format) + ) + } + + override fun resetScore(owner: String): PacketOperation = V26_2PacketOperationImpl.simple { + ClientboundResetScorePacket(owner, id) + } + + private fun objective(title: Component) = Objective( + scoreboard, + id, + ObjectiveCriteria.DUMMY, + title.toNms(), + ObjectiveCriteria.RenderType.INTEGER, + false, + null + ) + + companion object { + private val scoreboard = Scoreboard() + } +} diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/V26_3NmsProvider.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/V26_3NmsProvider.kt index 2ad2049cd..1106a7fe8 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/V26_3NmsProvider.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/V26_3NmsProvider.kt @@ -10,6 +10,7 @@ import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerChat import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerPackets import dev.slne.surf.api.paper.nms.bridges.packets.player.SurfPaperNmsPlayerToastPackets import dev.slne.surf.api.paper.nms.common.* +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard import dev.slne.surf.api.paper.packet.listener.listener.PacketListener import dev.slne.surf.api.paper.region.TickThreadGuard import dev.slne.surf.api.paper.server.nms.v26_3.bridges.* @@ -28,8 +29,10 @@ import dev.slne.surf.api.paper.server.nms.v26_3.packet.lore.V26_3PacketLoreListe import dev.slne.surf.api.paper.server.nms.v26_3.packet.lore.V26_3PacketLoreRegistry import dev.slne.surf.api.paper.server.nms.v26_3.reflection.V26_3Reflection import dev.slne.surf.api.paper.server.nms.v26_3.region.V26_3TickThreadGuard +import dev.slne.surf.api.paper.server.nms.v26_3.scoreboard.V26_3PlayerNmsScoreboardImpl import dev.slne.surf.api.shared.internal.nms.NmsProviderMarker import dev.slne.surf.api.shared.internal.nms.NmsVersion +import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import java.util.* @@ -68,6 +71,7 @@ class V26_3NmsProvider(override val plugin: JavaPlugin) : NmsProvider { override fun createPacketLoreRegistry(): PacketLoreRegistry = V26_3PacketLoreRegistry() override fun createGlowingLifecycleHandler(): GlowingLifecycleHandler = V26_3GlowingLifecycleHandler() override fun createGlowingApi(): SurfGlowingApi = V26_3SurfGlowingApiImpl + override fun createPlayerScoreboard(player: Player): PlayerNmsScoreboard = V26_3PlayerNmsScoreboardImpl(player) override fun createChannelInjector(): AbstractChannelInjector<*> = V26_3ChannelInjector override fun createPacketListenerApi(): InternalPacketListenerApiBridge = V26_3PacketListenerApiImpl() diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/bridges/packets/V26_3PacketOperationImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/bridges/packets/V26_3PacketOperationImpl.kt index 1fb71ebb3..9b4be1a7b 100644 --- a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/bridges/packets/V26_3PacketOperationImpl.kt +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/bridges/packets/V26_3PacketOperationImpl.kt @@ -99,6 +99,14 @@ class V26_3PacketOperationImpl : PacketOperation { return V26_3PacketOperationImpl(operation) } + @JvmStatic + fun multi(operation: (player: Player, packets: LinkedList>) -> Unit): V26_3PacketOperationImpl { + return V26_3PacketOperationImpl { player, packets -> + operation(player, packets) + packets + } + } + fun simple(packetSupplier: (Player) -> Packet): V26_3PacketOperationImpl { return V26_3PacketOperationImpl { player, packets -> packets.add(packetSupplier(player)) diff --git a/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/scoreboard/V26_3PlayerNmsScoreboardImpl.kt b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/scoreboard/V26_3PlayerNmsScoreboardImpl.kt new file mode 100644 index 000000000..6d4314c5e --- /dev/null +++ b/surf-api-paper/surf-api-paper-nms/surf-api-paper-nms-v26-3/src/main/kotlin/dev/slne/surf/api/paper/server/nms/v26_3/scoreboard/V26_3PlayerNmsScoreboardImpl.kt @@ -0,0 +1,83 @@ +package dev.slne.surf.api.paper.server.nms.v26_3.scoreboard + +import dev.slne.surf.api.paper.nms.bridges.packets.PacketOperation +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.server.nms.v26_3.bridges.packets.V26_3PacketOperationImpl +import dev.slne.surf.api.paper.server.nms.v26_3.extensions.toNms +import dev.slne.surf.api.paper.sidebar.SidebarLine +import net.kyori.adventure.text.Component +import net.minecraft.network.chat.numbers.BlankFormat +import net.minecraft.network.chat.numbers.FixedFormat +import net.minecraft.network.protocol.game.ClientboundResetScorePacket +import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket +import net.minecraft.network.protocol.game.ClientboundSetScorePacket +import net.minecraft.world.scores.DisplaySlot +import net.minecraft.world.scores.Objective +import net.minecraft.world.scores.Scoreboard +import net.minecraft.world.scores.criteria.ObjectiveCriteria +import org.bukkit.entity.Player +import java.util.* + +@Suppress("ClassName") +class V26_3PlayerNmsScoreboardImpl(player: Player) : PlayerNmsScoreboard(player) { + + override fun createObjective(title: Component): PacketOperation { + val objective = objective(title) + + return V26_3PacketOperationImpl.multi { _, packets -> + packets.add( + ClientboundSetObjectivePacket( + objective, + ClientboundSetObjectivePacket.METHOD_ADD + ) + ) + packets.add(ClientboundSetDisplayObjectivePacket(DisplaySlot.SIDEBAR, objective)) + } + } + + override fun updateObjective(title: Component): PacketOperation = + V26_3PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(title), + ClientboundSetObjectivePacket.METHOD_CHANGE + ) + } + + override fun removeObjective(): PacketOperation = V26_3PacketOperationImpl.simple { + ClientboundSetObjectivePacket( + objective(Component.empty()), + ClientboundSetObjectivePacket.METHOD_REMOVE + ) + } + + override fun setScore(owner: String, score: Int, line: SidebarLine): PacketOperation = + V26_3PacketOperationImpl.simple { + val format = line.score?.let { FixedFormat(it.toNms()) } ?: BlankFormat.INSTANCE + ClientboundSetScorePacket( + owner, + id, + score, + Optional.of(line.text.toNms()), + Optional.of(format) + ) + } + + override fun resetScore(owner: String): PacketOperation = V26_3PacketOperationImpl.simple { + ClientboundResetScorePacket(owner, id) + } + + private fun objective(title: Component) = Objective( + scoreboard, + id, + ObjectiveCriteria.DUMMY, + title.toNms(), + ObjectiveCriteria.RenderType.INTEGER, + false, + null + ) + + companion object { + private val scoreboard = Scoreboard() + } +} diff --git a/surf-api-paper/surf-api-paper-plugin-test/build.gradle.kts b/surf-api-paper/surf-api-paper-plugin-test/build.gradle.kts index cb6eb575a..05e226b59 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/build.gradle.kts +++ b/surf-api-paper/surf-api-paper-plugin-test/build.gradle.kts @@ -49,7 +49,7 @@ fun RunServer.configure(folia: Boolean) { minecraftVersion(findProperty("mcVersion") as String) downloadPlugins { - hangar("CommandAPI", libs.versions.commandapi.get()) + hangar("CommandAPI", libs.versions.commandapiplugin.get()) modrinth("packetevents", libs.versions.packetevents.plugin.get() + "+spigot") if (!folia) { 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 62e78c227..c0d73c301 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,8 +1,31 @@ package dev.slne.surf.api.paper.test.command; import dev.jorel.commandapi.CommandAPICommand; -import dev.slne.surf.api.paper.test.command.subcommands.*; -import dev.slne.surf.surfapi.bukkit.test.command.subcommands.*; +import dev.slne.surf.api.paper.test.command.subcommands.CommandExceptionTest; +import dev.slne.surf.api.paper.test.command.subcommands.MaxStacksizeTest; +import dev.slne.surf.api.paper.test.command.subcommands.PacketEntityTest; +import dev.slne.surf.api.paper.test.command.subcommands.PacketLoreTest; +import dev.slne.surf.api.paper.test.command.subcommands.PrefixConfigTest; +import dev.slne.surf.api.paper.test.command.subcommands.ReflectionTest; +import dev.slne.surf.api.paper.test.command.subcommands.ScoreboardTest; +import dev.slne.surf.api.paper.test.command.subcommands.SmoothTimeSkip; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.BlockPdcContainerTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.GlowingTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.InventoryTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.ModernSerializerTestConfigCommand; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.OfflineInventoryEditTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.PaginationTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.ShowItemCommand; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SidebarTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SignedMessageArgumentTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SortInvCommand; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.StableActionbarTestCommand; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SummonCommandTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SurfEventHandlerTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SuspendCommandExecutionTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.SuspendRequirementTestCommand; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.ToastTest; +import dev.slne.surf.surfapi.bukkit.test.command.subcommands.VisualizerTest; public class SurfApiTestCommand extends CommandAPICommand { @@ -14,6 +37,7 @@ public SurfApiTestCommand() { withSubcommands( new PacketLoreTest("packetlore"), new ScoreboardTest("scoreboard"), + new SidebarTest("sidebar"), new SmoothTimeSkip("smoothtimeskip"), new PacketEntityTest("packetentity"), new ReflectionTest("reflection"), 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 132e3bfa6..916d7c267 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 @@ -13,6 +13,7 @@ import net.minecraft.util.Util; import org.jetbrains.annotations.Contract; +@SuppressWarnings("deprecation") public class CreateScoreboard extends CommandAPICommand { private static final Map SCOREBOARDS = new HashMap<>(); diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StartScoreboard.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StartScoreboard.java index b6d435a8b..591912a93 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StartScoreboard.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StartScoreboard.java @@ -5,6 +5,7 @@ import dev.jorel.commandapi.arguments.StringArgument; import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatablePlayerScoreboard; +@SuppressWarnings("deprecation") public class StartScoreboard extends CommandAPICommand { public StartScoreboard(String commandName) { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StopScoreboard.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StopScoreboard.java index 7a5dad4ba..35a2ea398 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StopScoreboard.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/subcommands/scoreboard/StopScoreboard.java @@ -5,6 +5,7 @@ import dev.jorel.commandapi.arguments.StringArgument; import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatablePlayerScoreboard; +@SuppressWarnings("deprecation") public class StopScoreboard extends CommandAPICommand { public StopScoreboard(String commandName) { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SidebarTest.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SidebarTest.kt new file mode 100644 index 000000000..6190282ac --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/SidebarTest.kt @@ -0,0 +1,237 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import com.github.shynixn.mccoroutine.folia.entityDispatcher +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.* +import dev.slne.surf.api.core.messages.Colors +import dev.slne.surf.api.paper.sidebar.* +import dev.slne.surf.surfapi.bukkit.test.plugin +import io.papermc.paper.util.Tick +import kotlinx.coroutines.withContext +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.util.concurrent.atomic.AtomicInteger +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toKotlinDuration + +class SidebarTest(name: String) : CommandAPICommand(name) { + private var manual: SurfViewerSidebar? = null + private var auto: SurfAutoUpdatingSidebar? = null + private var global: SurfGlobalSidebar? = null + private var failing: SurfAutoUpdatingSidebar? = null + + init { + manualCommands() + autoCommands() + globalCommands() + failingCommands() + + subcommand("closeall") { + anyExecutor { sender, _ -> + closeAll() + sender.sendMessage("Closed all test sidebars.") + } + } + } + + private fun manualCommands() = subcommand("manual") { + subcommand("show") { + playerExecutor { player, _ -> + val sidebar = manual?.takeUnless { it.isClosed } ?: createManualSidebar().also { manual = it } + sidebar.addViewer(player) + player.sendMessage("Showing manual sidebar.") + } + } + + subcommand("hide") { + playerExecutor { player, _ -> + manual?.removeViewer(player) + player.sendMessage("Removed you from the manual sidebar.") + } + } + + subcommand("update") { + anyExecutor { sender, _ -> + val sidebar = manual ?: return@anyExecutor sender.noSidebar("manual") + sidebar.update() + sender.sendMessage("Requested an update for ${sidebar.viewers.size} viewer(s).") + } + } + + subcommand("close") { + anyExecutor { sender, _ -> + manual?.close() + manual = null + sender.sendMessage("Closed the manual sidebar.") + } + } + } + + private fun createManualSidebar(): SurfViewerSidebar { + val updates = AtomicInteger() + + return sidebar(plugin) { + buildTitle { primary("Manual Sidebar") } + buildLine { info("Static line") } + buildLine(score = Component.text("42")) { info("Static line with score") } + emptyLine() + buildSharedLine { info("Shared refreshes: "); variableValue(updates.incrementAndGet()) } + buildViewerLine { viewer -> info("Viewer: "); variableValue(viewer.name) } + animatedLine(listOf(Component.text("Frame 1"), Component.text("Frame 2"), Component.text("Frame 3"))) + separator() + } + } + + private fun autoCommands() = subcommand("auto") { + subcommand("show") { + integerArgument("ticks", 1, 200) + + playerExecutor { player, args -> + val ticks: Int by args + + auto?.close() + val sidebar = createAutoSidebar(ticks).also { auto = it } + sidebar.addViewer(player) + player.sendMessage("Showing auto-updating sidebar every $ticks tick(s). Sneak to add a line.") + } + } + + subcommand("hide") { + playerExecutor { player, _ -> + auto?.removeViewer(player) + player.sendMessage("Removed you from the auto-updating sidebar.") + } + } + + subcommand("close") { + anyExecutor { sender, _ -> + auto?.close() + auto = null + sender.sendMessage("Closed the auto-updating sidebar.") + } + } + } + + private fun createAutoSidebar(ticks: Int) = autoUpdatingSidebar(plugin) { + updateInterval = Tick.of(ticks.toLong()).toKotlinDuration() + maxLines = 12 + + buildViewerTitle { viewer -> primary("Auto "); variableValue(viewer.name) } + buildViewerLines { viewer -> + val state = withContext(plugin.entityDispatcher(viewer)) { + val location = viewer.location + ViewerState(viewer.health, location.blockX, location.blockY, location.blockZ, viewer.isSneaking) + } + + buildScoredLine({ info("Health") }, { variableValue("%.1f".format(state.health)) }) + buildScoredLine({ info("Position") }, { variableValue("${state.x} ${state.y} ${state.z}") }) + buildScoredLine({ info("Ping") }, { variableValue(viewer.ping) }) + if (state.sneaking) buildLine { error("Sneaking!") } + } + buildSharedLine { info("Online: "); variableValue(Bukkit.getOnlinePlayers().size) } + buildGradientLine(Colors.PRIMARY, Colors.SPACER) { text("Gradient line") } + lines { _ -> List(10) { SidebarLine(Component.text("Filler ${it + 1} (truncated at 12)")) } } + } + + private data class ViewerState( + val health: Double, + val x: Int, + val y: Int, + val z: Int, + val sneaking: Boolean, + ) + + private fun globalCommands() = subcommand("global") { + subcommand("start") { + integerArgument("ticks", 1, 200) + + anyExecutor { sender, args -> + val ticks: Int by args + + global?.close() + global = createGlobalSidebar(ticks) + sender.sendMessage("Started the global sidebar every $ticks tick(s) for all players.") + } + } + + subcommand("stop") { + anyExecutor { sender, _ -> + global?.close() + global = null + sender.sendMessage("Stopped the global sidebar.") + } + } + } + + private fun createGlobalSidebar(ticks: Int) = globalSidebar(plugin) { + updateInterval = Tick.of(ticks.toLong()).toKotlinDuration() + + buildSharedTitle { primary("Global "); variableValue(LocalTime.now().format(TIME_FORMAT)) } + buildSharedLines { + buildScoredLine({ info("TPS") }, { variableValue("%.2f".format(Bukkit.getTPS()[0])) }) + buildScoredLine({ info("Online") }, { variableValue(Bukkit.getOnlinePlayers().size) }) + } + emptyLine() + buildViewerLine { viewer -> info("Hello "); variableValue(viewer.name) } + separator() + } + + private fun failingCommands() = subcommand("failing") { + subcommand("show") { + playerExecutor { player, _ -> + val sidebar = failing?.takeUnless { it.isClosed } ?: createFailingSidebar().also { failing = it } + sidebar.addViewer(player) + player.sendMessage("Showing failing sidebar. Every third render throws; content must stay.") + } + } + + subcommand("close") { + anyExecutor { sender, _ -> + failing?.close() + failing = null + sender.sendMessage("Closed the failing sidebar.") + } + } + } + + private fun createFailingSidebar(): SurfAutoUpdatingSidebar { + val viewerRenders = AtomicInteger() + val sharedRenders = AtomicInteger() + + return autoUpdatingSidebar(plugin) { + updateInterval = 1.seconds + + buildTitle { error("Failing renderers") } + line { _ -> + val render = viewerRenders.incrementAndGet() + if (render % 3 == 0) throw IllegalStateException("Intentional viewer failure #$render") + Component.text("Viewer render #$render") + } + sharedLine { + val render = sharedRenders.incrementAndGet() + if (render % 3 == 0) throw IllegalStateException("Intentional shared failure #$render") + Component.text("Shared render #$render") + } + buildLine { info("Static line stays") } + } + } + + private fun closeAll() { + listOfNotNull(manual, auto, global, failing).forEach { it.close() } + manual = null + auto = null + global = null + failing = null + } + + private fun CommandSender.noSidebar(kind: String) { + sendMessage("No $kind sidebar exists. Create one with '/surfapitest sidebar $kind show'.") + } + + companion object { + private val TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss") + } +} diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/PaperMain.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/PaperMain.kt index 2de100c64..a59538012 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/PaperMain.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/PaperMain.kt @@ -3,6 +3,7 @@ package dev.slne.surf.api.paper.server import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.slne.surf.api.paper.api.metrics.Metrics import dev.slne.surf.api.paper.server.impl.scoreboard.SurfScoreboardApiImpl +import dev.slne.surf.api.paper.server.impl.sidebar.SidebarRegistry import dev.slne.surf.api.paper.server.libs.LibLoader import org.bukkit.plugin.java.JavaPlugin @@ -26,6 +27,7 @@ class PaperMain : SuspendingJavaPlugin() { bstats.shutdown() } + SidebarRegistry.closeAll() SurfScoreboardApiImpl.INSTANCE.onDisable() PaperInstance.onDisable() } 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 dbdeee29d..72d4c9517 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.server.impl.scoreboard import dev.slne.surf.api.paper.scoreboard.SurfAutoUpdatablePlayerScoreboard 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 ebcf48f98..68bd631f4 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,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.server.impl.scoreboard import com.github.shynixn.mccoroutine.folia.scope diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardApiImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardApiImpl.kt index 331b0847c..35a528822 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardApiImpl.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/scoreboard/SurfScoreboardApiImpl.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.server.impl.scoreboard import com.google.auto.service.AutoService 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 eb9af852e..92ad14fc4 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 @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.server.impl.scoreboard import dev.slne.surf.api.core.util.mutableObjectListOf 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 1ba4a9da3..860f17a7d 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,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.server.impl.scoreboard import dev.slne.surf.api.core.util.mutableObject2ObjectMapOf diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarBuilderImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarBuilderImpl.kt new file mode 100644 index 000000000..856110e55 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarBuilderImpl.kt @@ -0,0 +1,162 @@ +package dev.slne.surf.api.paper.server.impl.sidebar + +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.sidebar.AutoUpdatingSidebarBuilder +import dev.slne.surf.api.paper.sidebar.SidebarBuilder +import dev.slne.surf.api.paper.sidebar.SidebarLine +import io.papermc.paper.util.Tick +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.bukkit.entity.Player +import java.util.concurrent.atomic.AtomicInteger +import kotlin.time.Duration +import kotlin.time.toKotlinDuration + +sealed interface SidebarTitle { + class Static(val title: Component) : SidebarTitle + class Rendered(val render: suspend (Player) -> Component) : SidebarTitle + class Shared(val render: suspend () -> Component) : SidebarTitle { + @Volatile + var current: Component = Component.empty() + } +} + +sealed interface SidebarEntry { + class Static(val line: SidebarLine) : SidebarEntry + class Line(val render: suspend (Player) -> SidebarLine) : SidebarEntry + class Lines(val render: suspend (Player) -> List) : SidebarEntry + class Animated(val animation: FrameAnimation) : SidebarEntry + class Shared(val render: suspend () -> List) : SidebarEntry { + @Volatile + var current: List = emptyList() + } +} + +class FrameAnimation(frames: List) { + private val frames = Array(frames.size) { SidebarLine(frames[it]) } + private val index = AtomicInteger() + + val currentFrame: SidebarLine + get() = frames[index.get()] + + fun nextFrame() { + index.updateAndGet { (it + 1) % frames.size } + } +} + +class SidebarDefinition( + val title: SidebarTitle, + val entries: List, + val animations: List, + val maxLines: Int, +) + +open class SidebarBuilderImpl : SidebarBuilder { + private var title: SidebarTitle = SidebarTitle.Static(Component.empty()) + private val entries = ObjectArrayList() + private val animations = ObjectArrayList() + + override var maxLines: Int = MAX_LINES + set(value) { + require(value in 1..MAX_LINES) { "maxLines must be between 1 and $MAX_LINES: $value" } + field = value + } + + override fun title(title: Component) { + this.title = SidebarTitle.Static(title) + } + + override fun title(render: suspend (viewer: Player) -> Component) { + this.title = SidebarTitle.Rendered(render) + } + + override fun sharedTitle(render: suspend () -> Component) { + this.title = SidebarTitle.Shared(render) + } + + override fun line(text: Component, score: Component?) { + entries.add(SidebarEntry.Static(SidebarLine(text, score))) + } + + override fun line(render: suspend (viewer: Player) -> Component) { + entries.add(SidebarEntry.Line { viewer -> SidebarLine(render(viewer)) }) + } + + override fun scoredLine(render: suspend (viewer: Player) -> SidebarLine) { + entries.add(SidebarEntry.Line(render)) + } + + override fun lines(render: suspend (viewer: Player) -> List) { + entries.add(SidebarEntry.Lines(render)) + } + + override fun sharedLine(render: suspend () -> Component) { + entries.add(SidebarEntry.Shared { listOf(SidebarLine(render())) }) + } + + override fun sharedLines(render: suspend () -> List) { + entries.add(SidebarEntry.Shared(render)) + } + + override fun animatedLine(frames: List) { + require(frames.isNotEmpty()) { "frames must not be empty" } + addAnimation(FrameAnimation(frames)) + } + + override fun gradientLine(text: Component, start: TextColor, end: TextColor) { + addAnimation(FrameAnimation(gradientFrames(text, start.asHexString(), end.asHexString()))) + } + + private fun addAnimation(animation: FrameAnimation) { + animations.add(animation) + entries.add(SidebarEntry.Animated(animation)) + } + + fun definition() = SidebarDefinition(title, entries.clone(), animations.clone(), maxLines) + + companion object { + const val MAX_LINES = PlayerNmsScoreboard.MAX_LINES + + private fun gradientFrames( + text: Component, + firstHex: String, + secondHex: String, + ): List { + val step = 1f / 20f + val textPlaceholder = Placeholder.component("text", text) + val frames = ObjectArrayList() + + // Animation from left to right + var phase = -1f + while (phase < 1) { + frames.add( + MiniMessage.miniMessage().deserialize( + "", + textPlaceholder + ) + ) + phase += step + } + + // Animation from right to left + frames.addAll(frames.reversed()) + return frames + } + } +} + +class AutoUpdatingSidebarBuilderImpl : SidebarBuilderImpl(), AutoUpdatingSidebarBuilder { + override var updateInterval: Duration = DEFAULT_UPDATE_INTERVAL + set(value) { + require(value >= ONE_TICK) { "updateInterval must be at least one tick: $value" } + field = value + } + + companion object { + private val ONE_TICK = Tick.of(1).toKotlinDuration() + private val DEFAULT_UPDATE_INTERVAL = Tick.of(5).toKotlinDuration() + } +} diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarRegistry.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarRegistry.kt new file mode 100644 index 000000000..d22249a67 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SidebarRegistry.kt @@ -0,0 +1,40 @@ +package dev.slne.surf.api.paper.server.impl.sidebar + +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.server.PluginDisableEvent +import java.util.concurrent.ConcurrentHashMap + +object SidebarRegistry : Listener { + private val sidebars = ConcurrentHashMap.newKeySet() + + fun add(sidebar: SurfSidebarImpl) { + sidebars.add(sidebar) + } + + fun remove(sidebar: SurfSidebarImpl) { + sidebars.remove(sidebar) + } + + fun closeAll() { + sidebars.toList().forEach { it.close() } + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onPlayerJoin(event: PlayerJoinEvent) { + sidebars.forEach { it.onPlayerJoin(event.player) } + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onPlayerQuit(event: PlayerQuitEvent) { + sidebars.forEach { it.onPlayerQuit(event.player) } + } + + @EventHandler + fun onPluginDisable(event: PluginDisableEvent) { + sidebars.filter { it.plugin === event.plugin }.forEach { it.close() } + } +} diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarFactoryImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarFactoryImpl.kt new file mode 100644 index 000000000..7f17b886c --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarFactoryImpl.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.paper.server.impl.sidebar + +import com.google.auto.service.AutoService +import dev.slne.surf.api.paper.sidebar.* +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import org.bukkit.plugin.Plugin + +@OptIn(InternalSurfApi::class) +@AutoService(SurfSidebarFactory::class) +class SurfSidebarFactoryImpl : SurfSidebarFactory { + + override fun createSidebar(plugin: Plugin, block: SidebarBuilder.() -> Unit): SurfViewerSidebar { + val builder = SidebarBuilderImpl().apply(block) + return SurfViewerSidebarImpl(plugin, builder.definition()) + } + + override fun createAutoUpdatingSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit + ): SurfAutoUpdatingSidebar { + val builder = AutoUpdatingSidebarBuilderImpl().apply(block) + return SurfAutoUpdatingSidebarImpl(plugin, builder.definition(), builder.updateInterval) + } + + override fun createGlobalSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit + ): SurfGlobalSidebar { + val builder = AutoUpdatingSidebarBuilderImpl().apply(block) + return SurfGlobalSidebarImpl(plugin, builder.definition(), builder.updateInterval) + } +} diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarImpl.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarImpl.kt new file mode 100644 index 000000000..3b908f356 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/impl/sidebar/SurfSidebarImpl.kt @@ -0,0 +1,294 @@ +package dev.slne.surf.api.paper.server.impl.sidebar + +import dev.slne.surf.api.core.util.logger +import dev.slne.surf.api.core.util.runAtFixedRate +import dev.slne.surf.api.paper.nms.NmsUseWithCaution +import dev.slne.surf.api.paper.nms.common.NmsProvider +import dev.slne.surf.api.paper.nms.common.scoreboard.PlayerNmsScoreboard +import dev.slne.surf.api.paper.sidebar.* +import dev.slne.surf.api.paper.util.forEachPlayer +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +abstract class SurfSidebarImpl( + final override val plugin: Plugin, + private val definition: SidebarDefinition, +) : SurfSidebar { + private val sessions = ConcurrentHashMap() + private val lifecycleLock = Any() + + @Volatile + private var closed = false + + protected val scope = CoroutineScope( + SupervisorJob() + + Dispatchers.Default + + CoroutineName("surf-sidebar-${plugin.name}") + + CoroutineExceptionHandler { _, e -> + log.atWarning() + .withCause(e) + .log("Uncaught exception in sidebar of %s", plugin.name) + } + ) + + private val sharedTitle = definition.title as? SidebarTitle.Shared + private val sharedEntries = definition.entries.filterIsInstance() + private val hasSharedContent = sharedTitle != null || sharedEntries.isNotEmpty() + private val sharedRefreshRequests = Channel(Channel.CONFLATED) + + private val sharedReady = CompletableDeferred() + + init { + check(plugin.isEnabled) { "Plugin ${plugin.name} is not enabled" } + SidebarRegistry.add(this) + + if (!hasSharedContent) { + sharedReady.complete(Unit) + } else { + scope.launch { + for (request in sharedRefreshRequests) { + refreshSharedContent() + sharedReady.complete(Unit) + sessions.values.forEach { it.requestRender() } + } + } + sharedRefreshRequests.trySend(Unit) + } + } + + override val viewers: Collection + get() = sessions.values.map { it.player } + + override val isClosed: Boolean + get() = closed + + override fun update() { + if (closed) return + + definition.animations.forEach { it.nextFrame() } + if (!hasSharedContent) { + sessions.values.forEach { it.requestRender() } + } else { + sharedRefreshRequests.trySend(Unit) + } + } + + private suspend fun refreshSharedContent() { + if (sharedTitle != null) { + val rendered = renderShared("title") { sharedTitle.render() } + if (rendered != null && rendered != sharedTitle.current) { + sharedTitle.current = rendered + } + } + + for (entry in sharedEntries) { + val rendered = renderShared("line") { entry.render() } ?: continue + val previous = entry.current + entry.current = List(rendered.size) { index -> + val line = rendered[index] + val old = previous.getOrNull(index) + if (old != null && old == line) old else line + } + } + } + + private inline fun renderShared(content: String, render: () -> T): T? { + return try { + render() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + log.atWarning() + .atMostEvery(10, TimeUnit.SECONDS) + .withCause(e) + .log("Failed to render shared sidebar %s of %s", content, plugin.name) + null + } + } + + override fun close() { + synchronized(lifecycleLock) { + if (closed) return + closed = true + } + + SidebarRegistry.remove(this) + scope.cancel() + sessions.values.forEach { it.close() } + sessions.clear() + } + + open fun onPlayerJoin(player: Player) { + } + + fun onPlayerQuit(player: Player) { + val session = sessions[player.uniqueId] ?: return + if (session.player === player && sessions.remove(player.uniqueId, session)) { + session.close() + } + } + + protected fun addViewerInternal(player: Player) { + synchronized(lifecycleLock) { + check(!closed) { "Sidebar is closed" } + + val existing = sessions[player.uniqueId] + if (existing != null) { + if (existing.player === player) return + existing.close() + } + + sessions[player.uniqueId] = ViewerSession(player) + } + } + + protected fun removeViewerInternal(player: Player) { + sessions.remove(player.uniqueId)?.close() + } + + protected fun launchUpdater(interval: Duration) { + scope.runAtFixedRate(interval, initialDelay = interval, taskName = "surf-sidebar-${plugin.name}") { + update() + } + } + + @OptIn(NmsUseWithCaution::class) + private inner class ViewerSession(val player: Player) { + private val renderRequests = Channel(Channel.CONFLATED) + + private val scoreboard: PlayerNmsScoreboard = NmsProvider.current.createPlayerScoreboard(player) + private var closed = false + + private val renderJob = scope.launch { + for (request in renderRequests) { + render() + } + } + + init { + requestRender() + } + + fun requestRender() { + renderRequests.trySend(Unit) + } + + private suspend fun render() { + if (!player.isConnected) { + if (sessions.remove(player.uniqueId, this)) close() + return + } + + sharedReady.await() + + val title: Component + val lines: List + try { + title = renderTitle() + lines = renderLines() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + log.atWarning() + .atMostEvery(10, TimeUnit.SECONDS) + .withCause(e) + .log("Failed to render sidebar of %s for %s", plugin.name, player.name) + return + } + + synchronized(this) { + if (!closed) scoreboard.update(title, lines) + } + } + + private suspend fun renderTitle(): Component = when (val title = definition.title) { + is SidebarTitle.Static -> title.title + is SidebarTitle.Rendered -> title.render(player) + is SidebarTitle.Shared -> title.current + } + + private suspend fun renderLines(): List { + val maxLines = definition.maxLines + val lines = ObjectArrayList(maxLines) + for (entry in definition.entries) { + if (lines.size == maxLines) break + + when (entry) { + is SidebarEntry.Static -> lines.add(entry.line) + is SidebarEntry.Line -> lines.add(entry.render(player)) + is SidebarEntry.Animated -> lines.add(entry.animation.currentFrame) + is SidebarEntry.Lines -> for (line in entry.render(player)) { + if (lines.size == maxLines) break + lines.add(line) + } + + is SidebarEntry.Shared -> for (line in entry.current) { + if (lines.size == maxLines) break + lines.add(line) + } + } + } + return lines + } + + fun close() { + renderJob.cancel() + + synchronized(this) { + closed = true + scoreboard.delete() + } + } + } + + companion object { + private val log = logger() + } +} + +open class SurfViewerSidebarImpl( + plugin: Plugin, + definition: SidebarDefinition, +) : SurfSidebarImpl(plugin, definition), SurfViewerSidebar { + + override fun addViewer(player: Player) { + addViewerInternal(player) + } + + override fun removeViewer(player: Player) { + removeViewerInternal(player) + } +} + +class SurfAutoUpdatingSidebarImpl( + plugin: Plugin, + definition: SidebarDefinition, + override val updateInterval: Duration, +) : SurfViewerSidebarImpl(plugin, definition), SurfAutoUpdatingSidebar { + init { + launchUpdater(updateInterval) + } +} + +class SurfGlobalSidebarImpl( + plugin: Plugin, + definition: SidebarDefinition, + override val updateInterval: Duration, +) : SurfSidebarImpl(plugin, definition), SurfGlobalSidebar { + init { + forEachPlayer(::addViewerInternal) + launchUpdater(updateInterval) + } + + override fun onPlayerJoin(player: Player) { + addViewerInternal(player) + } +} diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt index fadc9f6da..19d001ba9 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/listener/ListenerManager.kt @@ -4,6 +4,7 @@ import dev.slne.surf.api.paper.event.register import dev.slne.surf.api.paper.server.command.SuspendRequirementServiceImpl import dev.slne.surf.api.paper.server.impl.glow.GlowingListener import dev.slne.surf.api.paper.server.impl.pdc.block.BlockDataListener +import dev.slne.surf.api.paper.server.impl.sidebar.SidebarRegistry import dev.slne.surf.api.paper.server.impl.visualizer.visualizer.VisualizerListener import dev.slne.surf.api.paper.server.plugin import org.bukkit.Bukkit @@ -16,6 +17,7 @@ object ListenerManager { Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, "BungeeCord") VisualizerListener.register() GlowingListener.register() + SidebarRegistry.register() BlockDataListener.register() 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 21df4224c..e4b660e17 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 @@ -3158,6 +3158,135 @@ public final class dev/slne/surf/api/paper/serializer/bukkit/player/PlayerSerial public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lorg/bukkit/entity/Player;)V } +public abstract interface class dev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder : dev/slne/surf/api/paper/sidebar/SidebarBuilder { + public abstract fun getUpdateInterval-UwyO8pc ()J + public abstract fun setUpdateInterval-LRDsOJo (J)V +} + +public final class dev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder$DefaultImpls { + public static fun buildGradientLine (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;Lkotlin/jvm/functions/Function1;)V + public static fun buildLine (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)V + public static fun buildSharedLine (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildSharedLines (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildSharedTitle (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildTitle (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function1;)V + public static fun buildViewerLine (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun buildViewerLines (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun buildViewerTitle (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun emptyLine (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;)V + public static fun separator (Ldev/slne/surf/api/paper/sidebar/AutoUpdatingSidebarBuilder;)V +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SidebarBuilder { + public abstract fun animatedLine (Ljava/util/List;)V + public fun buildGradientLine (Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;Lkotlin/jvm/functions/Function1;)V + public fun buildLine (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun buildLine$default (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public fun buildSharedLine (Lkotlin/jvm/functions/Function2;)V + public fun buildSharedLines (Lkotlin/jvm/functions/Function2;)V + public fun buildSharedTitle (Lkotlin/jvm/functions/Function2;)V + public fun buildTitle (Lkotlin/jvm/functions/Function1;)V + public fun buildViewerLine (Lkotlin/jvm/functions/Function3;)V + public fun buildViewerLines (Lkotlin/jvm/functions/Function3;)V + public fun buildViewerTitle (Lkotlin/jvm/functions/Function3;)V + public fun emptyLine ()V + public abstract fun getMaxLines ()I + public abstract fun gradientLine (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;)V + public abstract fun line (Lkotlin/jvm/functions/Function2;)V + public abstract fun line (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)V + public static synthetic fun line$default (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V + public abstract fun lines (Lkotlin/jvm/functions/Function2;)V + public abstract fun scoredLine (Lkotlin/jvm/functions/Function2;)V + public fun separator ()V + public abstract fun setMaxLines (I)V + public abstract fun sharedLine (Lkotlin/jvm/functions/Function1;)V + public abstract fun sharedLines (Lkotlin/jvm/functions/Function1;)V + public abstract fun sharedTitle (Lkotlin/jvm/functions/Function1;)V + public abstract fun title (Lkotlin/jvm/functions/Function2;)V + public abstract fun title (Lnet/kyori/adventure/text/Component;)V +} + +public final class dev/slne/surf/api/paper/sidebar/SidebarBuilder$DefaultImpls { + public static fun buildGradientLine (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/format/TextColor;Lnet/kyori/adventure/text/format/TextColor;Lkotlin/jvm/functions/Function1;)V + public static fun buildLine (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun buildLine$default (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public static fun buildSharedLine (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildSharedLines (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildSharedTitle (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function2;)V + public static fun buildTitle (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function1;)V + public static fun buildViewerLine (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun buildViewerLines (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun buildViewerTitle (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lkotlin/jvm/functions/Function3;)V + public static fun emptyLine (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;)V + public static synthetic fun line$default (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V + public static fun separator (Ldev/slne/surf/api/paper/sidebar/SidebarBuilder;)V +} + +public abstract interface annotation class dev/slne/surf/api/paper/sidebar/SidebarDsl : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/paper/sidebar/SidebarLine { + public fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)V + public synthetic fun (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lnet/kyori/adventure/text/Component; + public final fun component2 ()Lnet/kyori/adventure/text/Component; + public final fun copy (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)Ldev/slne/surf/api/paper/sidebar/SidebarLine; + public static synthetic fun copy$default (Ldev/slne/surf/api/paper/sidebar/SidebarLine;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)Ldev/slne/surf/api/paper/sidebar/SidebarLine; + public fun equals (Ljava/lang/Object;)Z + public final fun getScore ()Lnet/kyori/adventure/text/Component; + public final fun getText ()Lnet/kyori/adventure/text/Component; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/paper/sidebar/SidebarLinesBuilder { + public final fun buildLine (Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun buildLine$default (Ldev/slne/surf/api/paper/sidebar/SidebarLinesBuilder;Lnet/kyori/adventure/text/Component;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun buildScoredLine (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public final fun emptyLine ()V + public final fun line (Ldev/slne/surf/api/paper/sidebar/SidebarLine;)V + public final fun line (Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;)V + public static synthetic fun line$default (Ldev/slne/surf/api/paper/sidebar/SidebarLinesBuilder;Lnet/kyori/adventure/text/Component;Lnet/kyori/adventure/text/Component;ILjava/lang/Object;)V +} + +public final class dev/slne/surf/api/paper/sidebar/Sidebar_dslKt { + public static final fun autoUpdatingSidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfAutoUpdatingSidebar; + public static final fun globalSidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfGlobalSidebar; + public static final fun sidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfViewerSidebar; +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SurfAutoUpdatingSidebar : dev/slne/surf/api/paper/sidebar/SurfViewerSidebar { + public abstract fun getUpdateInterval-UwyO8pc ()J +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SurfGlobalSidebar : dev/slne/surf/api/paper/sidebar/SurfSidebar { + public abstract fun getUpdateInterval-UwyO8pc ()J +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SurfSidebar { + public abstract fun close ()V + public abstract fun getPlugin ()Lorg/bukkit/plugin/Plugin; + public abstract fun getViewers ()Ljava/util/Collection; + public abstract fun isClosed ()Z + public abstract fun update ()V +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SurfSidebarFactory { + public static final field Companion Ldev/slne/surf/api/paper/sidebar/SurfSidebarFactory$Companion; + public abstract fun createAutoUpdatingSidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfAutoUpdatingSidebar; + public abstract fun createGlobalSidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfGlobalSidebar; + public abstract fun createSidebar (Lorg/bukkit/plugin/Plugin;Lkotlin/jvm/functions/Function1;)Ldev/slne/surf/api/paper/sidebar/SurfViewerSidebar; +} + +public final class dev/slne/surf/api/paper/sidebar/SurfSidebarFactory$Companion { + public final fun getINSTANCE ()Ldev/slne/surf/api/paper/sidebar/SurfSidebarFactory; +} + +public abstract interface class dev/slne/surf/api/paper/sidebar/SurfViewerSidebar : dev/slne/surf/api/paper/sidebar/SurfSidebar { + public abstract fun addViewer (Lorg/bukkit/entity/Player;)V + public abstract fun removeViewer (Lorg/bukkit/entity/Player;)V +} + public final class dev/slne/surf/api/paper/time/SkipOperations { public static final field INSTANCE Ldev/slne/surf/api/paper/time/SkipOperations; public static final field NEXT_DAY Ldev/slne/surf/api/paper/time/SkipOperations$SkipOperation; diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard.kt index 560f495af..76fec754b 100644 --- a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard.kt +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatablePlayerScoreboard.kt @@ -7,6 +7,13 @@ import org.bukkit.entity.Player * includes all online players as viewers. This interface extends the SurfAutoUpdatableScoreboard * interface. */ +@Deprecated( + "Replaced by globalSidebar(plugin) { updateInterval = ...; ... } from " + + "dev.slne.surf.api.paper.sidebar, which returns a SurfGlobalSidebar that is shown to every " + + "online player, including players who join later.", + ReplaceWith("SurfGlobalSidebar", "dev.slne.surf.api.paper.sidebar.SurfGlobalSidebar") +) +@Suppress("DEPRECATION") interface SurfAutoUpdatablePlayerScoreboard : SurfAutoUpdatableScoreboard { /** * Adds a viewer to the scoreboard. In this implementation, this method does nothing and prints a diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard.kt index 812478eec..c5ed8772e 100644 --- a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard.kt +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfAutoUpdatableScoreboard.kt @@ -7,6 +7,13 @@ import org.jetbrains.annotations.ApiStatus * This scoreboard extends the SurfScoreboard interface and adds the ability to automatically update * itself every 5 ticks (0.25 seconds). Viewers can be added and removed from the scoreboard. */ +@Deprecated( + "Replaced by autoUpdatingSidebar(plugin) { updateInterval = ...; ... } from " + + "dev.slne.surf.api.paper.sidebar, which returns a SurfAutoUpdatingSidebar. " + + "The update interval is no longer fixed to five ticks but set in the builder.", + ReplaceWith("SurfAutoUpdatingSidebar", "dev.slne.surf.api.paper.sidebar.SurfAutoUpdatingSidebar") +) +@Suppress("DEPRECATION") interface SurfAutoUpdatableScoreboard : SurfScoreboard { /** * Updates the scoreboard. 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 c89983383..dae7c586a 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 @@ -8,6 +8,13 @@ import org.bukkit.entity.Player * * All methods are thread-safe. Viewers that disconnect are removed on the next [update]. */ +@Deprecated( + "Replaced by the sidebar API (dev.slne.surf.api.paper.sidebar). " + + "Create a sidebar with sidebar(plugin) { ... }; it returns a SurfViewerSidebar with the same " + + "addViewer, removeViewer and update methods. enable() is no longer needed because a sidebar " + + "is active once created, and close() replaces disable().", + ReplaceWith("SurfViewerSidebar", "dev.slne.surf.api.paper.sidebar.SurfViewerSidebar") +) interface SurfScoreboard { /** * Adds a viewer to the scoreboard. diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardApi.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardApi.kt index c853da4a4..9ebd8fc5a 100644 --- a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardApi.kt +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/scoreboard/SurfScoreboardApi.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package dev.slne.surf.api.paper.scoreboard import dev.slne.surf.api.core.util.requiredService @@ -6,6 +8,12 @@ import net.megavex.scoreboardlibrary.api.ScoreboardLibrary private val api = requiredService() +@Deprecated( + "Replaced by the functions sidebar, autoUpdatingSidebar and globalSidebar in " + + "dev.slne.surf.api.paper.sidebar. The sidebar API sends its packets itself and does not " + + "expose a ScoreboardLibrary instance.", + ReplaceWith("SurfSidebar", "dev.slne.surf.api.paper.sidebar.SurfSidebar") +) interface SurfScoreboardApi { fun scoreboardLibrary(): ScoreboardLibrary fun createScoreboard(title: Component): SurfScoreboardBuilder 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 d8dacd057..16916164f 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 @@ -15,6 +15,16 @@ import java.util.function.Supplier * The SurfScoreboardBuilder interface provides methods to build a SurfScoreboard, a * SurfAutoUpdatableScoreboard, or a SurfAutoUpdatablePlayerScoreboard. */ +@Deprecated( + "Replaced by the builder block of sidebar(plugin) { ... }, autoUpdatingSidebar(plugin) { ... } " + + "and globalSidebar(plugin) { ... } in dev.slne.surf.api.paper.sidebar. Equivalents: " + + "addLine -> line(text), addUpdatableLine -> sharedLine { }, addViewerLine -> line { viewer -> }, " + + "addViewerComponent -> buildViewerLines { viewer -> }, addAnimatedLine -> animatedLine(frames), " + + "addGradientLine -> gradientLine(text, start, end), addLineSeparator -> separator(). " + + "Render lambdas run asynchronously and may suspend.", + ReplaceWith("SidebarBuilder", "dev.slne.surf.api.paper.sidebar.SidebarBuilder") +) +@Suppress("DEPRECATION") interface SurfScoreboardBuilder { /** * Sets the maximum number of lines for the scoreboard. @@ -207,6 +217,12 @@ interface SurfScoreboardBuilder { * @param title the title of the scoreboard * @return a SurfScoreboardBuilder with the given title */ + @Deprecated( + "Use sidebar(plugin) { title(title) } from dev.slne.surf.api.paper.sidebar instead, or " + + "autoUpdatingSidebar / globalSidebar for the self-updating variants. Pass your own plugin: " + + "the sidebar is closed automatically when that plugin is disabled.", + ReplaceWith("sidebar(plugin) { title(title) }", "dev.slne.surf.api.paper.sidebar.sidebar") + ) @JvmStatic fun builder(title: Component): SurfScoreboardBuilder { return SurfScoreboardApi.createScoreboard(title) 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 index 74fac1def..008631262 100644 --- 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 @@ -5,10 +5,20 @@ import net.megavex.scoreboardlibrary.api.objective.ScoreFormat import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent /** Builds a [SidebarComponent] from the lines added in [block]. */ +@Deprecated( + "SidebarComponent belongs to the legacy scoreboard. In the sidebar API, collect lines per " + + "viewer with buildViewerLines { viewer -> buildLine { ... } } in the sidebar builder.", + ReplaceWith("buildList(block)") +) inline fun buildSidebarComponent(block: SidebarComponent.Builder.() -> Unit): SidebarComponent = SidebarComponent.builder().apply(block).build() /** Adds a static line built with [SurfComponentBuilder]. */ +@Deprecated( + "Inside buildViewerLines { } or buildSharedLines { } of the sidebar API, " + + "use buildLine { ... } instead.", + ReplaceWith("buildLine(text = line)") +) inline fun SidebarComponent.Builder.buildStaticLine( line: SurfComponentBuilder.() -> Unit ): SidebarComponent.Builder = addStaticLine(SurfComponentBuilder(line)) @@ -16,12 +26,26 @@ inline fun SidebarComponent.Builder.buildStaticLine( /** * Adds a static line built with [SurfComponentBuilder] and a custom score format. */ +@Deprecated( + "Inside buildViewerLines { } or buildSharedLines { } of the sidebar API, " + + "use buildLine(score) { ... } instead. The score is a plain Component there: " + + "ScoreFormat.fixed(content) corresponds to score = content.", + ReplaceWith( + "buildLine(score = (scoreFormat as? ScoreFormat.Fixed)?.content(), text = line)", + "net.megavex.scoreboardlibrary.api.objective.ScoreFormat" + ) +) 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. */ +@Deprecated( + "Viewer lines of the sidebar API are rebuilt on every update anyway. " + + "Inside buildViewerLines { } use buildLine { ... } instead.", + ReplaceWith("buildLine(text = line)") +) fun SidebarComponent.Builder.buildDynamicLine( line: SurfComponentBuilder.() -> Unit ): SidebarComponent.Builder = addDynamicLine { SurfComponentBuilder(line) } diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarBuilder.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarBuilder.kt new file mode 100644 index 000000000..f9731e5ad --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarBuilder.kt @@ -0,0 +1,298 @@ +package dev.slne.surf.api.paper.sidebar + +import dev.slne.surf.api.core.messages.Colors +import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor +import org.bukkit.entity.Player +import org.jetbrains.annotations.Range +import kotlin.time.Duration + +@DslMarker +annotation class SidebarDsl + +/** + * Configures the title and lines of a [SurfSidebar]. + * + * Lines are displayed in the order in which they are added. Viewer-dependent + * renderers are evaluated asynchronously for each viewer whenever the sidebar + * is updated. Shared renderers are evaluated once per update before the viewers + * are rendered, and all viewers receive the same result. See [SurfSidebar] for + * the threading contract. + * + * If a renderer fails, the failure is logged and the affected viewer keeps + * their previously rendered content. + * + * @see SurfSidebar + */ +@SidebarDsl +interface SidebarBuilder { + + /** + * The maximum number of lines displayed by the sidebar. + * + * Additional lines are discarded. + */ + var maxLines: @Range(from = 1, to = 15) Int + + /** + * Sets the title displayed by the sidebar. + * + * @param title the title to display + */ + fun title(title: Component) + + /** + * Sets a title rendered asynchronously for each viewer on every update. + * + * @param render produces the title for the given viewer + */ + fun title(render: suspend (viewer: Player) -> Component) + + /** + * Sets a title rendered once per update and shown to all viewers. + * + * @param render produces the title + */ + fun sharedTitle(render: suspend () -> Component) + + /** + * Adds a static line to the sidebar. + * + * @param text the line content + * @param score optional content displayed in the score area + */ + fun line(text: Component, score: Component? = null) + + /** + * Adds a line rendered asynchronously for each viewer on every update. + * + * @param render produces the line content for the given viewer + */ + fun line(render: suspend (viewer: Player) -> Component) + + /** + * Adds a line whose content and score are rendered asynchronously for each viewer. + * + * @param render produces the line for the given viewer + */ + fun scoredLine(render: suspend (viewer: Player) -> SidebarLine) + + /** + * Adds a dynamic collection of lines rendered asynchronously for each viewer. + * + * The returned number of lines may vary between viewers and between updates. + * + * @param render produces the lines for the given viewer + */ + fun lines(render: suspend (viewer: Player) -> List) + + /** + * Adds a line rendered once per update and shown to all viewers. + * + * @param render produces the line content + */ + fun sharedLine(render: suspend () -> Component) + + /** + * Adds a dynamic collection of lines rendered once per update and shown to all viewers. + * + * @param render produces the lines + * @see sharedLine + */ + fun sharedLines(render: suspend () -> List) + + /** + * Adds an empty line to the sidebar. + */ + fun emptyLine() { + line(Component.empty()) + } + + /** + * Adds an animated line that advances through [frames] on each update. + * + * @param frames the animation frames + */ + fun animatedLine(frames: List) + + /** + * Adds a line with an animated gradient applied across [text]. + * + * The gradient moves between [start] and [end] as the sidebar updates. + * + * @param text the content to apply the gradient to + * @param start the starting gradient color + * @param end the ending gradient color + */ + fun gradientLine(text: Component, start: TextColor, end: TextColor) + + /** + * Adds the default gradient separator line. + */ + fun separator() { + gradientLine(Component.text("--------------------"), Colors.WHITE, Colors.SPACER) + } + + /** + * Sets a static title built using [SurfComponentBuilder]. + * + * @param block configures the title component + */ + fun buildTitle(block: SurfComponentBuilder.() -> Unit) { + title(SurfComponentBuilder(block)) + } + + /** + * Sets a title built asynchronously for each viewer using [SurfComponentBuilder]. + * + * @param block configures the title for the given viewer + */ + fun buildViewerTitle(block: suspend SurfComponentBuilder.(viewer: Player) -> Unit) { + title { viewer -> SurfComponentBuilder { block(viewer) } } + } + + /** + * Sets a shared title built using [SurfComponentBuilder]. + * + * @param block configures the title component + * @see sharedTitle + */ + fun buildSharedTitle(block: suspend SurfComponentBuilder.() -> Unit) { + sharedTitle { SurfComponentBuilder { block() } } + } + + /** + * Adds a static line built using [SurfComponentBuilder]. + * + * @param score optional content displayed in the score area + * @param text configures the line content + */ + fun buildLine(score: Component? = null, text: SurfComponentBuilder.() -> Unit) { + line(SurfComponentBuilder(text), score) + } + + /** + * Adds a line built asynchronously for each viewer using [SurfComponentBuilder]. + * + * @param text configures the line for the given viewer + */ + fun buildViewerLine(text: suspend SurfComponentBuilder.(viewer: Player) -> Unit) { + line { viewer -> SurfComponentBuilder { text(viewer) } } + } + + /** + * Adds a dynamic collection of lines built asynchronously for each viewer. + * + * @param block collects the lines for the given viewer + */ + fun buildViewerLines(block: suspend SidebarLinesBuilder.(viewer: Player) -> Unit) { + lines { viewer -> SidebarLinesBuilder().apply { block(viewer) }.build() } + } + + /** + * Adds a shared line whose content is built using [SurfComponentBuilder]. + * + * @param text configures the line content + * @see sharedLine + */ + fun buildSharedLine(text: suspend SurfComponentBuilder.() -> Unit) { + sharedLine { SurfComponentBuilder { text() } } + } + + /** + * Adds a dynamic collection of shared lines collected by a [SidebarLinesBuilder]. + * + * @param block collects the lines + * @see sharedLines + */ + fun buildSharedLines(block: suspend SidebarLinesBuilder.() -> Unit) { + sharedLines { SidebarLinesBuilder().apply { block() }.build() } + } + + /** + * Adds a gradient line whose content is built using [SurfComponentBuilder]. + * + * @param start the starting gradient color + * @param end the ending gradient color + * @param text configures the line content + */ + fun buildGradientLine(start: TextColor, end: TextColor, text: SurfComponentBuilder.() -> Unit) { + gradientLine(SurfComponentBuilder(text), start, end) + } +} + +/** + * Collects lines for [SidebarBuilder.buildViewerLines]. + */ +@SidebarDsl +class SidebarLinesBuilder internal constructor() { + private val lines = ObjectArrayList() + + /** + * Adds [line] to the collected sidebar lines. + * + * @param line the line to add + */ + fun line(line: SidebarLine) { + lines.add(line) + } + + /** + * Adds a line with optional score content. + * + * @param text the line content + * @param score optional content displayed in the score area + */ + fun line(text: Component, score: Component? = null) { + line(SidebarLine(text, score)) + } + + /** + * Adds a line built using [SurfComponentBuilder]. + * + * @param score optional content displayed in the score area + * @param text configures the line content + */ + inline fun buildLine(score: Component? = null, text: SurfComponentBuilder.() -> Unit) { + line(SurfComponentBuilder(text), score) + } + + /** + * Adds a line whose content and score are both built using [SurfComponentBuilder]. + * + * @param text configures the line content + * @param score configures the score content + */ + inline fun buildScoredLine( + text: SurfComponentBuilder.() -> Unit, + score: SurfComponentBuilder.() -> Unit + ) { + line(SurfComponentBuilder(text), SurfComponentBuilder(score)) + } + + /** + * Adds an empty line. + */ + fun emptyLine() { + line(Component.empty()) + } + + internal fun build(): List = lines +} + +/** + * Configures a [SurfSidebar] that updates automatically at a fixed interval. + */ +@SidebarDsl +interface AutoUpdatingSidebarBuilder : SidebarBuilder { + /** + * The interval between automatic sidebar updates. + * + * The interval must be at least one tick. + * + * @see io.papermc.paper.util.Tick + */ + var updateInterval: Duration +} diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarLine.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarLine.kt new file mode 100644 index 000000000..70b3fd68c --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SidebarLine.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.api.paper.sidebar + +import net.kyori.adventure.text.Component + +/** + * Represents a single line displayed in a sidebar. + * + * @property text the main content of the line + * @property score optional content displayed in the score area + */ +data class SidebarLine(val text: Component, val score: Component? = null) diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SurfSidebar.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SurfSidebar.kt new file mode 100644 index 000000000..1df4fd2df --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/SurfSidebar.kt @@ -0,0 +1,119 @@ +package dev.slne.surf.api.paper.sidebar + +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import kotlin.time.Duration + +/** + * Represents a packet-based sidebar displayed to a collection of viewers. + * + * Titles and lines are rendered asynchronously on a background dispatcher for each viewer + * whenever the sidebar is updated. Renderers may suspend. Code that requires access to the + * viewer's region thread should switch to it using + * `withContext(plugin.entityDispatcher(viewer))`. + * + * Rendering for the same viewer never overlaps. If additional updates are requested while a + * render is already in progress, they are coalesced into a single follow-up render. Only content + * that changed since the previous render is sent to the viewer. + * + * The sidebar is closed automatically when [plugin] is disabled, and viewers are removed + * automatically when they leave the server. + * + * A player should only view one sidebar at a time, including legacy + * [dev.slne.surf.api.paper.scoreboard.SurfScoreboard] instances, as they all use the same + * sidebar display slot. + * + * All operations are thread-safe. + */ +interface SurfSidebar { + + /** + * The plugin that owns this sidebar. + */ + val plugin: Plugin + + /** + * A snapshot of the players currently viewing this sidebar. + */ + val viewers: Collection + + /** + * Whether this sidebar has been closed. + */ + val isClosed: Boolean + + /** + * Requests a render for all current viewers and advances animated lines by one frame. + * + * This method returns immediately while rendering continues asynchronously. Multiple update + * requests made while a viewer is already being rendered are coalesced into a single + * follow-up render. + * + * Does nothing after the sidebar has been closed. + */ + fun update() + + /** + * Removes the sidebar from all viewers and stops further rendering. + * + * Subsequent calls have no effect. + */ + fun close() +} + +/** + * A [SurfSidebar] whose viewers are managed explicitly by the caller. + * + * The sidebar is rendered when a viewer is added and subsequently whenever [update] is called. + * + * @see sidebar + */ +interface SurfViewerSidebar : SurfSidebar { + + /** + * Adds [player] as a viewer and schedules an initial render for them. + * + * Does nothing if [player] is already viewing this sidebar. If a different [Player] instance + * with the same UUID is added, such as after the player rejoins, it replaces the previous + * instance. + * + * @param player the player to add + * @throws IllegalStateException if this sidebar has already been closed + */ + fun addViewer(player: Player) + + /** + * Removes [player] as a viewer and clears the sidebar from their client. + * + * @param player the player to remove + */ + fun removeViewer(player: Player) +} + +/** + * A [SurfViewerSidebar] that automatically calls [update] at a fixed interval. + * + * @see autoUpdatingSidebar + */ +interface SurfAutoUpdatingSidebar : SurfViewerSidebar { + + /** + * The interval between automatic updates. + */ + val updateInterval: Duration +} + +/** + * A [SurfSidebar] shown to every online player and to players who join later. + * + * The sidebar automatically calls [update] at a fixed [updateInterval]. + * + * @see globalSidebar + */ +interface SurfGlobalSidebar : SurfSidebar { + + /** + * The interval between automatic updates. + */ + val updateInterval: Duration +} diff --git a/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/sidebar-dsl.kt b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/sidebar-dsl.kt new file mode 100644 index 000000000..a955eb711 --- /dev/null +++ b/surf-api-paper/surf-api-paper/src/main/kotlin/dev/slne/surf/api/paper/sidebar/sidebar-dsl.kt @@ -0,0 +1,68 @@ +package dev.slne.surf.api.paper.sidebar + +import dev.slne.surf.api.core.util.requiredService +import dev.slne.surf.api.shared.api.util.InternalSurfApi +import org.bukkit.plugin.Plugin + +/** + * Creates a sidebar with manually managed viewers and updates. + * + * Use [SurfViewerSidebar] to add or remove viewers and trigger updates when + * the displayed content should be refreshed. + * + * @param plugin the plugin that owns the sidebar + * @param block configures the sidebar + * @return the configured sidebar + */ +@OptIn(InternalSurfApi::class) +fun sidebar(plugin: Plugin, block: SidebarBuilder.() -> Unit): SurfViewerSidebar { + return SurfSidebarFactory.INSTANCE.createSidebar(plugin, block) +} + +/** + * Creates a sidebar that updates automatically while its viewers are managed manually. + * + * @param plugin the plugin that owns the sidebar + * @param block configures the sidebar and its update behavior + * @return the configured auto-updating sidebar + */ +@OptIn(InternalSurfApi::class) +fun autoUpdatingSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit +): SurfAutoUpdatingSidebar = SurfSidebarFactory.INSTANCE.createAutoUpdatingSidebar(plugin, block) + +/** + * Creates an automatically updating sidebar shown to all online players. + * + * @param plugin the plugin that owns the sidebar + * @param block configures the sidebar and its update behavior + * @return the configured global sidebar + */ +@OptIn(InternalSurfApi::class) +fun globalSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit +): SurfGlobalSidebar = SurfSidebarFactory.INSTANCE.createGlobalSidebar(plugin, block) + +/** + * Internal factory for creating sidebar implementations. + */ +@InternalSurfApi +interface SurfSidebarFactory { + fun createSidebar(plugin: Plugin, block: SidebarBuilder.() -> Unit): SurfViewerSidebar + + fun createAutoUpdatingSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit + ): SurfAutoUpdatingSidebar + + fun createGlobalSidebar( + plugin: Plugin, + block: AutoUpdatingSidebarBuilder.() -> Unit + ): SurfGlobalSidebar + + companion object { + val INSTANCE = requiredService() + } +}