summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-26 02:58:23 +0900
committerGitHub <noreply@github.com>2026-01-26 02:58:23 +0900
commit18a4bbc4ba1a6766af920fb0849215f058ff260e (patch)
treec570633f29f2398cb263e2eb90a7d56f0ea41e78
parent0fb8f4c92ad5dd7d20778de862b4d12f67f71d5e (diff)
parentc7714add1934baf55a5e6268b1a70780eaa84fea (diff)
downloadLunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.gz
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.bz2
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.zip
Merge pull request #69 from m1sk9/feat-core-channel-chat
feat: Core Channel Features (Phase 1)
-rw-r--r--CHANGELOG.md1
-rw-r--r--docs/.vitepress/config.mts3
-rw-r--r--docs/.vitepress/config/en.ts16
-rw-r--r--docs/.vitepress/config/ja.ts16
-rw-r--r--docs/.vitepress/public/favicon.ico (renamed from docs/src/public/favicon.ico)bin4286 -> 4286 bytes
-rw-r--r--docs/.vitepress/public/icon.png (renamed from docs/src/public/icon.png)bin122601 -> 122601 bytes
-rw-r--r--docs/bun.lock114
-rw-r--r--docs/src/assets/direct-message/minecraft-player-sound.png (renamed from docs/src/public/direct-message/minecraft-player-sound.png)bin1609414 -> 1609414 bytes
-rw-r--r--docs/src/en/guide/admin/configuration.md21
-rw-r--r--docs/src/en/guide/player/channel-chat/about.md112
-rw-r--r--docs/src/en/guide/player/channel-chat/chatmode.md30
-rw-r--r--docs/src/en/guide/player/direct-message.md2
-rw-r--r--docs/src/en/reference/commands/lc/channel.md77
-rw-r--r--docs/src/en/reference/commands/lc/chatmode.md17
-rw-r--r--docs/src/en/reference/commands/lc/settings.md2
-rw-r--r--docs/src/en/reference/permissions.md66
-rw-r--r--docs/src/guide/admin/configuration.md21
-rw-r--r--docs/src/guide/player/channel-chat.md13
-rw-r--r--docs/src/guide/player/channel-chat/about.md112
-rw-r--r--docs/src/guide/player/channel-chat/chatmode.md30
-rw-r--r--docs/src/guide/player/direct-message.md2
-rw-r--r--docs/src/reference/commands/lc/channel.md78
-rw-r--r--docs/src/reference/commands/lc/chatmode.md17
-rw-r--r--docs/src/reference/commands/lc/settings.md8
-rw-r--r--docs/src/reference/permissions.md66
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt43
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt14
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt40
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt11
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt22
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt17
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt5
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt5
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt6
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt12
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt12
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt6
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt20
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt2
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt106
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt103
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt98
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt288
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt224
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt99
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt80
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt (renamed from platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt)41
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/ReplyCommand.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/TellCommand.kt11
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt183
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt84
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt51
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt142
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt102
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt132
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt85
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt127
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt193
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt130
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt71
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt8
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt53
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt24
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt14
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt5
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt9
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt23
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt73
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt71
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt6
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt4
-rw-r--r--platform-paper/src/main/resources/config.yml6
-rw-r--r--platform-paper/src/main/resources/languages/en.yml102
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml102
-rw-r--r--platform-paper/src/main/resources/paper-plugin.yml20
90 files changed, 3830 insertions, 196 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e20ba1..334b542 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### v0.6.0
+- Added experimental feature to Channel Chat support.
- Fixed broken links to documentation referenced in config.yml
### v0.5.0
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 7e314f0..9e1f90f 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -9,6 +9,9 @@ export default defineConfig({
cleanUrls: true,
srcDir: './src',
outDir: './dist',
+ vite: {
+ publicDir: '../.vitepress/public',
+ },
head: [['link', { rel: 'icon', href: '/favicon.ico' }]],
themeConfig: {
socialLinks: [
diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts
index 0f83c9e..6aec245 100644
--- a/docs/.vitepress/config/en.ts
+++ b/docs/.vitepress/config/en.ts
@@ -37,7 +37,13 @@ export const en: DefaultTheme.Config = {
items: [
{
text: 'Channel Chat',
- link: '/en/guide/player/channel-chat',
+ link: '/en/guide/player/channel-chat/about',
+ items: [
+ {
+ text: 'Chat Mode',
+ link: '/en/guide/player/channel-chat/chatmode',
+ }
+ ]
},
{
text: 'Direct Messages',
@@ -98,6 +104,14 @@ export const en: DefaultTheme.Config = {
text: '/lc status',
link: '/en/reference/commands/lc/status',
},
+ {
+ text: '/lc channel',
+ link: '/en/reference/commands/lc/channel',
+ },
+ {
+ text: '/lc chatmode',
+ link: '/en/reference/commands/lc/chatmode',
+ },
],
},
],
diff --git a/docs/.vitepress/config/ja.ts b/docs/.vitepress/config/ja.ts
index 5264820..013b0d3 100644
--- a/docs/.vitepress/config/ja.ts
+++ b/docs/.vitepress/config/ja.ts
@@ -37,7 +37,13 @@ export const ja: DefaultTheme.Config = {
items: [
{
text: 'チャンネルチャット',
- link: '/guide/player/channel-chat',
+ link: "/guide/player/channel-chat/about",
+ items: [
+ {
+ text: 'チャットモード',
+ link: '/guide/player/channel-chat/chatmode',
+ }
+ ]
},
{
text: 'ダイレクトメッセージ',
@@ -98,6 +104,14 @@ export const ja: DefaultTheme.Config = {
text: '/lc status',
link: '/reference/commands/lc/status',
},
+ {
+ text: '/lc channel',
+ link: '/reference/commands/lc/channel',
+ },
+ {
+ text: '/lc chatmode',
+ link: '/reference/commands/lc/chatmode',
+ },
],
},
],
diff --git a/docs/src/public/favicon.ico b/docs/.vitepress/public/favicon.ico
index 46c0cad..46c0cad 100644
--- a/docs/src/public/favicon.ico
+++ b/docs/.vitepress/public/favicon.ico
Binary files differ
diff --git a/docs/src/public/icon.png b/docs/.vitepress/public/icon.png
index 064445a..064445a 100644
--- a/docs/src/public/icon.png
+++ b/docs/.vitepress/public/icon.png
Binary files differ
diff --git a/docs/bun.lock b/docs/bun.lock
index 6374c97..d62af2c 100644
--- a/docs/bun.lock
+++ b/docs/bun.lock
@@ -10,7 +10,7 @@
},
},
"packages": {
- "@algolia/abtesting": ["@algolia/abtesting@1.12.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ=="],
+ "@algolia/abtesting": ["@algolia/abtesting@1.13.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ=="],
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
@@ -20,39 +20,39 @@
"@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.17.7", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
- "@algolia/client-abtesting": ["@algolia/client-abtesting@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A=="],
+ "@algolia/client-abtesting": ["@algolia/client-abtesting@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w=="],
- "@algolia/client-analytics": ["@algolia/client-analytics@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA=="],
+ "@algolia/client-analytics": ["@algolia/client-analytics@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow=="],
- "@algolia/client-common": ["@algolia/client-common@5.46.2", "", {}, "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg=="],
+ "@algolia/client-common": ["@algolia/client-common@5.47.0", "", {}, "sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg=="],
- "@algolia/client-insights": ["@algolia/client-insights@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA=="],
+ "@algolia/client-insights": ["@algolia/client-insights@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA=="],
- "@algolia/client-personalization": ["@algolia/client-personalization@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA=="],
+ "@algolia/client-personalization": ["@algolia/client-personalization@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ=="],
- "@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w=="],
+ "@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA=="],
- "@algolia/client-search": ["@algolia/client-search@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA=="],
+ "@algolia/client-search": ["@algolia/client-search@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g=="],
- "@algolia/ingestion": ["@algolia/ingestion@1.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ=="],
+ "@algolia/ingestion": ["@algolia/ingestion@1.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ=="],
- "@algolia/monitoring": ["@algolia/monitoring@1.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw=="],
+ "@algolia/monitoring": ["@algolia/monitoring@1.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA=="],
- "@algolia/recommend": ["@algolia/recommend@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q=="],
+ "@algolia/recommend": ["@algolia/recommend@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA=="],
- "@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2" } }, "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg=="],
+ "@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0" } }, "sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw=="],
- "@algolia/requester-fetch": ["@algolia/requester-fetch@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2" } }, "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw=="],
+ "@algolia/requester-fetch": ["@algolia/requester-fetch@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0" } }, "sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA=="],
- "@algolia/requester-node-http": ["@algolia/requester-node-http@5.46.2", "", { "dependencies": { "@algolia/client-common": "5.46.2" } }, "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg=="],
+ "@algolia/requester-node-http": ["@algolia/requester-node-http@5.47.0", "", { "dependencies": { "@algolia/client-common": "5.47.0" } }, "sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
- "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
+ "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
- "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
+ "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
"@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="],
@@ -124,61 +124,61 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
- "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.65", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-v/O0UeqrDz6ASuRVE5g2Puo5aWyej4M/CxX6WYDBARgswwxK0mp3VQbGgPFEAAUU9QN02IjTgjMuO021gpWf2w=="],
+ "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.67", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-RGJRwlxyup54L1UDAjCshy3ckX5zcvYIU74YLSnUgHGvqh6B4mvksbGNHAIEp7dZQ6cM13RZVT5KC07CmnFNew=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
- "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
- "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="],
- "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="],
- "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="],
- "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="],
- "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="],
- "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="],
- "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="],
- "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="],
- "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="],
- "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="],
- "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="],
- "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="],
- "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="],
- "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="],
- "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="],
- "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="],
- "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="],
- "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="],
- "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="],
- "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="],
- "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="],
- "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="],
- "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="],
- "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
"@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
@@ -216,13 +216,13 @@
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
- "@vue/compiler-core": ["@vue/compiler-core@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.26", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w=="],
+ "@vue/compiler-core": ["@vue/compiler-core@3.5.27", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.27", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ=="],
- "@vue/compiler-dom": ["@vue/compiler-dom@3.5.26", "", { "dependencies": { "@vue/compiler-core": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A=="],
+ "@vue/compiler-dom": ["@vue/compiler-dom@3.5.27", "", { "dependencies": { "@vue/compiler-core": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w=="],
- "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.26", "@vue/compiler-dom": "3.5.26", "@vue/compiler-ssr": "3.5.26", "@vue/shared": "3.5.26", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA=="],
+ "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.27", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.27", "@vue/compiler-dom": "3.5.27", "@vue/compiler-ssr": "3.5.27", "@vue/shared": "3.5.27", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ=="],
- "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.26", "", { "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw=="],
+ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.27", "", { "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
@@ -230,15 +230,15 @@
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
- "@vue/reactivity": ["@vue/reactivity@3.5.26", "", { "dependencies": { "@vue/shared": "3.5.26" } }, "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ=="],
+ "@vue/reactivity": ["@vue/reactivity@3.5.27", "", { "dependencies": { "@vue/shared": "3.5.27" } }, "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ=="],
- "@vue/runtime-core": ["@vue/runtime-core@3.5.26", "", { "dependencies": { "@vue/reactivity": "3.5.26", "@vue/shared": "3.5.26" } }, "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q=="],
+ "@vue/runtime-core": ["@vue/runtime-core@3.5.27", "", { "dependencies": { "@vue/reactivity": "3.5.27", "@vue/shared": "3.5.27" } }, "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A=="],
- "@vue/runtime-dom": ["@vue/runtime-dom@3.5.26", "", { "dependencies": { "@vue/reactivity": "3.5.26", "@vue/runtime-core": "3.5.26", "@vue/shared": "3.5.26", "csstype": "^3.2.3" } }, "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ=="],
+ "@vue/runtime-dom": ["@vue/runtime-dom@3.5.27", "", { "dependencies": { "@vue/reactivity": "3.5.27", "@vue/runtime-core": "3.5.27", "@vue/shared": "3.5.27", "csstype": "^3.2.3" } }, "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg=="],
- "@vue/server-renderer": ["@vue/server-renderer@3.5.26", "", { "dependencies": { "@vue/compiler-ssr": "3.5.26", "@vue/shared": "3.5.26" }, "peerDependencies": { "vue": "3.5.26" } }, "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA=="],
+ "@vue/server-renderer": ["@vue/server-renderer@3.5.27", "", { "dependencies": { "@vue/compiler-ssr": "3.5.27", "@vue/shared": "3.5.27" }, "peerDependencies": { "vue": "3.5.27" } }, "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA=="],
- "@vue/shared": ["@vue/shared@3.5.26", "", {}, "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="],
+ "@vue/shared": ["@vue/shared@3.5.27", "", {}, "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ=="],
"@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
@@ -248,7 +248,7 @@
"@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
- "algoliasearch": ["algoliasearch@5.46.2", "", { "dependencies": { "@algolia/abtesting": "1.12.2", "@algolia/client-abtesting": "5.46.2", "@algolia/client-analytics": "5.46.2", "@algolia/client-common": "5.46.2", "@algolia/client-insights": "5.46.2", "@algolia/client-personalization": "5.46.2", "@algolia/client-query-suggestions": "5.46.2", "@algolia/client-search": "5.46.2", "@algolia/ingestion": "1.46.2", "@algolia/monitoring": "1.46.2", "@algolia/recommend": "5.46.2", "@algolia/requester-browser-xhr": "5.46.2", "@algolia/requester-fetch": "5.46.2", "@algolia/requester-node-http": "5.46.2" } }, "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q=="],
+ "algoliasearch": ["algoliasearch@5.47.0", "", { "dependencies": { "@algolia/abtesting": "1.13.0", "@algolia/client-abtesting": "5.47.0", "@algolia/client-analytics": "5.47.0", "@algolia/client-common": "5.47.0", "@algolia/client-insights": "5.47.0", "@algolia/client-personalization": "5.47.0", "@algolia/client-query-suggestions": "5.47.0", "@algolia/client-search": "5.47.0", "@algolia/ingestion": "1.47.0", "@algolia/monitoring": "1.47.0", "@algolia/recommend": "5.47.0", "@algolia/requester-browser-xhr": "5.47.0", "@algolia/requester-fetch": "5.47.0", "@algolia/requester-node-http": "5.47.0" } }, "sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
@@ -270,13 +270,13 @@
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
- "entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
- "focus-trap": ["focus-trap@7.7.1", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-Pkp8m55GjxBLnhBoT6OXdMvfRr4TjMAKLvFM566zlIryq5plbhaTmLAJWTGR0EkRwLjEte1lCOG9MxF1ipJrOg=="],
+ "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -332,7 +332,7 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
- "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
+ "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="],
"search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
@@ -358,7 +358,7 @@
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
- "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
@@ -370,7 +370,7 @@
"vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
- "vue": ["vue@3.5.26", "", { "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", "@vue/runtime-dom": "3.5.26", "@vue/server-renderer": "3.5.26", "@vue/shared": "3.5.26" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA=="],
+ "vue": ["vue@3.5.27", "", { "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", "@vue/runtime-dom": "3.5.27", "@vue/server-renderer": "3.5.27", "@vue/shared": "3.5.27" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
}
diff --git a/docs/src/public/direct-message/minecraft-player-sound.png b/docs/src/assets/direct-message/minecraft-player-sound.png
index 56059e5..56059e5 100644
--- a/docs/src/public/direct-message/minecraft-player-sound.png
+++ b/docs/src/assets/direct-message/minecraft-player-sound.png
Binary files differ
diff --git a/docs/src/en/guide/admin/configuration.md b/docs/src/en/guide/admin/configuration.md
index 0a0bee2..6ce63b4 100644
--- a/docs/src/en/guide/admin/configuration.md
+++ b/docs/src/en/guide/admin/configuration.md
@@ -51,6 +51,9 @@ features:
timeout: 3000
# Specify the number of retry attempts for failed API requests to the Romanization conversion service.
retryAttempts: 2
+ channelChat:
+ # If enabled, channel-based chat functionality will be activated.
+ enabled: false
# ----------------------------------------------
# --------- Message Format Settings --------
@@ -62,11 +65,14 @@ features:
# {sender} - The name of the message sender
# {recipient} - The name of the message recipient
# {message} - The content of the message
+# {channel} - The name of the chat channel (only for channel chat)
# ----------------------------------------------
messageFormat:
# Configure the format for direct messages sent via /tell or /msg
directMessageFormat: "§7[§e{sender} §7>> §e{recipient}§7] §f{message}"
+ # Configure the format for messages sent in channel chat
+ channelMessageFormat: "§7[§b#{channel}§7] §e{sender}: §f{message}"
```
## General Settings
@@ -165,6 +171,13 @@ Specifies the timeout duration (in milliseconds) for API requests to the romaniz
Specifies the number of retry attempts for failed API requests to the romanization conversion service.
+### `features.channelChat.enabled`
+
+- Type: `boolean`
+- Default: `false`
+
+Enables channel-based chat functionality.
+
## Message Format Settings
Available placeholders:
@@ -172,6 +185,7 @@ Available placeholders:
- `{sender}`: Name of the message sender
- `{recipient}`: Name of the message recipient
- `{message}`: Content of the message
+- `{channel}`: Name of the chat channel (only for channel chat)
### `messageFormat.directMessageFormat`
@@ -179,3 +193,10 @@ Available placeholders:
- Default: `§7[§e{sender} §7>> §e{recipient}§7] §f{message}`
Specifies the format for messages sent via direct message ([`/tell`](../../reference/commands/tell.md) or [`/reply`](../../reference/commands/reply.md) commands).
+
+### `messageFormat.channelMessageFormat`
+
+- Type: `string`
+- Default: `§7[§b#{channel}§7] §e{sender}: §f{message}`
+
+Specifies the format for messages sent in channel chat.
diff --git a/docs/src/en/guide/player/channel-chat/about.md b/docs/src/en/guide/player/channel-chat/about.md
new file mode 100644
index 0000000..3b9b088
--- /dev/null
+++ b/docs/src/en/guide/player/channel-chat/about.md
@@ -0,0 +1,112 @@
+# Channel Chat <Badge type="tip" text="v0.6.0" /> <Badge type="warning" text="Experimental" />
+
+This feature allows specific players to share chat.
+
+::: warning Experimental Feature
+
+This feature is currently implemented experimentally, and specifications may change in future updates.
+
+Since this is a large-scale feature, we plan to implement it gradually rather than all at once.
+
+For detailed implementation plans, see the [Roadmap (m1sk9/LunaticChat #54)](https://github.com/m1sk9/LunaticChat/issues/54).
+
+:::
+
+::: tip This feature must be enabled on the server
+
+If channel chat is disabled on the server, these features cannot be used.
+
+Please contact your server administrator.
+
+:::
+
+## Creating a Channel
+
+To use channel chat, you must first create a chat channel.
+
+Use the [`/lc channnel create`](../../../reference/commands/lc/channel.md#lc-channel-create-channel-id-channel-name-channel-description-private-setting) command to create a chat channel.
+
+```
+/lc channel create <channel-id> <channel-name> [channel-description] [private-setting]
+```
+
+- `<channel-id>` specifies the channel ID.
+- `<channel-name>` specifies the channel name.
+- `[channel-description]` specifies the channel description (optional).
+ - The description will be displayed when viewing the channel list with the `/lc channel list` command.
+- `[private-setting]` specifies the channel's privacy setting.
+ - If `true` is specified, it will be created as a private channel, and only invited players can join.
+ - If `false` is specified or omitted, it will be created as a public channel, and anyone can join.
+
+::: tip Channel ID Requirements
+
+Channel IDs must follow these requirements:
+
+- Must be between 3 and 30 characters long
+- Can only contain alphanumeric characters, underscores (_), and hyphens (-)
+
+:::
+
+::: warning Notes on Entering Channel Names and Descriptions
+
+When entering channel names and descriptions, arguments must be enclosed in `"` (double quotes).
+
+Example:
+
+```
+/lc channel create pvp_channel "PvP Enthusiasts Club" "A gathering of people who love PvP" false
+```
+
+:::
+
+The channel will be created and a confirmation message will be displayed in chat.
+
+## Joining a Channel
+
+To join a created channel, use the [`/lc channel join`](../../../reference/commands/lc/channel.md#lc-channel-join-channel-id) command.
+
+```
+/lc channel join <channel-id>
+```
+
+When you join a channel, your chat mode will switch from global chat to channel chat.
+
+## Viewing the Channel List
+
+To view the list of channels you can join, use the [`/lc channel list`](../../../reference/commands/lc/channel.md#lc-channel-list) command.
+
+```
+/lc channel list
+```
+
+A list of available channels will be displayed in the chat. You can click on them or use the [`/lc channel join`](#joining-a-channel) command to join a channel.
+
+## Chat Modes
+
+In LunaticChat, there are two types of chat modes for players when sending chat messages:
+
+1. Global chat mode: Share chat with all players.
+2. Channel chat mode: Share chat with players in a specific channel you have joined.
+
+Channel chat primarily uses chat mode `2`.
+
+For more details, see [here](./chatmode.md).
+
+## Difference Between Active Channel and Membership Status
+
+In LunaticChat, a player's active channel and membership status are managed separately.
+
+- Active channel: The channel used in channel chat mode
+- Membership status: List of channels the player has joined
+
+Only one channel can be designated as the active channel at a time. To switch the active channel to a channel in your membership status, use the [`/lc channel switch`](../../../reference/commands/lc/channel.md#lc-channel-switch-channel-id) command.
+
+```
+/lc channel switch <channel-id>
+```
+
+Removing a channel from your active status does not remove it from your membership status. To leave a channel, switch to it as your active channel first, then use the [`/lc channel leave`](../../../reference/commands/lc/channel.md#lc-channel-leave) command.
+
+```
+/lc channel leave
+```
diff --git a/docs/src/en/guide/player/channel-chat/chatmode.md b/docs/src/en/guide/player/channel-chat/chatmode.md
new file mode 100644
index 0000000..02d6a18
--- /dev/null
+++ b/docs/src/en/guide/player/channel-chat/chatmode.md
@@ -0,0 +1,30 @@
+# Chat Mode
+
+Chat mode refers to the mode in which players send chat messages in LunaticChat. LunaticChat has the following two types of chat modes:
+
+1. Global chat mode: Share chat with all players.
+2. Channel chat mode: Share chat with players in a specific channel you have joined.
+
+## Channel Chat Mode
+
+While in channel chat mode, all chat messages are displayed only to players in the channel you have joined and will not be displayed in global chat.
+
+::: tip Temporarily Send to Global Chat
+
+If you want to temporarily send a message to global chat while in channel chat mode, prefix your message with `!`.
+
+```
+!Hello everyone, how are you?
+```
+
+:::
+
+## Global Chat Mode
+
+While in global chat mode, you share chat with all players on the server. It functions the same as normal Minecraft chat.
+
+## Temporarily Switching Chat Modes
+
+To temporarily switch chat modes, use the `/chatmode toggle` command.
+
+When you switch using this command, you can change chat modes without joining or leaving channels.
diff --git a/docs/src/en/guide/player/direct-message.md b/docs/src/en/guide/player/direct-message.md
index 460de51..64c99b8 100644
--- a/docs/src/en/guide/player/direct-message.md
+++ b/docs/src/en/guide/player/direct-message.md
@@ -60,6 +60,6 @@ This notification sound depends on the **Players** category volume setting on th
If you cannot hear LunaticChat's notification sound, check your client's volume settings.
-![](/direct-message/minecraft-player-sound.png)
+![](../../../assets/direct-message/minecraft-player-sound.png)
:::
diff --git a/docs/src/en/reference/commands/lc/channel.md b/docs/src/en/reference/commands/lc/channel.md
new file mode 100644
index 0000000..31941ca
--- /dev/null
+++ b/docs/src/en/reference/commands/lc/channel.md
@@ -0,0 +1,77 @@
+# /lc channel <Badge type="tip" text="v0.6.0" /> <Badge type="tip" text="Paper" />
+
+### `/lc channel create <channel-id> <channel-name> [channel-description] [private-setting]`
+
+| Permission |
+|------------------------------|
+| `lunaticchat.command.lc.channel.create` |
+
+Creates a channel.
+
+- `<channel-id>` specifies the channel ID.
+- `<channel-name>` specifies the channel name.
+- `[channel-description]` specifies the channel description (optional).
+ - The description will be displayed when viewing the channel list with the `/lc channel list` command.
+- `[private-setting]` specifies the channel's privacy setting.
+ - If `true` is specified, it will be created as a private channel, and only invited players can join.
+ - If `false` is specified or omitted, it will be created as a public channel, and anyone can join.
+
+::: warning Notes on Entering Channel Names and Descriptions
+
+When entering channel names and descriptions, arguments must be enclosed in `"` (double quotes).
+
+Example:
+
+```
+/lc channel create pvp_channel "PvP Enthusiasts Club" "A gathering of people who love PvP" false
+```
+
+:::
+
+### `/lc channel list`
+
+| Permission |
+|---------------------------------------|
+| `lunaticchat.command.lc.channel.list` |
+
+Displays the list of channels.
+
+### `/lc channel join <channel-id>`
+
+| Permission |
+|---------------------------------------|
+| `lunaticchat.command.lc.channel.join` |
+
+Joins the specified channel.
+
+### `/lc channel leave`
+
+| Permission |
+|----------------------------------------|
+| `lunaticchat.command.lc.channel.leave` |
+
+Leaves the currently active channel.
+
+### `/lc channel switch <channel-id>`
+
+| Permission |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.switch` |
+
+Switches to the specified channel. Use this when you want to switch to another channel while in channel chat mode.
+
+### `/lc channel status`
+
+| Permission |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.status` |
+
+Displays information about the currently joined and active channels.
+
+### `/lc channel delete <channel-id>`
+
+| Permission |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.delete` |
+
+Deletes the specified channel. You must be the channel creator (owner).
diff --git a/docs/src/en/reference/commands/lc/chatmode.md b/docs/src/en/reference/commands/lc/chatmode.md
new file mode 100644
index 0000000..e3f335f
--- /dev/null
+++ b/docs/src/en/reference/commands/lc/chatmode.md
@@ -0,0 +1,17 @@
+# /lc chatmode <Badge type="tip" text="v0.6.0" /> <Badge type="tip" text="Paper" />
+
+### `/lc chatmode`
+
+| Permission |
+|-----------------------------------|
+| `lunaticchat.command.lc.chatmode` |
+
+Displays the current chat mode.
+
+### `/lc chatmode toggle`
+
+| Permission |
+|------------------------------------|
+| `lunaticchat.command.lc.chatmode.toggle` |
+
+Toggles the chat mode.
diff --git a/docs/src/en/reference/commands/lc/settings.md b/docs/src/en/reference/commands/lc/settings.md
index 742e85c..4d09301 100644
--- a/docs/src/en/reference/commands/lc/settings.md
+++ b/docs/src/en/reference/commands/lc/settings.md
@@ -14,5 +14,5 @@ This setting is saved by UUID, so the setting will not be lost even if you chang
| Key | Description | Default Value |
|----------|----------------------------------------------------------|--------|
-| `jp` | Toggles romanization conversion on or off. | `true` |
+| `japanese` | Toggles romanization conversion on or off. | `true` |
| `notice` | Toggles whether to receive notifications when receiving direct messages via `/tell` or `/reply`. | `true` |
diff --git a/docs/src/en/reference/permissions.md b/docs/src/en/reference/permissions.md
index ed30a77..65825c1 100644
--- a/docs/src/en/reference/permissions.md
+++ b/docs/src/en/reference/permissions.md
@@ -34,6 +34,12 @@ Toggles the use of the [`/tell`](commands/tell.md) command.
Toggles the use of the [`/reply`](commands/reply.md) command.
+### `lunaticchat.command.lc`
+
+- Default: `non OP`
+
+Toggles the use of the `/lc` command.
+
### `lunaticchat.command.lc.settings`
- Default: `non OP`
@@ -46,6 +52,66 @@ Toggles the use of the [`/lc settings`](commands/lc/settings.md) command.
Toggles the use of the [`/lc status`](commands/lc/status.md) command.
+### `lunaticchat.command.lc.channel`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.create`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel create`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.list`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel list`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.join`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel join`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.leave`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel leave`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.switch`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel switch`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.status`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel status`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.channel.delete`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc channel delete`](commands/lc/channel.md) command.
+
+### `lunaticchat.command.lc.chatmode`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc chatmode`](commands/lc/chatmode.md) command.
+
+### `lunaticchat.command.lc.chatmode.toggle`
+
+- Default: `non OP`
+
+Toggles the use of the [`/lc chatmode toggle`](commands/lc/chatmode.md) command.
+
### `lunaticchat.command.jp` <Badge type="danger" text="Deprecated: Will be removed in v1.0.0" />
- Default: `non OP`
diff --git a/docs/src/guide/admin/configuration.md b/docs/src/guide/admin/configuration.md
index 2c70f55..ac63eb7 100644
--- a/docs/src/guide/admin/configuration.md
+++ b/docs/src/guide/admin/configuration.md
@@ -51,6 +51,9 @@ features:
timeout: 3000
# Specify the number of retry attempts for failed API requests to the Romanization conversion service.
retryAttempts: 2
+ channelChat:
+ # If enabled, channel-based chat functionality will be activated.
+ enabled: false
# ----------------------------------------------
# --------- Message Format Settings --------
@@ -62,11 +65,14 @@ features:
# {sender} - The name of the message sender
# {recipient} - The name of the message recipient
# {message} - The content of the message
+# {channel} - The name of the chat channel (only for channel chat)
# ----------------------------------------------
messageFormat:
# Configure the format for direct messages sent via /tell or /msg
directMessageFormat: "§7[§e{sender} §7>> §e{recipient}§7] §f{message}"
+ # Configure the format for messages sent in channel chat
+ channelMessageFormat: "§7[§b#{channel}§7] §e{sender}: §f{message}"
```
## General Settings
@@ -165,6 +171,13 @@ LunaticChat の [`/reply`](../../reference/commands/reply.md) コマンドによ
ローマ字変換 API へのリクエストが失敗した場合の再試行回数を指定します.
+### `features.channelChat.enabled`
+
+- Type: `boolean`
+- Default: `false`
+
+チャンネルチャット機能を有効にします.
+
## Message Format Settings
使用できるプレースホルダー:
@@ -172,6 +185,7 @@ LunaticChat の [`/reply`](../../reference/commands/reply.md) コマンドによ
- `{sender}`: メッセージ送信者の名前
- `{recipient}`: メッセージ受信者の名前
- `{message}`: メッセージの内容
+- `{channel}`: チャットチャンネルの名前 (チャンネルチャットの場合のみ)
### `messageFormat.directMessageFormat`
@@ -179,3 +193,10 @@ LunaticChat の [`/reply`](../../reference/commands/reply.md) コマンドによ
- Default: `§7[§e{sender} §7>> §e{recipient}§7] §f{message}`
ダイレクトメッセージ( [`/tell`](../../reference/commands/tell.md) や [`/reply`](../../reference/commands/reply.md) コマンド)で送信されるメッセージのフォーマットを指定します.
+
+### `messageFormat.channelMessageFormat`
+
+- Type: `string`
+- Default: `§7[§b#{channel}§7] §e{sender}: §f{message}`
+
+チャンネルチャットで送信されるメッセージのフォーマットを指定します.
diff --git a/docs/src/guide/player/channel-chat.md b/docs/src/guide/player/channel-chat.md
deleted file mode 100644
index ce5a43e..0000000
--- a/docs/src/guide/player/channel-chat.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# チャンネルチャット <Badge type="tip" text="v0.6.0" /> <Badge type="warning" text="試験的" />
-
-特定のプレイヤー間で,チャットを共有できる機能です.
-
-::: warning 試験的機能
-
-この機能は現在,試験的に実装されており,今後のアップデートで仕様が変更される可能性があります.
-
-規模が大きい機能なので,一気に実装はせず段階的な展開を予定しています.
-
-詳しい実装計画は [ロードマップ (m1sk9/LunaticChat #54)](https://github.com/m1sk9/LunaticChat/issues/54) をご覧ください.
-
-:::
diff --git a/docs/src/guide/player/channel-chat/about.md b/docs/src/guide/player/channel-chat/about.md
new file mode 100644
index 0000000..67aa170
--- /dev/null
+++ b/docs/src/guide/player/channel-chat/about.md
@@ -0,0 +1,112 @@
+# チャンネルチャット <Badge type="tip" text="v0.6.0" /> <Badge type="warning" text="試験的" />
+
+特定のプレイヤー間で,チャットを共有できる機能です.
+
+::: warning 試験的機能
+
+この機能は現在,試験的に実装されており,今後のアップデートで仕様が変更される可能性があります.
+
+規模が大きい機能なので,一気に実装はせず段階的な展開を予定しています.
+
+詳しい実装計画は [ロードマップ (m1sk9/LunaticChat #54)](https://github.com/m1sk9/LunaticChat/issues/54) をご覧ください.
+
+:::
+
+::: tip この機能はサーバーで有効化する必要があります
+
+チャンネルチャットがサーバーで無効化されている場合,これらの機能は使用できません.
+
+サーバー管理者にお問い合わせください.
+
+:::
+
+## チャンネルを作成する
+
+チャンネルチャットを利用するには,まずチャットチャンネルを作成する必要があります.
+
+[`/lc channnel create`](../../../reference/commands/lc/channel.md#lc-channel-create-チャンネルid-チャンネル名-チャンネルの説明-プライベート設定) コマンドを使用して,チャットチャンネルを作成します.
+
+```
+/lc channel create <チャンネルID> <チャンネル名> [チャンネルの説明] [プライベート設定]
+```
+
+- `<チャンネルID>` はチャンネルのIDを指定します.
+- `<チャンネル名>` はチャンネルの名前を指定します.
+- `[チャンネルの説明]` はチャンネルの説明を指定します (省略可能).
+ - `/lc channel list` コマンドでチャンネル一覧を表示した際に,説明が表示されます.
+- `[プライベート設定]` はチャンネルの公開設定を指定します.
+ - `true` を指定するとプライベートチャンネルとして作成され,招待されたプレイヤーのみが参加できます.
+ - `false` を指定するか,省略した場合はパブリックチャンネルとして作成され,誰でも参加できます.
+
+::: tip チャンネルIDの要件
+
+チャンネルIDは次の要件に従う必要があります:
+
+- 3文字以上,30文字以下であること
+- 英数字,アンダースコア(_),ハイフン(-)のみを含むこと
+
+:::
+
+::: warning チャンネル名・チャンネルの説明を入力する際の注意点
+
+チャンネル名・チャンネルの説明を入力する際は,引数を `"` (ダブルクォーテーション) で囲む必要があります.
+
+実行例は次のとおりです:
+
+```
+/lc channel create pvp_channel "PvP大好きクラブ" "PvPが大好きな人たちの集まり" false
+```
+
+:::
+
+チャンネルが正常に作成されると,チャットで確認メッセージが表示されます.
+
+## チャンネルに参加する
+
+作成されたチャンネルに参加するには,[`/lc channel join`](../../../reference/commands/lc/channel.md#lc-channel-join-チャンネルid) コマンドを使用します.
+
+```
+/lc channel join <チャンネルID>
+```
+
+チャンネルに参加すると,チャットモードが全体チャットからチャンネルチャットに切り替わります.
+
+## チャンネル一覧を表示する
+
+参加可能なチャンネル一覧を表示するには,[`/lc channel list`](../../../reference/commands/lc/channel.md#lc-channel-list) コマンドを使用します.
+
+```
+/lc channel list
+```
+
+チャット欄に参加可能なチャンネルの一覧が表示されます.クリックするか [`/lc channel join`](#チャンネルに参加する) コマンドを使用して,チャンネルに参加できます.
+
+## チャットモード
+
+LunaticChat では,プレイヤーがチャットを送信する際のモードとして,以下の2種類のチャットモードが存在します:
+
+1. 全体チャットモード: すべてのプレイヤーとチャットを共有します.
+2. チャンネルチャットモード: 参加している特定のチャンネル内のプレイヤーとチャットを共有します.
+
+チャンネルチャットでは主に `2` のチャットモードが使用されます.
+
+詳しくは [こちら](./chatmode.md) をご覧ください.
+
+## アクティブチャンネルとメンバーシップ状態の違い
+
+LunaticChat では,プレイヤーのアクティブチャンネルとメンバーシップ状態は別々に管理されています.
+
+- アクティブチャンネル: チャンネルチャットモードで使用されるチャンネル
+- メンバーシップ状態: プレイヤーが参加しているチャンネルの一覧
+
+アクティブチャンネルは一度に一つのチャンネルのみを指定できます.メンバーシップ状態のチャンネルにアクティブチャンネルを切り替える場合は [`/lc channel switch`](../../../reference/commands/lc/channel.md#lc-channel-switch-チャンネルid) コマンドを使用します.
+
+```
+/lc channel switch <チャンネルID>
+```
+
+メンバーシップ状態のチャンネルからアクティブチャンネルを解除してもチャンネルからは削除されません.チャンネルから退出するにはアクティブチャンネルに切り替えてから [`/lc channel leave`](../../../reference/commands/lc/channel.md#lc-channel-leave) コマンドを使用します.
+
+```
+/lc channel leave
+```
diff --git a/docs/src/guide/player/channel-chat/chatmode.md b/docs/src/guide/player/channel-chat/chatmode.md
new file mode 100644
index 0000000..9e2712d
--- /dev/null
+++ b/docs/src/guide/player/channel-chat/chatmode.md
@@ -0,0 +1,30 @@
+# チャットモード
+
+チャットモードは LunaticChat におけるプレイヤーがチャットを送信する際のモードを指します.LunaticChat では以下の2種類のチャットモードが存在します.
+
+1. 全体チャットモード: すべてのプレイヤーとチャットを共有します.
+2. チャンネルチャットモード: 参加している特定のチャンネル内のプレイヤーとチャットを共有します.
+
+## チャットチャットモード
+
+チャンネルチャットモード中は,全てのチャットが参加しているチャンネルのプレイヤーのみに表示され,全体チャットには表示されません.
+
+::: tip 一時的に全体チャットを送信する
+
+チャンネルチャットモード中に,一時的に全体チャットを送信したい場合は,メッセージの先頭に `!` を付けて送信します.
+
+```
+!こんにちは、みんな元気?
+```
+
+:::
+
+## 全体チャットモード
+
+全体チャットモード中は,サーバー内のすべてのプレイヤーとチャットを共有します.通常の Minecraft チャットと同様の動作をします.
+
+## 一時的にチャットモードを切り替える
+
+チャットモードを一時的に切り替えるには,`/chatmode toggle` コマンドを使用します.
+
+このコマンドで切り替えた場合はチャンネルの入退出なしで,チャットモードを切り替えることができます.
diff --git a/docs/src/guide/player/direct-message.md b/docs/src/guide/player/direct-message.md
index b6f1424..8edd16e 100644
--- a/docs/src/guide/player/direct-message.md
+++ b/docs/src/guide/player/direct-message.md
@@ -60,6 +60,6 @@ v0.4.0 以降,ダイレクトメッセージの通知設定を変更できる
LunaticChat の通知音が聞こえない場合は,クライアントの音量設定を確認してください.
-![](/direct-message/minecraft-player-sound.png)
+![](../../assets/direct-message/minecraft-player-sound.png)
:::
diff --git a/docs/src/reference/commands/lc/channel.md b/docs/src/reference/commands/lc/channel.md
new file mode 100644
index 0000000..9cc96e2
--- /dev/null
+++ b/docs/src/reference/commands/lc/channel.md
@@ -0,0 +1,78 @@
+# /lc channel <Badge type="tip" text="v0.6.0" /> <Badge type="tip" text="Paper" />
+
+### `/lc channel create <チャンネルID> <チャンネル名> [チャンネルの説明] [プライベート設定]`
+
+| パーミッション |
+|------------------------------|
+| `lunaticchat.command.lc.channel.create` |
+
+チャンネルを作成します.
+
+- `<チャンネルID>` はチャンネルのIDを指定します.
+- `<チャンネル名>` はチャンネルの名前を指定します.
+- `[チャンネルの説明]` はチャンネルの説明を指定します (省略可能).
+ - `/lc channel list` コマンドでチャンネル一覧を表示した際に,説明が表示されます.
+- `[プライベート設定]` はチャンネルの公開設定を指定します.
+ - `true` を指定するとプライベートチャンネルとして作成され,招待されたプレイヤーのみが参加できます.
+ - `false` を指定するか,省略した場合はパブリックチャンネルとして作成され,誰でも参加できます.
+
+::: warning チャンネル名・チャンネルの説明を入力する際の注意点
+
+チャンネル名・チャンネルの説明を入力する際は,引数を `"` (ダブルクォーテーション) で囲む必要があります.
+
+実行例は次のとおりです:
+
+```
+/lc channel create pvp_channel "PvP大好きクラブ" "PvPが大好きな人たちの集まり" false
+```
+
+:::
+
+### `/lc channel list`
+
+| パーミッション |
+|---------------------------------------|
+| `lunaticchat.command.lc.channel.list` |
+
+チャンネルの一覧を表示します.
+
+### `/lc channel join <チャンネルID>`
+
+| パーミッション |
+|---------------------------------------|
+| `lunaticchat.command.lc.channel.join` |
+
+指定したチャンネルに参加します.
+
+### `/lc channel leave`
+
+| パーミッション |
+|----------------------------------------|
+| `lunaticchat.command.lc.channel.leave` |
+
+現在アクティブなチャンネルから退出します.
+
+### `/lc channel switch <チャンネルID>`
+
+| パーミッション |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.switch` |
+
+指定したチャンネルに切り替えます.チャンネルチャットモード中に他のチャンネルに切り替えたい場合に使用します.
+
+### `/lc channel status`
+
+| パーミッション |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.status` |
+
+現在参加・アクティブ状態のチャンネルの情報を表示します.
+
+### `/lc channel delete <チャンネルID>`
+
+| パーミッション |
+|-----------------------------------------|
+| `lunaticchat.command.lc.channel.delete` |
+
+指定したチャンネルを削除します.チャンネルの作成者(オーナー)である必要があります.
+
diff --git a/docs/src/reference/commands/lc/chatmode.md b/docs/src/reference/commands/lc/chatmode.md
new file mode 100644
index 0000000..659aca5
--- /dev/null
+++ b/docs/src/reference/commands/lc/chatmode.md
@@ -0,0 +1,17 @@
+# /lc chatmode <Badge type="tip" text="v0.6.0" /> <Badge type="tip" text="Paper" />
+
+### `/lc chatmode`
+
+| パーミッション |
+|-----------------------------------|
+| `lunaticchat.command.lc.chatmode` |
+
+現在のチャットモードを表示します.
+
+### `/lc chatmode toggle`
+
+| パーミッション |
+|------------------------------------|
+| `lunaticchat.command.lc.chatmode.toggle` |
+
+チャットモードを切り替えます.
diff --git a/docs/src/reference/commands/lc/settings.md b/docs/src/reference/commands/lc/settings.md
index 86e0a9d..ab595ef 100644
--- a/docs/src/reference/commands/lc/settings.md
+++ b/docs/src/reference/commands/lc/settings.md
@@ -12,7 +12,7 @@ LunaticChat のプレイヤー設定を管理します.値を指定しなか
#### 利用可能なキー
-| キー | 説明 | デフォルト値 |
-|----------|----------------------------------------------------------|--------|
-| `jp` | ローマ字変換の有効化・無効化を切り替えます. | `true` |
-| `notice` | `/tell` や `/reply` でダイレクトメッセージを受信した際に通知を受け取るかどうかを切り替えます. | `true` |
+| キー | 説明 | デフォルト値 |
+|------------|----------------------------------------------------------|--------|
+| `japanese` | ローマ字変換の有効化・無効化を切り替えます. | `true` |
+| `notice` | `/tell` や `/reply` でダイレクトメッセージを受信した際に通知を受け取るかどうかを切り替えます. | `true` |
diff --git a/docs/src/reference/permissions.md b/docs/src/reference/permissions.md
index 0f3cd97..b557ae9 100644
--- a/docs/src/reference/permissions.md
+++ b/docs/src/reference/permissions.md
@@ -34,6 +34,12 @@ LunaticChat のアップデート通知を受け取ります.
[`/reply`](commands/reply.md) コマンドの使用を切り替えます.
+### `lunaticchat.command.lc`
+
+- Default: `non OP`
+
+`/lc` コマンドの使用を切り替えます.
+
### `lunaticchat.command.lc.settings`
- Default: `non OP`
@@ -46,6 +52,66 @@ LunaticChat のアップデート通知を受け取ります.
[`/lc status`](commands/lc/status.md) コマンドの使用を切り替えます.
+### `lunaticchat.command.lc.channel`
+
+- Default: `non OP`
+
+[`/lc channel`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.create`
+
+- Default: `non OP`
+
+[`/lc channel create`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.list`
+
+- Default: `non OP`
+
+[`/lc channel list`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.join`
+
+- Default: `non OP`
+
+[`/lc channel join`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.leave`
+
+- Default: `non OP`
+
+[`/lc channel leave`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.switch`
+
+- Default: `non OP`
+
+[`/lc channel switch`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.status`
+
+- Default: `non OP`
+
+[`/lc channel status`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.channel.delete`
+
+- Default: `non OP`
+
+[`/lc channel delete`](commands/lc/channel.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.chatmode`
+
+- Default: `non OP`
+
+[`/lc chatmode`](commands/lc/chatmode.md) コマンドの使用を切り替えます.
+
+### `lunaticchat.command.lc.chatmode.toggle`
+
+- Default: `non OP`
+
+[`/lc chatmode toggle`](commands/lc/chatmode.md) コマンドの使用を切り替えます.
+
### `lunaticchat.command.jp` <Badge type="danger" text="非推奨: v1.0.0 で削除予定" />
- Default: `non OP`
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt
new file mode 100644
index 0000000..98bb79d
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt
@@ -0,0 +1,43 @@
+package dev.m1sk9.lunaticChat.engine.chat
+
+/**
+ * Represents the chat mode for a player.
+ *
+ * Chat mode determines where messages are sent by default:
+ * - GLOBAL: All messages go to global chat (visible to everyone)
+ * - CHANNEL: All messages go to the active channel (visible to channel members only)
+ *
+ * Players can temporarily override their mode by prefixing messages with '!'.
+ * Chat mode is persisted and maintains its state across server restarts.
+ * Default mode is GLOBAL.
+ */
+enum class ChatMode {
+ /**
+ * Global chat mode - messages visible to all players.
+ */
+ GLOBAL,
+
+ /**
+ * Channel chat mode - messages visible only to channel members.
+ * Requires player to be in an active channel.
+ */
+ CHANNEL,
+ ;
+
+ /**
+ * Returns the opposite chat mode.
+ * Used when player prefixes message with '!'.
+ */
+ fun toggle(): ChatMode =
+ when (this) {
+ GLOBAL -> CHANNEL
+ CHANNEL -> GLOBAL
+ }
+
+ companion object {
+ /**
+ * Default chat mode for all players.
+ */
+ val DEFAULT = GLOBAL
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt
new file mode 100644
index 0000000..b462ede
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt
@@ -0,0 +1,14 @@
+package dev.m1sk9.lunaticChat.engine.chat
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+@Serializable
+data class ChatModeData(
+ val modes: Map<
+ @Serializable(with = UUIDSerializer::class)
+ UUID,
+ ChatMode,
+ > = emptyMap(),
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt
new file mode 100644
index 0000000..cc1f743
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt
@@ -0,0 +1,40 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+/**
+ * Represents a communication channel within the LunaticChat application.
+ *
+ * @property id Unique identifier for the channel.
+ * @property name Name of the channel.
+ * @property description Optional description of the channel.
+ * @property isPrivate Indicates whether the channel is private or public.
+ * @property ownerId UUID of the user who owns the channel.
+ * @property createdAt Timestamp of when the channel was created.
+ */
+@Serializable
+data class Channel(
+ val id: String,
+ val name: String,
+ val description: String? = null,
+ val isPrivate: Boolean = false,
+ @Serializable(with = UUIDSerializer::class)
+ val ownerId: UUID,
+ val createdAt: Long = System.currentTimeMillis(),
+) {
+ init {
+ require(id.matches(CHANNEL_ID_PATTERN)) {
+ "Channel ID must be 3-30 characters long and can only contain letters, numbers, underscores, and hyphens."
+ }
+
+ require(name.isNotBlank()) {
+ "Channel name cannot be blank."
+ }
+ }
+
+ companion object {
+ val CHANNEL_ID_PATTERN = Regex("^[a-zA-Z0-9_-]{3,30}$")
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt
new file mode 100644
index 0000000..02dc1d3
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt
@@ -0,0 +1,7 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+data class ChannelContext(
+ val channelId: String,
+ val channel: Channel,
+ val members: List<ChannelMember>,
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt
new file mode 100644
index 0000000..de942f7
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt
@@ -0,0 +1,11 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ChannelData(
+ val version: Int = 1,
+ val channels: Map<String, Channel> = emptyMap(),
+ val members: Map<String, List<ChannelMember>> = emptyMap(),
+ val activeChannels: Map<String, String> = emptyMap(),
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt
new file mode 100644
index 0000000..1eb49f4
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt
@@ -0,0 +1,22 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+/**
+ * Represents a member of a channel with their role and join timestamp.
+ *
+ * @property channelId The ID of the channel.
+ * @property playerId The UUID of the player.
+ * @property role The role of the member in the channel.
+ * @property joinedAt The timestamp when the member joined the channel.
+ */
+@Serializable
+data class ChannelMember(
+ val channelId: String,
+ @Serializable(with = UUIDSerializer::class)
+ val playerId: UUID,
+ val role: ChannelRole,
+ val joinedAt: Long = System.currentTimeMillis(),
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt
new file mode 100644
index 0000000..be7bf58
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt
@@ -0,0 +1,17 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the role of a user within a channel.
+ *
+ * - OWNER: The creator and primary administrator of the channel.
+ * - MODERATOR: A user with elevated permissions to manage channel content and users.
+ * - MEMBER: A regular user with standard access to the channel.
+ */
+@Serializable
+enum class ChannelRole {
+ OWNER,
+ MODERATOR,
+ MEMBER,
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt
new file mode 100644
index 0000000..6d88a73
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelAlreadyActiveException(
+ val playerId: UUID,
+ val channelId: String,
+) : Exception("Channel $channelId is already active for player $playerId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt
new file mode 100644
index 0000000..deaacb1
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelMemberAlreadyException(
+ playerId: UUID,
+ val channelId: String,
+) : Exception("Player $playerId is already a member of channel $channelId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt
new file mode 100644
index 0000000..8dfd6f2
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelMemberNotFoundException(
+ channelId: String,
+) : Exception("Channel member not found in channel with ID $channelId.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt
new file mode 100644
index 0000000..2cc6625
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt
@@ -0,0 +1,7 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelNoOwnerPermissionException(
+ ownerId: UUID,
+) : Exception("No permission for channel owned by player with ID $ownerId.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt
new file mode 100644
index 0000000..0f60766
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelNotFoundException(
+ channelId: String,
+) : Exception("Channel with ID $channelId not found.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt
new file mode 100644
index 0000000..b66c791
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelNotMemberException(
+ playerId: UUID,
+ channelId: String,
+) : Exception("Player $playerId is not a member of channel $channelId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt
new file mode 100644
index 0000000..4805489
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt
@@ -0,0 +1,6 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelRuntimeException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt
new file mode 100644
index 0000000..21bd654
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt
@@ -0,0 +1,12 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+/**
+ * Exception thrown when there is an error loading channel storage.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception, if any.
+ */
+class ChannelStorageLoadException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt
new file mode 100644
index 0000000..0e9990b
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt
@@ -0,0 +1,12 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+/**
+ * Exception thrown when there is an error saving channel storage.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception, if any.
+ */
+class ChannelStorageSaveException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt
new file mode 100644
index 0000000..85c1e69
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt
@@ -0,0 +1,6 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChatModeStorageException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
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 9328423..838a48b 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,26 @@ sealed class LunaticChatPermissionNode(
object Status : LunaticChatPermissionNode("lunaticchat.command.lc.status")
+ object Channel : LunaticChatPermissionNode("lunaticchat.command.lc.channel")
+
+ object ChannelCreate : LunaticChatPermissionNode("lunaticchat.command.lc.channel.create")
+
+ object ChannelList : LunaticChatPermissionNode("lunaticchat.command.lc.channel.list")
+
+ object ChannelJoin : LunaticChatPermissionNode("lunaticchat.command.lc.channel.join")
+
+ object ChannelLeave : LunaticChatPermissionNode("lunaticchat.command.lc.channel.leave")
+
+ object ChannelSwitch : LunaticChatPermissionNode("lunaticchat.command.lc.channel.switch")
+
+ object ChannelStatus : LunaticChatPermissionNode("lunaticchat.command.lc.channel.status")
+
+ object ChannelDelete : LunaticChatPermissionNode("lunaticchat.command.lc.channel.delete")
+
+ object ChatMode : LunaticChatPermissionNode("lunaticchat.command.lc.chatmode")
+
+ object ChatModeToggle : LunaticChatPermissionNode("lunaticchat.command.lc.chatmode.toggle")
+
object Spy : LunaticChatPermissionNode("lunaticchat.spy")
object NoticeUpdate : LunaticChatPermissionNode("lunaticchat.noticeUpdate")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
index e970ae9..4cb44b4 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
@@ -9,6 +9,7 @@ import java.util.UUID
* @property uuid The unique identifier of the player
* @property japaneseConversionEnabled Whether romaji-to-Japanese conversion is enabled for this player
* @property directMessageNotificationEnabled Whether direct message notifications are enabled for this player
+ * @property channelMessageNotificationEnabled Whether channel message notifications are enabled for this player
*/
@Serializable
data class PlayerChatSettings(
@@ -16,4 +17,5 @@ data class PlayerChatSettings(
val uuid: UUID,
val japaneseConversionEnabled: Boolean = true,
val directMessageNotificationEnabled: Boolean = true,
+ val channelMessageNotificationEnabled: Boolean = true,
)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
index 3916bbf..0e3cd24 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
@@ -16,11 +16,15 @@ import java.util.UUID
* directMessageNotification:
* ceaea267-39dd-3bac-931c-761ada671ebe: true
* another-uuid-here: false
+ * channelMessageNotification:
+ * ceaea267-39dd-3bac-931c-761ada671ebe: true
+ * another-uuid-here: false
* ```
*
* @property version The schema version for future compatibility
* @property japaneseConversion Map of player UUIDs to their Japanese conversion enabled status
* @property directMessageNotification Map of player UUIDs to their direct message notification enabled status
+ * @property channelMessageNotification Map of player UUIDs to their channel message notification enabled status
*/
@Serializable
data class PlayerSettingsData(
@@ -35,4 +39,9 @@ data class PlayerSettingsData(
UUID,
Boolean,
> = emptyMap(),
+ val channelMessageNotification: Map<
+ @Serializable(with = UUIDASStringSerializer::class)
+ UUID,
+ Boolean,
+ > = emptyMap(),
)
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 6525b55..d238dd6 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
@@ -1,11 +1,16 @@
package dev.m1sk9.lunaticChat.paper
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand
import dev.m1sk9.lunaticChat.paper.command.impl.TellCommand
import dev.m1sk9.lunaticChat.paper.command.impl.lc.LunaticChatCommand
import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandlerRegistry
+import dev.m1sk9.lunaticChat.paper.command.setting.handler.ChannelMessageNoticeSettingHandler
import dev.m1sk9.lunaticChat.paper.command.setting.handler.DirectMessageNoticeSettingHandler
import dev.m1sk9.lunaticChat.paper.command.setting.handler.JapaneseConversionSettingHandler
import dev.m1sk9.lunaticChat.paper.common.UpdateCheckResult
@@ -27,6 +32,10 @@ class LunaticChat :
// Public API - accessed by commands (maintain backward compatibility)
lateinit var directMessageHandler: DirectMessageHandler
lateinit var languageManager: LanguageManager
+ var channelManager: ChannelManager? = null
+ var channelMembershipManager: ChannelMembershipManager? = null
+ var chatModeManager: ChatModeManager? = null
+ var channelMessageHandler: ChannelMessageHandler? = null
// Private services
private lateinit var services: ServiceContainer
@@ -60,6 +69,10 @@ class LunaticChat :
// Set public API properties (for command access)
directMessageHandler = services.directMessageHandler
languageManager = services.languageManager
+ channelManager = services.channelManager
+ channelMembershipManager = services.channelMembershipManager
+ chatModeManager = services.chatModeManager
+ channelMessageHandler = services.channelMessageHandler
// Schedule periodic tasks
serviceInitializer.schedulePeriodicTasks()
@@ -96,6 +109,16 @@ class LunaticChat :
),
)
+ // Always register channel message notification setting if channel is enabled
+ if (services.channelManager != null) {
+ settingHandlerRegistry.register(
+ ChannelMessageNoticeSettingHandler(
+ services.playerSettingsManager,
+ services.languageManager,
+ ),
+ )
+ }
+
// Conditionally register Japanese conversion setting
if (services.romajiConverter != null) {
settingHandlerRegistry.register(
@@ -113,7 +136,7 @@ class LunaticChat :
)
// Conditionally register /reply command if quick replies are enabled
- if (configuration.features.quickRepliesEnabled.enabled) {
+ if (configuration.features.quickReplies.enabled) {
commandRegistry.registerAll(
ReplyCommand(this, services.directMessageHandler, services.languageManager),
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt
index 6cd700b..3cd9e73 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt
@@ -1,6 +1,10 @@
package dev.m1sk9.lunaticChat.paper
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
@@ -15,10 +19,18 @@ import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
* @property playerSettingsManager Always available (required for DM notifications)
* @property directMessageHandler Always available (core feature)
* @property romajiConverter Optional (only when Japanese conversion feature is enabled)
+ * @property channelManager Optional (only when channel chat feature is enabled)
+ * @property channelMembershipManager Optional (only when channel chat feature is enabled)
+ * @property chatModeManager Optional (only when channel chat feature is enabled)
+ * @property channelMessageHandler Optional (only when channel chat feature is enabled)
*/
data class ServiceContainer(
val languageManager: LanguageManager,
val playerSettingsManager: PlayerSettingsManager,
val directMessageHandler: DirectMessageHandler,
val romajiConverter: RomanjiConverter? = null,
+ val channelManager: ChannelManager? = null,
+ val channelMembershipManager: ChannelMembershipManager? = null,
+ val chatModeManager: ChatModeManager? = null,
+ val channelMessageHandler: ChannelMessageHandler? = null,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
index e39f04f..35f70d8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
@@ -1,7 +1,13 @@
package dev.m1sk9.lunaticChat.paper
import dev.m1sk9.lunaticChat.engine.converter.GoogleIMEClient
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeStorage
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelStorage
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.ConversionCache
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
@@ -14,6 +20,16 @@ import java.util.logging.Logger
import kotlin.time.Duration.Companion.milliseconds
/**
+ * Container for channel-related components.
+ */
+private data class ChannelComponents(
+ val channelManager: ChannelManager,
+ val channelMembershipManager: ChannelMembershipManager,
+ val chatModeManager: ChatModeManager,
+ val channelMessageHandler: ChannelMessageHandler,
+)
+
+/**
* Handles initialization and shutdown of all plugin services.
*
* This class centralizes service initialization logic, ensuring proper
@@ -26,6 +42,10 @@ class ServiceInitializer(
private val logger: Logger,
) {
private var conversionCache: ConversionCache? = null
+ private var channelManager: ChannelManager? = null
+ private var channelMembershipManager: ChannelMembershipManager? = null
+ private var chatModeManager: ChatModeManager? = null
+ private var channelMessageHandler: ChannelMessageHandler? = null
/**
* Initializes all services in dependency order.
@@ -34,7 +54,8 @@ class ServiceInitializer(
* 1. LanguageManager (required by all features)
* 2. PlayerSettingsManager (required for DM notifications)
* 3. Japanese Conversion (optional, config-dependent)
- * 4. DirectMessageHandler (depends on settings manager and romaji converter)
+ * 4. ChannelStorage
+ * 5. DirectMessageHandler (depends on settings manager and romaji converter)
*
* @return ServiceContainer with all initialized services
*/
@@ -60,7 +81,19 @@ class ServiceInitializer(
null
}
- // 4. Initialize handlers
+ // 4. Initialize channel manager, membership manager, chat mode manager, and channel message handler
+ val channelComponents =
+ if (configuration.features.channelChat.enabled) {
+ initializeChannelManager(playerSettingsManager)
+ } else {
+ null
+ }
+ val channelManager = channelComponents?.channelManager
+ val channelMembershipManager = channelComponents?.channelMembershipManager
+ val chatModeManager = channelComponents?.chatModeManager
+ val channelMessageHandler = channelComponents?.channelMessageHandler
+
+ // 5. Initialize handlers
val directMessageHandler =
DirectMessageHandler(
settingsManager = playerSettingsManager,
@@ -72,6 +105,10 @@ class ServiceInitializer(
playerSettingsManager = playerSettingsManager,
directMessageHandler = directMessageHandler,
romajiConverter = romajiConverter,
+ channelManager = channelManager,
+ channelMembershipManager = channelMembershipManager,
+ chatModeManager = chatModeManager,
+ channelMessageHandler = channelMessageHandler,
)
}
@@ -136,6 +173,67 @@ class ServiceInitializer(
}
/**
+ * Initializes channel manager, membership manager, chat mode manager, and channel message handler with storage.
+ */
+ private fun initializeChannelManager(settingsManager: PlayerSettingsManager): ChannelComponents {
+ val channelsFile = plugin.dataFolder.resolve("channels.json").toPath()
+ val storage =
+ ChannelStorage(
+ channelsFile = channelsFile,
+ plugin = plugin,
+ logger = logger,
+ )
+
+ val manager =
+ ChannelManager(
+ storage = storage,
+ logger = logger,
+ )
+ manager.initialize()
+ channelManager = manager
+
+ val membershipManager =
+ ChannelMembershipManager(
+ channelManager = manager,
+ logger = logger,
+ )
+ channelMembershipManager = membershipManager
+
+ val chatModeFile = plugin.dataFolder.resolve("chatmodes.json").toPath()
+ val chatModeStorage =
+ ChatModeStorage(
+ dataFile = chatModeFile,
+ logger = logger,
+ )
+
+ val chatMode =
+ ChatModeManager(
+ storage = chatModeStorage,
+ logger = logger,
+ )
+ chatMode.initialize()
+ chatModeManager = chatMode
+
+ val messageHandler =
+ ChannelMessageHandler(
+ settingsManager = settingsManager,
+ channelManager = manager,
+ logger =
+ io.ktor.util.logging
+ .KtorSimpleLogger("ChannelMessageHandler"),
+ )
+ channelMessageHandler = messageHandler
+
+ logger.info("Channel manager, membership manager, chat mode manager, and channel message handler initialized successfully.")
+ return ChannelComponents(
+ channelManager = manager,
+ channelMembershipManager = membershipManager,
+ chatModeManager = chatMode,
+ channelMessageHandler = messageHandler,
+ )
+ }
+
+ /**
* Schedules periodic tasks such as cache saving.
*/
fun schedulePeriodicTasks() {
@@ -158,5 +256,7 @@ class ServiceInitializer(
fun shutdown(services: ServiceContainer) {
services.playerSettingsManager.saveToDisk()
conversionCache?.saveToDisk()
+ services.channelManager?.saveToDisk()
+ services.chatModeManager?.shutdown()
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt
new file mode 100644
index 0000000..93a7f04
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManager.kt
@@ -0,0 +1,103 @@
+package dev.m1sk9.lunaticChat.paper.chat
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.logging.Logger
+
+/**
+ * Manages player chat modes with persistence.
+ *
+ * Chat modes determine where player messages are sent by default.
+ * Modes are persisted across server restarts.
+ *
+ * @property storage Chat mode storage layer
+ * @property logger Logger for operations
+ */
+class ChatModeManager(
+ private val storage: ChatModeStorage,
+ private val logger: Logger,
+) {
+ private val chatModes = ConcurrentHashMap<UUID, ChatMode>()
+
+ /**
+ * Initializes the manager by loading data from storage.
+ */
+ fun initialize() {
+ val data = storage.loadFromDisk()
+ chatModes.putAll(data.modes)
+ logger.info("ChatModeManager initialized with ${chatModes.size} saved modes")
+ }
+
+ /**
+ * Gets a player's current chat mode.
+ *
+ * @param playerId The player's UUID
+ * @return The player's chat mode, or DEFAULT if not set
+ */
+ fun getChatMode(playerId: UUID): ChatMode = chatModes.getOrDefault(playerId, ChatMode.Companion.DEFAULT)
+
+ /**
+ * Sets a player's chat mode.
+ *
+ * @param playerId The player's UUID
+ * @param mode The chat mode to set
+ */
+ fun setChatMode(
+ playerId: UUID,
+ mode: ChatMode,
+ ) {
+ chatModes[playerId] = mode
+ saveToStorage()
+ }
+
+ /**
+ * Toggles a player's chat mode between GLOBAL and CHANNEL.
+ *
+ * @param playerId The player's UUID
+ * @return The new chat mode after toggling
+ */
+ fun toggleChatMode(playerId: UUID): ChatMode {
+ val currentMode = getChatMode(playerId)
+ val newMode = currentMode.toggle()
+ setChatMode(playerId, newMode)
+ return newMode
+ }
+
+ /**
+ * Removes a player's chat mode setting (reverts to default).
+ *
+ * @param playerId The player's UUID
+ */
+ fun removeChatMode(playerId: UUID) {
+ chatModes.remove(playerId)
+ saveToStorage()
+ }
+
+ /**
+ * Saves current state to storage asynchronously.
+ */
+ private fun saveToStorage() {
+ val data = ChatModeData(modes = chatModes.toMap())
+ storage.queueAsyncSave(data)
+ }
+
+ /**
+ * Forces a synchronous save to storage.
+ * Should only be called during plugin shutdown.
+ */
+ fun saveToDisk() {
+ val data = ChatModeData(modes = chatModes.toMap())
+ storage.saveToDisk(data)
+ }
+
+ /**
+ * Shuts down the chat mode manager and its storage executor.
+ * Should be called during plugin disable to prevent thread leaks.
+ */
+ fun shutdown() {
+ saveToDisk()
+ storage.shutdown()
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt
new file mode 100644
index 0000000..77fcdb3
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeStorage.kt
@@ -0,0 +1,98 @@
+package dev.m1sk9.lunaticChat.paper.chat
+
+import dev.m1sk9.lunaticChat.engine.chat.ChatModeData
+import dev.m1sk9.lunaticChat.engine.exception.ChatModeStorageException
+import kotlinx.serialization.json.Json
+import java.nio.file.Path
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.logging.Logger
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+
+/**
+ * Handles JSON storage for chat mode data.
+ * Provides async save queue and synchronous save for shutdown.
+ *
+ * @property dataFile Path to chatmodes.json
+ * @property logger Logger for operations
+ */
+class ChatModeStorage(
+ private val dataFile: Path,
+ private val logger: Logger,
+) {
+ private val json =
+ Json {
+ prettyPrint = true
+ ignoreUnknownKeys = true
+ }
+
+ private val saveExecutor = Executors.newSingleThreadExecutor()
+
+ /**
+ * Loads chat mode data from disk.
+ *
+ * @return ChatModeData loaded from file, or empty data if file doesn't exist
+ * @throws ChatModeStorageException if loading fails
+ */
+ fun loadFromDisk(): ChatModeData {
+ if (!dataFile.exists()) {
+ logger.info("Chat mode data file not found, starting with empty data")
+ return ChatModeData()
+ }
+
+ return try {
+ val jsonContent = dataFile.readText()
+ json.decodeFromString<ChatModeData>(jsonContent)
+ } catch (e: Exception) {
+ throw ChatModeStorageException("Failed to load chat mode data from ${dataFile.fileName}", e)
+ }
+ }
+
+ /**
+ * Queues an asynchronous save operation.
+ *
+ * @param data ChatModeData to save
+ */
+ fun queueAsyncSave(data: ChatModeData) {
+ saveExecutor.submit {
+ try {
+ saveToDisk(data)
+ } catch (e: Exception) {
+ logger.severe("Failed to save chat mode data: ${e.message}")
+ }
+ }
+ }
+
+ /**
+ * Saves chat mode data to disk synchronously.
+ *
+ * @param data ChatModeData to save
+ * @throws ChatModeStorageException if saving fails
+ */
+ fun saveToDisk(data: ChatModeData) {
+ try {
+ val jsonContent = json.encodeToString(data)
+ dataFile.writeText(jsonContent)
+ } catch (e: Exception) {
+ throw ChatModeStorageException("Failed to save chat mode data to ${dataFile.fileName}", e)
+ }
+ }
+
+ /**
+ * Shuts down the async save executor.
+ * Should be called during plugin disable.
+ */
+ fun shutdown() {
+ saveExecutor.shutdown()
+ try {
+ if (!saveExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ saveExecutor.shutdownNow()
+ }
+ } catch (e: InterruptedException) {
+ saveExecutor.shutdownNow()
+ Thread.currentThread().interrupt()
+ }
+ }
+}
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
new file mode 100644
index 0000000..fc8d6da
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt
@@ -0,0 +1,288 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelContext
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.logging.Logger
+import kotlin.collections.forEach
+
+class ChannelManager(
+ private val storage: ChannelStorage,
+ private val logger: Logger,
+) {
+ private val channelsCache = ConcurrentHashMap<String, Channel>()
+ private val membersCache = ConcurrentHashMap<String, CopyOnWriteArrayList<ChannelMember>>()
+ private val activeChannels = ConcurrentHashMap<UUID, String>()
+
+ /**
+ * Initializes the ChannelManager by loading data from storage.
+ */
+ fun initialize() {
+ val data = storage.loadFromDisk()
+ channelsCache.putAll(data.channels)
+ data.members.forEach { (channelId, members) ->
+ membersCache[channelId] = CopyOnWriteArrayList(members)
+ }
+ data.activeChannels.forEach { (playerIdStr, channelId) ->
+ try {
+ val playerId = UUID.fromString(playerIdStr)
+ activeChannels[playerId] = channelId
+ } catch (e: IllegalArgumentException) {
+ logger.warning("Invalid UUID in activeChannels: $playerIdStr")
+ }
+ }
+ logger.info("ChannelManager initialized with ${channelsCache.size} channels and ${activeChannels.size} active channels.")
+ }
+
+ /**
+ * Creates a new channel.
+ *
+ * @param channel The channel to create.
+ * @return Result containing the created channel or an error if the channel already exists.
+ * @throws ChannelNotFoundException if a channel with the same ID already exists.
+ */
+ fun createChannel(channel: Channel): Result<Channel> {
+ if (channelsCache.containsKey(channel.id)) {
+ return Result.failure(ChannelNotFoundException(channel.id))
+ }
+
+ channelsCache[channel.id] = channel
+
+ val ownerMember =
+ ChannelMember(
+ channelId = channel.id,
+ playerId = channel.ownerId,
+ role = ChannelRole.OWNER,
+ )
+ membersCache[channel.id] = CopyOnWriteArrayList(listOf(ownerMember))
+
+ // Set the owner's active channel
+ setPlayerChannel(channel.ownerId, channel.id)
+
+ saveToStorage()
+ logger.info("Created new channel with ID ${channel.id}.")
+ return Result.success(channel)
+ }
+
+ /**
+ * Deletes a channel.
+ *
+ * @param channelId The ID of the channel to delete.
+ * @param requesterId The ID of the player requesting the deletion.
+ * @return Result indicating success or failure of the deletion.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun deleteChannel(
+ channelId: String,
+ requesterId: UUID,
+ ): Result<Unit> {
+ val channel =
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ if (channel.ownerId != requesterId) {
+ return Result.failure(ChannelNoOwnerPermissionException(requesterId))
+ }
+
+ // Clear active channel for all players who have this channel active
+ activeChannels.entries.removeIf { it.value == channelId }
+
+ channelsCache.remove(channelId)
+ membersCache.remove(channelId)
+
+ saveToStorage()
+ logger.info("Owner with ID $requesterId deleted channel with ID $channelId.")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Retrieves a channel by its ID.
+ *
+ * @param channelId The ID of the channel to retrieve.
+ * @return Result containing the channel or an error if not found.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun getChannel(channelId: String): Result<Channel> {
+ val channel =
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+ return Result.success(channel)
+ }
+
+ /**
+ * Retrieves all channels.
+ *
+ * @return Result containing the list of all channels.
+ */
+ fun getAllChannels(): Result<List<Channel>> = Result.success(channelsCache.values.toList())
+
+ /**
+ * Retrieves all public channels.
+ *
+ * @return Result containing the list of public channels.
+ */
+ fun getPublicChannels(): Result<List<Channel>> {
+ val channels =
+ channelsCache.values
+ .filter { !it.isPrivate }
+ .sortedBy { it.name }
+ return Result.success(channels)
+ }
+
+ /**
+ * Retrieves members of a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @return Result containing the list of channel members or an error if the channel is not found.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun getChannelMembers(channelId: String): Result<List<ChannelMember>> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members = membersCache[channelId]?.toList() ?: emptyList()
+ return Result.success(members)
+ }
+
+ /**
+ * Adds a member to a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @param playerId The UUID of the player to add.
+ * @param role The role of the new member.
+ * @return Result indicating success or failure of the operation.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun addMember(
+ channelId: String,
+ playerId: UUID,
+ role: ChannelRole,
+ ): Result<Unit> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members =
+ membersCache.getOrPut(channelId) {
+ CopyOnWriteArrayList()
+ }
+ val newMember =
+ ChannelMember(
+ channelId = channelId,
+ playerId = playerId,
+ role = role,
+ joinedAt = System.currentTimeMillis(),
+ )
+
+ members.add(newMember)
+ saveToStorage()
+
+ return Result.success(Unit)
+ }
+
+ /**
+ * Removes a member from a channel.
+ *
+ * @param channelId The ID of the channel.
+ * @param playerId The UUID of the player to remove.
+ * @return Result indicating success or failure of the operation.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ */
+ fun removeMember(
+ channelId: String,
+ playerId: UUID,
+ ): Result<Unit> {
+ channelsCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val members =
+ membersCache[channelId]
+ ?: return Result.failure(ChannelNotFoundException(channelId))
+
+ val removed = members.removeIf { it.playerId == playerId }
+ if (!removed) {
+ return Result.failure(ChannelNotFoundException(channelId))
+ }
+
+ saveToStorage()
+ return Result.success(Unit)
+ }
+
+ /**
+ * Saves the current state of channels and members to storage asynchronously.
+ */
+ private fun saveToStorage() {
+ val data =
+ ChannelData(
+ channels = channelsCache.toMap(),
+ members = membersCache.mapValues { it.value.toList() },
+ activeChannels = activeChannels.mapKeys { it.key.toString() },
+ )
+ storage.queueAsyncSave(data)
+ logger.fine("${channelsCache.size} channels queued for saving to storage.")
+ }
+
+ /**
+ * Saves the current state of channels and members to storage synchronously.
+ * Should only br called during server shutdown.
+ */
+ fun saveToDisk() {
+ val data =
+ ChannelData(
+ channels = channelsCache.toMap(),
+ members = membersCache.mapValues { it.value.toList() },
+ activeChannels = activeChannels.mapKeys { it.key.toString() },
+ )
+ storage.saveToDisk(data)
+ }
+
+ /**
+ * Gets the active channel of a player.
+ *
+ * @param playerId The UUID of the player.
+ * @return The ID of the active channel or null if none is set.
+ */
+ fun getPlayerChannel(playerId: UUID): String? = activeChannels[playerId]
+
+ /**
+ * Gets the full channel context (channel and members) of a player's active channel.
+ *
+ * @param playerId The UUID of the player.
+ * @return The ChannelContext of the active channel or null if none is set.
+ */
+ fun getPlayerChannelContext(playerId: UUID): ChannelContext? {
+ val channelId = activeChannels[playerId] ?: return null
+ val channel = channelsCache[channelId] ?: return null
+ val members = membersCache[channelId]?.toList() ?: return null
+
+ return ChannelContext(
+ channelId = channelId,
+ channel = channel,
+ members = members,
+ )
+ }
+
+ /**
+ * Sets the active channel of a player.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel to set as active, or null to clear the active channel.
+ */
+ fun setPlayerChannel(
+ playerId: UUID,
+ channelId: String?,
+ ) {
+ if (channelId == null) {
+ activeChannels.remove(playerId)
+ } else {
+ activeChannels[playerId] = channelId
+ }
+ saveToStorage()
+ }
+}
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
new file mode 100644
index 0000000..6d6ad8d
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt
@@ -0,0 +1,224 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelRuntimeException
+import java.util.UUID
+import java.util.logging.Logger
+
+class ChannelMembershipManager(
+ private val channelManager: ChannelManager,
+ private val logger: Logger,
+) {
+ /**
+ * Checks if a player is a member of a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result containing true if the player is a member, false otherwise.
+ */
+ fun isMember(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Boolean> =
+ channelManager.getChannelMembers(channelId).map { members ->
+ members.any { it.playerId == playerId }
+ }
+
+ /**
+ * Gets the role of a member in a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result containing the ChannelRole of the member.
+ * @throws ChannelNotMemberException if the player is not a member of the channel.
+ */
+ fun getMemberRole(
+ playerId: UUID,
+ channelId: String,
+ ): Result<ChannelRole> =
+ channelManager.getChannelMembers(channelId).mapCatching { members ->
+ members
+ .find {
+ it.playerId == playerId
+ }?.role ?: throw ChannelNotMemberException(playerId, channelId)
+ }
+
+ /**
+ * Checks if a player has a specific role or higher in a channel.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @param requireRole The required ChannelRole.
+ * @return Result containing true if the player has the required role or higher, false otherwise.
+ */
+ fun hasRole(
+ playerId: UUID,
+ channelId: String,
+ requireRole: ChannelRole,
+ ): Result<Boolean> =
+ getMemberRole(playerId, channelId).fold(
+ onSuccess = { playerRole ->
+ val hasRequiredRole =
+ when (requireRole) {
+ ChannelRole.MEMBER -> true
+ ChannelRole.MODERATOR -> playerRole in setOf(ChannelRole.MODERATOR, ChannelRole.OWNER)
+ ChannelRole.OWNER -> playerRole == ChannelRole.OWNER
+ }
+ Result.success(hasRequiredRole)
+ },
+ onFailure = { Result.success(false) },
+ )
+
+ /**
+ * Adds a player to a channel as a member and sets it as their active channel.
+ * If the player is already a member of other channels, they remain members of those channels.
+ * Only one channel can be active at a time.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ * @throws ChannelMemberAlreadyException if the player is already a member of this channel.
+ * @throws ChannelRuntimeException for other runtime errors.
+ */
+ fun joinChannel(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Unit> {
+ // Check if channel exists
+ val channel =
+ channelManager.getChannel(channelId).getOrElse {
+ return Result.failure(
+ ChannelNotFoundException(channelId),
+ )
+ }
+
+ // Check if this channel is already active
+ val currentActiveChannel = channelManager.getPlayerChannel(playerId)
+ if (currentActiveChannel == channelId) {
+ return Result.failure(
+ ChannelAlreadyActiveException(playerId, channelId),
+ )
+ }
+
+ // Check if player is already a member
+ val isAlreadyMember =
+ isMember(playerId, channelId).getOrElse {
+ return Result.failure(
+ ChannelRuntimeException("Failed to check membership for player $playerId in channel $channelId", it),
+ )
+ }
+
+ // If already a member, return error
+ if (isAlreadyMember) {
+ return Result.failure(
+ ChannelMemberAlreadyException(playerId, channelId),
+ )
+ }
+
+ // Add as member
+ channelManager.addMember(channelId, playerId, ChannelRole.MEMBER).getOrElse {
+ return Result.failure(it)
+ }
+
+ // Set as active channel
+ channelManager.setPlayerChannel(playerId, channelId)
+ logger.info("Player $playerId joined channel $channelId")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Clears the player's active channel without removing them from channel membership.
+ * The player remains a member of the channel and can rejoin by using the join command.
+ *
+ * @param playerId The UUID of the player.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotMemberException if the player does not have an active channel.
+ */
+ fun leaveChannel(playerId: UUID): Result<Unit> {
+ val currentChannel =
+ channelManager.getPlayerChannel(playerId)
+ ?: return Result.failure(
+ ChannelNotMemberException(playerId, "no active channel"),
+ )
+
+ // Clear active channel
+ channelManager.setPlayerChannel(playerId, null)
+ logger.info("Player $playerId left active channel $currentChannel (still a member)")
+ return Result.success(Unit)
+ }
+
+ /**
+ * Switches the player's active channel to a channel they are already a member of.
+ *
+ * @param playerId The UUID of the player.
+ * @param channelId The ID of the channel to switch to.
+ * @return Result indicating success or failure.
+ * @throws ChannelNotFoundException if the channel does not exist.
+ * @throws ChannelNotMemberException if the player is not a member of the channel.
+ */
+ fun switchChannel(
+ playerId: UUID,
+ channelId: String,
+ ): Result<Unit> {
+ // Check if channel exists
+ val channel =
+ channelManager.getChannel(channelId).getOrElse {
+ return Result.failure(
+ ChannelNotFoundException(channelId),
+ )
+ }
+
+ // Check if this channel is already active
+ val currentActiveChannel = channelManager.getPlayerChannel(playerId)
+ if (currentActiveChannel == channelId) {
+ return Result.failure(
+ ChannelAlreadyActiveException(playerId, channelId),
+ )
+ }
+
+ // Check if player is a member
+ val isAlreadyMember =
+ isMember(playerId, channelId).getOrElse {
+ return Result.failure(
+ ChannelRuntimeException("Failed to check membership for player $playerId in channel $channelId", it),
+ )
+ }
+
+ if (!isAlreadyMember) {
+ return Result.failure(
+ ChannelNotMemberException(playerId, channelId),
+ )
+ }
+
+ // Set as active channel
+ channelManager.setPlayerChannel(playerId, channelId)
+ logger.info("Player $playerId switched to channel $channelId")
+ return Result.success(Unit)
+ }
+
+ /**
+ * 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.
+ */
+ fun getPlayerChannels(playerId: UUID): Result<List<String>> {
+ val allChannels =
+ channelManager.getAllChannels().getOrElse {
+ return Result.failure(it)
+ }
+
+ val playerChannels =
+ allChannels
+ .filter { channel ->
+ isMember(playerId, channel.id).getOrElse { false }
+ }.map { it.id }
+
+ return Result.success(playerChannels)
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt
new file mode 100644
index 0000000..7176014
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelStorage.kt
@@ -0,0 +1,99 @@
+package dev.m1sk9.lunaticChat.paper.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData
+import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageLoadException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelStorageSaveException
+import kotlinx.serialization.json.Json
+import org.bukkit.plugin.java.JavaPlugin
+import java.nio.file.Path
+import java.util.logging.Logger
+import kotlin.io.path.bufferedReader
+import kotlin.io.path.exists
+import kotlin.io.path.writeText
+
+/**
+ * Manages the storage of channel data on disk.
+ *
+ * @property channelsFile The path to the file where channel data is stored.
+ * @property plugin The JavaPlugin instance for accessing plugin resources.
+ * @property logger The logger for logging messages.
+ */
+class ChannelStorage(
+ private val channelsFile: Path,
+ private val plugin: JavaPlugin,
+ private val logger: Logger,
+) {
+ private val json =
+ Json {
+ prettyPrint = true
+ ignoreUnknownKeys = true
+ }
+
+ /**
+ * Loads channel data from disk.
+ *
+ * @return The loaded ChannelData.
+ * @throws ChannelStorageLoadException if there is an error loading the data.
+ */
+ fun loadFromDisk(): ChannelData {
+ if (!channelsFile.exists()) {
+ logger.warning("Channel storage not found, will create a new one.")
+ return ChannelData()
+ }
+
+ return try {
+ val jsonContent =
+ channelsFile.bufferedReader().use {
+ it.readText()
+ }
+ json.decodeFromString(ChannelData.serializer(), jsonContent).also {
+ logger.info("Successfully loaded channels from ${channelsFile.fileName}.")
+ }
+ } catch (e: Exception) {
+ throw ChannelStorageLoadException(
+ "Failed to load channels from ${channelsFile.fileName}: ${e.message}",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Saves channel data to disk.
+ *
+ * @param data The ChannelData to save.
+ * @throws ChannelStorageSaveException if there is an error saving the data.
+ */
+ fun saveToDisk(data: ChannelData) {
+ try {
+ val jsonContent = json.encodeToString(ChannelData.serializer(), data)
+ channelsFile.writeText(jsonContent).also {
+ logger.fine("Successfully saved channels from ${channelsFile.fileName}.")
+ }
+ } catch (e: Exception) {
+ throw ChannelStorageSaveException(
+ "Failed to save channels to ${channelsFile.fileName}: ${e.message}",
+ e,
+ )
+ }
+ }
+
+ /**
+ * Queues an asynchronous save of channel data to disk.
+ *
+ * @param data The ChannelData to save.
+ * @throws ChannelStorageSaveException if there is an error saving the data.
+ */
+ fun queueAsyncSave(data: ChannelData) {
+ plugin.server.scheduler.runTaskAsynchronously(
+ plugin,
+ Runnable {
+ try {
+ saveToDisk(data)
+ } catch (e: ChannelStorageSaveException) {
+ logger.severe("Error saving channel data asynchronously: ${e.message}")
+ e.printStackTrace()
+ }
+ },
+ )
+ }
+}
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
new file mode 100644
index 0000000..70756d9
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt
@@ -0,0 +1,80 @@
+package dev.m1sk9.lunaticChat.paper.chat.handler
+
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
+import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification
+import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
+import dev.m1sk9.lunaticChat.paper.config.ConfigManager
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import io.ktor.util.logging.Logger
+import net.kyori.adventure.text.Component
+import org.bukkit.Bukkit
+import org.bukkit.entity.Player
+
+class ChannelMessageHandler(
+ private val settingsManager: PlayerSettingsManager?,
+ private val channelManager: ChannelManager,
+ private val logger: Logger,
+) {
+ private var lunaticChatConfiguration = ConfigManager.getConfiguration()
+
+ fun sendChannelMessage(
+ player: Player,
+ message: String,
+ ): Boolean {
+ val playerId = player.uniqueId
+ val context =
+ channelManager.getPlayerChannelContext(playerId)
+ ?: return false
+ val formattedMessage = formatChannelMessage(player.name, context.channel.name, message)
+
+ // Play notification sound to sender if enabled
+ settingsManager?.let { manager ->
+ val senderSettings = manager.getSettings(playerId)
+ if (senderSettings.channelMessageNotificationEnabled) {
+ player.playMessageSendNotification()
+ }
+ }
+
+ SpyPermissionManager
+ .getDirectMessageSpyPlayers()
+ .values
+ .filter { it.isOnline && it.uniqueId != playerId }
+ .forEach { it.sendMessage(formattedMessage) }
+ context.members.forEach { member ->
+ Bukkit.getPlayer(member.playerId)?.let { memberPlayer ->
+ if (memberPlayer.isOnline) {
+ memberPlayer.sendMessage(formattedMessage)
+
+ // Play notification sound to receiver if enabled and not the sender
+ if (memberPlayer.uniqueId != playerId) {
+ settingsManager?.let { manager ->
+ val receiverSettings = manager.getSettings(memberPlayer.uniqueId)
+ if (receiverSettings.channelMessageNotificationEnabled) {
+ memberPlayer.playChannelReceiveNotification()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ logger.info("Channel Message from ${player.name} in ${context.channel.name}: $message")
+ return true
+ }
+
+ private fun formatChannelMessage(
+ senderName: String,
+ channelName: String,
+ message: String,
+ ): Component {
+ val format = lunaticChatConfiguration.messageFormat.channelMessageFormat
+ val text =
+ format
+ .replace("{sender}", senderName)
+ .replace("{channel}", channelName)
+ .replace("{message}", message)
+
+ return Component.text(text)
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
index 4683c08..6155381 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/handler/DirectMessageHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt
@@ -1,8 +1,8 @@
-package dev.m1sk9.lunaticChat.paper.command.handler
+package dev.m1sk9.lunaticChat.paper.chat.handler
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
import dev.m1sk9.lunaticChat.paper.common.playDirectMessageNotification
-import dev.m1sk9.lunaticChat.paper.common.playDirectMessageSendNotification
+import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification
import dev.m1sk9.lunaticChat.paper.config.ConfigManager
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
@@ -59,10 +59,18 @@ class DirectMessageHandler(
/**
* Clears message history for a player (called on disconnect).
+ * Removes entries where this player is either the sender or recipient.
*/
fun clearPlayer(player: Player) {
- lastMessager.remove(player.uniqueId)
- lastRecipient.remove(player.uniqueId)
+ val playerId = player.uniqueId
+
+ // Remove entries where this player is the sender
+ lastMessager.remove(playerId)
+ lastRecipient.remove(playerId)
+
+ // Remove entries where this player is the recipient
+ lastMessager.entries.removeIf { it.value == playerId }
+ lastRecipient.entries.removeIf { it.value == playerId }
}
/**
@@ -72,7 +80,7 @@ class DirectMessageHandler(
*
* @return true if message was sent successfully
*/
- suspend fun sendDirectMessage(
+ fun sendDirectMessage(
sender: Player,
recipient: Player,
message: String,
@@ -82,15 +90,20 @@ class DirectMessageHandler(
val senderSettings = settingsManager?.getSettings(sender.uniqueId)
val recipientSettings = settingsManager?.getSettings(recipient.uniqueId)
+ // Handle romaji conversion if enabled (requires blocking for HTTP call)
val displayMessage =
- senderSettings
- ?.takeIf { it.japaneseConversionEnabled }
- ?.let { romanjiConverter }
- ?.runCatching {
- convert(message)
- ?.let { "$message §e($it)" }
- ?: message
- }?.getOrNull() ?: message
+ if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) {
+ runCatching {
+ kotlinx.coroutines.runBlocking {
+ romanjiConverter
+ ?.convert(message)
+ ?.let { "$message §e($it)" }
+ ?: message
+ }
+ }.getOrNull() ?: message
+ } else {
+ message
+ }
val format = lunaticChatConfiguration.messageFormat.directMessageFormat
@@ -105,7 +118,7 @@ class DirectMessageHandler(
sender.apply {
sendMessage(userMessage)
takeIf { senderSettings?.directMessageNotificationEnabled == true }
- ?.playDirectMessageSendNotification()
+ ?.playMessageSendNotification()
}
recipient.apply {
sendMessage(userMessage)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
index 817c002..1fe4835 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/core/LunaticCommand.kt
@@ -98,7 +98,7 @@ abstract class LunaticCommand(
if (isPlayerOnly && !ctx.isPlayer) {
return CommandResult.Failure(
MessageFormatter.formatError(
- plugin.languageManager.getMessage("playerOnlyCommand"),
+ plugin.languageManager.getMessage("general.playerOnlyCommand"),
),
)
}
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 af7a32c..e1a997e 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
@@ -5,17 +5,16 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.m1sk9.lunaticChat.engine.command.CommandResult
import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.command.annotation.Command
import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import kotlinx.coroutines.runBlocking
@Command(
name = "reply",
@@ -58,13 +57,11 @@ class ReplyCommand(
dmHandler.getReplyTarget(sender)
?: return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("replyTargetNotFound"),
+ languageManager.getMessage("directMessage.replyTargetNotFound"),
),
)
- runBlocking {
- dmHandler.sendDirectMessage(sender, target, message)
- }
+ dmHandler.sendDirectMessage(sender, target, message)
return CommandResult.Success
}
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 1238138..5375b08 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
@@ -7,17 +7,16 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder
import dev.m1sk9.lunaticChat.engine.command.CommandResult
import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.command.annotation.Command
import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
-import dev.m1sk9.lunaticChat.paper.command.handler.DirectMessageHandler
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
import io.papermc.paper.command.brigadier.CommandSourceStack
import io.papermc.paper.command.brigadier.Commands
-import kotlinx.coroutines.runBlocking
import org.bukkit.Bukkit
import java.util.concurrent.CompletableFuture
@@ -69,21 +68,19 @@ class TellCommand(
Bukkit.getPlayer(targetName)
?: return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("tellTargetOffline", mapOf("target" to targetName)),
+ languageManager.getMessage("directMessage.targetOffline", mapOf("target" to targetName)),
),
)
if (recipient.uniqueId == sender.uniqueId) {
return CommandResult.Failure(
MessageFormatter.formatError(
- languageManager.getMessage("tellYourself"),
+ languageManager.getMessage("directMessage.yourself"),
),
)
}
- runBlocking {
- directMessageHandler.sendDirectMessage(sender, recipient, message)
- }
+ directMessageHandler.sendDirectMessage(sender, recipient, message)
return CommandResult.Success
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt
new file mode 100644
index 0000000..3e47c7e
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt
@@ -0,0 +1,183 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelCreateCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelDeleteCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelJoinCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelLeaveCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelListCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelStatusCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelSwitchCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+import net.kyori.adventure.text.Component
+
+@PlayerOnly
+class ChannelCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val membershipManager: ChannelMembershipManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.Channel::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> {
+ val channelCommand = Commands.literal("channel")
+
+ // Add subcommands
+ channelCommand
+ .then(
+ ChannelCreateCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelListCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelJoinCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelLeaveCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelSwitchCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelStatusCommand(
+ plugin,
+ channelManager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ ChannelDeleteCommand(
+ plugin,
+ channelManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Default help message when no subcommand is provided
+ channelCommand.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = showHelp(context)
+ handleResult(context, result)
+ }
+
+ return channelCommand
+ }
+
+ private fun showHelp(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.help.header"),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.create"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.list"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.join"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.leave"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.switch"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.status"),
+ ),
+ ),
+ )
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channel.help.delete"),
+ ),
+ ),
+ )
+
+ return CommandResult.Success
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "Should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt
new file mode 100644
index 0000000..73a50e5
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt
@@ -0,0 +1,84 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.command.impl.lc.chatmode.ChatModeToggleCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.format.NamedTextColor
+
+@PlayerOnly
+class ChatModeCommand(
+ plugin: LunaticChat,
+ private val chatModeManager: ChatModeManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChatMode::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> {
+ val chatModeCommand = Commands.literal("chatmode")
+
+ // Add toggle subcommand
+ chatModeCommand.then(
+ ChatModeToggleCommand(
+ plugin,
+ chatModeManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Default behavior: show current chat mode
+ chatModeCommand.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = showCurrentMode(context)
+ handleResult(context, result)
+ }
+
+ return chatModeCommand
+ }
+
+ private fun showCurrentMode(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+ val currentMode = chatModeManager.getChatMode(sender.uniqueId)
+
+ val modeKey =
+ when (currentMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> "chatmode.mode.global"
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+
+ val modeColor =
+ when (currentMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> NamedTextColor.GREEN
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> NamedTextColor.AQUA
+ }
+
+ sender.sendMessage(
+ Component
+ .text(languageManager.getMessage("chatmode.current") + ": ", NamedTextColor.GRAY)
+ .append(Component.text(languageManager.getMessage(modeKey), modeColor)),
+ )
+
+ return CommandResult.Success
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "Should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt
index 2d634be..af08089 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt
@@ -31,19 +31,48 @@ class LunaticChatCommand(
override val description: String
get() = languageManager.getMessage("commandDescription.lc")
- override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
- Commands
- .literal(name)
- .then(
- SettingsCommand(
- plugin,
- settingHandlerRegistry,
- languageManager,
- ).buildWithPermissionCheck(),
- ).then(
- StatusCommand(
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> {
+ val command =
+ Commands
+ .literal(name)
+ .then(
+ SettingsCommand(
+ plugin,
+ settingHandlerRegistry,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ ).then(
+ StatusCommand(
+ plugin,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+
+ // Add channel command if channel manager is available
+ plugin.channelManager?.let { manager ->
+ plugin.channelMembershipManager?.let { membershipManager ->
+ command.then(
+ ChannelCommand(
+ plugin,
+ manager,
+ membershipManager,
+ languageManager,
+ ).buildWithPermissionCheck(),
+ )
+ }
+ }
+
+ // Add chatmode command if chat mode manager is available
+ plugin.chatModeManager?.let { chatModeManager ->
+ command.then(
+ ChatModeCommand(
plugin,
+ chatModeManager,
languageManager,
).buildWithPermissionCheck(),
)
+ }
+
+ return command
+ }
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt
index 51ddac8..9cd6645 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/SettingsCommand.kt
@@ -105,7 +105,7 @@ class SettingsCommand(
val availableKeys = settingHandlerRegistry.getAvailableKeys()
val helpMessage =
MessageFormatter.format(
- languageManager.getMessage("settingsAvailableValues", mapOf("values" to availableKeys.joinToString(", "))),
+ languageManager.getMessage("settings.availableValues", mapOf("values" to availableKeys.joinToString(", "))),
)
ctx.reply(helpMessage)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
index b91c8ca..20971ee 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/StatusCommand.kt
@@ -48,8 +48,8 @@ class StatusCommand(
sendMessage(
MessageFormatter
.format(
- languageManager.getMessage("statusRunningVersion", mapOf("version" to meta.version)),
- ).hoverEvent(HoverEvent.showText(Component.text(languageManager.getMessage("statusHover")))),
+ languageManager.getMessage("status.runningVersion", mapOf("version" to meta.version)),
+ ).hoverEvent(HoverEvent.showText(Component.text(languageManager.getMessage("status.hover")))),
)
urls.forEach { (label, url) ->
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
new file mode 100644
index 0000000..29da990
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt
@@ -0,0 +1,142 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.BoolArgumentType
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.chat.channel.Channel
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+class ChannelCreateCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelCreate::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("create")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .then(
+ Commands
+ .argument("name", StringArgumentType.string())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+
+ val result = execute(context, channelId, name, null, false)
+ handleResult(context, result)
+ }.then(
+ Commands
+ .argument("description", StringArgumentType.string())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+ val description = StringArgumentType.getString(ctx, "description")
+
+ val result = execute(context, channelId, name, description, false)
+ handleResult(context, result)
+ }.then(
+ Commands
+ .argument("isPrivate", BoolArgumentType.bool())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val name = StringArgumentType.getString(ctx, "name")
+ val description = StringArgumentType.getString(ctx, "description")
+ val isPrivate = BoolArgumentType.getBool(ctx, "isPrivate")
+
+ val result = execute(context, channelId, name, description, isPrivate)
+ handleResult(context, result)
+ },
+ ),
+ ),
+ ),
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ name: String,
+ description: String?,
+ isPrivate: Boolean,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // Validate channel ID pattern
+ if (!channelId.matches(Channel.CHANNEL_ID_PATTERN)) {
+ return CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.create.invalidId",
+ mapOf("id" to channelId),
+ ),
+ ),
+ )
+ }
+
+ val channel =
+ Channel(
+ id = channelId,
+ name = name,
+ description = description,
+ isPrivate = isPrivate,
+ ownerId = sender.uniqueId,
+ )
+
+ val result = channelManager.createChannel(channel)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.create.success",
+ mapOf("name" to name, "id" to channelId),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.create.alreadyExists",
+ mapOf("id" to channelId),
+ ),
+ ),
+ )
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelCreateCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
new file mode 100644
index 0000000..9d39c70
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt
@@ -0,0 +1,102 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+class ChannelDeleteCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelDelete::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("delete")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = channelManager.deleteChannel(channelId, sender.uniqueId)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.delete.success",
+ mapOf("id" to channelId),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ when (error) {
+ is ChannelNotFoundException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.delete.notFound",
+ mapOf("id" to channelId),
+ ),
+ ),
+ )
+ }
+ is ChannelNoOwnerPermissionException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.delete.noPermission"),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.delete.error"),
+ ),
+ )
+ }
+ }
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelDeleteCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
new file mode 100644
index 0000000..2a73199
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt
@@ -0,0 +1,132 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.common.playChannelJoinNotification
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+class ChannelJoinCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val membershipManager: ChannelMembershipManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelJoin::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("join")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .suggests { _, builder ->
+ // Tab completion: suggest all public channel IDs
+ val channels = channelManager.getPublicChannels().getOrNull() ?: emptyList()
+ channels.forEach { channel ->
+ builder.suggest(channel.id)
+ }
+ builder.buildFuture()
+ }.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = membershipManager.joinChannel(sender.uniqueId, channelId)
+ return result.fold(
+ onSuccess = {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+
+ // Play notification sound
+ sender.playChannelJoinNotification()
+
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.join.success",
+ mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ when (error) {
+ is ChannelNotFoundException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.join.notFound",
+ mapOf("channelId" to channelId),
+ ),
+ ),
+ )
+ }
+ is ChannelAlreadyActiveException -> {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.join.alreadyActive",
+ mapOf("channelName" to (channel?.name ?: channelId)),
+ ),
+ ),
+ )
+ }
+ is ChannelMemberAlreadyException -> {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.join.alreadyMember",
+ mapOf("channelName" to (channel?.name ?: channelId)),
+ ),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.join.error"),
+ ),
+ )
+ }
+ }
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelJoinCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
new file mode 100644
index 0000000..299ad47
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt
@@ -0,0 +1,85 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+class ChannelLeaveCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val membershipManager: ChannelMembershipManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelLeave::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("leave").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // Get current channel before leaving
+ val currentChannelId = channelManager.getPlayerChannel(sender.uniqueId)
+ val currentChannel = currentChannelId?.let { channelManager.getChannel(it).getOrNull() }
+
+ val result = membershipManager.leaveChannel(sender.uniqueId)
+ return result.fold(
+ onSuccess = {
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.leave.success",
+ mapOf("channelName" to (currentChannel?.name ?: currentChannelId ?: "Unknown")),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ when (error) {
+ is ChannelNotMemberException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.leave.noActiveChannel"),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.leave.error"),
+ ),
+ )
+ }
+ }
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelLeaveCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
new file mode 100644
index 0000000..aaf05af
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt
@@ -0,0 +1,127 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.event.ClickEvent
+import net.kyori.adventure.text.event.HoverEvent
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.format.TextDecoration
+
+@PlayerOnly
+class ChannelListCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelList::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("list").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = channelManager.getPublicChannels()
+ return result.fold(
+ onSuccess = { channels ->
+ if (channels.isEmpty()) {
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.list.empty"),
+ ),
+ )
+ } else {
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.list.header",
+ mapOf("count" to channels.size.toString()),
+ ),
+ ),
+ )
+
+ channels.forEach { channel ->
+ val memberCountResult = channelManager.getChannelMembers(channel.id)
+ val memberCount =
+ memberCountResult.getOrNull()?.size
+ ?: 0
+
+ val hoverTextBuilder =
+ Component
+ .text()
+ .append(Component.text("ID: ", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.newline())
+ .append(Component.text("Members: ", NamedTextColor.GRAY))
+ .append(Component.text(memberCount.toString(), NamedTextColor.WHITE))
+
+ channel.description?.let { desc ->
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.text("Description: ", NamedTextColor.GRAY))
+ .append(Component.text(desc, NamedTextColor.WHITE))
+ }
+
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.newline())
+ .append(Component.text(languageManager.getMessage("channel.list.clickToJoin"), NamedTextColor.GREEN))
+ .append(Component.text(" ▶", NamedTextColor.YELLOW))
+
+ val hoverText = hoverTextBuilder.build()
+
+ val channelInfo =
+ Component
+ .text(" • ", NamedTextColor.GRAY)
+ .append(
+ Component
+ .text(channel.name, NamedTextColor.AQUA)
+ .decorate(TextDecoration.BOLD),
+ ).append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+ .hoverEvent(HoverEvent.showText(hoverText))
+ .clickEvent(ClickEvent.runCommand("/lc channel join ${channel.id}"))
+
+ sender.sendMessage(channelInfo)
+ }
+ }
+ CommandResult.Success
+ },
+ onFailure = { error ->
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.list.error"),
+ ),
+ )
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelListCommand should use build() method instead of buildCommand()",
+ )
+}
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
new file mode 100644
index 0000000..fa03965
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt
@@ -0,0 +1,193 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.event.ClickEvent
+import net.kyori.adventure.text.event.HoverEvent
+import net.kyori.adventure.text.format.NamedTextColor
+import net.kyori.adventure.text.format.TextDecoration
+
+@PlayerOnly
+class ChannelStatusCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val membershipManager: ChannelMembershipManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelStatus::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("status").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ // Get active channel
+ val activeChannelId = channelManager.getPlayerChannel(sender.uniqueId)
+ val activeChannel = activeChannelId?.let { channelManager.getChannel(it).getOrNull() }
+
+ // Get all player's channels
+ val playerChannelIds =
+ membershipManager.getPlayerChannels(sender.uniqueId).getOrElse {
+ return CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.status.error"),
+ ),
+ )
+ }
+
+ // Display header
+ sender.sendMessage(
+ MessageFormatter.format(
+ languageManager.getMessage("channel.status.header"),
+ ),
+ )
+
+ // Display active channel
+ if (activeChannel != null) {
+ val activeText =
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.activeChannel"),
+ NamedTextColor.GREEN,
+ ),
+ ).append(Component.text(": ", NamedTextColor.GRAY))
+ .append(
+ Component
+ .text(activeChannel.name, NamedTextColor.AQUA),
+ ).append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(activeChannel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+
+ sender.sendMessage(activeText)
+ } else {
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.noActiveChannel"),
+ NamedTextColor.GRAY,
+ ),
+ ),
+ )
+ }
+
+ sender.sendMessage(Component.empty())
+
+ // Display channels list
+ if (playerChannelIds.isEmpty()) {
+ sender.sendMessage(
+ Component
+ .text(" ")
+ .append(
+ Component.text(
+ languageManager.getMessage("channel.status.noChannels"),
+ NamedTextColor.GRAY,
+ ),
+ ),
+ )
+ } else {
+ sender.sendMessage(
+ Component.text(
+ languageManager.getMessage(
+ "channel.status.channelList",
+ mapOf("count" to playerChannelIds.size.toString()),
+ ),
+ NamedTextColor.GOLD,
+ ),
+ )
+
+ playerChannelIds.forEach { channelId ->
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ if (channel != null) {
+ val isOwner = channel.ownerId == sender.uniqueId
+ val isActive = channelId == activeChannelId
+
+ val hoverTextBuilder =
+ Component
+ .text()
+ .append(Component.text("ID: ", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+
+ if (isOwner) {
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.text("Role: ", NamedTextColor.GRAY))
+ .append(Component.text("Owner", NamedTextColor.GOLD))
+ }
+
+ if (!isActive) {
+ hoverTextBuilder
+ .append(Component.newline())
+ .append(Component.newline())
+ .append(Component.text(languageManager.getMessage("channel.status.clickToSwitch"), NamedTextColor.GREEN))
+ .append(Component.text(" ▶", NamedTextColor.YELLOW))
+ }
+
+ val hoverText = hoverTextBuilder.build()
+
+ var channelInfo =
+ Component
+ .text(" • ", NamedTextColor.GRAY)
+ .append(
+ Component
+ .text(channel.name, if (isActive) NamedTextColor.GREEN else NamedTextColor.AQUA)
+ .decorate(TextDecoration.ITALIC),
+ )
+
+ // Add owner indicator
+ if (isOwner) {
+ channelInfo = channelInfo.append(Component.text(" *", NamedTextColor.YELLOW))
+ }
+
+ channelInfo =
+ channelInfo
+ .append(Component.text(" (", NamedTextColor.GRAY))
+ .append(Component.text(channel.id, NamedTextColor.YELLOW))
+ .append(Component.text(")", NamedTextColor.GRAY))
+ .hoverEvent(HoverEvent.showText(hoverText))
+
+ // Add click event if not active
+ if (!isActive) {
+ channelInfo = channelInfo.clickEvent(ClickEvent.runCommand("/lc channel switch ${channel.id}"))
+ }
+
+ sender.sendMessage(channelInfo)
+ }
+ }
+ }
+
+ return CommandResult.Success
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelStatusCommand should use build() method instead of buildCommand()",
+ )
+}
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
new file mode 100644
index 0000000..13d44d2
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSwitchCommand.kt
@@ -0,0 +1,130 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException
+import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+
+@PlayerOnly
+class ChannelSwitchCommand(
+ plugin: LunaticChat,
+ private val channelManager: ChannelManager,
+ private val membershipManager: ChannelMembershipManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChannelSwitch::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands
+ .literal("switch")
+ .then(
+ Commands
+ .argument("channelId", StringArgumentType.word())
+ .suggests { ctx, builder ->
+ // 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 ->
+ builder.suggest(channelId)
+ }
+ }
+ builder.buildFuture()
+ }.executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val channelId = StringArgumentType.getString(ctx, "channelId")
+ val result = execute(context, channelId)
+ handleResult(context, result)
+ },
+ )
+
+ private fun execute(
+ ctx: CommandContext,
+ channelId: String,
+ ): CommandResult {
+ val sender = ctx.requirePlayer()
+
+ val result = membershipManager.switchChannel(sender.uniqueId, channelId)
+ return result.fold(
+ onSuccess = {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+
+ CommandResult.SuccessWithMessage(
+ MessageFormatter.format(
+ languageManager.getMessage(
+ "channel.switch.success",
+ mapOf("channelName" to (channel?.name ?: channelId), "channelId" to channelId),
+ ),
+ ),
+ )
+ },
+ onFailure = { error ->
+ when (error) {
+ is ChannelNotFoundException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.switch.notFound",
+ mapOf("channelId" to channelId),
+ ),
+ ),
+ )
+ }
+ is ChannelAlreadyActiveException -> {
+ val channel = channelManager.getChannel(channelId).getOrNull()
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.switch.alreadyActive",
+ mapOf("channelName" to (channel?.name ?: channelId)),
+ ),
+ ),
+ )
+ }
+ is ChannelNotMemberException -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage(
+ "channel.switch.notMember",
+ mapOf("channelId" to channelId),
+ ),
+ ),
+ )
+ }
+ else -> {
+ CommandResult.Failure(
+ MessageFormatter.formatError(
+ languageManager.getMessage("channel.switch.error"),
+ ),
+ )
+ }
+ }
+ },
+ )
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChannelSwitchCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
new file mode 100644
index 0000000..cea1814
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/chatmode/ChatModeToggleCommand.kt
@@ -0,0 +1,71 @@
+package dev.m1sk9.lunaticChat.paper.command.impl.lc.chatmode
+
+import com.mojang.brigadier.builder.LiteralArgumentBuilder
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
+import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.command.annotation.Permission
+import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import io.papermc.paper.command.brigadier.CommandSourceStack
+import io.papermc.paper.command.brigadier.Commands
+import net.kyori.adventure.text.Component
+import net.kyori.adventure.text.format.NamedTextColor
+
+@PlayerOnly
+class ChatModeToggleCommand(
+ plugin: LunaticChat,
+ private val chatModeManager: ChatModeManager,
+ private val languageManager: LanguageManager,
+) : LunaticCommand(plugin) {
+ fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> {
+ val builder = build()
+ return applyMethodPermission("build", builder)
+ }
+
+ @Permission(LunaticChatPermissionNode.ChatModeToggle::class)
+ fun build(): LiteralArgumentBuilder<CommandSourceStack> =
+ Commands.literal("toggle").executes { ctx ->
+ val context = wrapContext(ctx)
+ checkPlayerOnly(context)?.let { return@executes handleResult(context, it) }
+
+ val result = execute(context)
+ handleResult(context, result)
+ }
+
+ private fun execute(ctx: CommandContext): CommandResult {
+ val sender = ctx.requirePlayer()
+ val newMode = chatModeManager.toggleChatMode(sender.uniqueId)
+
+ val modeKey =
+ when (newMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> "chatmode.mode.global"
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+
+ val modeColor =
+ when (newMode) {
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.GLOBAL -> NamedTextColor.GREEN
+ dev.m1sk9.lunaticChat.engine.chat.ChatMode.CHANNEL -> NamedTextColor.AQUA
+ }
+
+ sender.sendMessage(
+ Component
+ .text()
+ .append(MessageFormatter.formatSuccess(languageManager.getMessage("chatmode.toggle.success") + ": "))
+ .append(Component.text(languageManager.getMessage(modeKey), modeColor))
+ .build(),
+ )
+
+ return CommandResult.Success
+ }
+
+ override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> =
+ throw UnsupportedOperationException(
+ "ChatModeToggleCommand should use build() method instead of buildCommand()",
+ )
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt
index 26a1f06..076a7aa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/SettingKey.kt
@@ -19,11 +19,17 @@ sealed class SettingKey(
*/
data object Notice : SettingKey("notice")
+ /**
+ * Channel message notification setting
+ * Command: /lc setting chNotice <on|off>
+ */
+ data object ChNotice : SettingKey("chNotice")
+
companion object {
/**
* Returns all available setting keys.
*/
- fun values(): List<SettingKey> = listOf(Japanese, Notice)
+ fun values(): List<SettingKey> = listOf(Japanese, Notice, ChNotice)
/**
* Finds a setting key by its string representation.
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt
new file mode 100644
index 0000000..8867e7c
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/ChannelMessageNoticeSettingHandler.kt
@@ -0,0 +1,53 @@
+package dev.m1sk9.lunaticChat.paper.command.setting.handler
+
+import dev.m1sk9.lunaticChat.engine.command.CommandResult
+import dev.m1sk9.lunaticChat.paper.command.core.CommandContext
+import dev.m1sk9.lunaticChat.paper.command.setting.SettingHandler
+import dev.m1sk9.lunaticChat.paper.command.setting.SettingKey
+import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+
+/**
+ * Handles the channel message notification setting.
+ * Manages enabling/disabling channel message notifications for players.
+ */
+class ChannelMessageNoticeSettingHandler(
+ private val settingsManager: PlayerSettingsManager,
+ private val languageManager: LanguageManager,
+) : SettingHandler {
+ override val key: SettingKey = SettingKey.ChNotice
+
+ override fun execute(
+ ctx: CommandContext,
+ enable: Boolean,
+ ): CommandResult {
+ val player = ctx.requirePlayer()
+ val currentSettings = settingsManager.getSettings(player.uniqueId)
+ val updatedSettings = currentSettings.copy(channelMessageNotificationEnabled = enable)
+ settingsManager.updateSettings(updatedSettings)
+
+ val toggleText = languageManager.getToggleText(enable)
+ val message =
+ MessageFormatter.formatSuccess(
+ languageManager.getMessage("channelMessage.noticeToggle", mapOf("toggle" to toggleText)),
+ )
+
+ player.sendMessage(message)
+ return CommandResult.Success
+ }
+
+ override fun showStatus(ctx: CommandContext): CommandResult {
+ val player = ctx.requirePlayer()
+ val settings = settingsManager.getSettings(player.uniqueId)
+
+ val toggleText = languageManager.getToggleText(settings.channelMessageNotificationEnabled)
+ val message =
+ MessageFormatter.format(
+ languageManager.getMessage("channelMessage.noticeStatus", mapOf("toggle" to toggleText)),
+ )
+
+ player.sendMessage(message)
+ return CommandResult.Success
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt
index 90ff0ad..1564615 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/DirectMessageNoticeSettingHandler.kt
@@ -30,7 +30,7 @@ class DirectMessageNoticeSettingHandler(
val toggleText = languageManager.getToggleText(enable)
val message =
MessageFormatter.formatSuccess(
- languageManager.getMessage("directMessageNoticeToggle", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("directMessage.noticeToggle", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
@@ -44,7 +44,7 @@ class DirectMessageNoticeSettingHandler(
val toggleText = languageManager.getToggleText(settings.directMessageNotificationEnabled)
val message =
MessageFormatter.format(
- languageManager.getMessage("directMessageNoticeStatus", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("directMessage.noticeStatus", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt
index 39cbc0d..2c682ba 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/setting/handler/JapaneseConversionSettingHandler.kt
@@ -30,7 +30,7 @@ class JapaneseConversionSettingHandler(
val toggleText = languageManager.getToggleText(enable)
val message =
MessageFormatter.formatSuccess(
- languageManager.getMessage("romajiConversionToggle", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("romajiConversion.toggle", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
@@ -44,7 +44,7 @@ class JapaneseConversionSettingHandler(
val toggleText = languageManager.getToggleText(settings.japaneseConversionEnabled)
val message =
MessageFormatter.format(
- languageManager.getMessage("romajiConversionStatus", mapOf("toggle" to toggleText)),
+ languageManager.getMessage("romajiConversion.status", mapOf("toggle" to toggleText)),
)
player.sendMessage(message)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
index 16c8d75..e56c356 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/common/SoundCollector.kt
@@ -31,6 +31,14 @@ object SoundCollector {
0.6f,
2.0f,
)
+
+ val LUNATIC_BELL_SOUND: Sound =
+ Sound.sound(
+ Key.key("block.note_block.bell"),
+ Sound.Source.PLAYER,
+ 0.7f,
+ 1.2f,
+ )
}
/**
@@ -43,6 +51,20 @@ fun Player.playDirectMessageNotification() {
/**
* Plays the message sent sound to the player.
*/
-fun Player.playDirectMessageSendNotification() {
+fun Player.playMessageSendNotification() {
playSound(SoundCollector.LUNATIC_POP_SOUND)
}
+
+/**
+ * Plays the channel join notification sound to the player.
+ */
+fun Player.playChannelJoinNotification() {
+ playSound(SoundCollector.LUNATIC_SOFT_SOUND)
+}
+
+/**
+ * Plays the channel message receive notification sound to the player.
+ */
+fun Player.playChannelReceiveNotification() {
+ playSound(SoundCollector.LUNATIC_BELL_SOUND)
+}
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 7661089..9aa563a 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
@@ -1,5 +1,6 @@
package dev.m1sk9.lunaticChat.paper.config
+import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.FeaturesConfig
import dev.m1sk9.lunaticChat.paper.config.key.JapaneseConversionFeatureConfig
import dev.m1sk9.lunaticChat.paper.config.key.MessageFormatConfig
@@ -7,6 +8,8 @@ import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig
import dev.m1sk9.lunaticChat.paper.i18n.Language
import org.bukkit.configuration.file.FileConfiguration
+// FIXME: ConfigManager uses mutable static state which makes testing difficult
+// and creates hidden global dependencies. Consider refactoring to dependency injection.
object ConfigManager {
private var lunaticChatConfiguration: LunaticChatConfiguration? = null
@@ -18,7 +21,7 @@ object ConfigManager {
LunaticChatConfiguration(
features =
FeaturesConfig(
- quickRepliesEnabled =
+ quickReplies =
QuickRepliesFeatureConfig(
enabled =
configFile.getBoolean("features.quickReplies.enabled", true),
@@ -44,6 +47,10 @@ object ConfigManager {
),
apiRetryAttempts = configFile.getInt("features.japaneseConversion.api.retryAttempts", 2),
),
+ channelChat =
+ ChannelChatFeatureConfig(
+ enabled = configFile.getBoolean("features.channelChat.enabled", false),
+ ),
),
messageFormat =
MessageFormatConfig(
@@ -52,6 +59,11 @@ object ConfigManager {
"messageFormat.directMessageFormat",
"§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
)!!,
+ channelMessageFormat =
+ configFile.getString(
+ "messageFormat.channelMessageFormat",
+ "§7[§b#{channel}§7] §e{sender}: §f{message}",
+ )!!,
),
debug = configFile.getBoolean("debug", false),
checkForUpdates = configFile.getBoolean("checkForUpdates", false),
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt
new file mode 100644
index 0000000..828bdb3
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.paper.config.key
+
+data class ChannelChatFeatureConfig(
+ val enabled: Boolean,
+)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
index 703ddcc..190e00c 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/FeaturesConfig.kt
@@ -1,6 +1,7 @@
package dev.m1sk9.lunaticChat.paper.config.key
data class FeaturesConfig(
- val quickRepliesEnabled: QuickRepliesFeatureConfig,
+ val quickReplies: QuickRepliesFeatureConfig,
val japaneseConversion: JapaneseConversionFeatureConfig,
+ val channelChat: ChannelChatFeatureConfig,
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
index 53d4399..2e623fa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/MessageFormatConfig.kt
@@ -2,4 +2,5 @@ package dev.m1sk9.lunaticChat.paper.config.key
data class MessageFormatConfig(
val directMessageFormat: String = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}",
+ val channelMessageFormat: String = "§7[§b#{channel}§7] §e{sender}: §f{message}",
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
index d2ba632..da79cfa 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
@@ -110,17 +110,20 @@ class ConversionCache(
private fun queueSaveToDisk() {
if (conversionSaveQueue.compareAndSet(false, true)) {
- Bukkit.getScheduler().runTaskAsynchronously(
+ Bukkit.getScheduler().runTaskLaterAsynchronously(
plugin,
Runnable {
- Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
- conversionSaveQueue.set(true)
+ conversionSaveQueue.set(false)
saveToDisk()
},
+ 100L, // 5 seconds = 100 ticks
)
}
}
+ // FIXME: ConcurrentHashMap keys are unordered, so evicting "oldest" entries
+ // actually evicts random entries. Consider using LinkedHashMap with access-order
+ // or implement proper LRU cache with timestamp tracking.
private fun evictOldestEntry() {
val toRemove = conversionMemoryCache.size / 10
conversionMemoryCache.keys.take(toRemove).forEach {
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
index 8d4070d..f8e8181 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt
@@ -29,14 +29,29 @@ object EventListenerRegistry {
// Always register these listeners
pluginManager.registerEvents(SpyPermissionManager, plugin)
pluginManager.registerEvents(
- PlayerPresenceListener(plugin, services.languageManager, updateAvailable),
+ PlayerPresenceListener(
+ lunaticChat = plugin,
+ languageManager = services.languageManager,
+ updateCheckerFlag = updateAvailable,
+ playerSettingsManager = services.playerSettingsManager,
+ chatModeManager = services.chatModeManager,
+ channelManager = services.channelManager,
+ ),
plugin,
)
- // Conditionally register Japanese conversion listener
- if (services.romajiConverter != null) {
+ // Conditionally register chat listener when all required components are available
+ if (services.chatModeManager != null &&
+ services.channelMessageHandler != null &&
+ services.romajiConverter != null
+ ) {
pluginManager.registerEvents(
- PlayerChatListener(services.romajiConverter, services.playerSettingsManager),
+ PlayerChatListener(
+ chatModeManager = services.chatModeManager,
+ channelMessageHandler = services.channelMessageHandler,
+ romajiConverter = services.romajiConverter,
+ settingsManager = services.playerSettingsManager,
+ ),
plugin,
)
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
index 17df427..47c8cd4 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt
@@ -1,5 +1,8 @@
package dev.m1sk9.lunaticChat.paper.listener
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import io.papermc.paper.event.player.AsyncChatEvent
@@ -10,30 +13,66 @@ import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class PlayerChatListener(
+ private val chatModeManager: ChatModeManager,
+ private val channelMessageHandler: ChannelMessageHandler,
private val romajiConverter: RomanjiConverter,
private val settingsManager: PlayerSettingsManager,
) : Listener {
private val plainTextSerializer = PlainTextComponentSerializer.plainText()
@EventHandler(ignoreCancelled = true)
- fun onChat(event: AsyncChatEvent) =
- runBlocking {
- val player = event.player
- val settings = settingsManager.getSettings(player.uniqueId)
- val message = event.message()
- val originalMessage = plainTextSerializer.serialize(message)
-
- val displayMessage =
- settings
- .takeIf {
- it.japaneseConversionEnabled
- }?.runCatching {
+ fun onChat(event: AsyncChatEvent) {
+ val player = event.player
+ val settings = settingsManager.getSettings(player.uniqueId)
+
+ val originalMessage = plainTextSerializer.serialize(event.message())
+
+ // Handle chat mode switching with '!' prefix
+ val hasPrefix = originalMessage.startsWith('!')
+ val messageWithoutPrefix =
+ if (hasPrefix) {
+ originalMessage.removePrefix("!").trim()
+ } else {
+ originalMessage
+ }
+
+ if (hasPrefix && messageWithoutPrefix.isEmpty()) {
+ event.isCancelled = true
+ return
+ }
+
+ val effectiveMode =
+ if (hasPrefix) {
+ val currentMode = chatModeManager.getChatMode(player.uniqueId)
+ currentMode.toggle()
+ } else {
+ chatModeManager.getChatMode(player.uniqueId)
+ }
+
+ // Handle romaji conversion if enabled (requires blocking for HTTP call)
+ val displayMessage =
+ if (settings.japaneseConversionEnabled) {
+ runCatching {
+ runBlocking {
romajiConverter
- .convert(originalMessage)
- ?.let { "$originalMessage §e($it)" }
- ?: originalMessage
- }?.getOrNull() ?: originalMessage
+ .convert(messageWithoutPrefix)
+ ?.let { "$messageWithoutPrefix §e($it)" }
+ ?: messageWithoutPrefix
+ }
+ }.getOrNull() ?: messageWithoutPrefix
+ } else {
+ messageWithoutPrefix
+ }
- event.message(Component.text(displayMessage))
+ // Route message based on chat mode
+ when (effectiveMode) {
+ ChatMode.GLOBAL -> {
+ event.message(Component.text(displayMessage))
+ }
+ ChatMode.CHANNEL -> {
+ event.isCancelled = true
+ channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix)
+ }
}
+ }
}
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 1b53453..fe4239d 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
@@ -1,9 +1,14 @@
package dev.m1sk9.lunaticChat.paper.listener
+import dev.m1sk9.lunaticChat.engine.chat.ChatMode
import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode
import dev.m1sk9.lunaticChat.paper.LunaticChat
+import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager
+import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager
import dev.m1sk9.lunaticChat.paper.common.hasAnyPermission
import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager
+import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter
+import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.ClickEvent
import org.bukkit.event.EventHandler
@@ -16,24 +21,72 @@ class PlayerPresenceListener(
private val lunaticChat: LunaticChat,
private val languageManager: LanguageManager,
private val updateCheckerFlag: AtomicBoolean,
+ private val playerSettingsManager: PlayerSettingsManager,
+ private val chatModeManager: ChatModeManager? = null,
+ private val channelManager: ChannelManager? = null,
) : Listener {
@EventHandler(ignoreCancelled = true)
fun onJoin(event: PlayerJoinEvent) {
val player = event.player
- if (!updateCheckerFlag.get() || !player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) return
-
- player.sendMessage {
- Component
- .text(
- languageManager.getMessage("newUpdateAvailable"),
- ).clickEvent(
- ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"),
+
+ // Send update notification if available
+ if (updateCheckerFlag.get() && player.hasAnyPermission { +LunaticChatPermissionNode.NoticeUpdate }) {
+ player.sendMessage {
+ Component
+ .text(
+ languageManager.getMessage("general.newUpdateAvailable"),
+ ).clickEvent(
+ ClickEvent.openUrl("https://github.com/m1sk9/LunaticChat/releases/latest"),
+ )
+ }
+ }
+
+ // Send chat mode notification
+ chatModeManager?.let { manager ->
+ val chatMode = manager.getChatMode(player.uniqueId)
+ val modeKey =
+ when (chatMode) {
+ ChatMode.GLOBAL -> "chatmode.mode.global"
+ ChatMode.CHANNEL -> "chatmode.mode.channel"
+ }
+ val modeText = languageManager.getMessage(modeKey)
+ val notification =
+ languageManager.getMessage(
+ "chatmode.notification.login",
+ mapOf("mode" to modeText),
)
+ player.sendMessage(MessageFormatter.format(notification))
+ }
+
+ // Send channel notification if in a channel
+ channelManager?.let { manager ->
+ val context = manager.getPlayerChannelContext(player.uniqueId)
+ context?.let {
+ val notification =
+ languageManager.getMessage(
+ "channel.notification.login",
+ mapOf("channelName" to it.channel.name),
+ )
+ player.sendMessage(Component.text(notification))
+ }
}
}
@EventHandler(ignoreCancelled = true)
fun onQuit(event: PlayerQuitEvent) {
- lunaticChat.directMessageHandler.clearPlayer(event.player)
+ val player = event.player
+ val playerId = player.uniqueId
+
+ // 1. Clear direct message references
+ lunaticChat.directMessageHandler.clearPlayer(player)
+
+ // 2. Clear active channel for this player
+ channelManager?.setPlayerChannel(playerId, null)
+
+ // 3. Trigger async save of chat mode data
+ chatModeManager?.saveToDisk()
+
+ // 4. Trigger async save of player settings
+ playerSettingsManager.saveToDisk()
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
index a30618b..f0927d8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
@@ -19,6 +19,7 @@ class PlayerSettingsManager(
) {
private val japaneseConversionCache = ConcurrentHashMap<UUID, Boolean>()
private val directMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>()
+ private val channelMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>()
private lateinit var settingsData: PlayerSettingsData
/**
@@ -29,6 +30,7 @@ class PlayerSettingsManager(
settingsData = storage.loadFromDisk()
japaneseConversionCache.putAll(settingsData.japaneseConversion)
directMessageNotificationCache.putAll(settingsData.directMessageNotification)
+ channelMessageNotificationCache.putAll(settingsData.channelMessageNotification)
logger.info("Loaded settings for ${japaneseConversionCache.size} players")
}
@@ -42,10 +44,12 @@ class PlayerSettingsManager(
fun getSettings(uuid: UUID): PlayerChatSettings {
val japaneseConversionEnabled = japaneseConversionCache.getOrDefault(uuid, true)
val directMessageNotificationEnabled = directMessageNotificationCache.getOrDefault(uuid, true)
+ val channelMessageNotificationEnabled = channelMessageNotificationCache.getOrDefault(uuid, true)
return PlayerChatSettings(
uuid = uuid,
japaneseConversionEnabled = japaneseConversionEnabled,
directMessageNotificationEnabled = directMessageNotificationEnabled,
+ channelMessageNotificationEnabled = channelMessageNotificationEnabled,
)
}
@@ -57,11 +61,13 @@ class PlayerSettingsManager(
fun updateSettings(settings: PlayerChatSettings) {
japaneseConversionCache[settings.uuid] = settings.japaneseConversionEnabled
directMessageNotificationCache[settings.uuid] = settings.directMessageNotificationEnabled
+ channelMessageNotificationCache[settings.uuid] = settings.channelMessageNotificationEnabled
settingsData =
settingsData.copy(
japaneseConversion = japaneseConversionCache.toMap(),
directMessageNotification = directMessageNotificationCache.toMap(),
+ channelMessageNotification = channelMessageNotificationCache.toMap(),
)
storage.queueAsyncSave(settingsData)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
index 754c82b..0aa8eef 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
@@ -87,13 +87,13 @@ class YamlPlayerSettingsStorage(
*/
fun queueAsyncSave(data: PlayerSettingsData) {
if (saveFlag.compareAndSet(false, true)) {
- Bukkit.getScheduler().runTaskAsynchronously(
+ Bukkit.getScheduler().runTaskLaterAsynchronously(
plugin,
Runnable {
- Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
saveFlag.set(false)
saveToDisk(data)
},
+ 100L, // 5 seconds = 100 ticks
)
}
}
diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml
index d4ca25b..839e0e3 100644
--- a/platform-paper/src/main/resources/config.yml
+++ b/platform-paper/src/main/resources/config.yml
@@ -48,6 +48,9 @@ features:
timeout: 3000
# Specify the number of retry attempts for failed API requests to the Romanization conversion service.
retryAttempts: 2
+ channelChat:
+ # If enabled, channel-based chat functionality will be activated.
+ enabled: false
# ----------------------------------------------
# --------- Message Format Settings --------
@@ -59,9 +62,12 @@ features:
# {sender} - The name of the message sender
# {recipient} - The name of the message recipient
# {message} - The content of the message
+# {channel} - The name of the chat channel (only for channel chat)
# ----------------------------------------------
messageFormat:
# Configure the format for direct messages sent via /tell or /msg
directMessageFormat: "§7[§e{sender} §7>> §e{recipient}§7] §f{message}"
+ # Configure the format for messages sent in channel chat
+ channelMessageFormat: "§7[§b#{channel}§7] §e{sender}: §f{message}"
diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml
index d61d03f..925d7e7 100644
--- a/platform-paper/src/main/resources/languages/en.yml
+++ b/platform-paper/src/main/resources/languages/en.yml
@@ -14,23 +14,99 @@
commandDescription:
jp: "Toggle romaji-to-kana conversion on/off"
notice: "Toggle direct message notifications on/off"
+ chNotice: "Toggle channel message notifications on/off"
reply: "Reply to the player who last sent/received a direct message"
tell: "Send a direct message to another player"
lc: "LunaticChat Main Command"
-directMessageNoticeStatus: "Direct message notifications are currently {toggle}"
-directMessageNoticeToggle: "Direct message notifications have been set to {toggle}"
-playerOnlyCommand: "This command can only be executed by players."
-replyTargetNotFound: "Reply target player not found."
-romajiConversionToggle: "Romaji-to-kana conversion has been set to {toggle}"
-romajiConversionStatus: "Romaji-to-kana conversion is currently {toggle}"
-settingsAvailableValues: "Available settings value: {values}"
-statusHover: "LunaticChat was born as the successor to LunaChat. With respect to its original creator, uccchyocean."
-statusRunningVersion: "Running LunaticChat v{version}"
-tellTargetOffline: "Player '{target}' is currently offline."
-tellYourself: "You cannot send a message to yourself."
-newUpdateAvailable: "The new version of LunaticChat is now available! You can download it from GitHub or Modrinth."
-noPermission: "You do not have permission to execute this command."
+directMessage:
+ noticeStatus: "Direct message notifications are currently {toggle}"
+ noticeToggle: "Direct message notifications have been set to {toggle}"
+ targetOffline: "Player '{target}' is currently offline."
+ yourself: "You cannot send a message to yourself."
+ replyTargetNotFound: "Reply target player not found."
+
+romajiConversion:
+ toggle: "Romaji-to-kana conversion has been set to {toggle}"
+ status: "Romaji-to-kana conversion is currently {toggle}"
+
+channelMessage:
+ noticeToggle: "Channel message notifications have been set to {toggle}"
+ noticeStatus: "Channel message notifications are currently {toggle}"
+
+status:
+ hover: "LunaticChat was born as the successor to LunaChat. With respect to its original creator, uccchyocean."
+ runningVersion: "Running LunaticChat v{version}"
+
+settings:
+ availableValues: "Available settings value: {values}"
+
+channel:
+ help:
+ header: "=== Channel Commands ==="
+ create: "/lc channel create <channelId> <name> [description] [isPrivate] - Create a new channel"
+ list: "/lc channel list - List all public channels"
+ join: "/lc channel join <channelId> - Join a channel"
+ leave: "/lc channel leave - Leave the active channel"
+ switch: "/lc channel switch <channelId> - Switch to a channel you're already a member of"
+ status: "/lc channel status - Show your channel status and memberships"
+ delete: "/lc channel delete <channelId> - Delete a channel"
+ create:
+ success: "Created channel '{name}' (ID: {id})"
+ alreadyExists: "Channel ID '{id}' is already in use"
+ invalidId: "Invalid channel ID '{id}'. Must be 3-30 characters using only letters, numbers, underscores, and hyphens"
+ list:
+ header: "=== Public Channels ({count}) ==="
+ empty: "No public channels available"
+ error: "Failed to retrieve channel list"
+ clickToJoin: "Click to join"
+ delete:
+ success: "Deleted channel '{id}'"
+ notFound: "Channel '{id}' not found"
+ noPermission: "You don't have permission to delete this channel. Only the channel owner can delete it"
+ error: "Failed to delete channel"
+ join:
+ success: "Joined channel '{channelName}' (ID: {channelId})"
+ alreadyActive: "Channel '{channelName}' is already your active channel"
+ alreadyMember: "You are already a member of channel '{channelName}'"
+ notFound: "Channel '{channelId}' not found"
+ privateChannel: "This is a private channel and requires an invitation"
+ error: "Failed to join channel"
+ leave:
+ success: "Left channel '{channelName}'"
+ noActiveChannel: "You don't have an active channel"
+ error: "Failed to leave channel"
+ switch:
+ success: "Switched to channel '{channelName}' (ID: {channelId})"
+ alreadyActive: "Channel '{channelName}' is already your active channel"
+ notFound: "Channel '{channelId}' not found"
+ notMember: "You are not a member of channel '{channelId}'"
+ error: "Failed to switch channel"
+ status:
+ header: "=== Your Channel Status ==="
+ activeChannel: "Active Channel"
+ noActiveChannel: "No active channel"
+ channelList: "Your Channels ({count})"
+ noChannels: "You are not a member of any channels"
+ clickToSwitch: "Click to switch"
+ error: "Failed to retrieve channel status"
+ notification:
+ login: "You are in channel '{channelName}'"
+
+chatmode:
+ current: "Current chat mode"
+ mode:
+ global: "GLOBAL"
+ channel: "CHANNEL"
+ toggle:
+ success: "Chat mode switched to"
+ notification:
+ login: "Current chat mode: {mode}"
+
+general:
+ playerOnlyCommand: "This command can only be executed by players."
+ newUpdateAvailable: "The new version of LunaticChat is now available! You can download it from GitHub or Modrinth."
+ noPermission: "You do not have permission to execute this command."
toggle:
off: "Disabled"
diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml
index 5241cc8..17b0bf6 100644
--- a/platform-paper/src/main/resources/languages/ja.yml
+++ b/platform-paper/src/main/resources/languages/ja.yml
@@ -16,21 +16,97 @@ commandDescription:
reply: "最後にダイレクトメッセージを送信/受信したプレイヤーに返信します"
jp: "かな・ローマ字変換機能のオン/オフを切り替えます"
notice: "ダイレクトメッセージ通知のオン/オフを切り替えます"
+ chNotice: "チャンネルメッセージ通知のオン/オフを切り替えます"
lc: "LunaticChat のメインコマンド"
-directMessageNoticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
-directMessageNoticeStatus: "現在ダイレクトメッセージ通知は{toggle}です"
-playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます"
-replyTargetNotFound: "返信対象のプレイヤーが見つかりません"
-romajiConversionToggle: "かな・ローマ字変換機能を{toggle}にしました"
-romajiConversionStatus: "現在かな・ローマ字変換機能は{toggle}です"
-settingsAvailableValues: "使用可能な設定値: {values}"
-statusHover: "LunaticChat は LunaChatの後継として生まれました。\nオリジナルの作者であるucchyoceanに敬意を込めて。"
-statusRunningVersion: "LunaticChat v{version} が使用されています"
-tellTargetOffline: "プレイヤー '{target}' は現在オフラインです"
-tellYourself: "自分自身にメッセージを送信することはできません"
-newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます"
-noPermission: "このコマンドを実行する権限がありません"
+directMessage:
+ noticeToggle: "ダイレクトメッセージ通知を{toggle}にしました"
+ noticeStatus: "現在ダイレクトメッセージ通知は{toggle}です"
+ targetOffline: "プレイヤー '{target}' は現在オフラインです"
+ yourself: "自分自身にメッセージを送信することはできません"
+ replyTargetNotFound: "返信対象のプレイヤーが見つかりません"
+
+romajiConversion:
+ toggle: "かな・ローマ字変換機能を{toggle}にしました"
+ status: "現在かな・ローマ字変換機能は{toggle}です"
+
+channelMessage:
+ noticeToggle: "チャンネルメッセージ通知を{toggle}にしました"
+ noticeStatus: "現在チャンネルメッセージ通知は{toggle}です"
+
+status:
+ hover: "LunaticChat は LunaChatの後継として生まれました。\nオリジナルの作者であるucchyoceanに敬意を込めて。"
+ runningVersion: "LunaticChat v{version} が使用されています"
+
+settings:
+ availableValues: "使用可能な設定値: {values}"
+
+channel:
+ help:
+ header: "=== チャンネルコマンド ==="
+ create: "/lc channel create <チャンネルID> <名前> [説明] [プライベート] - チャンネルを作成します"
+ list: "/lc channel list - 公開チャンネル一覧を表示します"
+ join: "/lc channel join <チャンネルID> - チャンネルに参加します"
+ leave: "/lc channel leave - アクティブチャンネルから退出します"
+ switch: "/lc channel switch <チャンネルID> - 既に参加しているチャンネルに切り替えます"
+ status: "/lc channel status - チャンネルのステータスとメンバーシップを表示します"
+ delete: "/lc channel delete <チャンネルID> - チャンネルを削除します"
+ create:
+ success: "チャンネル '{name}' (ID: {id}) を作成しました"
+ alreadyExists: "チャンネルID '{id}' は既に使用されています"
+ invalidId: "無効なチャンネルID '{id}' です。3-30文字の英数字、アンダースコア、ハイフンのみ使用できます"
+ list:
+ header: "=== 公開チャンネル ({count}件) ==="
+ empty: "公開チャンネルはありません"
+ error: "チャンネル一覧の取得に失敗しました"
+ clickToJoin: "クリックして参加"
+ delete:
+ success: "チャンネル '{id}' を削除しました"
+ notFound: "チャンネル '{id}' が見つかりません"
+ noPermission: "このチャンネルを削除する権限がありません。チャンネルのオーナーのみが削除できます"
+ error: "チャンネルの削除に失敗しました"
+ join:
+ success: "チャンネル '{channelName}' (ID: {channelId}) に参加しました"
+ alreadyActive: "チャンネル '{channelName}' は既にアクティブです"
+ alreadyMember: "既にチャンネル '{channelName}' のメンバーです"
+ notFound: "チャンネル '{channelId}' が見つかりません"
+ privateChannel: "プライベートチャンネルには招待が必要です"
+ error: "チャンネルへの参加に失敗しました"
+ leave:
+ success: "チャンネル '{channelName}' から退出しました"
+ noActiveChannel: "アクティブなチャンネルがありません"
+ error: "チャンネルからの退出に失敗しました"
+ switch:
+ success: "チャンネル '{channelName}' (ID: {channelId}) に切り替えました"
+ alreadyActive: "チャンネル '{channelName}' は既にアクティブです"
+ notFound: "チャンネル '{channelId}' が見つかりません"
+ notMember: "チャンネル '{channelId}' のメンバーではありません"
+ error: "チャンネルの切り替えに失敗しました"
+ status:
+ header: "=== あなたのチャンネルステータス ==="
+ activeChannel: "アクティブチャンネル"
+ noActiveChannel: "アクティブなチャンネルがありません"
+ channelList: "参加中のチャンネル ({count}件)"
+ noChannels: "どのチャンネルにも参加していません"
+ clickToSwitch: "クリックして切り替え"
+ error: "チャンネルステータスの取得に失敗しました"
+ notification:
+ login: "チャンネル '{channelName}' に入室中です"
+
+chatmode:
+ current: "現在のチャットモード"
+ mode:
+ global: "グローバル"
+ channel: "チャンネル"
+ toggle:
+ success: "チャットモードを切り替えました"
+ notification:
+ login: "現在のチャットモード: {mode}"
+
+general:
+ playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます"
+ newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます"
+ noPermission: "このコマンドを実行する権限がありません"
toggle:
on: "有効"
diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml
index 3f991a9..b3a9d8f 100644
--- a/platform-paper/src/main/resources/paper-plugin.yml
+++ b/platform-paper/src/main/resources/paper-plugin.yml
@@ -19,6 +19,26 @@ permissions:
default: true
lunaticchat.command.lc.status:
default: true
+ lunaticchat.command.lc.channel:
+ default: true
+ lunaticchat.command.lc.channel.create:
+ default: true
+ lunaticchat.command.lc.channel.list:
+ default: true
+ lunaticchat.command.lc.channel.join:
+ default: true
+ lunaticchat.command.lc.channel.leave:
+ default: true
+ lunaticchat.command.lc.channel.switch:
+ default: true
+ lunaticchat.command.lc.channel.status:
+ default: true
+ lunaticchat.command.lc.channel.delete:
+ default: true
+ lunaticchat.command.lc.chatmode:
+ default: true
+ lunaticchat.command.lc.chatmode.toggle:
+ default: true
lunaticchat.spy:
default: op