summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-05 03:32:33 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 03:32:33 +0900
commite25bfe0e39771ceac287f93a235aa5954a154236 (patch)
tree229bb10c921745cb9d011bdb7a625faf19fb71c1
parent7ed4bbbcf375c4f136a3b90bb6105c278901f654 (diff)
downloadLunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.tar.gz
LunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.tar.bz2
LunaticChat-e25bfe0e39771ceac287f93a235aa5954a154236.zip
refactor: drop what no longer carries its weight
- getDirectMessageSpyPlayers had no callers left; handing out a copy of the spy map is the shape notifySpies was introduced to replace. - pluginScope was widened to public for "commands that must not block the tick thread", but commands take the delivery queue and only LunaticChat reads it. - getPlayerChannels returned a Result that cannot fail, so three callers carried unreachable error paths and channel.status.error could never be shown. - The cross-server managers were gated on velocityIntegration.enabled as well as on a manager that is non-null only when it is enabled, letting the two conditions disagree. - LenientBoolean's non-YamlInput fallback was observationally identical to the cast failing, since both land in ConfigManager's catch-all. - sendCrossServerMessage caught its own failures underneath the delivery queue, which already reports them without stopping the sender's later messages. The second boundary is what forced a CancellationException clause here. - handleOutgoingCrossServerMessage still recorded the reply target after the commands took that over, so remote targets were recorded twice - re-inserting entries clearPlayer had swept, which is the bug the local path was fixed for. - The reason delivery is queued was written out in both command constructors and twice more in KDoc; it now lives where the queueing happens. Spy notification also defers its notice lookup and member set until a spy is actually online, which is not the normal case. Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt11
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt12
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt7
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt10
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt19
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManager.kt60
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt4
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt33
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt20
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt9
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt14
17 files changed, 102 insertions, 134 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index 3f9cb76..85d87cd 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
@@ -41,11 +41,14 @@ class LunaticChat :
private lateinit var configuration: LunaticChatConfiguration
private lateinit var serviceInitializer: ServiceInitializer
- // Read by commands that must not block the tick thread.
- lateinit var pluginScope: PluginCoroutineScope
- private set
+ private lateinit var pluginScope: PluginCoroutineScope
- /** Serializes each player's outgoing messages so they arrive in the order they were sent. */
+ /**
+ * Serializes each player's outgoing messages so they arrive in the order they were sent.
+ *
+ * Commands submit delivery here rather than running it inline: romaji conversion can reach the
+ * Google IME API, and a command executor runs on the tick thread.
+ */
lateinit var deliveryQueue: PerPlayerWorkQueue
private set
private var updateChecker: UpdateChecker? = null
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt
index 6ce2672..72a016d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt
@@ -176,13 +176,7 @@ class ChannelMembershipManager(
// Check if player has reached max membership limit (0 means unlimited)
if (config.maxMembershipPerPlayer > 0) {
- val playerChannelCount =
- getPlayerChannels(playerId)
- .getOrElse {
- return Result.failure(
- ChannelRuntimeException("Failed to get player channels for $playerId", it),
- )
- }.size
+ val playerChannelCount = getPlayerChannels(playerId).size
if (playerChannelCount >= config.maxMembershipPerPlayer) {
return Result.failure(
@@ -333,7 +327,7 @@ class ChannelMembershipManager(
* Gets all channels where the player is a member.
*
* @param playerId The UUID of the player.
- * @return Result containing a list of channel IDs where the player is a member.
+ * @return The channel IDs where the player is a member.
*/
- fun getPlayerChannels(playerId: UUID): Result<List<String>> = Result.success(channelManager.channelIdsOf(playerId))
+ fun getPlayerChannels(playerId: UUID): List<String> = channelManager.channelIdsOf(playerId)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
index 4da0c1a..0155a33 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
@@ -41,10 +41,11 @@ class ChannelMessageHandler(
player.playMessageSendNotification()
}
- // Send to spy players (exclude sender and channel members)
- val memberIds = context.members.map { it.playerId }.toSet()
+ // Send to spy players (exclude sender and channel members). The member set is built lazily
+ // because notifySpies only consults exclude when a spy is actually online.
+ val memberIds by lazy { context.members.mapTo(HashSet()) { it.playerId } }
SpyPermissionManager.notifySpies(
- noticeText = languageManager.getMessage("general.spyMessage"),
+ noticeText = { languageManager.getMessage("general.spyMessage") },
exclude = { it.uniqueId == playerId || it.uniqueId in memberIds },
) { formattedMessage }
context.members.forEach { member ->
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
index 2033661..7eb077a 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
@@ -156,8 +156,10 @@ class DirectMessageHandler(
/**
* Handles the sender-side display of an outgoing cross-server direct message.
- * Applies romaji conversion, shows the message to the sender, notifies spies,
- * and records the remote reply target.
+ * Applies romaji conversion, shows the message to the sender and notifies spies.
+ *
+ * The reply target is recorded by the caller via [recordRemoteRecipient] before the delivery is
+ * queued, for the same reason as [sendDirectMessage]: /reply reads it on the command thread.
*
* @return the message body to relay (romaji-converted if applicable), since the
* receiving server has no access to the sender's settings.
@@ -184,7 +186,6 @@ class DirectMessageHandler(
?.playMessageSendNotification()
}
- recordRemoteRecipient(sender, targetName, targetServerName)
return displayMessage
}
@@ -230,7 +231,7 @@ class DirectMessageHandler(
rawMessage: String,
) {
SpyPermissionManager.notifySpies(
- noticeText = languageManager.getMessage("general.spyMessage"),
+ noticeText = { languageManager.getMessage("general.spyMessage") },
exclude = { it.name == senderName || it.name == recipientName },
) {
formatMessage(format, senderName, recipientName, rawMessage, replyTo = senderName)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
index df5b2a9..e999a60 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt
@@ -31,9 +31,6 @@ class ReplyCommand(
private val dmHandler: DirectMessageHandler,
override val languageManager: LanguageManager,
private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null,
- // Delivery is queued rather than run inline: romaji conversion can reach the Google IME API,
- // and a command executor runs on the tick thread. Queueing per sender keeps their messages in
- // the order they typed them.
private val deliveryQueue: PerPlayerWorkQueue = plugin.deliveryQueue,
) : LunaticCommand(plugin) {
override val description: String
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
index 5e6e2c7..82c1eef 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt
@@ -38,9 +38,6 @@ class TellCommand(
private val crossServerDirectMessageManager: CrossServerDirectMessageManager? = null,
private val remotePlayerRegistry: RemotePlayerRegistry? = null,
private val localServerName: String = "",
- // Delivery is queued rather than run inline: romaji conversion can reach the Google IME API,
- // and a command executor runs on the tick thread. Queueing per sender keeps their messages in
- // the order they typed them.
private val deliveryQueue: PerPlayerWorkQueue = plugin.deliveryQueue,
) : LunaticCommand(plugin) {
override val description: String
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
index 78871b7..ae2ebfe 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
@@ -49,10 +49,7 @@ class ChannelStatusCommand(
val activeChannel = activeChannelId?.let { channelManager.getChannel(it).getOrNull() }
// Get all player's channels
- val playerChannelIds =
- membershipManager.getPlayerChannels(sender.uniqueId).getOrElse {
- return fail("channel.status.error")
- }
+ val playerChannelIds = membershipManager.getPlayerChannels(sender.uniqueId)
// Display header
sender.sendMessage(
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
index 2f71b7d..c558e31 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
@@ -38,8 +38,7 @@ class ChannelSwitchCommand(
// Tab completion: suggest channels the player is a member of
val sender = ctx.source.executor
if (sender is org.bukkit.entity.Player) {
- val playerChannels = membershipManager.getPlayerChannels(sender.uniqueId).getOrNull() ?: emptyList()
- playerChannels.forEach { channelId ->
+ membershipManager.getPlayerChannels(sender.uniqueId).forEach { channelId ->
builder.suggest(channelId)
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
index 2ce24b6..e01942d 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
@@ -22,6 +22,10 @@ import java.util.logging.Logger
class ConfigManager(
private val logger: Logger,
) {
+ private companion object {
+ const val UNREADABLE = "config.yml could not be read"
+ }
+
private val yaml =
Yaml(
configuration =
@@ -44,7 +48,7 @@ class ConfigManager(
// Editors that write a UTF-8 BOM would otherwise leave it on the first key, which
// strictMode = false then drops as an unknown setting without a word.
yaml.parseToYamlNode(contents.removePrefix("\uFEFF"))
- } catch (e: EmptyYamlDocumentException) {
+ } catch (_: EmptyYamlDocumentException) {
// A file that only holds comments is a valid way of saying "use the defaults", so it
// is not reported as a failure the operator has to act on.
return LunaticChatConfiguration()
@@ -67,13 +71,13 @@ class ConfigManager(
val setting = e.path.settingKeys()
val remaining =
document.without(setting)
- ?: return allDefaults("config.yml could not be read", e)
+ ?: return allDefaults(UNREADABLE, e)
logger.warning("${setting.joinToString(".")} in config.yml fell back to its default: ${e.message}")
document = remaining
} catch (e: Exception) {
// A serializer can fail without kaml turning it into a YamlException, and there is
// no path to prune a single setting by without one.
- return allDefaults("config.yml could not be read", e)
+ return allDefaults(UNREADABLE, e)
}
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt
index 3c93fb8..72c1395 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LenientBoolean.kt
@@ -4,7 +4,6 @@ import com.charleskorn.kaml.YamlException
import com.charleskorn.kaml.YamlInput
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
-import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
@@ -31,18 +30,15 @@ object LenientBooleanSerializer : KSerializer<Boolean> {
override val descriptor = PrimitiveSerialDescriptor("LenientBoolean", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Boolean {
- // Captured before decoding: ConfigManager prunes the offending setting by path, and only a
- // YamlException carries one. A plain SerializationException would escape it and cost the
- // operator the whole file.
- val path = (decoder as? YamlInput)?.node?.path
+ // Captured before decoding: ConfigManager prunes the offending setting by the path its
+ // exception carries, and only a YamlException carries one. Throwing without a path would
+ // cost the operator every other setting in the file.
+ val path = (decoder as YamlInput).node.path
val raw = decoder.decodeString()
return when (raw.lowercase()) {
in trueWords -> true
in falseWords -> false
- else -> {
- val reason = "expected true/false, yes/no or on/off but found '$raw'"
- throw path?.let { YamlException(reason, it) } ?: SerializationException(reason)
- }
+ else -> throw YamlException("expected true/false, yes/no or on/off but found '$raw'", path)
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt
index 2e50081..20dd819 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt
@@ -20,30 +20,29 @@ object SpyPermissionManager : Listener {
private val directMessageSpyPlayers: ConcurrentHashMap<UUID, Player> = ConcurrentHashMap()
/**
- * Gets all players with spy permission as a map of UUID to Player.
- */
- fun getDirectMessageSpyPlayers(): Map<UUID, Player> = directMessageSpyPlayers.toMap()
-
- /**
* Sends a copy of a message to every online spy that [exclude] does not reject.
*
- * [message] is only invoked when someone will actually read the result, and the "you are
- * seeing this because you have spy permission" hover is built once for the whole audience
- * rather than per recipient. Spies are rare, so both matter on the message path.
+ * [noticeText], [exclude] and [message] are only invoked when someone will actually read the
+ * result, and the "you are seeing this because you have spy permission" hover is built once for
+ * the whole audience rather than per recipient. Spies are rare, so all of that matters on the
+ * message path - callers otherwise pay a translation lookup, and channel chat an O(members) set,
+ * for an audience that is usually empty.
*
* @param noticeText Text shown on hover, explaining why the reader is seeing the message
* @param exclude Rejects players who are party to the message already
* @param message Builds the message body
*/
fun notifySpies(
- noticeText: String,
+ noticeText: () -> String,
exclude: (Player) -> Boolean,
message: () -> Component,
) {
+ if (directMessageSpyPlayers.isEmpty()) return
+
val spies = directMessageSpyPlayers.values.filter { it.isOnline && !exclude(it) }
if (spies.isEmpty()) return
- val withNotice = message().hoverEvent(HoverEvent.showText(Component.text(noticeText)))
+ val withNotice = message().hoverEvent(HoverEvent.showText(Component.text(noticeText())))
spies.forEach { it.sendMessage(withNotice) }
}
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 8328ad8..574cb30 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
@@ -7,7 +7,6 @@ import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
-import kotlinx.coroutines.CancellationException
import org.bukkit.entity.Player
import org.bukkit.plugin.Plugin
import java.util.UUID
@@ -40,6 +39,11 @@ class CrossServerDirectMessageManager(
* goes through may wait on the Google IME API. The reply target is recorded by the command
* before the work is queued; the sender-side display and the spy notification are handled by
* [DirectMessageHandler], and the (possibly romaji-converted) body is what gets relayed.
+ *
+ * Failures are deliberately not caught here. The delivery queue is the error boundary for
+ * queued work and already reports them without stopping the sender's later messages; a second
+ * boundary underneath it only obscured where failures on this path are handled - and made an
+ * ordinary cancellation at shutdown look like a delivery failure.
*/
suspend fun sendCrossServerMessage(
sender: Player,
@@ -47,40 +51,32 @@ class CrossServerDirectMessageManager(
targetServerName: String,
message: String,
) {
- try {
- val messageId = UUID.randomUUID().toString()
- processedMessages.markProcessed(messageId)
+ val messageId = UUID.randomUUID().toString()
+ processedMessages.markProcessed(messageId)
- val relayedMessage =
- directMessageHandler.handleOutgoingCrossServerMessage(
- sender = sender,
- targetName = targetName,
- targetServerName = targetServerName,
- message = message,
- )
+ val relayedMessage =
+ directMessageHandler.handleOutgoingCrossServerMessage(
+ sender = sender,
+ targetName = targetName,
+ targetServerName = targetServerName,
+ message = message,
+ )
- val relay =
- PluginMessage.DirectMessageRelay(
- messageId = messageId,
- sourceServerName = configuration.features.velocityIntegration.serverName,
- senderId = sender.uniqueId.toString(),
- senderName = sender.name,
- targetServerName = targetServerName,
- targetName = targetName,
- message = relayedMessage,
- )
+ val relay =
+ PluginMessage.DirectMessageRelay(
+ messageId = messageId,
+ sourceServerName = configuration.features.velocityIntegration.serverName,
+ senderId = sender.uniqueId.toString(),
+ senderName = sender.name,
+ targetServerName = targetServerName,
+ targetName = targetName,
+ message = relayedMessage,
+ )
- sender.sendPluginMessage(plugin, PluginMessageChannel.ID, PluginMessageCodec.encode(relay))
- logger.fine {
- "Sent direct message to Velocity: messageId=$messageId, " +
- "target=$targetName@$targetServerName"
- }
- } catch (e: CancellationException) {
- // Shutdown cancelling the delivery queue is not a delivery failure, and reporting it as
- // SEVERE while carrying on past the cancellation would be wrong twice over.
- throw e
- } catch (e: Exception) {
- logger.log(Level.SEVERE, "Failed to send cross-server direct message", e)
+ sender.sendPluginMessage(plugin, PluginMessageChannel.ID, PluginMessageCodec.encode(relay))
+ logger.fine {
+ "Sent direct message to Velocity: messageId=$messageId, " +
+ "target=$targetName@$targetServerName"
}
}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt
index 02c4d1a..f0c41c9 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt
@@ -382,7 +382,7 @@ class ChannelMembershipManagerTest {
channelManager.setPlayerChannel(playerId, null)
membership.joinChannel(playerId, "ch2")
- val channels = membership.getPlayerChannels(playerId).getOrThrow()
+ val channels = membership.getPlayerChannels(playerId)
assertEquals(2, channels.size)
assertTrue(channels.contains("ch1"))
assertTrue(channels.contains("ch2"))
@@ -495,6 +495,6 @@ class ChannelMembershipManagerTest {
channelManager.deleteChannel("drop-ch", ownerId)
- assertEquals(listOf("keep-ch"), membership.getPlayerChannels(playerId).getOrThrow())
+ assertEquals(listOf("keep-ch"), membership.getPlayerChannels(playerId))
}
}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
index a2ba325..1edef92 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt
@@ -24,8 +24,6 @@ import kotlin.test.assertTrue
* Validates message handling with dependency injection and conversion features.
*/
class DirectMessageHandlerTest {
- private fun <T> sync(block: suspend () -> T): T = runBlocking { block() }
-
private fun createHandler(
configuration: dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration? = null,
settingsManager: PlayerSettingsManager? = null,
@@ -43,7 +41,7 @@ class DirectMessageHandlerTest {
val sender = TestUtils.createMockPlayer()
val recipient = TestUtils.createMockPlayer()
- val result = sync { handler.sendDirectMessage(sender, recipient, "Test message") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test message") }
assertTrue(result)
}
@@ -65,7 +63,7 @@ class DirectMessageHandlerTest {
val sender = TestUtils.createMockPlayer(name = "Alice")
val recipient = TestUtils.createMockPlayer(name = "Bob")
- val result = sync { handler.sendDirectMessage(sender, recipient, "Test") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") }
assertTrue(result)
}
@@ -84,7 +82,7 @@ class DirectMessageHandlerTest {
val sender = TestUtils.createMockPlayer()
val recipient = TestUtils.createMockPlayer()
- val result = sync { handler.sendDirectMessage(sender, recipient, "konnichiwa") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "konnichiwa") }
assertTrue(result)
}
@@ -110,7 +108,7 @@ class DirectMessageHandlerTest {
val recipient = TestUtils.createMockPlayer()
// Should not throw exception and should complete quickly (within timeout)
- val result = sync { handler.sendDirectMessage(sender, recipient, "konnichiwa") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "konnichiwa") }
assertTrue(result)
}
@@ -125,7 +123,7 @@ class DirectMessageHandlerTest {
// This validates Issue #1 refactoring - ConfigManager DI
val sender = TestUtils.createMockPlayer()
val recipient = TestUtils.createMockPlayer()
- val result = sync { handler.sendDirectMessage(sender, recipient, "Test") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") }
assertTrue(result)
}
@@ -139,15 +137,24 @@ class DirectMessageHandlerTest {
}
@Test
- fun `handleOutgoingCrossServerMessage records a remote reply target and returns the body`() {
+ fun `handleOutgoingCrossServerMessage returns the body to relay`() {
+ val handler = createHandler()
+ val sender = TestUtils.createMockPlayer(name = "Alice")
+
+ val relayed = runBlocking { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
+
+ assertEquals("hi", relayed)
+ }
+
+ @Test
+ fun `recordRemoteRecipient makes a remote player the reply target`() {
val handler = createHandler()
val registry = RemotePlayerRegistry(localServerName = "lobby")
registry.replaceAll(listOf(PresenceEntry("Bob", "survival")))
handler.remotePlayerRegistry = registry
val sender = TestUtils.createMockPlayer(name = "Alice")
- val relayed = sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
- assertEquals("hi", relayed)
+ handler.recordRemoteRecipient(sender, "Bob", "survival")
val target = handler.getReplyTarget(sender)
assertIs<ReplyTarget.Remote>(target)
@@ -177,7 +184,7 @@ class DirectMessageHandlerTest {
handler.remotePlayerRegistry = RemotePlayerRegistry(localServerName = "lobby") // empty roster
val sender = TestUtils.createMockPlayer(name = "Alice")
- sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
+ handler.recordRemoteRecipient(sender, "Bob", "survival")
assertNull(handler.getReplyTarget(sender))
}
@@ -209,7 +216,7 @@ class DirectMessageHandlerTest {
handler.remotePlayerRegistry = registry
val sender = TestUtils.createMockPlayer(name = "Alice")
- sync { handler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
+ handler.recordRemoteRecipient(sender, "Bob", "survival")
handler.clearPlayer(sender)
assertNull(handler.getReplyTarget(sender))
@@ -228,7 +235,7 @@ class DirectMessageHandlerTest {
val sender = TestUtils.createMockPlayer()
val recipient = TestUtils.createMockPlayer()
- val result = sync { handler.sendDirectMessage(sender, recipient, "Test") }
+ val result = runBlocking { handler.sendDirectMessage(sender, recipient, "Test") }
assertTrue(result)
}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt
index 301160b..bbcfafb 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommandTest.kt
@@ -1,7 +1,6 @@
package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
import dev.m1sk9.lunaticChat.engine.command.CommandResult
-import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
import dev.m1sk9.lunaticChat.paper.LunaticChat
import dev.m1sk9.lunaticChat.paper.TestUtils
import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
@@ -64,7 +63,7 @@ class ChannelStatusCommandTest {
every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId
every { deps.channelManager.getChannel(channelId) } returns Result.success(channel)
every { deps.channelManager.getChannelMembers(channelId) } returns Result.success(members)
- every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(listOf(channelId))
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns listOf(channelId)
mockkStatic(Bukkit::class)
try {
@@ -83,7 +82,7 @@ class ChannelStatusCommandTest {
val deps = createDependencies()
every { deps.channelManager.getPlayerChannel(testUUID) } returns null
- every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(emptyList())
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns emptyList()
mockkStatic(Bukkit::class)
try {
@@ -113,7 +112,7 @@ class ChannelStatusCommandTest {
Result.success(
listOf(TestUtils.createTestChannelMember(playerId = testUUID)),
)
- every { deps.membershipManager.getPlayerChannels(testUUID) } returns Result.success(channelIds)
+ every { deps.membershipManager.getPlayerChannels(testUUID) } returns channelIds
mockkStatic(Bukkit::class)
try {
@@ -126,17 +125,4 @@ class ChannelStatusCommandTest {
unmockkStatic(Bukkit::class)
}
}
-
- @Test
- fun `execute should return Failure on error`() {
- val deps = createDependencies()
-
- every { deps.channelManager.getPlayerChannel(testUUID) } returns null
- every { deps.membershipManager.getPlayerChannels(testUUID) } returns
- Result.failure(ChannelNotFoundException("error"))
-
- val result = deps.command.execute(deps.ctx)
-
- assertIs<CommandResult.Failure>(result)
- }
}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt
index d415e97..fc7afc9 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt
@@ -24,15 +24,6 @@ class ConfigManagerTest {
}.bufferedReader().use { it.readText() }
@Test
- fun `the bundled config parses`() {
- val config = load(bundledConfig)
-
- assertFalse(config.debug)
- assertEquals("player-settings.yaml", config.userSettingsFilePath)
- assertEquals(Language.EN, config.language)
- }
-
- @Test
fun `the bundled config agrees with the declared defaults`() {
// The data class is meant to be the single source of every default. If config.yml ships a
// different value for a key, one of the two is lying to the operator.
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt
index 03938fc..a6060ce 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/velocity/CrossServerDirectMessageManagerTest.kt
@@ -4,6 +4,8 @@ import dev.m1sk9.lunaticChat.engine.protocol.PluginMessage
import dev.m1sk9.lunaticChat.paper.TestUtils
import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@@ -15,8 +17,6 @@ import java.util.logging.Logger
import kotlin.test.Test
class CrossServerDirectMessageManagerTest {
- private fun <T> sync(block: suspend () -> T): T = runBlocking { block() }
-
private class Fixture(
cacheSize: Int = 100,
) {
@@ -56,11 +56,11 @@ class CrossServerDirectMessageManagerTest {
fun `sendCrossServerMessage relays via plugin channel and delegates display`() {
val f = Fixture()
val sender = TestUtils.createMockPlayer(name = "Alice")
- every { sync { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } } returns "hi"
+ coEvery { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } returns "hi"
- sync { f.manager.sendCrossServerMessage(sender, "Bob", "survival", "hi") }
+ runBlocking { f.manager.sendCrossServerMessage(sender, "Bob", "survival", "hi") }
- verify { sync { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") } }
+ coVerify { f.dmHandler.handleOutgoingCrossServerMessage(sender, "Bob", "survival", "hi") }
verify { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) }
}
@@ -143,9 +143,9 @@ class CrossServerDirectMessageManagerTest {
fun `sendCrossServerMessage prunes the dedup cache when over capacity`() {
val f = Fixture(cacheSize = 1)
val sender = TestUtils.createMockPlayer(name = "Alice")
- every { sync { f.dmHandler.handleOutgoingCrossServerMessage(any(), any(), any(), any()) } } returns "hi"
+ coEvery { f.dmHandler.handleOutgoingCrossServerMessage(any(), any(), any(), any()) } returns "hi"
- repeat(3) { sync { f.manager.sendCrossServerMessage(sender, "Bob$it", "survival", "hi") } }
+ repeat(3) { runBlocking { f.manager.sendCrossServerMessage(sender, "Bob$it", "survival", "hi") } }
verify(atLeast = 1) { sender.sendPluginMessage(f.plugin, "lunaticchat:main", any<ByteArray>()) }
}