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 @@ -55,7 +55,7 @@ internal interface MinestomPermissionRuntimePlatform {

fun removeEventNode(node: EventNode<Event>)

fun startNameTags(permissions: Permissions): AutoCloseable? = null
fun startNameTags(permissions: Permissions): MinestomRankNameTags? = null
}

class GroundsPermissionsModule
Expand Down Expand Up @@ -92,7 +92,7 @@ internal constructor(
private var prefetchExecutor: ExecutorService? = null
private var prefetches: PermissionSnapshotPrefetches? = null
private var eventNode: EventNode<Event>? = null
private var nameTags: AutoCloseable? = null
private var nameTags: MinestomRankNameTags? = null
private val snapshotInvalidationLifecycle =
PermissionSnapshotInvalidationLifecycle(runtime = "minestom", logger = logger)

Expand Down Expand Up @@ -187,7 +187,12 @@ internal constructor(
)

ctx.services.register<Permissions>(permissions)
nameTags = runtimePlatform.startNameTags(permissions)
val nameTags = runtimePlatform.startNameTags(permissions)
this.nameTags = nameTags
// 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 }
)
ctx.services.registerPlayerPrefetch(MODULE_ID, prefetches)

val node = ctx.eventNode("grounds-permissions")
Expand Down Expand Up @@ -305,7 +310,7 @@ internal constructor(
MinecraftServer.getGlobalEventHandler().removeChild(node)
}

override fun startNameTags(permissions: Permissions): AutoCloseable =
override fun startNameTags(permissions: Permissions): MinestomRankNameTags =
MinestomRankNameTags(permissions)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ internal class MinestomRankNameTags(private val permissions: Permissions) : Auto
MinecraftServer.getSchedulerManager()
.scheduleTask(::refresh, TaskSchedule.nextTick(), TaskSchedule.seconds(1))

fun isBedrock(id: UUID): Boolean = editions.isBedrock(id)

@Synchronized
private fun refresh() {
val players = MinecraftServer.getConnectionManager().onlinePlayers.associateBy { it.uuid }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package gg.grounds.permissions.minestom

import gg.grounds.permissions.Permissions
import gg.grounds.permissions.RoleMetadata
import gg.grounds.permissions.minestom.badge.BedrockRankBadges
import gg.grounds.permissions.minestom.badge.TabBadge
import gg.grounds.permissions.minestom.badge.TabRankLabels
import java.util.UUID
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor

/**
* The rank badge the tab list and nameplates show, for other modules to place elsewhere.
*
* Registered in the module ServiceRegistry; resolve it with `services.get<RankBadges>()`.
*/
interface RankBadges {
/**
* The badge for [playerId]'s highest role as [viewerId] should see it: the Java font chip, or
* the Bedrock atlas glyph for a Bedrock viewer. Null when the player has no role to show.
*/
fun badge(playerId: UUID, viewerId: UUID): RankBadge?
}

/**
* [component] is the bare chip without trailing spacing; [name] and [color] are the role's display
* name and colour for a text fallback.
*/
data class RankBadge(val component: Component, val name: String, val color: TextColor)

internal class PermissionRankBadges(
private val permissions: Permissions,
private val isBedrock: (UUID) -> Boolean,
) : RankBadges {
override fun badge(playerId: UUID, viewerId: UUID): RankBadge? {
val role =
permissions
.snapshot(playerId)
?.roleMetadata
?.minWithOrNull(compareBy<RoleMetadata> { it.sortOrder }.thenBy { it.key })
?.takeIf { it.name.isNotBlank() } ?: return null
val label = TabRankLabels.resolve(role) ?: return null
val colour =
role.color?.let(TextColor::fromHexString) ?: TextColor.fromHexString("#F3F0EA")!!
return RankBadge(
component =
if (isBedrock(viewerId)) BedrockRankBadges.chip(label, colour)
else TabBadge.chip(label, colour),
name = role.name,
color = colour,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package gg.grounds.permissions.minestom

import gg.grounds.permissions.PermissionSnapshot
import gg.grounds.permissions.RoleMetadata
import gg.grounds.permissions.SnapshotPermissions
import gg.grounds.permissions.minestom.badge.BedrockRankBadges
import gg.grounds.permissions.minestom.badge.TabBadge
import java.time.Instant
import java.util.UUID
import net.kyori.adventure.text.format.TextColor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class RankBadgesTest {
private val player = UUID.randomUUID()
private val javaViewer = UUID.randomUUID()
private val bedrockViewer = UUID.randomUUID()

private fun badges(vararg roles: RoleMetadata) =
PermissionRankBadges(
SnapshotPermissions(mapOf(player to snapshot(roles.toList()))),
isBedrock = { it == bedrockViewer },
)

@Test
fun `java viewers get the tab badge of the highest role with its name and colour`() {
val colour = TextColor.fromHexString("#C9826B")!!
assertEquals(
RankBadge(TabBadge.chip("ADMIN", colour), "Administrator", colour),
badges(
RoleMetadata("developer", "Developer", color = "#A3B18A", sortOrder = 2),
RoleMetadata("administrator", "Administrator", color = "#C9826B", sortOrder = 1),
)
.badge(player, javaViewer),
)
}

@Test
fun `bedrock viewers get the atlas badge`() {
val colour = TextColor.fromHexString("#C9826B")!!
assertEquals(
RankBadge(BedrockRankBadges.chip("ADMIN", colour), "Administrator", colour),
badges(RoleMetadata("administrator", "Administrator", color = "#C9826B"))
.badge(player, bedrockViewer),
)
}

@Test
fun `roles without a colour fall back to the default chip colour`() {
assertEquals(
TextColor.fromHexString("#F3F0EA"),
badges(RoleMetadata("builder", "Builder")).badge(player, javaViewer)?.color,
)
}

@Test
fun `players without a role or a displayable name have no badge`() {
assertNull(badges().badge(UUID.randomUUID(), javaViewer))
assertNull(badges().badge(player, javaViewer))
assertNull(badges(RoleMetadata("custom", " ")).badge(player, javaViewer))
}

private fun snapshot(roles: List<RoleMetadata>): PermissionSnapshot {
val now = Instant.now()
return PermissionSnapshot(
player,
1,
now.minusSeconds(30),
now.plusSeconds(300),
now.plusSeconds(3600),
emptyList(),
emptyList(),
roles.map { it.key }.toSet(),
roles,
)
}
}
Loading