diff --git a/Server/src/main/core/api/DialUtils.kt b/Server/src/main/core/api/DialUtils.kt index 1149906cd..20750eb6f 100644 --- a/Server/src/main/core/api/DialUtils.kt +++ b/Server/src/main/core/api/DialUtils.kt @@ -4,9 +4,29 @@ import kotlin.math.ceil object DialUtils { val tagRegex = "<([A-Za-z0-9=/]+)>".toRegex() + private val visibleCharacterTags = setOf("", "", "", "", "", "", "", "") + private val formattingTags = setOf("col", "shad", "u", "str", "trans") - fun removeMatches(message: String, regex: Regex): String { - return regex.replace(message, "") + fun renderedLength(message: String): Int { + var length = 0 + var position = 0 + + for (match in tagRegex.findAll(message)) { + length += match.range.first - position + length += when { + match.value == "" -> 2 + match.value.startsWith(" 1 + match.value in visibleCharacterTags -> 1 + else -> 0 + } + position = match.range.last + 1 + } + + return length + message.length - position + } + + fun isFormattingTag(tag: String): Boolean { + return tag.removePrefix("<").removePrefix("/").substringBefore("=").substringBefore(">") in formattingTags } } @@ -15,7 +35,7 @@ object DialUtils { * Should this not work out for any reason, you should fallback to standard npc and player methods for dialogue. */ fun splitLines(message: String, perLineLimit: Int = 54): Array { - var lines = Array(ceil(DialUtils.removeMatches(message, DialUtils.tagRegex).length / perLineLimit.toFloat()).toInt()) { "" } + var lines = Array(ceil(DialUtils.renderedLength(message) / perLineLimit.toFloat()).toInt()) { "" } //short circuit when possible because it's cheaper. if (lines.size == 1) { @@ -45,7 +65,7 @@ fun splitLines(message: String, perLineLimit: Int = 54): Array { break } } - } else { + } else if (DialUtils.isFormattingTag(lineTag.value)) { openTags.add(lineTag.value) } } @@ -65,11 +85,14 @@ fun splitLines(message: String, perLineLimit: Int = 54): Array { } while (!tokenQueue.isEmpty()) { - val shouldSpace = DialUtils.removeMatches(line.toString(), DialUtils.tagRegex).isNotEmpty() - accumulator += DialUtils.removeMatches(tokenQueue.peek(), DialUtils.tagRegex).length + val shouldSpace = DialUtils.renderedLength(line.toString()) > 0 + accumulator += DialUtils.renderedLength(tokenQueue.peek()) if (shouldSpace) accumulator += 1 if (accumulator > perLineLimit) { + if (line.isEmpty()) { + line.append(tokenQueue.pop()) + } pushLine() continue } diff --git a/Server/src/main/core/game/system/communication/GlobalChat.kt b/Server/src/main/core/game/system/communication/GlobalChat.kt index 6e8ed2161..7dae7d5d6 100644 --- a/Server/src/main/core/game/system/communication/GlobalChat.kt +++ b/Server/src/main/core/game/system/communication/GlobalChat.kt @@ -6,7 +6,6 @@ import core.api.sendMessage import core.api.setAttribute import core.game.system.command.Privilege import core.game.world.repository.Repository -import core.game.node.entity.player.info.Rights import core.tools.colorize class GlobalChat : Commands { @@ -21,9 +20,34 @@ class GlobalChat : Commands { companion object { val ATTR_GLOBAL_MUTE = "/save:globalmute" - fun process(sender: String, message: String, rights: Int) { - val msgSD = prepare(sender, message, false, rights) - val msgHD = prepare(sender, message, true, rights) + private val ANGLE_BRACKETS = Regex("[<>]") + private val COLORED_STYLE = Regex("%(u|str|shad)=([0-9a-fA-F]{6})") + private val TRANSPARENCY = Regex("%trans=([0-9]{1,3})") + private val COMMON_SHORTCUTS = mapOf( + "%ironman" to "", + "%chat" to "", + "%hcim" to "", + "%uim" to "", + "%/trans" to "", + "%trans" to "", + "%/shad" to "", + "%shad" to "", + "%/str" to "", + "%str" to "", + "%/u" to "", + "%u" to "", + "%" to "" + ) + private val COMMON_SHORTCUT_PATTERN = Regex("%ironman|%chat|%hcim|%uim|%/trans|%trans(?!=)|%/shad|%shad(?!=)|%/str|%str(?!=)|%/u|%u(?!=)|%") + private const val MAX_ENCODED_MESSAGE_LENGTH = 255 + + fun process(sender: String, message: String, chatIcon: Int) { + val msgSD = prepare(sender, message, false, chatIcon) + val msgHD = prepare(sender, message, true, chatIcon) + if (msgSD.length > MAX_ENCODED_MESSAGE_LENGTH || msgHD.length > MAX_ENCODED_MESSAGE_LENGTH) { + return + } + for (player in Repository.players.filter { !getAttribute(it, ATTR_GLOBAL_MUTE, false) }) { if (player.interfaceManager.isResizable) sendMessage(player, msgHD) @@ -32,10 +56,36 @@ class GlobalChat : Commands { } } - private fun prepare(sender: String, message: String, isResizable: Boolean, rights: Int): String { + private fun prepare(sender: String, message: String, isResizable: Boolean, chatIcon: Int): String { val baseColor = if (isResizable) "%f1b04c" else "%7512ff" val bracketColor = if (isResizable) "%ffffff" else "%000000" - return colorize("$bracketColor[${baseColor}G$bracketColor] ${if (rights > 0) "" else ""}$sender: ${baseColor}$message") + val formattedMessage = formatPlayerMessage(message, chatIcon) + return colorize("$bracketColor[${baseColor}G$bracketColor] ${if (chatIcon > 0) "" else ""}$sender: ${baseColor}$formattedMessage") + } + + private fun formatPlayerMessage(message: String, chatIcon: Int): String { + var formatted = ANGLE_BRACKETS.replace(message) { + if (it.value == "<") "" else "" + } + + formatted = COLORED_STYLE.replace(formatted) { + "<${it.groupValues[1]}=${it.groupValues[2]}>" + } + formatted = TRANSPARENCY.replace(formatted) { + val transparency = it.groupValues[1].toInt() + if (transparency in 0..255) "" else it.value + } + + formatted = COMMON_SHORTCUT_PATTERN.replace(formatted) { + COMMON_SHORTCUTS[it.value] ?: it.value + } + + if (chatIcon == 1 || chatIcon == 2) + formatted = formatted.replace("%mod", "") + if (chatIcon == 2) + formatted = formatted.replace("%admin", "") + + return formatted } } }