diff --git a/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/Sideboard.kt b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/Sideboard.kt index ef1bc1b2..faa9708e 100644 --- a/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/Sideboard.kt +++ b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/Sideboard.kt @@ -1,17 +1,17 @@ package net.silkmc.silk.game.sideboard +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import net.minecraft.network.chat.Component import net.minecraft.server.level.ServerPlayer -import net.silkmc.silk.core.Silk import net.silkmc.silk.core.annotations.InternalSilkApi -import net.silkmc.silk.core.task.initWithServerAsync import net.silkmc.silk.core.task.silkCoroutineScope import net.silkmc.silk.game.sideboard.internal.SideboardScoreboard /** * A sideboard which can be displayed to a variable collection of players - * using the [displayToPlayer] function. A sideboard is an abstraction of Mincraft's + * using the [displayToPlayer] function. A sideboard is an abstraction of Minecraft's * server side scoreboards displayed on the right-hand side of the screen. * * **Note:** You probably want to build this class using the sideboard builder API. See [sideboard]! @@ -21,17 +21,16 @@ class Sideboard( displayName: Component, lines: List, ) { - @InternalSilkApi - val scoreboardDeferred = initWithServerAsync { - SideboardScoreboard(name, displayName).also { scoreboard -> - lines.forEachIndexed { index, line -> - val team = scoreboard.addPlayerTeam("team_$index") - scoreboard.addPlayerToTeam("§$index", team) - scoreboard.setPlayerScore("§$index", lines.size - index) + @InternalSilkApi + val scoreboard = SideboardScoreboard(name, displayName).also { scoreboard -> + @OptIn(ExperimentalCoroutinesApi::class) + silkCoroutineScope.launch(Dispatchers.Default.limitedParallelism(1)) { + lines.forEach { line -> + val scoreboardLine = scoreboard.addLine() silkCoroutineScope.launch { line.textFlow.collect { - team.playerPrefix = it + scoreboardLine.setContent(it) } } } @@ -43,10 +42,9 @@ class Sideboard( * for receiving updates on the sideboard, until he leaves the server. */ fun displayToPlayer(player: ServerPlayer) { - if (Silk.currentServer?.isRunning == true) - silkCoroutineScope.launch { - scoreboardDeferred.await().displayToPlayer(player) - } + silkCoroutineScope.launch { + scoreboard.displayToPlayer(player) + } } /** @@ -54,9 +52,8 @@ class Sideboard( * player from receiving any further updates on the sideboard. */ fun hideFromPlayer(player: ServerPlayer) { - if (Silk.currentServer != null) - silkCoroutineScope.launch { - scoreboardDeferred.await().hideFromPlayer(player) - } + silkCoroutineScope.launch { + scoreboard.displayToPlayer(player) + } } } diff --git a/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/NoopScoreboard.kt b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/NoopScoreboard.kt new file mode 100644 index 00000000..6e411409 --- /dev/null +++ b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/NoopScoreboard.kt @@ -0,0 +1,5 @@ +package net.silkmc.silk.game.sideboard.internal + +import net.minecraft.world.scores.Scoreboard + +internal object NoopScoreboard : Scoreboard() diff --git a/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/SideboardScoreboard.kt b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/SideboardScoreboard.kt index f00686dd..c0693362 100644 --- a/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/SideboardScoreboard.kt +++ b/silk-game/src/main/kotlin/net/silkmc/silk/game/sideboard/internal/SideboardScoreboard.kt @@ -1,5 +1,7 @@ package net.silkmc.silk.game.sideboard.internal +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow import net.minecraft.network.chat.Component import net.minecraft.network.protocol.Packet import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket @@ -8,12 +10,17 @@ import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket import net.minecraft.network.protocol.game.ClientboundSetScorePacket import net.minecraft.server.ServerScoreboard import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.scores.Objective import net.minecraft.world.scores.PlayerTeam -import net.minecraft.world.scores.Score import net.minecraft.world.scores.Scoreboard import net.minecraft.world.scores.criteria.ObjectiveCriteria import net.silkmc.silk.core.annotations.InternalSilkApi -import net.silkmc.silk.core.packet.sendPacket +import net.silkmc.silk.core.event.Events +import net.silkmc.silk.core.event.Player +import net.silkmc.silk.core.kotlin.LimitedAccessWrapper +import net.silkmc.silk.core.task.silkCoroutineScope +import java.util.* +import java.util.concurrent.ConcurrentHashMap /** * A server side scoreboard which is only displayed to @@ -31,78 +38,138 @@ import net.silkmc.silk.core.packet.sendPacket class SideboardScoreboard( name: String, displayName: Component, -) : Scoreboard() { +) { + companion object { - private val sidebarId = getDisplaySlotByName("sidebar") + /** + * The ID of the sideboard display slot. + */ + private val sidebarId = Scoreboard.getDisplaySlotByName("sidebar") + + private val playerBoards = ConcurrentHashMap().also { map -> + Events.Player.preQuit.listen { event -> + silkCoroutineScope.launch { + map.remove(event.player.uuid)?.hideFromPlayer(event.player, sendRemove = false) + } + } + } } - private val players = HashSet() + private val dummyObjective = Objective(NoopScoreboard, name, ObjectiveCriteria.DUMMY, displayName, ObjectiveCriteria.RenderType.INTEGER) + + // scoreboard state + private val lines = LimitedAccessWrapper(ArrayList()) + + // for sending state to player + private val packetFlow = MutableSharedFlow>() + private val playerJobs = ConcurrentHashMap() + + /** + * See [net.silkmc.silk.game.sideboard.Sideboard.displayToPlayer]. + */ + suspend fun displayToPlayer(player: ServerPlayer) { + // already start to create the init packets in the background + val initPacketsDeferred = silkCoroutineScope.async { + buildList { + // send required dummy objective + add(ClientboundSetObjectivePacket(dummyObjective, ClientboundSetObjectivePacket.METHOD_ADD)) + + // add line packets + lines.access { + it.forEach { line -> + this@buildList.addAll(line.createInitPackets(0)) + } + } + + // display at the side + add(ClientboundSetDisplayObjectivePacket(sidebarId, dummyObjective)) + } + } - private val dummyObjective = - addObjective(name, ObjectiveCriteria.DUMMY, displayName, ObjectiveCriteria.RenderType.INTEGER) + // remove the previous board + playerBoards.put(player.uuid, this) + ?.hideFromPlayer(player) - init { - setDisplayObjective(sidebarId, dummyObjective) - } + val packetJob = silkCoroutineScope.launch(start = CoroutineStart.LAZY) { + initPacketsDeferred.await().forEach(player.connection::send) + packetFlow.collect(player.connection::send) + } - fun displayToPlayer(player: ServerPlayer) { - players += player + playerJobs.put(player.uuid, packetJob) + // shouldn't be necessary, but leaving this here to really make sure there are no competing jobs + ?.cancelAndJoin() - val updatePackets = ArrayList>() + packetJob.start() + } - updatePackets += ClientboundSetObjectivePacket(dummyObjective, ClientboundSetObjectivePacket.METHOD_ADD) - updatePackets += ClientboundSetDisplayObjectivePacket(sidebarId, dummyObjective) - getPlayerScores(dummyObjective).mapTo(updatePackets) { - ClientboundSetScorePacket( - ServerScoreboard.Method.CHANGE, it.objective!!.name, it.owner, it.score - ) + /** + * See [net.silkmc.silk.game.sideboard.Sideboard.hideFromPlayer]. + */ + suspend fun hideFromPlayer(player: ServerPlayer, sendRemove: Boolean = true) { + // cancel packet collector job + playerJobs.remove(player.uuid)?.cancelAndJoin() + // might already be removed, but make sure it is + playerBoards.remove(player.uuid, this) + + if (sendRemove) { + lines.access { + it.forEach { line -> + line.createRemovePackets().forEach(player.connection::send) + } + } + player.connection.send(ClientboundSetObjectivePacket(dummyObjective, ClientboundSetObjectivePacket.METHOD_REMOVE)) } - playerTeams.mapTo(updatePackets) { ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(it, true) } - - updatePackets.forEach(player.connection::send) } - fun hideFromPlayer(player: ServerPlayer) { - players -= players - - player.connection.send(ClientboundSetObjectivePacket(dummyObjective, ClientboundSetObjectivePacket.METHOD_REMOVE)) + private fun emitPacket(packet: Packet<*>) { + silkCoroutineScope.launch { + packetFlow.emit(packet) + } } - fun setPlayerScore(player: String, score: Int) { - getOrCreatePlayerScore(player, dummyObjective).score = score - } + inner class Line(number: Int) { + private val fakePlayerName = number.toString().map { "§${it}" }.joinToString("") + private val teamName = "team_${number}" - override fun onScoreChanged(score: Score) { - super.onScoreChanged(score) + private val unsafeTeam = PlayerTeam(NoopScoreboard, teamName) + private val wrappedTeam = LimitedAccessWrapper(unsafeTeam) - if (dummyObjective == score.objective) { - players.sendPacket( - ClientboundSetScorePacket( - ServerScoreboard.Method.CHANGE, score.objective!!.name, score.owner, score.score - ) - ) + suspend fun setContent(text: Component) { + val packet: Packet<*> + wrappedTeam.access { team -> + team.playerPrefix = text + packet = ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, false) + } + emitPacket(packet) } - } - - override fun addPlayerToTeam(playerName: String, team: PlayerTeam): Boolean { - return if (super.addPlayerToTeam(playerName, team)) { - players.sendPacket(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, ClientboundSetPlayerTeamPacket.Action.ADD)) - true - } else false - } - override fun removePlayerFromTeam(playerName: String, team: PlayerTeam) { - super.removePlayerFromTeam(playerName, team) - players.sendPacket(ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, ClientboundSetPlayerTeamPacket.Action.REMOVE)) - } + suspend fun createInitPackets(score: Int): List> { + return buildList { + add(ClientboundSetScorePacket(ServerScoreboard.Method.CHANGE, dummyObjective.name, fakePlayerName, score)) + wrappedTeam.access { team -> + add(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)) + } + add(ClientboundSetPlayerTeamPacket.createPlayerPacket(unsafeTeam, fakePlayerName, ClientboundSetPlayerTeamPacket.Action.ADD)) + } + } - override fun onTeamAdded(team: PlayerTeam) { - super.onTeamAdded(team) - players.sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)) + fun createRemovePackets(): List> { + return buildList { + add(ClientboundSetScorePacket(ServerScoreboard.Method.REMOVE, dummyObjective.name, fakePlayerName, 0)) + add(ClientboundSetPlayerTeamPacket.createRemovePacket(unsafeTeam)) + } + } } - override fun onTeamChanged(team: PlayerTeam) { - super.onTeamChanged(team) - players.sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, false)) + suspend fun addLine(): Line { + val line: Line + lines.access { + val index = it.lastIndex + 1 + line = Line(index) + it.add(line) + } + line.createInitPackets(0) + .forEach(::emitPacket) + return line } }