summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategories.kt70
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategory.kt32
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugLogger.kt27
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugState.kt32
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt6
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt3
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt2
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategoriesTest.kt93
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugStateTest.kt45
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt10
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt17
11 files changed, 334 insertions, 3 deletions
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategories.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategories.kt
new file mode 100644
index 0000000..c3d7d95
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategories.kt
@@ -0,0 +1,70 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+/**
+ * Reads a debug switch written as text.
+ *
+ * Shared by both platforms so that `debug: velocity,protocol` in config.yml and
+ * `-Dlunaticchat.debug=velocity,protocol` on the proxy mean the same thing. A grammar owned by one
+ * side would drift from the other the first time a category was added.
+ */
+object DebugCategories {
+ /** What a debug switch asked for. */
+ data class Parsed(
+ val enabled: Boolean,
+ val categories: Set<DebugCategory>,
+ /** Names that match no category, reported to the operator rather than thrown. */
+ val unknown: List<String>,
+ ) {
+ /** The categories that actually log. Empty while [enabled] is false, whatever was listed. */
+ val active: Set<DebugCategory> get() = if (enabled) categories else emptySet()
+ }
+
+ /** The word that stands for every category at once, wherever a category may be named. */
+ const val ALL = "all"
+
+ private val allWords = setOf("true", "yes", "on", "y", ALL)
+ private val noneWords = setOf("false", "no", "off", "n", "none")
+
+ /**
+ * Reads a whole switch: a boolean spelling, or a comma-separated list of category keys.
+ *
+ * The boolean spellings match the ones config.yml already accepts elsewhere: `debug: yes`
+ * worked while Bukkit read the file as YAML 1.1, and reading it as a category name instead
+ * would silently switch debugging off for a file that had it on.
+ */
+ fun parse(raw: String): Parsed {
+ val trimmed = raw.trim()
+ val word = trimmed.lowercase()
+ return when {
+ trimmed.isEmpty() || word in noneWords -> Parsed(false, DebugCategory.entries.toSet(), emptyList())
+ word in allWords -> Parsed(true, DebugCategory.entries.toSet(), emptyList())
+ else -> resolve(trimmed.split(','))
+ }
+ }
+
+ /** Reads category keys that arrived already separated, as config.yml's `categories:` list does. */
+ fun resolve(names: Iterable<String>): Parsed {
+ val categories = LinkedHashSet<DebugCategory>()
+ val unknown = mutableListOf<String>()
+
+ names
+ .map { it.trim() }
+ .filter { it.isNotEmpty() }
+ .forEach { name ->
+ // [ALL] is honoured here as well as in [parse] and /lc debug: an operator who has
+ // read `debug: all` or `/lc debug all on` writes `categories: [all]` next, and
+ // reporting that as an unknown name would leave them with a switch that logs
+ // nothing while saying it is on.
+ if (name.equals(ALL, ignoreCase = true)) {
+ categories += DebugCategory.entries
+ return@forEach
+ }
+ val category = DebugCategory.fromKey(name)
+ if (category == null) unknown += name else categories += category
+ }
+
+ // A switch that named nothing this build knows is off rather than on-with-no-categories:
+ // reporting it as enabled would put "debug is on" in /lc status for a typo that logs nothing.
+ return Parsed(enabled = categories.isNotEmpty(), categories = categories, unknown = unknown)
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategory.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategory.kt
new file mode 100644
index 0000000..40cc0e8
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategory.kt
@@ -0,0 +1,32 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+/**
+ * The area of the plugin a debug line belongs to.
+ *
+ * Categories exist so that instrumenting the chat path - which runs once per message - does not
+ * make the log unusable for someone chasing a handshake. [key] is the spelling operators write in
+ * config.yml and on the Velocity command line; the enum name is never exposed to them.
+ */
+enum class DebugCategory(
+ val key: String,
+) {
+ CONFIG("config"),
+ CHAT("chat"),
+ CHANNEL("channel"),
+ CONVERSION("conversion"),
+ PROTOCOL("protocol"),
+ VELOCITY("velocity"),
+ STORAGE("storage"),
+ COMMAND("command"),
+ ;
+
+ companion object {
+ private val byKey = entries.associateBy { it.key }
+
+ /** Every [key], for the "known categories are ..." half of a message about a typo. */
+ val keyList: String = entries.joinToString(", ") { it.key }
+
+ /** The category [key] names, or null when no category is spelled that way. */
+ fun fromKey(key: String): DebugCategory? = byKey[key.trim().lowercase()]
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugLogger.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugLogger.kt
new file mode 100644
index 0000000..5f235f6
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugLogger.kt
@@ -0,0 +1,27 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+/**
+ * Writes a diagnostic line, but only for the categories that are switched on.
+ *
+ * [message] is a lambda rather than a String so that instrumenting a path taken once per chat
+ * message costs nothing while debugging is off: the interpolation never runs. The
+ * `if (debugMode) logger.info("... $word")` guards this replaced paid for their strings either way.
+ */
+interface DebugLogger {
+ fun isEnabled(category: DebugCategory): Boolean
+
+ fun log(
+ category: DebugCategory,
+ message: () -> String,
+ )
+
+ /** A logger for callers that have no debug state to consult, such as tests of unrelated code. */
+ object Disabled : DebugLogger {
+ override fun isEnabled(category: DebugCategory): Boolean = false
+
+ override fun log(
+ category: DebugCategory,
+ message: () -> String,
+ ) = Unit
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugState.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugState.kt
new file mode 100644
index 0000000..b4fe9db
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugState.kt
@@ -0,0 +1,32 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+/**
+ * The debug categories currently switched on, which a reload or `/lc debug` may replace while the
+ * server runs.
+ *
+ * Held apart from the configuration for the same reason the message formats are: taking this type
+ * says the value is live, while taking a configuration says it was frozen at startup. Every writer
+ * replaces the whole set under a lock, so a reader never sees a half-built one.
+ */
+class DebugState(
+ initial: Set<DebugCategory> = emptySet(),
+) {
+ @Volatile
+ var enabled: Set<DebugCategory> = initial.toSet()
+ private set
+
+ fun isEnabled(category: DebugCategory): Boolean = category in enabled
+
+ @Synchronized
+ fun replace(categories: Set<DebugCategory>) {
+ enabled = categories.toSet()
+ }
+
+ @Synchronized
+ fun set(
+ category: DebugCategory,
+ on: Boolean,
+ ) {
+ enabled = if (on) enabled + category else enabled - category
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt
index 6291553..9831b3c 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt
@@ -13,6 +13,12 @@ sealed class LunaticChatPermissionNode(
object Status : LunaticChatPermissionNode("lunaticchat.command.lc.status")
+ object Reload : LunaticChatPermissionNode("lunaticchat.command.lc.reload")
+
+ object Debug : LunaticChatPermissionNode("lunaticchat.command.lc.debug")
+
+ object Dump : LunaticChatPermissionNode("lunaticchat.command.lc.dump")
+
object Channel : LunaticChatPermissionNode("lunaticchat.command.lc.channel")
object ChannelCreate : LunaticChatPermissionNode("lunaticchat.command.lc.channel.create")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt
index 689a57c..82cff37 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt
@@ -93,6 +93,8 @@ sealed interface PluginMessage {
* @property targetName Target player name
* @property message Message content (romaji-converted if applicable)
* @property timestamp Message timestamp (milliseconds since epoch)
+ * @property senderWorld Name of the world the sender is in; empty when relayed by a
+ * pre-1.0.2 peer
*/
@Serializable
data class DirectMessageRelay(
@@ -104,6 +106,7 @@ sealed interface PluginMessage {
val targetName: String,
val message: String,
val timestamp: Long = System.currentTimeMillis(),
+ val senderWorld: String = "",
) : PluginMessage
/**
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt
index 6585c86..cdf17e7 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt
@@ -33,7 +33,7 @@ package dev.m1sk9.lunaticChat.engine.protocol
object ProtocolVersion {
const val MAJOR = 1
const val MINOR = 0
- const val PATCH = 1
+ const val PATCH = 2
/**
* Minimum MINOR version this build can interoperate with (same MAJOR).
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategoriesTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategoriesTest.kt
new file mode 100644
index 0000000..5931c60
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugCategoriesTest.kt
@@ -0,0 +1,93 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DebugCategoriesTest {
+ @Test
+ fun `a true spelling switches every category on`() {
+ listOf("true", "TRUE", "yes", "on", "all", " true ").forEach { raw ->
+ val parsed = DebugCategories.parse(raw)
+
+ assertTrue(parsed.enabled, raw)
+ assertEquals(DebugCategory.entries.toSet(), parsed.active, raw)
+ }
+ }
+
+ @Test
+ fun `a false spelling switches every category off`() {
+ listOf("false", "FALSE", "no", "off", "none", "", " ").forEach { raw ->
+ val parsed = DebugCategories.parse(raw)
+
+ assertFalse(parsed.enabled, raw)
+ assertEquals(emptySet(), parsed.active, raw)
+ }
+ }
+
+ @Test
+ fun `a comma separated list switches on exactly the categories it names`() {
+ val parsed = DebugCategories.parse("velocity,protocol")
+
+ assertTrue(parsed.enabled)
+ assertEquals(setOf(DebugCategory.VELOCITY, DebugCategory.PROTOCOL), parsed.active)
+ assertEquals(emptyList(), parsed.unknown)
+ }
+
+ @Test
+ fun `surrounding whitespace, casing and empty entries are ignored`() {
+ val parsed = DebugCategories.parse(" Velocity , , PROTOCOL ,")
+
+ assertEquals(setOf(DebugCategory.VELOCITY, DebugCategory.PROTOCOL), parsed.active)
+ assertEquals(emptyList(), parsed.unknown)
+ }
+
+ @Test
+ fun `an unknown name is reported without discarding the categories beside it`() {
+ val parsed = DebugCategories.parse("velocity,proxy")
+
+ assertEquals(setOf(DebugCategory.VELOCITY), parsed.active)
+ assertEquals(listOf("proxy"), parsed.unknown)
+ }
+
+ @Test
+ fun `a switch naming only unknown categories logs nothing`() {
+ val parsed = DebugCategories.parse("maybe")
+
+ assertEquals(emptySet(), parsed.active)
+ assertEquals(listOf("maybe"), parsed.unknown)
+ }
+
+ @Test
+ fun `resolve reads a list the way parse reads the same names spelled with commas`() {
+ assertEquals(
+ DebugCategories.parse("velocity,protocol"),
+ DebugCategories.resolve(listOf("velocity", "protocol")),
+ )
+ }
+
+ @Test
+ fun `all names every category wherever a category may be named`() {
+ // The same word works as `debug: all`, as `categories: [all]` and as `/lc debug all on`;
+ // reading it as a category name in only two of the three would log nothing for the third.
+ assertEquals(DebugCategory.entries.toSet(), DebugCategories.parse("all").active)
+ assertEquals(DebugCategory.entries.toSet(), DebugCategories.resolve(listOf("all")).active)
+ assertEquals(emptyList(), DebugCategories.resolve(listOf("ALL")).unknown)
+ }
+
+ @Test
+ fun `all beside a named category still names every category`() {
+ val parsed = DebugCategories.resolve(listOf("velocity", "all"))
+
+ assertEquals(DebugCategory.entries.toSet(), parsed.active)
+ assertEquals(emptyList(), parsed.unknown)
+ }
+
+ @Test
+ fun `every category is reachable by the key operators write`() {
+ DebugCategory.entries.forEach { category ->
+ assertEquals(category, DebugCategory.fromKey(category.key))
+ }
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugStateTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugStateTest.kt
new file mode 100644
index 0000000..ae1456d
--- /dev/null
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/debug/DebugStateTest.kt
@@ -0,0 +1,45 @@
+package dev.m1sk9.lunaticChat.engine.debug
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DebugStateTest {
+ @Test
+ fun `a state starts on the categories it was built with`() {
+ val state = DebugState(setOf(DebugCategory.VELOCITY))
+
+ assertTrue(state.isEnabled(DebugCategory.VELOCITY))
+ assertFalse(state.isEnabled(DebugCategory.CHAT))
+ }
+
+ @Test
+ fun `replace drops the categories it does not list`() {
+ val state = DebugState(setOf(DebugCategory.VELOCITY, DebugCategory.CHAT))
+
+ state.replace(setOf(DebugCategory.PROTOCOL))
+
+ assertEquals(setOf(DebugCategory.PROTOCOL), state.enabled)
+ }
+
+ @Test
+ fun `set switches one category without touching the others`() {
+ val state = DebugState(setOf(DebugCategory.VELOCITY))
+
+ state.set(DebugCategory.CHAT, on = true)
+ state.set(DebugCategory.VELOCITY, on = false)
+
+ assertEquals(setOf(DebugCategory.CHAT), state.enabled)
+ }
+
+ @Test
+ fun `a state built from a set is unaffected by later changes to it`() {
+ val categories = mutableSetOf(DebugCategory.VELOCITY)
+ val state = DebugState(categories)
+
+ categories += DebugCategory.CHAT
+
+ assertEquals(setOf(DebugCategory.VELOCITY), state.enabled)
+ }
+}
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt
index d6d74f7..e5e43cb 100644
--- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNodeTest.kt
@@ -12,6 +12,9 @@ class LunaticChatPermissionNodeTest {
LunaticChatPermissionNode.Reply,
LunaticChatPermissionNode.Settings,
LunaticChatPermissionNode.Status,
+ LunaticChatPermissionNode.Reload,
+ LunaticChatPermissionNode.Debug,
+ LunaticChatPermissionNode.Dump,
LunaticChatPermissionNode.Channel,
LunaticChatPermissionNode.ChannelCreate,
LunaticChatPermissionNode.ChannelList,
@@ -44,8 +47,8 @@ class LunaticChatPermissionNodeTest {
}
@Test
- fun `should have 24 permission nodes`() {
- assertEquals(24, allNodes.size)
+ fun `should have 27 permission nodes`() {
+ assertEquals(27, allNodes.size)
}
@Test
@@ -54,6 +57,9 @@ class LunaticChatPermissionNodeTest {
assertEquals("lunaticchat.command.tell", LunaticChatPermissionNode.Tell.permissionNode)
assertEquals("lunaticchat.command.reply", LunaticChatPermissionNode.Reply.permissionNode)
assertEquals("lunaticchat.command.lc.settings", LunaticChatPermissionNode.Settings.permissionNode)
+ assertEquals("lunaticchat.command.lc.reload", LunaticChatPermissionNode.Reload.permissionNode)
+ assertEquals("lunaticchat.command.lc.debug", LunaticChatPermissionNode.Debug.permissionNode)
+ assertEquals("lunaticchat.command.lc.dump", LunaticChatPermissionNode.Dump.permissionNode)
assertEquals("lunaticchat.command.lc.channel.create", LunaticChatPermissionNode.ChannelCreate.permissionNode)
assertEquals("lunaticchat.command.lcv.status", LunaticChatPermissionNode.VelocityStatus.permissionNode)
assertEquals("lunaticchat.spy", LunaticChatPermissionNode.Spy.permissionNode)
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt
index e70edda..095ecd1 100644
--- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt
@@ -42,6 +42,10 @@ class ProtocolBackwardCompatibilityTest {
const val PRESENCE_SNAPSHOT_V1_0_1 =
"""{"players":[{"playerName":"Alice","serverName":"lobby"},{"playerName":"Bob","serverName":"survival"}],"timestamp":5000}"""
+
+ // Protocol 1.0.2 snapshots — NEVER MODIFY after commit
+ const val DIRECT_MESSAGE_V1_0_2 =
+ """{"messageId":"dm-321","sourceServerName":"survival","senderId":"00000007-0000-0000-0000-000000000000","senderName":"Sender","targetServerName":"lobby","targetName":"Recipient","message":"Hello!","timestamp":6000,"senderWorld":"world_nether"}"""
}
private fun buildRawMessage(
@@ -162,6 +166,19 @@ class ProtocolBackwardCompatibilityTest {
assertEquals("Recipient", decoded.targetName)
assertEquals("Hello!", decoded.message)
assertEquals(4000L, decoded.timestamp)
+ assertEquals("", decoded.senderWorld)
+ }
+
+ @Test
+ fun `current codec can decode protocol 1_0_2 DirectMessageRelay`() {
+ val data = buildRawMessage("direct_message", DIRECT_MESSAGE_V1_0_2)
+ val decoded = PluginMessageCodec.decode(data)
+
+ assertIs<PluginMessage.DirectMessageRelay>(decoded)
+ assertEquals("dm-321", decoded.messageId)
+ assertEquals("Sender", decoded.senderName)
+ assertEquals("Hello!", decoded.message)
+ assertEquals("world_nether", decoded.senderWorld)
}
@Test