Skip to content

Commit

Permalink
game: Improve sideboard implementation and concurrency
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobkmar committed Sep 21, 2022
1 parent fd2aae7 commit cd09ebb
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -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]!
Expand All @@ -21,17 +21,16 @@ class Sideboard(
displayName: Component,
lines: List<SideboardLine>,
) {
@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)
}
}
}
Expand All @@ -43,20 +42,18 @@ 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)
}
}

/**
* Hides this sideboard from the given [player]. This also unregisters the
* 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.silkmc.silk.game.sideboard.internal

import net.minecraft.world.scores.Scoreboard

internal object NoopScoreboard : Scoreboard()
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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<UUID, SideboardScoreboard>().also { map ->
Events.Player.preQuit.listen { event ->
silkCoroutineScope.launch {
map.remove(event.player.uuid)?.hideFromPlayer(event.player, sendRemove = false)
}
}
}
}

private val players = HashSet<ServerPlayer>()
private val dummyObjective = Objective(NoopScoreboard, name, ObjectiveCriteria.DUMMY, displayName, ObjectiveCriteria.RenderType.INTEGER)

// scoreboard state
private val lines = LimitedAccessWrapper(ArrayList<Line>())

// for sending state to player
private val packetFlow = MutableSharedFlow<Packet<*>>()
private val playerJobs = ConcurrentHashMap<UUID, Job>()

/**
* 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<Packet<*>>()
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<Packet<*>> {
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<Packet<*>> {
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
}
}

0 comments on commit cd09ebb

Please sign in to comment.