diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-03-18 14:06:07 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-18 14:06:07 +0900 |
| commit | 74124f8b6317e2f361ed3389b2bc4e3bab2ba270 (patch) | |
| tree | 98abdffa811500ee08cc2f97c027561d560eb078 | |
| parent | d36966f01d1f4418de9e939631ba50a1c4d9545f (diff) | |
| parent | 0daefc528a9bbfc68bbe81bd35ed1baf34622ba6 (diff) | |
| download | LunaticChat-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
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() |
