Merge branch 'escapeTags' into 'master'

The sign "<" now displays in global and more short-hands %R %U

Closes #2611

See merge request 2009scape/2009scape!2492
This commit is contained in:
Tooze 2026-08-01 05:13:22 +00:00
commit 7f3ce3e02c
2 changed files with 85 additions and 12 deletions

View file

@ -4,9 +4,29 @@ import kotlin.math.ceil
object DialUtils {
val tagRegex = "<([A-Za-z0-9=/]+)>".toRegex()
private val visibleCharacterTags = setOf("<lt>", "<gt>", "<nbsp>", "<shy>", "<times>", "<euro>", "<copy>", "<reg>")
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 == "<img=2>" -> 2
match.value.startsWith("<img=") -> 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<String> {
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<String> {
break
}
}
} else {
} else if (DialUtils.isFormattingTag(lineTag.value)) {
openTags.add(lineTag.value)
}
}
@ -65,11 +85,14 @@ fun splitLines(message: String, perLineLimit: Int = 54): Array<String> {
}
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
}

View file

@ -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 "<img=4>",
"%chat" to "<img=3>",
"%hcim" to "<img=5>",
"%uim" to "<img=6>",
"%/trans" to "</trans>",
"%trans" to "<trans=128>",
"%/shad" to "</shad>",
"%shad" to "<shad>",
"%/str" to "</str>",
"%str" to "<str>",
"%/u" to "</u>",
"%u" to "<u>",
"%<gt><gt>" to "<img=2>"
)
private val COMMON_SHORTCUT_PATTERN = Regex("%ironman|%chat|%hcim|%uim|%/trans|%trans(?!=)|%/shad|%shad(?!=)|%/str|%str(?!=)|%/u|%u(?!=)|%<gt><gt>")
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) "<img=${rights - 1}>" else ""}$sender: ${baseColor}$message")
val formattedMessage = formatPlayerMessage(message, chatIcon)
return colorize("$bracketColor[${baseColor}G$bracketColor] ${if (chatIcon > 0) "<img=${chatIcon - 1}>" else ""}$sender: ${baseColor}$formattedMessage")
}
private fun formatPlayerMessage(message: String, chatIcon: Int): String {
var formatted = ANGLE_BRACKETS.replace(message) {
if (it.value == "<") "<lt>" else "<gt>"
}
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) "<trans=$transparency>" else it.value
}
formatted = COMMON_SHORTCUT_PATTERN.replace(formatted) {
COMMON_SHORTCUTS[it.value] ?: it.value
}
if (chatIcon == 1 || chatIcon == 2)
formatted = formatted.replace("%mod", "<img=0>")
if (chatIcon == 2)
formatted = formatted.replace("%admin", "<img=1>")
return formatted
}
}
}