Port placeholders to MiniPlaceholders

Port all PlaceholderAPI usage to MiniPlaceholders / Utilities-OG APIs.

Fix Unions-OG union colors breaking the MC -> Discord link.

Drop the per-Discord-role suffix configuration. Player suffixes shown in messages now come exclusively from each player's individual LuckPerms suffix.

Fix LuckPerms spacing so exactly one space appears between the player name and any prefix/suffix.

Configure Color Code Roles.
This commit is contained in:
NotAlexNoyle committed 2026-05-06 16:10:11 -04:00
1 parent 149a16067a
commit 37237cb504
11 files changed
+166 -79

No files matched your search

+5 -4
View File
@@ -2,10 +2,11 @@
NotAlexNoyle 05/06/26:
- Drop PlaceholderAPI dependency. Resolve Unions-OG union color tags
through MiniPlaceholders via Utilities-OG.
- Forward more types of exceptions to the server console.
- Tighten LuckPerms prefix/suffix spacing and trim handling.
- Port all uses of PlaceholderAPI to MiniPlaceholders / Utilities-OG APIs.
- Fix Unions-OG Union colors breaking MC -> Discord link.
- Drop the per-Discord-role suffix configuration. Player suffixes shown in messages now come exclusively from each player's individual LuckPerms suffix.
- Fix LuckPerms spacing so exactly one space appears between the player name and any prefix/suffix.
- Configure Color Code Roles.
NotAlexNoyle 05/04/26:
+3
View File
@@ -58,6 +58,9 @@ dependencies {
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
compileOnlyApi(project(":libs:Vanish-OG")) // Import TrueOG Network Vanish-OG Java API (from source).
compileOnly("net.luckperms:api:5.5") // Import LuckPerms API.
compileOnly(
"io.github.miniplaceholders:miniplaceholders-api:2.2.3"
) // Import MiniPlaceholders API for direct tag access.
implementation("tools.jackson.core:jackson-databind:3.1.1")
implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.1.1")
+7 -8
View File
@@ -42,7 +42,7 @@ internal class Events : Listener {
return
}
val playerPartString = ChatUtil.getPlayerPartString(event.player)
val playerPartString = ChatUtil.getDiscordPlayerPartString(event.player)
scope.launch {
discordBridgeLock.read {
@@ -66,7 +66,7 @@ internal class Events : Listener {
return
}
val playerPartString = ChatUtil.getPlayerPartString(event.player)
val playerPartString = ChatUtil.getDiscordPlayerPartString(event.player)
scope.launch {
discordBridgeLock.read {
@@ -90,7 +90,7 @@ internal class Events : Listener {
return
}
val playerPartString = ChatUtil.getPlayerPartString(event.player)
val playerPartString = ChatUtil.getDiscordPlayerPartString(event.player)
val reason = PlainTextComponentSerializer.plainText().serialize(event.reason())
@@ -116,7 +116,7 @@ internal class Events : Listener {
return
}
val playerPartString = ChatUtil.getPlayerPartString(event.player)
val playerPartString = ChatUtil.getDiscordPlayerPartString(event.player)
val advancementTitleKey = event.advancement.display?.title() ?: return
val advancementTitle = PlainTextComponentSerializer.plainText().serialize(advancementTitleKey)
@@ -190,10 +190,9 @@ internal class Events : Listener {
var nameString = "${PlayerAffix.getPrefix(event.player.uniqueId)}${event.player.name}"
val unionColorTag =
PlainTextComponentSerializer.plainText()
.serialize(UtilitiesOG.trueogExpand("<simpleclans_union_color_tag>", event.player))
if (unionColorTag.isNotEmpty() && unionColorTag != "&8None") {
val unionColorTag = ChatUtil.fetchUnionColorTag(event.player)
val unionPlain = ChatUtil.stripFormatting(unionColorTag)
if (unionPlain.isNotEmpty() && unionPlain != "None") {
nameString = "&8[$unionColorTag&8] $nameString"
}
val nameComponent = UtilitiesOG.trueogColorize(legacyToMm(nameString))
@@ -23,7 +23,10 @@ internal abstract class ChatSystem {
var playerPartString = ChatUtil.getPlayerPartString(player)
playerPartString = listOfNotNull(prefix, playerPartString).joinToString(" | ")
sendDiscordMessage(text, playerPartString, player.uniqueId)
val discordPlayerPartString =
listOfNotNull(prefix, ChatUtil.getDiscordPlayerPartString(player)).joinToString(" | ")
sendDiscordMessage(text, discordPlayerPartString, player.uniqueId)
val messageComponent = ChatUtil.processText(text, player) ?: return
@@ -1,10 +1,12 @@
package nl.skbotnl.chatog.config.discord
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
@JsonIgnoreProperties(ignoreUnknown = true)
data class DiscordConfig
@JsonCreator
constructor(
@@ -23,7 +25,4 @@ constructor(
@param:JsonProperty("useColorCodeRoles") val useColorCodeRoles: Boolean,
@param:JsonProperty("colorCodeRoles") @field:JsonSetter(nulls = Nulls.AS_EMPTY) val colorCodeRoles: List<String>,
@param:JsonProperty("roles") @field:JsonSetter(nulls = Nulls.AS_EMPTY) val roles: Map<String, DiscordRoleConfig>,
@param:JsonProperty("roleSuffixes")
@field:JsonSetter(nulls = Nulls.AS_EMPTY)
val roleSuffixes: Map<String, DiscordSuffixConfig>,
)
@@ -1,6 +0,0 @@
package nl.skbotnl.chatog.config.discord
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
data class DiscordSuffixConfig @JsonCreator constructor(@param:JsonProperty("suffix") val suffix: String)
@@ -187,12 +187,7 @@ internal class DiscordBridge private constructor() {
?: NamedTextColor.GRAY
}
val suffix =
if (config.discord.roleSuffixes.isEmpty()) {
"> "
} else {
sortedRoles.firstNotNullOfOrNull { r -> config.discord.roleSuffixes[r.id] }?.suffix ?: "> "
}
val suffix = " > "
val messageComponents = mutableListOf<Component>(UtilitiesOG.trueogColorize(suffix))
@@ -368,7 +363,7 @@ internal class DiscordBridge private constructor() {
message.contentDisplay,
member.effectiveName,
Component.join(JoinConfiguration.noSeparators(), discordComponent, userComponent),
Component.text("$suffix "),
Component.text(suffix),
)
}
return discordBridge
@@ -398,7 +393,7 @@ internal class DiscordBridge private constructor() {
suspend fun sendMessage(message: String, player: Player) {
val webhookMessage =
WebhookMessageBuilder()
.setUsername(UtilitiesOG.stripFormatting(ChatUtil.getPlayerPartString(player)))
.setUsername(UtilitiesOG.stripFormatting(ChatUtil.getDiscordPlayerPartString(player)))
.setContent(UtilitiesOG.stripFormatting(ChatUtil.stripGroupMentions(ChatUtil.convertMentions(message))))
.setAvatarUrl("https://minotar.net/helm/${player.uniqueId}.png")
@@ -4,7 +4,6 @@ import java.util.*
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.JoinConfiguration
import net.kyori.adventure.text.format.NamedTextColor
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
import net.trueog.utilitiesog.UtilitiesOG
import nl.skbotnl.chatog.ChatOG.Companion.config
import nl.skbotnl.chatog.ChatOG.Companion.languageDatabase
@@ -142,10 +141,9 @@ internal class TranslateMessage : CommandExecutor {
"${PlayerAffix.getPrefix(sentChatMessage.player.uniqueId)}${sentChatMessage.player.name}${
PlayerAffix.getSuffix(sentChatMessage.player.uniqueId)
}"
val unionColorTag =
PlainTextComponentSerializer.plainText()
.serialize(UtilitiesOG.trueogExpand("<simpleclans_clan_color_tag>", sentMessage.player))
if (unionColorTag.isNotEmpty() && unionColorTag != "&8None") {
val unionColorTag = ChatUtil.fetchUnionColorTag(sentMessage.player)
val unionPlain = ChatUtil.stripFormatting(unionColorTag)
if (unionPlain.isNotEmpty() && unionPlain != "None") {
playerString = "&8[$unionColorTag&8] $playerString"
}
val playerComponent =
@@ -155,7 +153,6 @@ internal class TranslateMessage : CommandExecutor {
)
)
// Don't convert color in translated messages
translateMessage =
Component.join(
JoinConfiguration.noSeparators(),
@@ -1,6 +1,7 @@
package nl.skbotnl.chatog.util
import dev.minn.jda.ktx.coroutines.await
import io.github.miniplaceholders.api.MiniPlaceholders
import java.util.*
import kotlin.concurrent.read
import net.kyori.adventure.text.Component
@@ -9,6 +10,7 @@ import net.kyori.adventure.text.event.HoverEvent
import net.kyori.adventure.text.format.TextColor
import net.kyori.adventure.text.minimessage.Context
import net.kyori.adventure.text.minimessage.MiniMessage
import net.kyori.adventure.text.minimessage.tag.PreProcess
import net.kyori.adventure.text.minimessage.tag.Tag
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver
@@ -25,19 +27,45 @@ import org.bukkit.Sound
import org.bukkit.entity.Player
internal object ChatUtil {
fun getPlayerPartString(player: Player): String {
var playerPart = "${PlayerAffix.getPrefix(player.uniqueId)}${player.name}"
fun getPlayerPartString(player: Player, includeSuffix: Boolean = false): String {
var playerPart =
"${PlayerAffix.getPrefix(player.uniqueId)}${player.name}${if (includeSuffix) PlayerAffix.getSuffix(player.uniqueId) else ""}"
val unionColorTag =
PlainTextComponentSerializer.plainText()
.serialize(UtilitiesOG.trueogExpand("<simpleclans_clan_color_tag>", player))
if (unionColorTag.isNotEmpty() && unionColorTag != "&8None") {
val unionColorTag = fetchUnionColorTag(player)
val unionPlain = stripFormatting(unionColorTag)
if (unionPlain.isNotEmpty() && unionPlain != "None") {
playerPart = legacyToMm("&8[$unionColorTag&8] ") + playerPart
}
return playerPart
}
fun getDiscordPlayerPartString(player: Player): String {
return getPlayerPartString(player, includeSuffix = true)
}
fun fetchUnionColorTag(player: Player): String {
var captured = ""
val original = MiniPlaceholders.getAudiencePlaceholders(player)
val capturingResolver =
object : TagResolver {
override fun resolve(name: String, arguments: ArgumentQueue, ctx: Context): Tag? {
val tag = original.resolve(name, arguments, ctx) ?: return null
if (tag is PreProcess) {
captured = tag.value().replace('§', '&')
return Tag.preProcessParsed("")
}
return tag
}
override fun has(name: String): Boolean = original.has(name)
}
val resolver =
TagResolver.builder().resolver(MiniPlaceholders.getGlobalPlaceholders()).resolver(capturingResolver).build()
MiniMessage.miniMessage().deserialize("<simpleclans_clan_color_tag>", resolver)
return captured
}
val mentionRegex = Regex("@([A-Za-z0-9_]{3,16})")
fun dingForMentions(dontDingFor: UUID, component: Component) {
@@ -102,6 +130,10 @@ internal object ChatUtil {
return legacyRegex.replace(text) { legacyToMmMap[it.groupValues[1].lowercase()] ?: it.value }
}
fun stripFormatting(text: String): String {
return MiniMessage.miniMessage().stripTags(legacyRegex.replace(text, ""))
}
private val getHandle = Regex("@([a-z0-9_.]{2,32})")
suspend fun convertMentions(text: String): String {
@@ -170,8 +202,10 @@ internal object ChatUtil {
private fun processText(text: String, player: Player?, username: String?, color: Boolean): Component? {
val words: MutableList<String> = mutableListOf()
val canUseFormatting = player?.hasPermission("chat-og.color") == true || color
val textToProcess = if (canUseFormatting) legacyToMm(text) else stripFormatting(text)
legacyToMm(text).split(" ").forEach { word ->
textToProcess.split(" ").forEach { word ->
val url = urlRegex.find(word)
if (url != null) {
@@ -206,14 +240,10 @@ internal object ChatUtil {
}
}
return if (player?.hasPermission("chat-og.color") == true) {
return if (canUseFormatting) {
colorMm.deserialize(words.joinToString(" "))
} else {
if (color) {
colorMm.deserialize(words.joinToString(" "))
} else {
noColorMm.deserialize(words.joinToString(" "))
}
noColorMm.deserialize(words.joinToString(" "))
}
}
@@ -6,13 +6,57 @@ import nl.skbotnl.chatog.util.ChatUtil.legacyToMm
internal object PlayerAffix {
fun getPrefix(uuid: UUID): String {
val prefix = getMetaData(uuid).prefix ?: return ""
return legacyToMm(prefix).trimEnd() + " "
val raw = getMetaData(uuid).prefix ?: return ""
val cleaned = stripOuterSpaces(legacyToMm(raw))
if (ChatUtil.stripFormatting(cleaned).isEmpty()) return ""
return "$cleaned "
}
fun getSuffix(uuid: UUID): String {
val suffix = getMetaData(uuid).suffix ?: return ""
return " " + legacyToMm(suffix).trim()
val raw = getMetaData(uuid).suffix ?: return ""
val cleaned = stripOuterSpaces(legacyToMm(raw))
if (ChatUtil.stripFormatting(cleaned).isEmpty()) return ""
return " $cleaned"
}
private fun stripOuterSpaces(mm: String): String = stripTrailingSpaces(stripLeadingSpaces(mm))
private fun stripLeadingSpaces(mm: String): String {
val keptTags = StringBuilder()
var i = 0
while (i < mm.length) {
val c = mm[i]
when {
c.isWhitespace() -> i++
c == '<' -> {
val close = mm.indexOf('>', i)
if (close < 0) break
keptTags.append(mm, i, close + 1)
i = close + 1
}
else -> return keptTags.toString() + mm.substring(i)
}
}
return keptTags.toString()
}
private fun stripTrailingSpaces(mm: String): String {
val keptTags = StringBuilder()
var end = mm.length
while (end > 0) {
val c = mm[end - 1]
when {
c.isWhitespace() -> end--
c == '>' -> {
val open = mm.lastIndexOf('<', end - 1)
if (open < 0) break
keptTags.insert(0, mm.substring(open, end))
end = open
}
else -> return mm.substring(0, end) + keptTags.toString()
}
}
return keptTags.toString()
}
private fun getMetaData(uuid: UUID) =
+48 -26
View File
@@ -64,31 +64,53 @@ discord:
listCommandText: "There are %onlineplayers% out of maximum 200 players online."
# Color code roles
# TODO: Configure these roles.
useColorCodeRoles: false
# Configure which roles can use color codes with Discord -> Minecraft
# For example:
#
# colorCodeRoles: [1234567891274567890, 0987654321983654321]
# When true, only Discord users with one of the colorCodeRoles below can use
# Utilities-OG ColorCodeMap Bukkit codes (&c, &l, &*) or MiniMessage tags
# in Discord -> Minecraft chat. All others have their input stripped of
# formatting before it is rendered in-game.
useColorCodeRoles: true
# Configure the text color (after the username (Discord -> Minecraft)) for certain roles
# For example:
#
# roles:
# 1234567891239567890:
# messageColor: red
#
# message_color can be one of the following: black, dark_blue, dark_green, dark_aqua, dark_red, dark_purple, gold, gray, dark_gray, blue, green, aqua, red, light_purple, yellow, white
#
# You can also use RGB for the color:
#
# roles:
# 1234567891234567890:
# messageColor: [255, 255, 255]
# Discord role IDs allowed to use color codes from Discord.
colorCodeRoles:
- "897300356395073546" # OG-Pro
- "934876475285467247" # OG Master
- "897299946926125167" # Tester (same level as OG Master)
- "897299018407890945" # YouTube
- "1391741516204081162" # Technical Contributor
- "1096297706034569287" # Helper
- "897776965849858101" # Builder
- "916661431850639420" # MVP
- "1080912883694719047" # Developer
- "897297817574440970" # Moderator
- "897296878490763344" # Admin
# Configure the suffix for roles (has to include the space). color codes are allowed.
# For example:
# roleSuffixes:
# 1234567891234527890:
# suffix: ": "
# Default Minecraft chat color applied to a Discord -> Minecraft message when
# the sender did not type their own color code. If a user has multiple
# configured roles the entry for their highest-positioned role wins.
roles:
"897657230575431710": # Member
messageColor: gray
"897300768619659314": # OG
messageColor: white
"897300356395073546": # OG-Pro
messageColor: white
"934876475285467247": # OG Master
messageColor: white
"897299946926125167": # Tester
messageColor: white
"897299018407890945": # YouTube
messageColor: white
"1391741516204081162": # Technical Contributor
messageColor: white
"1096297706034569287": # Helper
messageColor: white
"897776965849858101": # Builder
messageColor: white
"916661431850639420": # MVP
messageColor: white
"1080912883694719047": # Developer
messageColor: white
"897297817574440970": # Moderator
messageColor: white
"897296878490763344": # Admin
messageColor: white