summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-03 15:12:42 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 01:15:29 +0900
commitdaf9060561656f468a0ae366e5695f5eb74f48f7 (patch)
tree4e096081884cc5039edb198b77d2bd0f70e5751e
parentfd56ebaa1dc2cfa8f834edb06395e8fc02375d1d (diff)
downloadLunaticChat-daf9060561656f468a0ae366e5695f5eb74f48f7.tar.gz
LunaticChat-daf9060561656f468a0ae366e5695f5eb74f48f7.tar.bz2
LunaticChat-daf9060561656f468a0ae366e5695f5eb74f48f7.zip
refactor: make the direct message failure reason an enum
DirectMessageError.reason was a String backed by two constants, so the receiving side matched one case and let everything else fall through to "the target is offline". Adding a third reason on the proxy would have shipped it to Paper servers that silently reported the wrong thing - the one string-keyed dispatch sitting next to a protocol layer whose messages are otherwise a sealed hierarchy with exhaustiveness checking. As an enum, the reader must decide what to show for each case, and CrossServerDirectMessageManager's when no longer needs an else. The wire format is unchanged: kotlinx serializes an enum as its name, so the existing snapshots still decode. What did need care is the reverse direction - a reason from a newer proxy would now fail to parse, where the String version degraded. The property has a default and the codec enables coerceInputValues, so an unknown reason lands on TARGET_OFFLINE, exactly the old else branch. There is a compatibility test for that case. Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt19
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodec.kt8
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt2
-rw-r--r--engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt16
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt2
-rw-r--r--platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt2
6 files changed, 39 insertions, 10 deletions
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 1ba5c25..689a57c 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
@@ -113,7 +113,9 @@ sealed interface PluginMessage {
* @property senderId Sender UUID as string (used to locate the sender to notify)
* @property targetName Target player name that was requested
* @property targetServerName Target server name that was requested
- * @property reason Failure reason: [Reason.TARGET_OFFLINE] or [Reason.SERVER_NOT_FOUND]
+ * @property reason Why delivery failed. Defaults to [Reason.TARGET_OFFLINE] so that a reason
+ * added by a newer peer degrades to the generic failure rather than failing to decode -
+ * [PluginMessageCodec] enables coerceInputValues for exactly this.
*/
@Serializable
data class DirectMessageError(
@@ -121,11 +123,18 @@ sealed interface PluginMessage {
val senderId: String,
val targetName: String,
val targetServerName: String,
- val reason: String,
+ val reason: Reason = Reason.TARGET_OFFLINE,
) : PluginMessage {
- object Reason {
- const val TARGET_OFFLINE = "TARGET_OFFLINE"
- const val SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
+ /**
+ * Why a cross-server direct message could not be delivered.
+ *
+ * An enum rather than string constants so that adding a case forces every reader to
+ * decide what to show for it, instead of silently reporting the existing default.
+ */
+ @Serializable
+ enum class Reason {
+ TARGET_OFFLINE,
+ SERVER_NOT_FOUND,
}
}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodec.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodec.kt
index 14d1f82..f3b52c6 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodec.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodec.kt
@@ -13,7 +13,13 @@ import java.io.DataOutputStream
* Format: [subChannel: UTF][messageJson: UTF]
*/
object PluginMessageCodec {
- private val json = Json { ignoreUnknownKeys = true }
+ private val json =
+ Json {
+ ignoreUnknownKeys = true
+ // An enum value this build does not know falls back to the property's default rather
+ // than failing the whole message, matching how unknown fields are treated.
+ coerceInputValues = true
+ }
/**
* Sub-channel names
diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt
index 0eb56c1..e1c65ba 100644
--- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt
+++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt
@@ -291,7 +291,7 @@ class PluginMessageCodecTest {
PluginMessage.StatusResponse("1.0.0", "1.0.0", true),
PluginMessage.GlobalChatMessage("id", "srv", "pid", "name", "msg", 0L),
PluginMessage.DirectMessageRelay("id", "src", "sid", "sname", "tsrv", "tname", "msg", 0L),
- PluginMessage.DirectMessageError("id", "sid", "tname", "tsrv", "TARGET_OFFLINE"),
+ PluginMessage.DirectMessageError("id", "sid", "tname", "tsrv", PluginMessage.DirectMessageError.Reason.TARGET_OFFLINE),
PluginMessage.PresenceSnapshot(listOf(PresenceEntry("p", "s")), 0L),
PluginMessage.PresenceRequest,
)
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 c9bc6c0..e70edda 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
@@ -174,7 +174,7 @@ class ProtocolBackwardCompatibilityTest {
assertEquals("00000005-0000-0000-0000-000000000000", decoded.senderId)
assertEquals("Ghost", decoded.targetName)
assertEquals("lobby", decoded.targetServerName)
- assertEquals("TARGET_OFFLINE", decoded.reason)
+ assertEquals(PluginMessage.DirectMessageError.Reason.TARGET_OFFLINE, decoded.reason)
}
@Test
@@ -196,4 +196,18 @@ class ProtocolBackwardCompatibilityTest {
assertIs<PluginMessage.PresenceRequest>(decoded)
}
+
+ @Test
+ fun `a failure reason this build does not know decodes to the generic failure`() {
+ val fromNewerPeer =
+ """{"messageId":"dm-789","senderId":"00000006-0000-0000-0000-000000000000",""" +
+ """"targetName":"Ghost","targetServerName":"lobby","reason":"RATE_LIMITED"}"""
+
+ val decoded = PluginMessageCodec.decode(buildRawMessage("direct_message_error", fromNewerPeer))
+
+ // A proxy that learns a new reason must not make this build drop the whole message.
+ assertIs<PluginMessage.DirectMessageError>(decoded)
+ assertEquals("dm-789", decoded.messageId)
+ assertEquals(PluginMessage.DirectMessageError.Reason.TARGET_OFFLINE, decoded.reason)
+ }
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt
index ccd8d40..8328ad8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt
@@ -132,7 +132,7 @@ class CrossServerDirectMessageManager(
val messageKey =
when (error.reason) {
PluginMessage.DirectMessageError.Reason.SERVER_NOT_FOUND -> "directMessage.remoteServerNotFound"
- else -> "directMessage.remoteTargetOffline"
+ PluginMessage.DirectMessageError.Reason.TARGET_OFFLINE -> "directMessage.remoteTargetOffline"
}
val text =
languageManager.getMessage(
diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt
index f9cc52d..70ca525 100644
--- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt
+++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/CrossServerDirectMessageRelay.kt
@@ -78,7 +78,7 @@ class CrossServerDirectMessageRelay(
private fun sendError(
sourceServer: RegisteredServer,
message: PluginMessage.DirectMessageRelay,
- reason: String,
+ reason: PluginMessage.DirectMessageError.Reason,
) {
val error =
PluginMessage.DirectMessageError(