diff --git a/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java b/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java index ad124cde7..4401326b8 100644 --- a/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java +++ b/Server/src/main/java/core/game/content/dialogue/DialoguePlugin.java @@ -353,7 +353,7 @@ public abstract class DialoguePlugin implements Plugin { * @param msg the message for the NPC to say */ public Component npcl(FacialExpression expr, String msg){ - return npc(expr, splitLines(msg)); + return npc(expr, splitLines(msg, 54)); } /** @@ -362,7 +362,7 @@ public abstract class DialoguePlugin implements Plugin { * @param msg the message for the player to say */ public Component playerl(FacialExpression expr, String msg){ - return player(expr, splitLines(msg)); + return player(expr, splitLines(msg, 54)); } public void showTopics(Topic... topics) { diff --git a/Server/src/main/kotlin/api/DialUtils.kt b/Server/src/main/kotlin/api/DialUtils.kt index e2c6dfda8..d5361bd59 100644 --- a/Server/src/main/kotlin/api/DialUtils.kt +++ b/Server/src/main/kotlin/api/DialUtils.kt @@ -1,50 +1,46 @@ package api -import rs09.game.system.SystemLogger import java.util.* -import kotlin.system.exitProcess - -//Number of chars per line. Line is split at the nearest whole word that doesn't exceed this length. This should remain constant once a perfect value is found -const val LINE_SIZE = 52.0 +import kotlin.math.ceil /** * Automatically split a single continuous string into multiple comma-separated lines. * Should this not work out for any reason, you should fallback to standard npc and player methods for dialogue. */ -fun splitLines(message: String): Array{ - val chars = message.split("").toTypedArray().size.toDouble() - val words = message.split(" ").toTypedArray() - val lines = Math.ceil(chars / LINE_SIZE).toInt() +fun splitLines(message: String, perLineLimit: Int = 54) : Array { + val lines = Array(ceil(message.length / perLineLimit.toFloat()).toInt()) {""} - if(lines > 4){ - SystemLogger.logErr("INVALID NUM LINES. NUM CHARS: $chars") + //short circuit when possible because it's cheaper. + if (lines.size == 1) { + lines[0] = message + return lines } - val messages = ArrayList() - + val tokenQueue = LinkedList(message.split(" ")) var index = 0 + val line = StringBuilder() + var accumulator = 0 - for (msg in 0 until lines) { - var length = 0 - val line = StringBuilder() - while (length <= LINE_SIZE && !(index == words.size)) { - var currentWord = "" - try { - currentWord = words[index] + if (length + words[index].length + 1 <= LINE_SIZE) " " else "" - } catch (e: Exception){ - SystemLogger.logWarn("INDEX: $index WORDSIZE: ${words.size}") - for(word in words.indices){ - SystemLogger.logErr("Word $word: ${words[word]}") - } - exitProcess(0) - } - length += currentWord.length - if (length <= LINE_SIZE) { - line.append(currentWord) - ++index - } - } - messages.add(line.toString()) + fun pushLine() { + if (line.isEmpty()) return + lines[index++] = line.toString() + line.clear() + accumulator = 0 } - return messages.toTypedArray() + while (!tokenQueue.isEmpty()) { + val shouldSpace = line.isNotEmpty() + accumulator += tokenQueue.peek().length + + if (shouldSpace) accumulator += 1 + if (accumulator > perLineLimit) { + pushLine() + continue + } + + if (shouldSpace) line.append(" ") + line.append(tokenQueue.pop()) + } + + pushLine() + return lines } \ No newline at end of file diff --git a/Server/src/test/kotlin/APITests.kt b/Server/src/test/kotlin/APITests.kt index 66bd618ee..991bccc38 100644 --- a/Server/src/test/kotlin/APITests.kt +++ b/Server/src/test/kotlin/APITests.kt @@ -1,18 +1,14 @@ import api.IfaceSettingsBuilder -import core.game.node.entity.player.Player -import core.game.node.entity.player.info.PlayerDetails +import api.splitLines import core.game.node.entity.skill.slayer.Master import core.game.node.entity.skill.slayer.Tasks import org.json.simple.JSONObject import org.json.simple.parser.JSONParser -import org.junit.Assert import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import rs09.game.node.entity.skill.slayer.SlayerFlags import rs09.game.node.entity.skill.slayer.SlayerManager -import rs09.game.system.SystemLogger -object APITests { +class APITests { val testPlayer = TestUtils.getMockPlayer("test") val testPlayer2 = TestUtils.getMockPlayer("test2") @@ -142,4 +138,29 @@ object APITests { Assertions.assertNotEquals(0, manager.flags.getPoints()) Assertions.assertEquals(true, manager.flags.isHelmUnlocked()) } + + @Test fun lineSplitShouldSplitAtLimitAndPreserveAllWords() { + var testCase = "The monks are running a ship from Port Sarim to Entrana, I hear too. Now leave me alone yer elephant!" + var expectedLine1 = "The monks are running a ship from Port Sarim to" + var expectedLine2 = "Entrana, I hear too. Now leave me alone yer elephant!" + var lines = splitLines(testCase, 54) + Assertions.assertEquals(expectedLine1, lines.getOrNull(0) ?: "") + Assertions.assertEquals(expectedLine2, lines.getOrNull(1) ?: "") + Assertions.assertEquals(2, lines.size) + + testCase = "Dramenwood staffs are crafted from branches of the Dramen tree, so they are. I hear there's a Dramen tree over on the island of Entrana in a cave." + expectedLine1 = "Dramenwood staffs are crafted from branches of the" + expectedLine2 = "Dramen tree, so they are. I hear there's a Dramen tree" + var expectedLine3 = "over on the island of Entrana in a cave." + lines = splitLines(testCase, 54) + Assertions.assertEquals(expectedLine1, lines.getOrNull(0) ?: "") + Assertions.assertEquals(expectedLine2, lines.getOrNull(1) ?: "") + Assertions.assertEquals(expectedLine3, lines.getOrNull(2) ?: "") + Assertions.assertEquals(3, lines.size) + + testCase = "This should be one line." + lines = splitLines(testCase) + Assertions.assertEquals(testCase, lines[0]) + Assertions.assertEquals(1, lines.size) + } } \ No newline at end of file