summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-03-18 14:06:07 +0900
committerGitHub <noreply@github.com>2026-03-18 14:06:07 +0900
commit74124f8b6317e2f361ed3389b2bc4e3bab2ba270 (patch)
tree98abdffa811500ee08cc2f97c027561d560eb078
parentd36966f01d1f4418de9e939631ba50a1c4d9545f (diff)
parent0daefc528a9bbfc68bbe81bd35ed1baf34622ba6 (diff)
downloadLunaticChat-0.11.0.tar.gz
LunaticChat-0.11.0.tar.bz2
LunaticChat-0.11.0.zip
Merge pull request #146 from m1sk9/fix/channel-joined-notificationv0.11.0
fix: Restore the Channel Manager at player login
-rw-r--r--CHANGELOG.md4
-rw-r--r--codecov.yml3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt40
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt40
-rw-r--r--platform-paper/src/main/resources/languages/en.yml2
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml2
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt113
7 files changed, 187 insertions, 17 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fff3c3b..ec0507c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,11 +20,13 @@
#### Features
-- We have added a notification that appears when you log in to the server while already in a channel.
+- Clicking the notification message now displays the channel's status.
#### Feature Improvements
- Optimization of internal logic.
+- We have improved the notification message that appears when you log in to the server while in a channel.
+- Added a delay to prevent other plugins from interfering with login notifications.
### v0.10.1
diff --git a/codecov.yml b/codecov.yml
index dcf139a..9bc314e 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -13,3 +13,6 @@ comment:
require_changes: false
ignore:
- "dokka/**"
+ # Listener classes are tightly coupled to the Paper/Minecraft runtime and exercised via
+ # end-to-end/server tests, so we exclude them from unit-test coverage metrics.
+ - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/**"
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt
index ffc6e26..7305266 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt
@@ -436,9 +436,49 @@ class ChannelManager(
*/
fun getPlayerChannelContext(playerId: UUID): ChannelContext? {
val channelId = activeChannels[playerId] ?: return null
+ val channel = channelsCache[channelId]
+ val members = membersCache[channelId]?.toList()
+
+ if (channel == null || members == null) {
+ activeChannels.remove(playerId)
+ saveToStorage()
+ return null
+ }
+
+ return ChannelContext(
+ channelId = channelId,
+ channel = channel,
+ members = members,
+ )
+ }
+
+ /**
+ * Restores the active channel of a player based on their channel membership.
+ * If the player is a member of multiple channels, the most recently joined one is selected.
+ *
+ * @param playerId The UUID of the player.
+ * @return The restored ChannelContext, or null if the player is not a member of any channel.
+ */
+ fun restorePlayerChannel(playerId: UUID): ChannelContext? {
+ // Already has an active channel
+ getPlayerChannelContext(playerId)?.let { return it }
+
+ // Find the most recently joined channel for this player
+ val channelId =
+ membersCache.entries
+ .mapNotNull { (channelId, members) ->
+ members.find { it.playerId == playerId }?.let { member ->
+ channelId to member.joinedAt
+ }
+ }.maxByOrNull { it.second }
+ ?.first ?: return null
+
val channel = channelsCache[channelId] ?: return null
val members = membersCache[channelId]?.toList() ?: return null
+ activeChannels[playerId] = channelId
+ saveToStorage()
+
return ChannelContext(
channelId = channelId,
channel = channel,
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
index 3979246..56cbe59 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
@@ -12,6 +12,7 @@ import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
+import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class PlayerPresenceListener(
@@ -27,25 +28,36 @@ class PlayerPresenceListener(
// Send update notification if available
if (updateCheckerFlag.get() && player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) {
- player.sendMessage(
- MessageFormatter
- .format(languageManager.getMessage("general.newUpdateAvailable"))
- .clickEvent(ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest")),
- )
+ lunaticChat.server.asyncScheduler.runDelayed(lunaticChat, { _ ->
+ if (player.isOnline) {
+ player.sendMessage(
+ MessageFormatter
+ .format(languageManager.getMessage("general.newUpdateAvailable"))
+ .clickEvent(ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest")),
+ )
+ }
+ }, 3, TimeUnit.SECONDS)
}
- // Send channel notification if in a channel
+ // Restore active channel from membership and notify (delayed to avoid being buried by other plugins' join messages)
channelManager?.let { manager ->
- val context = manager.getPlayerChannelContext(player.uniqueId)
+ val context = manager.restorePlayerChannel(player.uniqueId)
context?.let {
val notification =
- MessageFormatter.format(
- languageManager.getMessage(
- "channel.notification.login",
- mapOf("channelName" to it.channel.name),
- ),
- )
- player.sendMessage(notification)
+ MessageFormatter
+ .format(
+ languageManager.getMessage(
+ "channel.notification.login",
+ mapOf("channelName" to it.channel.name),
+ ),
+ ).clickEvent(
+ ClickEvent.runCommand("/lc channel status"),
+ )
+ lunaticChat.server.asyncScheduler.runDelayed(lunaticChat, { _ ->
+ if (player.isOnline) {
+ player.sendMessage(notification)
+ }
+ }, 3, TimeUnit.SECONDS)
}
}
}
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
index d932105..04ca56a 100644
--- a/platform-paper/src/main/resources/languages/en.yml
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -132,7 +132,7 @@ channel:
noActiveChannel: "You don't have an active channel. Please specify a channel ID"
error: "Failed to retrieve channel information"
notification:
- login: "You are in channel '{channelName}'"
+ login: "You are currently in the channel '{channelName}'. You can check the channel's status by typing /lc channel status."
playerJoined: "{player} joined {channel}"
playerLeft: "{player} left {channel}"
playerKicked: "{player} was kicked from {channel} by {kicker}"
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
index fbf09f9..74f2dfb 100644
--- a/platform-paper/src/main/resources/languages/ja.yml
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -132,7 +132,7 @@ channel:
noActiveChannel: "アクティブなチャンネルがありません。チャンネルIDを指定してください"
error: "チャンネル情報の取得に失敗しました"
notification:
- login: "チャンネル '{channelName}' に入室中です"
+ login: "現在、チャンネル '{channelName}' に入室中です。チャンネルの状態は /lc channel status で確認できます"
playerJoined: "{player}がチャンネル {channel} に参加しました"
playerLeft: "{player}がチャンネル {channel} から退出しました"
playerKicked: "{player}は{kicker}によってチャンネル {channel} から追放されました"
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt
index 349903a..d5ae373 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManagerTest.kt
@@ -503,6 +503,119 @@ class ChannelManagerTest {
}
@Test
+ fun `getPlayerChannelContext should clear stale active channel when channel is deleted`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "stale-ch", name = "Stale", ownerId = ownerId))
+ assertNotNull(manager.getPlayerChannelContext(ownerId))
+
+ // Delete the channel, which clears activeChannels for affected players
+ manager.deleteChannel("stale-ch", ownerId)
+
+ // Active channel should be cleared
+ assertNull(manager.getPlayerChannel(ownerId))
+ assertNull(manager.getPlayerChannelContext(ownerId))
+ }
+
+ @Test
+ fun `getPlayerChannelContext should clear stale active channel pointing to nonexistent channel`() {
+ // Simulate a stale activeChannels entry pointing to a channel that no longer exists
+ val playerId = createTestUUID(1)
+ val data =
+ ChannelData(
+ activeChannels = mapOf(playerId.toString() to "deleted-ch"),
+ )
+
+ val (manager, storage, _) = createManager(initialData = data)
+ manager.initialize()
+
+ // activeChannels has an entry but the channel doesn't exist
+ assertEquals("deleted-ch", manager.getPlayerChannel(playerId))
+
+ // getPlayerChannelContext should detect the stale entry and clean it up
+ assertNull(manager.getPlayerChannelContext(playerId))
+ assertNull(manager.getPlayerChannel(playerId))
+ verify { storage.queueAsyncSave(any()) }
+ }
+
+ @Test
+ fun `restorePlayerChannel should restore from membership when no active channel`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ val memberId = createTestUUID(2)
+ manager.createChannel(createTestChannel(id = "restore-ch", name = "Restore Channel", ownerId = ownerId))
+ manager.addMember("restore-ch", memberId, ChannelRole.MEMBER)
+
+ // Clear active channel (simulates quit)
+ manager.setPlayerChannel(memberId, null)
+ assertNull(manager.getPlayerChannel(memberId))
+
+ // Restore should find membership and set active channel
+ val context = manager.restorePlayerChannel(memberId)
+ assertNotNull(context)
+ assertEquals("restore-ch", context.channelId)
+ assertEquals("Restore Channel", context.channel.name)
+ assertEquals("restore-ch", manager.getPlayerChannel(memberId))
+ }
+
+ @Test
+ fun `restorePlayerChannel should return existing context if already active`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ val ownerId = createTestUUID(1)
+ manager.createChannel(createTestChannel(id = "active-ch", name = "Active Channel", ownerId = ownerId))
+
+ // Owner already has active channel from createChannel
+ val context = manager.restorePlayerChannel(ownerId)
+ assertNotNull(context)
+ assertEquals("active-ch", context.channelId)
+ }
+
+ @Test
+ fun `restorePlayerChannel should return null for non-member`() {
+ val (manager, _, _) = createManager()
+ manager.initialize()
+
+ assertNull(manager.restorePlayerChannel(createTestUUID(99)))
+ }
+
+ @Test
+ fun `restorePlayerChannel should select most recently joined channel`() {
+ val memberId = createTestUUID(3)
+ val oldMember =
+ ChannelMember(channelId = "old-ch", playerId = memberId, role = ChannelRole.MEMBER, joinedAt = 1000L)
+ val newMember =
+ ChannelMember(channelId = "new-ch", playerId = memberId, role = ChannelRole.MEMBER, joinedAt = 2000L)
+ val owner1 = createTestUUID(1)
+ val owner2 = createTestUUID(2)
+ val oldChannel = createTestChannel(id = "old-ch", name = "Old Channel", ownerId = owner1)
+ val newChannel = createTestChannel(id = "new-ch", name = "New Channel", ownerId = owner2)
+
+ val data =
+ ChannelData(
+ channels = mapOf("old-ch" to oldChannel, "new-ch" to newChannel),
+ members =
+ mapOf(
+ "old-ch" to listOf(oldMember),
+ "new-ch" to listOf(newMember),
+ ),
+ )
+
+ val (manager, _, _) = createManager(initialData = data)
+ manager.initialize()
+
+ // Should restore to the most recently joined channel
+ val context = manager.restorePlayerChannel(memberId)
+ assertNotNull(context)
+ assertEquals("new-ch", context.channelId)
+ }
+
+ @Test
fun `saveToDisk should call storage saveToDisk`() {
val (manager, storage, _) = createManager()
manager.initialize()