Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ internal constructor(
ctx.services.register<Permissions>(permissions)
val nameTags = runtimePlatform.startNameTags(permissions)
this.nameTags = nameTags
ctx.services.register<NameplateStatuses>(nameTags?.statuses ?: NativeNameplateStatuses())
// The name tags own the only edition roster; without them every viewer counts as Java.
ctx.services.register<RankBadges>(
PermissionRankBadges(permissions) { id -> nameTags?.isBedrock(id) ?: false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ class GroundsPermissionsModuleProvider : GroundsModuleProvider {
override val version: String = BuildInfo.VERSION
override val serverTypes: Set<ServerType> = ServerType.entries.toSet()
override val descriptor: ModuleDescriptor =
ModuleDescriptor(id = id, version = version, provides = setOf(serviceKey<Permissions>()))
ModuleDescriptor(
id = id,
version = version,
provides =
setOf(
serviceKey<Permissions>(),
serviceKey<RankBadges>(),
serviceKey<NameplateStatuses>(),
),
)

override fun create(): GroundsModule = GroundsPermissionsModule()
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ internal class MinestomRankNameTags(private val permissions: Permissions) : Auto
private val entries = mutableMapOf<UUID, Entry>()
private val editions = ProxyPlayerEditions()
private val viewerEditions = mutableMapOf<UUID, Boolean>()
private val statusLine = RankNameTagStatusLine()
val statuses = NativeNameplateStatuses()
private val statusLine = RankNameTagStatusLine(statuses::status)
private val node = EventNode.all("grounds-rank-nameplates")

init {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gg.grounds.permissions.minestom

import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.JoinConfiguration

/** Supplies viewer-specific icons for the native row below a player's name. */
fun interface NameplateStatusProvider {
fun status(playerId: UUID, viewerId: UUID): Component
}

/** Registered by grounds.permissions; other modules contribute icons without creating entities. */
interface NameplateStatuses {
/** The returned handle removes this provider when its module stops. */
fun register(provider: NameplateStatusProvider): AutoCloseable
}

internal class NativeNameplateStatuses : NameplateStatuses {
private class Registration(val provider: NameplateStatusProvider)

private val registrations = CopyOnWriteArrayList<Registration>()

override fun register(provider: NameplateStatusProvider): AutoCloseable {
val registration = Registration(provider)
registrations.add(registration)
return AutoCloseable { registrations.remove(registration) }
}

fun status(playerId: UUID, viewerId: UUID): Component =
Component.join(
JoinConfiguration.separator(Component.space()),
registrations.mapNotNull { registration ->
runCatching { registration.provider.status(playerId, viewerId) }
.getOrNull()
?.takeUnless { it == Component.empty() }
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
import net.kyori.adventure.key.Key
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.JoinConfiguration
import net.kyori.adventure.text.format.NamedTextColor
import net.minestom.server.entity.Player
import net.minestom.server.network.packet.server.ServerPacket
Expand All @@ -15,7 +16,9 @@ import net.minestom.server.network.packet.server.play.UpdateScorePacket
import net.minestom.server.scoreboard.Sidebar.NumberFormat

/** Native second nameplate row, without extra entities or movement packets. */
internal class RankNameTagStatusLine : AutoCloseable {
internal class RankNameTagStatusLine(
private val extraStatus: (UUID, UUID) -> Component = { _, _ -> Component.empty() }
) : AutoCloseable {
private data class Viewer(
val player: Player,
var scores: Map<String, Component> = emptyMap(),
Expand Down Expand Up @@ -61,11 +64,27 @@ internal class RankNameTagStatusLine : AutoCloseable {
val online = players.mapTo(mutableSetOf()) { it.uuid }
viewers.keys.retainAll(online)
gameObjectives.keys.retainAll(online)
val subjects = players.filter { isBedrock(it.uuid) }
val javaScores = subjects.associate { it.username to JAVA_ICON }
val bedrockScores = subjects.associate { it.username to BEDROCK_ICON }
val bedrockSubjects =
players.filter { isBedrock(it.uuid) }.mapTo(mutableSetOf()) { it.uuid }
players.forEach { player ->
if (subjects.isEmpty()) {
val icon = if (isBedrock(player.uuid)) BEDROCK_ICON else JAVA_ICON
val scores = buildMap {
players.forEach { subject ->
val parts =
listOfNotNull(
extraStatus(subject.uuid, player.uuid).takeUnless {
it == Component.empty()
},
icon.takeIf { subject.uuid in bedrockSubjects },
)
if (parts.isNotEmpty())
put(
subject.username,
Component.join(JoinConfiguration.separator(Component.space()), parts),
)
}
}
if (scores.isEmpty()) {
viewers.remove(player.uuid)?.let { remove(it.player) }
return@forEach
}
Expand All @@ -83,7 +102,6 @@ internal class RankNameTagStatusLine : AutoCloseable {
)
Viewer(player)
}
val scores = if (isBedrock(player.uuid)) bedrockScores else javaScores
(state.scores.keys - scores.keys).forEach { name ->
player.sendPacket(ResetScorePacket(name, objective))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gg.grounds.permissions.minestom

import java.util.UUID
import net.kyori.adventure.text.Component
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

class NameplateStatusesTest {
@Test
fun `providers receive subject and viewer and release their icons on close`() {
val statuses = NativeNameplateStatuses()
val subject = UUID.randomUUID()
val viewer = UUID.randomUUID()
val icon = Component.text('\uE005')
val registration =
statuses.register { playerId, viewerId ->
if (playerId == subject && viewerId == viewer) icon else Component.empty()
}
assertEquals(icon, statuses.status(subject, viewer))
assertEquals(Component.empty(), statuses.status(viewer, subject))
registration.close()
registration.close()
assertEquals(Component.empty(), statuses.status(subject, viewer))
}

@Test
fun `a failed status provider cannot interrupt other modules or the nameplate refresh`() {
val statuses = NativeNameplateStatuses()
statuses.register { _, _ -> error("unavailable") }
statuses.register { _, _ -> Component.text('\uE005') }
assertEquals(
Component.text('\uE005'),
statuses.status(UUID.randomUUID(), UUID.randomUUID()),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class RankNameTagStatusLineTest {
@Test
fun `voice status shares the bedrock row and is visible even without bedrock players`() {
val connection = RecordingConnection()
val viewer = player("Viewer", connection)
val other = player("Other", RecordingConnection())
val line = RankNameTagStatusLine { subject, target ->
if (subject == other.uuid && target == viewer.uuid) Component.text('\uE005')
else Component.empty()
}
line.refresh(listOf(viewer, other)) { false }
var score = connection.packets.filterIsInstance<UpdateScorePacket>().single()
assertEquals("Other", score.entityName())
assertEquals("\uE005", plain(score.numberFormat()!!.content()!!))
line.refresh(listOf(viewer, other)) { it == other.uuid }
score = connection.packets.filterIsInstance<UpdateScorePacket>().last()
assertEquals("\uE005 \uE004", plain(score.numberFormat()!!.content()!!))
line.close()
}

private fun plain(c: Component): String =
(if (c is TextComponent) c.content() else "") + c.children().joinToString("") { plain(it) }

@Test
fun `bedrock status uses native below-name slot and the viewers own font`() {
val javaConnection = RecordingConnection()
Expand Down
Loading