diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-27 22:43:16 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-27 22:43:16 +0900 |
| commit | 679b66396f9995894cd69e6aba93288f25db26c3 (patch) | |
| tree | b756e17bce529ca92340d92fbbaec7bbb8f7b22f | |
| parent | babea569a40a9e80d3c8bb63ae48d3c021238258 (diff) | |
| parent | 15fc0758b688549278069b7a9cf3ecaf6e7b4471 (diff) | |
| download | LunaticChat-679b66396f9995894cd69e6aba93288f25db26c3.tar.gz LunaticChat-679b66396f9995894cd69e6aba93288f25db26c3.tar.bz2 LunaticChat-679b66396f9995894cd69e6aba93288f25db26c3.zip | |
Merge pull request #74 from m1sk9/update-docs-channel-chat
docs: Update docs
35 files changed, 1832 insertions, 90 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 334b542..df917cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ### v0.6.0 - Added experimental feature to Channel Chat support. -- Fixed broken links to documentation referenced in config.yml +- Fixed broken links to documentation referenced in config.yml. +- Added a description indicating this to messages sent in Spy Mode. +- Other bug fixes. ### v0.5.0 @@ -22,6 +22,13 @@ _[Supports Minecraft 1.21.X](https://minecraft.wiki/w/Java_Edition_version_histo ## Installation +LunaticChat can be obtained from: + +- [GitHub](https://github.com/m1sk9/LunaticChat/releases) +- [Modrinth](https://modrinth.com/project/lunaticchat) + +Place the downloaded plugin file in your server's `plugins` folder and restart the server. + See the [Documentation](https://lc.m1sk9.dev/guide/getting-started). ## Features diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index 6aec245..5718a4e 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -30,6 +30,14 @@ export const en: DefaultTheme.Config = { text: 'Velocity Integration', link: '/en/guide/admin/velocity', }, + { + text: 'Channel Chat Deployment Guide', + link: '/en/guide/admin/introduction-channel-chat', + }, + { + text: 'Data and Logs', + link: '/en/guide/admin/data-and-logs', + }, ], }, { @@ -42,7 +50,15 @@ export const en: DefaultTheme.Config = { { text: 'Chat Mode', link: '/en/guide/player/channel-chat/chatmode', - } + }, + { + text: 'Private Channel', + link: '/en/guide/player/channel-chat/private-channel', + }, + { + text: 'Moderation', + link: '/en/guide/player/channel-chat/moderation', + }, ] }, { diff --git a/docs/.vitepress/config/ja.ts b/docs/.vitepress/config/ja.ts index 013b0d3..9882687 100644 --- a/docs/.vitepress/config/ja.ts +++ b/docs/.vitepress/config/ja.ts @@ -30,6 +30,14 @@ export const ja: DefaultTheme.Config = { text: 'Velocity 連携', link: '/guide/admin/velocity', }, + { + text: "チャンネルチャット展開ガイド", + link: "/guide/admin/introduction-channel-chat", + }, + { + text: "データ・ログ", + link: "/guide/admin/data-and-logs", + } ], }, { @@ -42,7 +50,15 @@ export const ja: DefaultTheme.Config = { { text: 'チャットモード', link: '/guide/player/channel-chat/chatmode', - } + }, + { + text: 'プライベートチャンネル', + link: '/guide/player/channel-chat/private-channel', + }, + { + text: 'モデレーション', + link: '/guide/player/channel-chat/moderation', + }, ] }, { diff --git a/docs/src/en/guide/admin/data-and-logs.md b/docs/src/en/guide/admin/data-and-logs.md new file mode 100644 index 0000000..e869552 --- /dev/null +++ b/docs/src/en/guide/admin/data-and-logs.md @@ -0,0 +1,119 @@ +# Data and Logs + +::: danger Do Not Edit + +These data files are essential to the operation of LunaticChat. Direct editing may cause data corruption or unexpected behavior. Do not directly edit these files unless you are backing up data. + +::: + +## Data Storage Location + +LunaticChat saves channel data and configuration information to local disk. + +- `channels.json`: Stores channel information. +- `chatmodes.json`: Stores player chat mode settings. +- `conversion_cache.json`: Stores cache for channel conversion. +- `player-settings.yaml`: Stores player-specific settings. + +::: tip Regular Backups + +To ensure the safety of your LunaticChat data, we recommend creating regular backups. + +::: + +### `channels.json` + +The `channels.json` file stores information about channels managed by LunaticChat. This file contains information such as channel names, participant lists, and chat modes. + +```json +{ + "channels": { + "general-channel": { + "id": "general-channel", + "name": "General Channel", + "ownerId": "a01e3843-e521-3998-958a-f459800e4d11", + "createdAt": 1769507213150, + "bannedPlayers": [ + "ceaea267-39dd-3bac-931c-761ada671ebe" + ] + } + }, + "members": { + "general-channel": [ + { + "channelId": "test2", + "playerId": "a01e3843-e521-3998-958a-f459800e4d11", + "role": "OWNER", + "joinedAt": 1769507213150 + } + ] + }, + "activeChannels": { + "a01e3843-e521-3998-958a-f459800e4d11": "test2" + } +} +``` + +### `chatmodes.json` + +The `chatmodes.json` file stores player chat mode settings. This file contains chat mode information for each player. + +```json +{ + "modes": { + "aed5efd4-551b-3965-bc28-ae21aa072a66": "CHANNEL", + "ceaea267-39dd-3bac-931c-761ada671ebe": "CHANNEL", + "a01e3843-e521-3998-958a-f459800e4d11": "CHANNEL", + "681f539b-8bb8-3f85-85e5-a2945f6c6539": "GLOBAL" + } +} +``` + +### `conversion_cache.json` + +The `conversion_cache.json` file stores cache for channel conversion. This file contains channel conversion information for each player. + +For more information about the cache system, see [here](./cache.md). + + +```json +{"version":"1","entries":{"hi":"日"}} +``` + +### `player-settings.yaml` + +The `player-settings.yaml` file stores player-specific settings. This file contains individual player settings. + +```yaml +version: 1 +japaneseConversion: + "aed5efd4-551b-3965-bc28-ae21aa072a66": false + "ceaea267-39dd-3bac-931c-761ada671ebe": false +directMessageNotification: + "aed5efd4-551b-3965-bc28-ae21aa072a66": true + "ceaea267-39dd-3bac-931c-761ada671ebe": true +channelMessageNotification: + "ceaea267-39dd-3bac-931c-761ada671ebe": true +``` + +## Cache Version + +Files used for disk caching include a `version` field to accommodate changes in cache format as LunaticChat is upgraded. + +If the version does not match, LunaticChat recognizes the cache file as **old format cache**, ignores the contents, and recreates it in the new format. + +```json +{"version":"1","entries":{}} +``` + +## About CoreProtect + +Various chat logs in LunaticChat can also be recorded in CoreProtect without requiring an API. + +Logs for each feature can be checked with the following actions. When using the `/co lookup` command, specify the following actions: + +- Direct messages: `command` +- Global chat and channel chat: `chat` + - Japanese and romanization conversion is saved according to the player's settings. + + diff --git a/docs/src/en/guide/admin/introduction-channel-chat.md b/docs/src/en/guide/admin/introduction-channel-chat.md new file mode 100644 index 0000000..2a81d63 --- /dev/null +++ b/docs/src/en/guide/admin/introduction-channel-chat.md @@ -0,0 +1,56 @@ +# Channel Chat Deployment Guide + +This guide explains how to deploy channel chat. + +## What is Channel Chat + +Channel chat is a feature that allows players to create channels and share chat among specific players. + +For detailed features, please refer to the [Player Guide](../player/channel-chat/about.md). + +## Preparing to Deploy Channel Chat + +To deploy channel chat, you need to enable the channel chat feature in the LunaticChat configuration file `config.yml`. + +1. Stop the server. +2. Open `plugins/LunaticChat/config.yml`. +3. Set `features.channelChat.enabled` to `true`. +4. Restart the server. + +This will enable the channel chat feature and allow players to use channel chat. + +## Channel Chat Configuration + +The configuration items related to channel chat are as follows: + +- `features.channelChat.maxChannelsPerPlayer`: Specifies the maximum number of channels a single player can create. +- `features.channelChat.maxMembersPerChannel`: Specifies the maximum number of members that can join a single channel. +- `features.channelChat.maxMembershipPerPlayer`: Specifies the maximum number of channels a single player can join. + +The default is set to `0`, which means there is no limit. + +::: tip Recommended Settings + +If you want to actively use the channel chat feature, we recommend setting these values higher. + +However, as this may impact server performance, please set them appropriately according to your server's resource situation. + +Recommended settings are as follows: + +- `features.channelChat.maxChannelsPerPlayer`: `3` to `5` +- `features.channelChat.maxMembersPerChannel`: `20` to `50` +- `features.channelChat.maxMembershipPerPlayer`: `5` to `10` + +::: + +## Channel Management + +Basically, [players manage channels themselves](../player/channel-chat/moderation.md). + +However, as a server administrator, please note the following: + +- Server administrators have owner permissions for all channels. If operations are required, please respond appropriately. +- To maintain server performance, set limits on the number of channels and members as needed. +- If inappropriate channels or member behavior occurs, take appropriate action. + +If you want to avoid troubles related to channel management, consider restricting moderate commands such as `/lc channel ban`. diff --git a/docs/src/en/guide/player/channel-chat/moderation.md b/docs/src/en/guide/player/channel-chat/moderation.md new file mode 100644 index 0000000..3bf8609 --- /dev/null +++ b/docs/src/en/guide/player/channel-chat/moderation.md @@ -0,0 +1,95 @@ +# Channel Management + +You can manage member behavior within a channel using channel chat moderation features. + +::: danger Warning + +These moderation features are only available for LunaticChat channel chat. + +If you need to punish actual rule violators, we recommend reporting them to the server administrator. + +::: + +## Moderation Feature Specifications + +Moderation features have several specifications: + +- Players with Bypass permissions are not affected by moderation features. + - This means you cannot use these features against server administrators. +- To use moderation features, you need the respective channel role and server permissions. +- The effects of moderation features are independent for each channel. + - For example, a player permanently banned (BAN) from one channel can chat normally in other channels. + +## Roles + +Channels have the following roles: + +- Member: Regular players participating in the channel +- Moderator: Players who can use channel moderation features +- Owner: The channel creator who can use all moderation features + +::: tip Moderation by Server Administrators + +Server administrators uniformly have the same permissions as owner permissions in all channels. + +::: + +## Temporarily Expel a Member (Kick) + +To temporarily expel a member from a channel, use the [`/lc channel kick`](../../../reference/commands/lc/channel.md#lc-channel-kick-player-id) command. + +``` +/lc channel kick <player ID> +``` + +Executing this command will immediately expel the specified player from the channel. + +However, the expelled player can rejoin. + +## Permanently Ban a Member (Ban) + +To permanently ban a member from a channel, use the [`/lc channel ban`](../../../reference/commands/lc/channel.md#lc-channel-ban-player-id) command. + +``` +/lc channel ban <player ID> +``` + +Executing this command will immediately expel the specified player from the channel and also prohibit them from rejoining. + +## Unban a Permanently Banned Member + +To unban a permanently banned member, use the [`/lc channel unban`](../../../reference/commands/lc/channel.md#lc-channel-unban-player-id) command. + +``` +/lc channel unban <player ID> +``` + +## Appoint/Dismiss Moderators + +To appoint/dismiss a channel member as a moderator, use the [`/lc channel mod`](../../../reference/commands/lc/channel.md#lc-channel-mod-player-id) command. + +``` +/lc channel mod <player ID> +``` + +Executing this command will appoint the specified player as a moderator. Executing this command again on an appointed player will dismiss them from being a moderator. + +## Transfer Owner Permissions + +To transfer channel owner permissions to another member, use the [`/lc channel ownership`](../../../reference/commands/lc/channel.md#lc-channel-ownership-player-id) command. + +``` +/lc channel ownership <player ID> +``` + +Executing this command will transfer all owner permissions to the specified player. The original owner becomes a moderator. + +::: danger Important Notes on Execution + +- Owner permission transfer is an irreversible operation. + - Once transferred, the original owner cannot regain owner permissions unless the player with owner permissions transfers them again. + - This applies even to server administrators. +- The recipient of owner permissions must be a member of the channel. +- Owner permissions can use all moderation features and delete the channel, which is a powerful privilege. Choose carefully to whom you transfer permissions. + +::: diff --git a/docs/src/en/guide/player/channel-chat/private-channel.md b/docs/src/en/guide/player/channel-chat/private-channel.md new file mode 100644 index 0000000..3b33559 --- /dev/null +++ b/docs/src/en/guide/player/channel-chat/private-channel.md @@ -0,0 +1,25 @@ +# Private Channels + +Private channels are chat channels that only specific users can join. + +## Creating a Private Channel + +To create a private channel, use the [`/lc channel create`](../../../reference/commands/lc/channel.md) command and specify `true` for the private setting argument. + +``` +/lc channel create <channel ID> <channel name> [channel description] [private setting] +``` + +## Inviting to a Channel + +Private channels are not listed in the `/lc channel list` command and cannot be joined using the `/lc channel join` command. + +To join, you need to receive an invitation from the owner or moderator. + +To invite someone, use the [`/lc channel invite`](../../../reference/commands/lc/channel.md#lc-channel-invite-player-id) command. + +``` +/lc channel invite <player ID> +``` + +The invited player automatically joins the channel. diff --git a/docs/src/guide/about.md b/docs/src/guide/about.md index 41df712..933426f 100644 --- a/docs/src/guide/about.md +++ b/docs/src/guide/about.md @@ -12,9 +12,10 @@ LunaticChat は Minecraft サーバソフトウェアである Paper / Velocity - **1on1 ダイレクトメッセージ機能**: /tell や /msg コマンドで,1対1のチャットが可能です. - **クイック返信**: /reply で,直前にメッセージを送信した相手に素早く返信可能です. - **かな・ローマ字変換**: ローマ字で入力したメッセージを,自動的に日本語に変換します. +- **チャンネルチャット機能**: 複数のチャットチャンネルを作成し,特定のチャンネルでのみメッセージを送信できる環境を実現します. - **CoreProtect への対応**: LunaticChat のチャットログは CoreProtect と互換性があります. - **キャッシュによる高速動作**: ローマ字から日本語の変換はキャッシュを使用し,従来の LunaChat よりも高速に動作します. -- **最新版の対応**: 他プラグインとの依存を減らし,常に最新の Minecraft バージョンに対応します. +- **最新版の対応**: 他プラグインとの依存を完全に排除し,常に最新の Minecraft バージョンに対応します. ## 比較 diff --git a/docs/src/guide/admin/cache.md b/docs/src/guide/admin/cache.md index 9e45ff2..afacac5 100644 --- a/docs/src/guide/admin/cache.md +++ b/docs/src/guide/admin/cache.md @@ -29,13 +29,3 @@ LunaticChat では,キャッシュのパージ 機能は実装されていま これはプレイヤー側にホストのファイルシステムを操作させることがセキュリティ上好ましくないためです. ::: - -## キャッシュバージョン - -ディスクキャッシュに使用されるファイルには `version` フィールドが含まれており,LunaticChat のバージョンアップに伴うキャッシュフォーマットの変更に対応しています. - -バージョンが不一致の場合,LunaticChat はキャッシュファイルを **古い形式のキャッシュ** として認識し,内容を無視して新しい形式で再作成します. - -```json -{"version":"1","entries":{}} -``` diff --git a/docs/src/guide/admin/configuration.md b/docs/src/guide/admin/configuration.md index ac63eb7..dfa41f9 100644 --- a/docs/src/guide/admin/configuration.md +++ b/docs/src/guide/admin/configuration.md @@ -54,6 +54,12 @@ features: channelChat: # If enabled, channel-based chat functionality will be activated. enabled: false + # Maximum number of channels that can be created per server. Set to 0 for unlimited. + maxChannelsPerServer: 0 + # Maximum number of members allowed in a single channel. Set to 0 for unlimited. + maxMembersPerChannel: 0 + # Maximum number of channels a single player can join. Set to 0 for unlimited. + maxMembershipPerPlayer: 0 # ---------------------------------------------- # --------- Message Format Settings -------- @@ -171,13 +177,42 @@ LunaticChat の [`/reply`](../../reference/commands/reply.md) コマンドによ ローマ字変換 API へのリクエストが失敗した場合の再試行回数を指定します. -### `features.channelChat.enabled` +### `features.channelChat` + +#### `enabled` - Type: `boolean` - Default: `false` チャンネルチャット機能を有効にします. +#### `maxChannelsPerServer` + +- Type: `integer` +- Default: `0` + +サーバーあたりで作成可能なチャンネルの最大数を指定します. + +`0` に設定すると無制限になります. + +#### `maxMembersPerChannel` + +- Type: `integer` +- Default: `0` + +1 つのチャンネルに参加可能なメンバーの最大数を指定します. + +`0` に設定すると無制限になります. + +#### `maxMembershipPerPlayer` + +- Type: `integer` +- Default: `0` + +1 人のプレイヤーが参加可能なチャンネルの最大数を指定します. + +`0` に設定すると無制限になります. + ## Message Format Settings 使用できるプレースホルダー: diff --git a/docs/src/guide/admin/data-and-logs.md b/docs/src/guide/admin/data-and-logs.md new file mode 100644 index 0000000..6f48857 --- /dev/null +++ b/docs/src/guide/admin/data-and-logs.md @@ -0,0 +1,119 @@ +# データ・ログ + +::: danger 編集厳禁 + +これらのデータファイルは LunaticChat の動作に不可欠です.直接編集すると,データの破損や予期せぬ動作を引き起こす可能性があります.データのバックアップを取る場合を除き,これらのファイルを直接編集しないでください. + +::: + +## データの保存場所 + +LunaticChat は、チャンネルデータや設定情報をローカルディスクに保存します. + +- `channels.json`: チャンネル情報を保存するファイルです. +- `chatmodes.json`: プレイヤーのチャットモード設定を保存するファイルです. +- `conversion_cache.json`: チャンネル変換のキャッシュを保存するファイルです. +- `player-settings.yaml`: プレイヤーごとの設定情報を保存するファイルです. + +::: tip 定期的なバックアップ + +LunaticChat のデータの安全性を確保するために,定期的にバックアップを作成することをお勧めします. + +::: + +### `channels.json` + +`channels.json` ファイルは、LunaticChat が管理するチャンネルの情報を保存します.このファイルには,チャンネル名、参加者リスト、チャットモードなどの情報が含まれます. + +```json +{ + "channels": { + "general-channel": { + "id": "general-channel", + "name": "一般チャンネル", + "ownerId": "a01e3843-e521-3998-958a-f459800e4d11", + "createdAt": 1769507213150, + "bannedPlayers": [ + "ceaea267-39dd-3bac-931c-761ada671ebe" + ] + } + }, + "members": { + "general-channel": [ + { + "channelId": "test2", + "playerId": "a01e3843-e521-3998-958a-f459800e4d11", + "role": "OWNER", + "joinedAt": 1769507213150 + } + ] + }, + "activeChannels": { + "a01e3843-e521-3998-958a-f459800e4d11": "test2" + } +} +``` + +### `chatmodes.json` + +`chatmodes.json` ファイルは、プレイヤーのチャットモード設定を保存します.このファイルには,プレイヤーごとのチャットモード情報が含まれます. + +```json +{ + "modes": { + "aed5efd4-551b-3965-bc28-ae21aa072a66": "CHANNEL", + "ceaea267-39dd-3bac-931c-761ada671ebe": "CHANNEL", + "a01e3843-e521-3998-958a-f459800e4d11": "CHANNEL", + "681f539b-8bb8-3f85-85e5-a2945f6c6539": "GLOBAL" + } +} +``` + +### `conversion_cache.json` + +`conversion_cache.json` ファイルは、チャンネル変換のキャッシュを保存します.このファイルには,プレイヤーごとのチャンネル変換情報が含まれます. + +キャッシュシステムに関する詳細は [こちら](./cache.md) をご覧ください. + + +```json +{"version":"1","entries":{"hi":"日"}} +``` + +### `player-settings.yaml` + +`player-settings.yaml` ファイルは、プレイヤーごとの設定情報を保存します.このファイルには,プレイヤーの個別設定が含まれます. + +```yaml +version: 1 +japaneseConversion: + "aed5efd4-551b-3965-bc28-ae21aa072a66": false + "ceaea267-39dd-3bac-931c-761ada671ebe": false +directMessageNotification: + "aed5efd4-551b-3965-bc28-ae21aa072a66": true + "ceaea267-39dd-3bac-931c-761ada671ebe": true +channelMessageNotification: + "ceaea267-39dd-3bac-931c-761ada671ebe": true +``` + +## キャッシュバージョン + +ディスクキャッシュに使用されるファイルには `version` フィールドが含まれており,LunaticChat のバージョンアップに伴うキャッシュフォーマットの変更に対応しています. + +バージョンが不一致の場合,LunaticChat はキャッシュファイルを **古い形式のキャッシュ** として認識し,内容を無視して新しい形式で再作成します. + +```json +{"version":"1","entries":{}} +``` + +## CoreProtect について + +LunaticChat の各種チャットログは API 不要で CoreProtect でも記録することができます. + +各機能のログは次のアクションで確認できます. `/co lookup` コマンド使用時に以下のアクションを指定してください: + +- ダイレクトメッセージ: `command` +- 全体チャット・チャンネルチャット: `chat` + - かな・ローマ字変換はそのプレイヤーの設定状況により保存されます. + + diff --git a/docs/src/guide/admin/introduction-channel-chat.md b/docs/src/guide/admin/introduction-channel-chat.md new file mode 100644 index 0000000..6993435 --- /dev/null +++ b/docs/src/guide/admin/introduction-channel-chat.md @@ -0,0 +1,56 @@ +# チャンネルチャット展開ガイド + +このガイドでは、チャンネルチャットの展開方法について説明します. + +## チャンネルチャットとは + +プレイヤー間でチャンネルを作成し,特定のプレイヤー間で,チャットを共有できる機能です. + +詳しい機能については [プレイヤー向けガイド版](../player/channel-chat/about.md) を参照してください. + +## チャンネルチャットの展開準備 + +チャンネルチャットを展開するには,LunaticChat の設定ファイル `config.yml` でチャンネルチャット機能を有効化する必要があります. + +1. サーバーを停止します. +2. `plugins/LunaticChat/config.yml` を開きます. +3. `features.channelChat.enabled` を `true` に設定します. +4. サーバーを再起動します. + +以上でチャンネルチャット機能が有効化され,プレイヤーがチャンネルチャットを使用できるようになります. + +## チャンネルチャットの設定 + +チャンネルチャットに関する設定項目は以下の通りです. + +- `features.channelChat.maxChannelsPerPlayer`: 1人のプレイヤーが作成できるチャンネルの最大数を指定します. +- `features.channelChat.maxMembersPerChannel`: 1つのチャンネルに参加できるメンバーの最大数を指定します. +- `features.channelChat.maxMembershipPerPlayer`: 1人のプレイヤーが参加できるチャンネルの最大数を指定します. + +デフォルトは `0` に設定されており,制限はありません. + +::: tip 設定のおすすめは? + +チャンネルチャット機能を活発に使用したい場合は,これらの値を高めに設定することをお勧めします. + +ただし,サーバーのパフォーマンスに影響を与える可能性があるため,サーバーのリソース状況に応じて適切に設定してください. + +おすすめの設定は次のとおりです. + +- `features.channelChat.maxChannelsPerPlayer`: `3` 〜 `5` +- `features.channelChat.maxMembersPerChannel`: `20` 〜 `50` +- `features.channelChat.maxMembershipPerPlayer`: `5` 〜 `10` + +::: + +## チャンネルの管理 + +基本的に,[チャンネルの管理はプレイヤー自身が行います](../player/channel-chat/moderation.md). + +ただし,サーバー管理者として,以下の点に注意してください: + +- サーバー管理者はすべてのチャンネルに対してオーナー権限を持ちます.操作が必要な場合は,適切に対応してください. +- サーバーのパフォーマンスを維持するために,必要に応じてチャンネル数やメンバー数の制限を設定してください. +- 不適切なチャンネルやメンバー行動が発生した場合は,適切な措置を講じてください. + +また,それらのチャンネルの管理によるトラブルを回避したい場合は `/lc channel ban` などのモデレートコマンドの制限を検討してください. diff --git a/docs/src/guide/admin/velocity.md b/docs/src/guide/admin/velocity.md index bc7f8d5..bbf5db6 100644 --- a/docs/src/guide/admin/velocity.md +++ b/docs/src/guide/admin/velocity.md @@ -4,10 +4,6 @@ Velocity を使用して,プロキシサーバーを繋いだ Paper サーバ ::: warning 試験的機能 -この機能は現在,試験的に実装されており,今後のアップデートで仕様が変更される可能性があります. - -規模が大きい機能なので,一気に実装はせず段階的な展開を予定しています. - -詳しい実装計画は [ロードマップ (m1sk9/LunaticChat #54)](https://github.com/m1sk9/LunaticChat/issues/54) をご覧ください. +この機能は現在試験的な機能として提供されています.今後のアップデートで仕様が変更される可能性があります. ::: diff --git a/docs/src/guide/getting-started.md b/docs/src/guide/getting-started.md index 08fe38a..3c55628 100644 --- a/docs/src/guide/getting-started.md +++ b/docs/src/guide/getting-started.md @@ -1,5 +1,27 @@ # はじめる +## 事前準備 + +LunaticChat を使用するには,以下のソフトウェアが必要です: + +- Java 21 (LTS 以降) +- Paper 1.21 以降 + - 最新版は [こちら](https://papermc.io/downloads/paper), それ以降のバージョンは [こちら](https://fill-ui.papermc.io/projects/paper/family/1.21) から入手できます. + +::: warning Spigot 系プラットフォームでの動作について + +LunaticChat は Paper プラグインです.Spigot / Bukkit / CraftBukkit では動作しません. + +::: + +::: tip Folia のサポートについて + +Folia は現在サポートされていません.将来的に対応する可能性がありますが,現時点では Folia 上での動作は保証されていません. + +詳しくは [Folia Support (m1sk9/LunaticChat #42)](https://github.com/m1sk9/LunaticChat/issues/42) を参照してください. + +::: + ## インストール LunaticChat をインストールします.LunaticChat は以下から入手できます: @@ -7,23 +29,48 @@ LunaticChat をインストールします.LunaticChat は以下から入手 - [GitHub](https://github.com/m1sk9/LunaticChat/releases) - [Modrinth](https://modrinth.com/project/lunaticchat) -ダウンロードしたプラグインファイルをサーバーの `plugins` フォルダに配置し,サーバーを再起動します. +ダウンロードしたプラグインファイルをサーバーの `plugins` フォルダに配置し,サーバーを起動します. ## 設定 -LunaticChat を起動すると以下のファイルが作成されます. +LunaticChat を起動すると設定ファイル `config.yml` が作成されます. + +該当のファイルを開き,初期設定を変更してください.なお,全設定項目の詳細については,[設定ガイド](admin/configuration.md)を参照してください. -- `plugins/LunaticChat/config.yml`:プラグインの設定ファイル -- `plugins/LunaticChat/player-settings.yaml`:ユーザーごとの設定ファイル -- `plugins/LunaticChat/conversion_cache.json`: ローマ字変換のキャッシュファイル +### 推奨する初期設定 -設定ファイル `config.yml` を開き,必要に応じて設定を変更します.設定項目の詳細については,[設定ガイド](admin/configuration.md)を参照してください. +`config.yml` の以下の項目を確認・変更してください: + +- `checkForUpdates`: LunaticChat のアップデートチェックを有効にするかどうかを指定します.`true` に設定することを推奨します. +- `language`: LunaticChat のメッセージ言語を指定します.日本語環境の場合は `ja` に設定してください. + - なお,日本語環境が使用できないサーバー向けにプラグイン内のログは全て英語で出力されます. +- `features.quickReplies.enabled`: クイックリプライ機能を有効にするかどうかを指定します.`true` に設定することを推奨します. +- `features.japaneseConversion.enabled`: ローマ字からひらがなへの変換機能を有効にするかどうかを指定します.日本人向けにサーバーを開放する場合は `true` に設定することを推奨します. + +::: tip その他,設定項目について + +チャンネルチャットなどの各機能に関する設定項目も存在します. + +必要に応じて設定を変更してください. + +::: ## パーミッション LunaticChat のパーミッションを LuckPerms などのパーミッション管理プラグインで設定します. -基本的なパーミッションは Paper や Velocity のデフォルトパーミッションシステム ( `OP` / `non OP` ) でも設定できますが,より詳細な制御を行う場合は LuckPerms の使用を推奨します. +基本的なパーミッションは Paper や Velocity のデフォルトパーミッションシステム ( `OP` / `non OP` ) でも使用できるように開発されていますが,より詳細な制御を行う場合は LuckPerms の使用を推奨します. - パーミッションノードの詳細については [パーミッションガイド](../reference/permissions.md)を参照してください. - コマンドの各機能に対応するパーミッションノードは,[コマンドリファレンス](../reference/index.md)を参照してください. + +## サーバーの再起動 + +設定が完了したら,サーバーを再起動して設定を反映させてください. + +以上で LunaticChat の基本的なセットアップは完了です. + +## 次は? + +- [キャッシュシステム](./admin/cache.md): LunaticChat のキャッシュシステムについて説明します. +- [] diff --git a/docs/src/guide/player/channel-chat/about.md b/docs/src/guide/player/channel-chat/about.md index 67aa170..9d1ae33 100644 --- a/docs/src/guide/player/channel-chat/about.md +++ b/docs/src/guide/player/channel-chat/about.md @@ -4,11 +4,7 @@ ::: warning 試験的機能 -この機能は現在,試験的に実装されており,今後のアップデートで仕様が変更される可能性があります. - -規模が大きい機能なので,一気に実装はせず段階的な展開を予定しています. - -詳しい実装計画は [ロードマップ (m1sk9/LunaticChat #54)](https://github.com/m1sk9/LunaticChat/issues/54) をご覧ください. +この機能は現在試験的な機能として提供されています.今後のアップデートで仕様が変更される可能性があります. ::: @@ -65,22 +61,22 @@ 作成されたチャンネルに参加するには,[`/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 join`](#チャンネルに参加する) コマンドを使用して,チャンネルに参加できます. + ``` /lc channel list ``` -チャット欄に参加可能なチャンネルの一覧が表示されます.クリックするか [`/lc channel join`](#チャンネルに参加する) コマンドを使用して,チャンネルに参加できます. - ## チャットモード LunaticChat では,プレイヤーがチャットを送信する際のモードとして,以下の2種類のチャットモードが存在します: diff --git a/docs/src/guide/player/channel-chat/moderation.md b/docs/src/guide/player/channel-chat/moderation.md new file mode 100644 index 0000000..821c635 --- /dev/null +++ b/docs/src/guide/player/channel-chat/moderation.md @@ -0,0 +1,95 @@ +# チャンネルの管理 + +チャンネルチャットのモデレート機能を使用して,チャンネル内のメンバー行動を管理できます. + +::: danger 注意 + +これらのモデレート機能はあくまで LunaticChat のチャンネルチャットに対して使用できるものです. + +実際にルール違反者に対して処罰が必要な場合は,サーバー管理者に通報することをお勧めします. + +::: + +## モデレート機能の仕様 + +モデレート機能にはいくつかの仕様があります. + +- Bypass 権限を持つプレイヤーは,モデレート機能の影響を受けません. + - つまり,サーバー管理者に対してはこれらの機能を使用できません. +- モデレート機能を使用するには,それぞれのチャンネルのロールとサーバー自体の権限が必要です. +- モデレート機能の効果はチャンネルごとに独立しています. + - 例えば,あるチャンネルで永久追放(BAN)されたプレイヤーは,他のチャンネルでは通常通りチャットできます. + +## ロール + +チャンネルには以下のロールが存在しています. + +- メンバー (Member): チャンネルに参加している通常のプレイヤー +- モデレーター (Moderator): チャンネルのモデレート機能を使用できるプレイヤー +- オーナー (Owner): チャンネルの作成者であり,すべてのモデレート機能を使用できるプレイヤー + +::: tip サーバー管理者によるモデレート + +サーバー管理者は一律で全てのチャンネルにおいてオーナー権限と同じ権限を持ちます. + +::: + +## メンバーを一時的に追放する (Kick) + +メンバーを一時的にチャンネルから追放するには,[`/lc channel kick`](../../../reference/commands/lc/channel.md#lc-channel-kick-プレイヤーid) コマンドを使用します. + +``` +/lc channel kick <プレイヤーID> +``` + +このコマンドを実行すると,指定したプレイヤーがチャンネルから即座に退出されます. + +ただし,追放されたプレイヤーは再参加が可能です. + +## メンバーを永久に追放する (Ban) + +メンバーを永久にチャンネルから追放するには,[`/lc channel ban`](../../../reference/commands/lc/channel.md#lc-channel-ban-プレイヤーid) コマンドを使用します. + +``` +/lc channel ban <プレイヤーID> +``` + +このコマンドを実行すると,指定したプレイヤーがチャンネルから即座に退出され,さらに再参加も禁止されます. + +## 永久追放を解除する + +永久追放されたメンバーの追放を解除するには,[`/lc channel unban`](../../../reference/commands/lc/channel.md#lc-channel-unban-プレイヤーid) コマンドを使用します. + +``` +/lc channel unban <プレイヤーID> +``` + +## モデレーターを任命・解任する + +チャンネルのメンバーをモデレーターに任命/解任するには,[`/lc channel mod`](../../../reference/commands/lc/channel.md#lc-channel-mod-プレイヤーid) コマンドを使用します. + +``` +/lc channel mod <プレイヤーID> +``` + +このコマンドを実行すると,指定したプレイヤーがモデレーターに任命されます.また任命状態のプレイヤーに対して再度このコマンドを実行すると,モデレーターから解任されます. + +## オーナー権限を譲渡する + +チャンネルのオーナー権限を他のメンバーに譲渡するには,[`/lc channel ownership`](../../../reference/commands/lc/channel.md#lc-channel-ownership-プレイヤーid) コマンドを使用します. + +``` +/lc channel ownership <プレイヤーID> +``` + +このコマンドを実行すると,指定したプレイヤーに全てのオーナー権限が譲渡されます.元のオーナーはモデレーターになります. + +::: danger 実行時の注意点 + +- オーナー権限の譲渡は不可逆的な操作です. + - つまり,一度譲渡してしまうとオーナー権限を持つプレイヤーが再度譲渡しない限り,元のオーナーがオーナー権限を取り戻すことはできません. + - これはサーバー管理者であってもです. +- オーナー権限を譲渡する相手は,必ずチャンネルのメンバーである必要があります. +- オーナー権限は全てのモデレート機能を使用でき,かつチャンネルを削除できる強力な権限です.譲渡する相手を慎重に選んでください. + +::: diff --git a/docs/src/guide/player/channel-chat/private-channel.md b/docs/src/guide/player/channel-chat/private-channel.md new file mode 100644 index 0000000..6123310 --- /dev/null +++ b/docs/src/guide/player/channel-chat/private-channel.md @@ -0,0 +1,25 @@ +# プライベートチャンネル + +プライベートチャンネルは,特定のユーザーのみが参加できるチャットチャンネルです. + +## プライベートチャンネルの作成 + +プライベートを作成する場合は [`/lc channel create`](../../../reference/commands/lc/channel.md) コマンドで,プライベート設定引数に `true` を指定します. + +``` +/lc channel create <チャンネルID> <チャンネル名> [チャンネルの説明] [プライベート設定] +``` + +## チャンネルへの招待 + +プライベートチャンネルは `/lc channel list` コマンドでは一覧表示されず, `/lc channel join` コマンドで参加することもできません. + +参加するにはオーナーもしくはモデレーターから招待を受ける必要があります. + +招待するには,[`/lc channel invite`](../../../reference/commands/lc/channel.md#lc-channel-invite-プレイヤーid) コマンドを使用します. + +``` +/lc channel invite <プレイヤーID> +``` + +招待を受けたプレイヤーは自動でチャンネルに参加します. diff --git a/docs/src/index.md b/docs/src/index.md index 2d06bdd..430077f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -35,7 +35,7 @@ features: - title: CoreProtect への対応 details: LunaticChat のチャットログは CoreProtect と互換性があります icon: 🗒️ - - title: チャンネルチャット機能 (実装予定) + - title: チャンネルチャット機能 details: チャットチャンネルを作成・管理し,特定のプレイヤー間でのプライベートメッセージを送信可能 icon: ☎️ - title: マルチプラットフォーム対応 (実装予定) diff --git a/docs/src/reference/commands/lc/channel.md b/docs/src/reference/commands/lc/channel.md index 9cc96e2..07381a6 100644 --- a/docs/src/reference/commands/lc/channel.md +++ b/docs/src/reference/commands/lc/channel.md @@ -74,5 +74,68 @@ |-----------------------------------------| | `lunaticchat.command.lc.channel.delete` | -指定したチャンネルを削除します.チャンネルの作成者(オーナー)である必要があります. +指定したチャンネルを削除します.チャンネルの作成者(オーナー) である必要があります. +### `/lc channel info [<チャンネルID>]` + +| パーミッション | +|---------------------------------------| +| `lunaticchat.command.lc.channel.info` | + +指定したチャンネルの情報を表示します. + +引数を指定した場合は現在アクティブ状態のチャンネルの情報を表示します. + +### `/lc channel invite <プレイヤーID>` + +| パーミッション | +|-----------------------------------------| +| `lunaticchat.command.lc.channel.invite` | + +指定したプレイヤーを現在アクティブ状態のチャンネルに招待します. + +### `/lc channel kick <プレイヤーID>` + +| パーミッション | +|---------------------------------------| +| `lunaticchat.command.lc.channel.kick` | + +指定したプレイヤーを現在アクティブ状態のチャンネルから一時的に追放します. + +追放されたプレイヤーはそのチャンネルから即時退出されますが,再参加できます. + +### `/lc channel ban <プレイヤーID>` + +| パーミッション | +|-----------------------------------------| +| `lunaticchat.command.lc.channel.ban` | + +指定したプレイヤーを現在アクティブ状態のチャンネルから永久追放します. + +永久追放されたプレイヤーはそのチャンネルから即時退出され,再参加できません. + +### `/lc channel unban <プレイヤーID>` + +| パーミッション | +|----------------------------------------| +| `lunaticchat.command.lc.channel.unban` | + +指定したプレイヤーの永久追放の解除を行います. + +### `/lc channel mod <プレイヤーID>` + +| パーミッション | +|--------------------------------------| +| `lunaticchat.command.lc.channel.mod` | + +指定したプレイヤーを現在アクティブ状態のチャンネルのモデレーターに昇格/降格させます. + +### `/lc channel ownership <プレイヤーID>` + +| パーミッション | +|--------------------------------------------| +| `lunaticchat.command.lc.channel.ownership` | + +指定したプレイヤーに現在アクティブ状態のチャンネルのオーナー権を譲渡します. + +譲渡後,元のオーナーにはモデレーター権が付与されます. diff --git a/docs/src/reference/permissions.md b/docs/src/reference/permissions.md index b557ae9..fb30a36 100644 --- a/docs/src/reference/permissions.md +++ b/docs/src/reference/permissions.md @@ -8,9 +8,9 @@ LuckPerms を使用した設定方法に関する詳細は [LuckPerms Wiki](http - Default: `OP` -[`/tell`](commands/tell.md) / [`/reply`](commands/reply.md) コマンドでのやり取りを可視化します. +各種プレイヤー間のやり取りを可視化します. -この権限を持つプレイヤーは他プレイヤーの [`/tell`](commands/tell.md) / [`/reply`](commands/reply.md) でのメッセージがブロードキャストされます. +この権限を持つプレイヤーは他プレイヤーの [`/tell`](commands/tell.md) / [`/reply`](commands/reply.md) でのメッセージ・全チャンネルチャットがブロードキャストされます. ### `lunaticchat.noticeUpdate` @@ -20,6 +20,12 @@ LunaticChat のアップデート通知を受け取ります. [受け取るには `checkForUpdates` を有効にしておく](../guide/admin/configuration.md#checkforupdates) 必要があります. +### `lunaticchat.channelbypass` + +- Default: `OP` + +各チャンネルのモデレート機能やプライベートチャンネルに対する制限を無視します. + ## `lunaticchat.command.*` ### `lunaticchat.command.tell` @@ -94,12 +100,54 @@ LunaticChat のアップデート通知を受け取ります. [`/lc channel status`](commands/lc/channel.md) コマンドの使用を切り替えます. +### `lunaticchat.command.lc.channel.info` + +- Default: `non OP` + +[`/lc channel info`](commands/lc/channel.md) コマンドの使用を切り替えます. + ### `lunaticchat.command.lc.channel.delete` - Default: `non OP` [`/lc channel delete`](commands/lc/channel.md) コマンドの使用を切り替えます. +### `lunaticchat.command.lc.channel.invite` + +- Default: `non OP` + +[`/lc channel invite`](commands/lc/channel.md) コマンドの使用を切り替えます. + +### `lunaticchat.command.lc.channel.kick` + +- Default: `non OP` + +[`/lc channel kick`](commands/lc/channel.md) コマンドの使用を切り替えます. + +### `lunaticchat.command.lc.channel.ban` + +- Default: `non OP` + +[`/lc channel ban`](commands/lc/channel.md) コマンドの使用を切り替えます. + +### `lunaticchat.command.lc.channel.unban` + +- Default: `non OP` + +[`/lc channel unban`](commands/lc/channel.md) コマンドの使用を切り替えます. + +### `lunaticchat.command.lc.channel.mod` + +- Default: `non OP` + +[`/lc channel mod`](commands/lc/channel.md) コマンドの使用を切り替えます. + +### `lunaticchat.command.lc.channel.ownership` + +- Default: `non OP` + +[`/lc channel ownership`](commands/lc/channel.md) コマンドの使用を切り替えます. + ### `lunaticchat.command.lc.chatmode` - Default: `non OP` diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 67265e6..0a86c20 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { // Test dependencies testImplementation("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT") + testImplementation("io.mockk:mockk:1.13.13") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") } tasks { 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 ba2eca6..31a6886 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 @@ -22,7 +22,7 @@ import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.listener.EventListenerRegistry import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import org.bukkit.event.Listener import org.bukkit.plugin.java.JavaPlugin import java.util.concurrent.atomic.AtomicBoolean @@ -43,13 +43,15 @@ class LunaticChat : private lateinit var services: ServiceContainer private lateinit var configuration: LunaticChatConfiguration private lateinit var serviceInitializer: ServiceInitializer + private lateinit var pluginScope: PluginCoroutineScope private var updateChecker: UpdateChecker? = null private val updateAvailable = AtomicBoolean(false) override fun onEnable() { saveDefaultConfig() - configuration = ConfigManager.loadConfiguration(config) + val configManager = ConfigManager() + configuration = configManager.loadConfiguration(config) if (configuration.debug) { logger.warning("LunaticChat is running in debug mode.") @@ -58,6 +60,9 @@ class LunaticChat : val httpClient = HttpClient(CIO) + // Initialize plugin coroutine scope + pluginScope = PluginCoroutineScope(this, logger) + // Initialize all services serviceInitializer = ServiceInitializer( @@ -93,6 +98,7 @@ class LunaticChat : } override fun onDisable() { + pluginScope.cancel() serviceInitializer.shutdown(services) logger.info("LunaticChat disabled.") } @@ -157,6 +163,7 @@ class LunaticChat : /** * Initializes the update checker. + * Uses plugin coroutine scope instead of runBlocking for non-blocking async execution. */ private fun initializeUpdateChecker(httpClient: HttpClient) { updateChecker = @@ -165,14 +172,9 @@ class LunaticChat : logger = logger, httpClient = httpClient, ) - server.scheduler.runTaskAsynchronously( - this, - Runnable { - runBlocking { - checkUpdates() - } - }, - ) + pluginScope.scope.launch { + checkUpdates() + } } private suspend fun checkUpdates() { diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt new file mode 100644 index 0000000..34ae0fc --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/PluginCoroutineScope.kt @@ -0,0 +1,49 @@ +package dev.m1sk9.lunaticChat.paper + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import org.bukkit.plugin.java.JavaPlugin +import java.util.logging.Logger + +/** + * Provides a coroutine scope tied to the plugin lifecycle. + * + * This scope: + * - Uses Dispatchers.Default for background CPU-bound work + * - Uses SupervisorJob to prevent child failures from canceling the entire scope + * - Is properly cancelled when the plugin disables + * + * Usage: + * ```kotlin + * pluginScope.launch { + * val result = withTimeout(5000) { + * someAsyncOperation() + * } + * // Handle result... + * } + * ``` + */ +class PluginCoroutineScope( + private val plugin: JavaPlugin, + private val logger: Logger, +) { + private val job = SupervisorJob() + val scope = CoroutineScope(Dispatchers.Default + job) + + /** + * Cancels all coroutines in this scope. + * Should be called during plugin disable. + */ + fun cancel() { + logger.info("Cancelling plugin coroutine scope...") + scope.cancel() + logger.info("Plugin coroutine scope cancelled.") + } + + /** + * Returns true if the scope is still active. + */ + fun isActive(): Boolean = job.isActive +} 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 cea20bf..daba3dd 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 @@ -100,8 +100,10 @@ class ServiceInitializer( // 5. Initialize handlers val directMessageHandler = DirectMessageHandler( + configuration = configuration, settingsManager = playerSettingsManager, romanjiConverter = romajiConverter, + languageManager = languageManager, ) return ServiceContainer( @@ -227,9 +229,11 @@ class ServiceInitializer( val messageHandler = ChannelMessageHandler( + configuration = configuration, settingsManager = settingsManager, channelManager = manager, romanjiConverter = romajiConverter, + languageManager = languageManager, logger = io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index 89adb6f..12426cf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -4,22 +4,24 @@ 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.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.ktor.util.logging.Logger import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.HoverEvent import org.bukkit.Bukkit import org.bukkit.entity.Player class ChannelMessageHandler( + private val configuration: LunaticChatConfiguration, private val settingsManager: PlayerSettingsManager?, private val channelManager: ChannelManager, private val romanjiConverter: RomanjiConverter?, + private val languageManager: LanguageManager, private val logger: Logger, ) { - private var lunaticChatConfiguration = ConfigManager.getConfiguration() - fun sendChannelMessage( player: Player, message: String, @@ -31,17 +33,19 @@ class ChannelMessageHandler( val senderSettings = settingsManager?.getSettings(playerId) - // Handle romaji conversion if enabled (requires blocking for HTTP call) + // Handle romaji conversion if enabled + // Uses explicit timeout to prevent long blocking (1s max instead of 3s) val displayMessage = if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) { runCatching { kotlinx.coroutines.runBlocking { - romanjiConverter - ?.convert(message) - ?.let { "$message §e($it)" } - ?: message + kotlinx.coroutines + .withTimeoutOrNull(1000) { + romanjiConverter!! + .convert(message) + }?.let { "$message §e($it)" } ?: message } - }.getOrNull() ?: message + }.getOrElse { message } } else { message } @@ -60,7 +64,15 @@ class ChannelMessageHandler( .getDirectMessageSpyPlayers() .values .filter { it.isOnline && it.uniqueId != playerId && it.uniqueId !in memberIds } - .forEach { it.sendMessage(spyMessage) } + .forEach { + it.sendMessage( + spyMessage.hoverEvent( + HoverEvent.showText( + Component.text(languageManager.getMessage("general.spyMessage")), + ), + ), + ) + } context.members.forEach { member -> Bukkit.getPlayer(member.playerId)?.let { memberPlayer -> if (memberPlayer.isOnline) { @@ -88,7 +100,7 @@ class ChannelMessageHandler( channelName: String, message: String, ): Component { - val format = lunaticChatConfiguration.messageFormat.channelMessageFormat + val format = configuration.messageFormat.channelMessageFormat val text = format .replace("{sender}", senderName) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt index 6155381..3f7950a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt @@ -3,11 +3,13 @@ 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.playMessageSendNotification -import dev.m1sk9.lunaticChat.paper.config.ConfigManager +import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent import org.bukkit.Bukkit import org.bukkit.entity.Player import java.util.UUID @@ -18,11 +20,11 @@ import java.util.concurrent.ConcurrentHashMap * Tracks the last player who messaged each player for /reply functionality. */ class DirectMessageHandler( + private val configuration: LunaticChatConfiguration, private val settingsManager: PlayerSettingsManager?, private val romanjiConverter: RomanjiConverter?, + private val languageManager: LanguageManager, ) { - private var lunaticChatConfiguration = ConfigManager.getConfiguration() - private val lastMessager: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap() private val lastRecipient: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap() @@ -90,29 +92,39 @@ class DirectMessageHandler( val senderSettings = settingsManager?.getSettings(sender.uniqueId) val recipientSettings = settingsManager?.getSettings(recipient.uniqueId) - // Handle romaji conversion if enabled (requires blocking for HTTP call) + // Handle romaji conversion if enabled + // Uses explicit timeout to prevent long blocking (1s max instead of 3s) val displayMessage = if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) { runCatching { kotlinx.coroutines.runBlocking { - romanjiConverter - ?.convert(message) - ?.let { "$message §e($it)" } - ?: message + kotlinx.coroutines + .withTimeoutOrNull(1000) { + romanjiConverter!! + .convert(message) + }?.let { "$message §e($it)" } ?: message } - }.getOrNull() ?: message + }.getOrElse { message } } else { message } - val format = lunaticChatConfiguration.messageFormat.directMessageFormat + val format = configuration.messageFormat.directMessageFormat val spyMessage = formatMessage(format, sender.name, recipient.name, message) SpyPermissionManager .getDirectMessageSpyPlayers() .values .filter { it.isOnline && it.uniqueId !in setOf(sender.uniqueId, recipient.uniqueId) } - .forEach { it.sendMessage(spyMessage) } + .forEach { + it.sendMessage( + spyMessage.hoverEvent( + HoverEvent.showText( + Component.text(languageManager.getMessage("general.spyMessage")), + ), + ), + ) + } val userMessage = formatMessage(format, sender.name, recipient.name, displayMessage) sender.apply { 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 c53d154..1bd969d 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 @@ -8,14 +8,11 @@ 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 - - fun getConfiguration(): LunaticChatConfiguration = - lunaticChatConfiguration ?: IllegalStateException("LunaticChat Config not loaded").let { throw it } - +/** + * Manages loading and parsing of plugin configuration. + * Converted from singleton to dependency injection pattern for better testability. + */ +class ConfigManager { fun loadConfiguration(configFile: FileConfiguration): LunaticChatConfiguration { val loadedConfig = LunaticChatConfiguration( @@ -81,7 +78,6 @@ object ConfigManager { ), ) - lunaticChatConfiguration = loadedConfig return loadedConfig } } 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 0fa4fa1..ec8804d 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 @@ -54,17 +54,20 @@ class PlayerChatListener( chatModeManager.getChatMode(player.uniqueId) } - // Handle romaji conversion if enabled (requires blocking for HTTP call) + // Handle romaji conversion if enabled + // Uses explicit timeout to prevent long blocking (1s max instead of 3s) + // Note: AsyncChatEvent runs on async thread, so runBlocking here doesn't block main thread val displayMessage = if (settings.japaneseConversionEnabled) { runCatching { runBlocking { - romajiConverter - .convert(messageWithoutPrefix) - ?.let { "$messageWithoutPrefix §e($it)" } - ?: messageWithoutPrefix + kotlinx.coroutines + .withTimeoutOrNull(1000) { + romajiConverter + .convert(messageWithoutPrefix) + }?.let { "$messageWithoutPrefix §e($it)" } ?: messageWithoutPrefix } - }.getOrNull() ?: messageWithoutPrefix + }.getOrElse { messageWithoutPrefix } } else { messageWithoutPrefix } diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index 2cbff48..767a9b3 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -191,6 +191,7 @@ 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." + spyMessage: "You have been granted permission, so this message is displayed in spy mode." toggle: off: "Disabled" diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index b51d605..a6f2526 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -53,9 +53,9 @@ channel: info: "/lc channel info [チャンネルID] - チャンネルの詳細情報を表示します" delete: "/lc channel delete <チャンネルID> - チャンネルを削除します" invite: "/lc channel invite <プレイヤー名> - プレイヤーをアクティブチャンネルに招待します" - kick: "/lc channel kick <プレイヤー名> - プレイヤーをアクティブチャンネルからキックします" - ban: "/lc channel ban <プレイヤー名> - プレイヤーをアクティブチャンネルからバンします" - unban: "/lc channel unban <プレイヤー名> - プレイヤーをアクティブチャンネルからアンバンします" + kick: "/lc channel kick <プレイヤー名> - プレイヤーをアクティブチャンネルから追放します" + ban: "/lc channel ban <プレイヤー名> - プレイヤーをアクティブチャンネルから永久追放します" + unban: "/lc channel unban <プレイヤー名> - プレイヤーをアクティブチャンネルからの追放を解除します" mod: "/lc channel mod <プレイヤー名> - プレイヤーをモデレーターに任命 / 任命解除します" ownership: "/lc channel ownership <プレイヤー名> - チャンネルのオーナー権限を譲渡します" create: @@ -191,6 +191,7 @@ general: playerOnlyCommand: "このコマンドはプレイヤーのみが実行できます" newUpdateAvailable: "LunaticChat の新しいバージョンが利用可能です。GitHubまたはModrinthからダウンロードできます" noPermission: "このコマンドを実行する権限がありません" + spyMessage: "あなたに権限が付与されているため、このメッセージはスパイ状態で表示されています" toggle: on: "有効" diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt new file mode 100644 index 0000000..66fdbd4 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/TestUtils.kt @@ -0,0 +1,208 @@ +package dev.m1sk9.lunaticChat.paper + +import dev.m1sk9.lunaticChat.engine.chat.channel.Channel +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole +import dev.m1sk9.lunaticChat.engine.settings.PlayerChatSettings +import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration +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 +import dev.m1sk9.lunaticChat.paper.config.key.QuickRepliesFeatureConfig +import dev.m1sk9.lunaticChat.paper.i18n.Language +import io.mockk.mockk +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin +import java.util.UUID +import java.util.logging.Logger + +/** + * Common test utilities for LunaticChat tests. + * Provides mock factories, test data builders, and assertion helpers. + */ +object TestUtils { + /** + * Creates a test logger that collects log messages for verification. + */ + class TestLogger : Logger("test", null) { + val infoMessages = mutableListOf<String>() + val warningMessages = mutableListOf<String>() + val severeMessages = mutableListOf<String>() + + override fun info(msg: String) { + infoMessages.add(msg) + } + + override fun warning(msg: String) { + warningMessages.add(msg) + } + + override fun severe(msg: String) { + severeMessages.add(msg) + } + + fun clear() { + infoMessages.clear() + warningMessages.clear() + severeMessages.clear() + } + } + + /** + * Creates a default test configuration with sensible defaults. + */ + fun createTestConfiguration( + quickRepliesEnabled: Boolean = true, + japaneseConversionEnabled: Boolean = false, + channelChatEnabled: Boolean = false, + maxChannelsPerServer: Int = 10, + maxMembersPerChannel: Int = 50, + maxMembershipPerPlayer: Int = 5, + debug: Boolean = false, + checkForUpdates: Boolean = false, + language: Language = Language.EN, + ): LunaticChatConfiguration = + LunaticChatConfiguration( + features = + FeaturesConfig( + quickReplies = QuickRepliesFeatureConfig(enabled = quickRepliesEnabled), + japaneseConversion = + JapaneseConversionFeatureConfig( + enabled = japaneseConversionEnabled, + cacheMaxEntries = 500, + cacheSaveIntervalSeconds = 300, + cacheFilePath = "test-conversion-cache.json", + apiTimeout = 3000, + apiRetryAttempts = 2, + ), + channelChat = + ChannelChatFeatureConfig( + enabled = channelChatEnabled, + maxChannelsPerServer = maxChannelsPerServer, + maxMembersPerChannel = maxMembersPerChannel, + maxMembershipPerPlayer = maxMembershipPerPlayer, + ), + ), + messageFormat = + MessageFormatConfig( + directMessageFormat = "§7[§e{sender} §7>> §e{recipient}§7] §f{message}", + channelMessageFormat = "§7[§b#{channel}§7] §e{sender}: §f{message}", + ), + debug = debug, + checkForUpdates = checkForUpdates, + userSettingsFilePath = "test-player-settings.yaml", + language = language, + ) + + /** + * Creates a default player settings for testing. + */ + fun createTestPlayerSettings( + uuid: UUID = UUID.randomUUID(), + japaneseConversionEnabled: Boolean = true, + directMessageNotificationEnabled: Boolean = true, + channelMessageNotificationEnabled: Boolean = true, + ): PlayerChatSettings = + PlayerChatSettings( + uuid = uuid, + japaneseConversionEnabled = japaneseConversionEnabled, + directMessageNotificationEnabled = directMessageNotificationEnabled, + channelMessageNotificationEnabled = channelMessageNotificationEnabled, + ) + + /** + * Creates a test channel with default values. + */ + fun createTestChannel( + id: String = "test-channel-1", + name: String = "Test Channel", + description: String? = null, + ownerId: UUID = UUID.randomUUID(), + isPrivate: Boolean = false, + createdAt: Long = System.currentTimeMillis(), + bannedPlayers: Set<UUID> = emptySet(), + ): Channel = + Channel( + id = id, + name = name, + description = description, + ownerId = ownerId, + createdAt = createdAt, + isPrivate = isPrivate, + bannedPlayers = bannedPlayers, + ) + + /** + * Creates a test channel member. + */ + fun createTestChannelMember( + channelId: String = "test-channel-1", + playerId: UUID = UUID.randomUUID(), + role: ChannelRole = ChannelRole.MEMBER, + joinedAt: Long = System.currentTimeMillis(), + ): ChannelMember = + ChannelMember( + channelId = channelId, + playerId = playerId, + role = role, + joinedAt = joinedAt, + ) + + /** + * Creates a mock Player with the given UUID and name. + */ + fun createMockPlayer( + uuid: UUID = UUID.randomUUID(), + name: String = "TestPlayer", + isOnline: Boolean = true, + ): Player { + val player = mockk<Player>(relaxed = true) + io.mockk.every { player.uniqueId } returns uuid + io.mockk.every { player.name } returns name + io.mockk.every { player.isOnline } returns isOnline + return player + } + + /** + * Creates a mock JavaPlugin for testing. + */ + fun createMockPlugin(): JavaPlugin = mockk<JavaPlugin>(relaxed = true) + + /** + * Creates a test UUID from an integer for deterministic testing. + */ + fun createTestUUID(value: Int): UUID = + UUID.fromString( + String.format( + "%08x-0000-0000-0000-000000000000", + value, + ), + ) + + /** + * Assertion helper to check if a string contains all given substrings. + */ + fun assertContainsAll( + actual: String, + vararg expected: String, + ) { + expected.forEach { substring -> + if (!actual.contains(substring)) { + throw AssertionError("Expected '$actual' to contain '$substring'") + } + } + } + + /** + * Assertion helper to check if a list contains items matching a predicate. + */ + fun <T> assertAny( + list: List<T>, + predicate: (T) -> Boolean, + ) { + if (!list.any(predicate)) { + throw AssertionError("Expected list to contain at least one matching item") + } + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt new file mode 100644 index 0000000..d548f60 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/ChatModeManagerTest.kt @@ -0,0 +1,223 @@ +package dev.m1sk9.lunaticChat.paper.chat + +import dev.m1sk9.lunaticChat.engine.chat.ChatMode +import dev.m1sk9.lunaticChat.engine.chat.ChatModeData +import dev.m1sk9.lunaticChat.paper.TestUtils +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Tests for ChatModeManager. + * Verifies chat mode management, toggling, and persistence. + */ +class ChatModeManagerTest { + private fun createChatModeManager( + initialData: ChatModeData = ChatModeData(), + ): Triple<ChatModeManager, ChatModeStorage, TestUtils.TestLogger> { + val logger = TestUtils.TestLogger() + val storage = mockk<ChatModeStorage>(relaxed = true) + + every { storage.loadFromDisk() } returns initialData + + val manager = ChatModeManager(storage, logger) + return Triple(manager, storage, logger) + } + + @Test + fun `initialize should load data from storage`() { + val playerId = UUID.randomUUID() + val initialData = ChatModeData(modes = mapOf(playerId to ChatMode.CHANNEL)) + val (manager, _, logger) = createChatModeManager(initialData) + + manager.initialize() + + assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId)) + assert(logger.infoMessages.any { it.contains("ChatModeManager initialized with 1 saved modes") }) + } + + @Test + fun `getChatMode should return DEFAULT for unknown player`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + val mode = manager.getChatMode(playerId) + + assertEquals(ChatMode.DEFAULT, mode) + assertEquals(ChatMode.GLOBAL, mode) // DEFAULT is GLOBAL + } + + @Test + fun `getChatMode should return set mode`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + + assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId)) + } + + @Test + fun `setChatMode should save to storage`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + + verify(exactly = 1) { storage.queueAsyncSave(any()) } + } + + @Test + fun `toggleChatMode should switch from GLOBAL to CHANNEL`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + // Initial mode is GLOBAL (default) + assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId)) + + val newMode = manager.toggleChatMode(playerId) + + assertEquals(ChatMode.CHANNEL, newMode) + assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId)) + } + + @Test + fun `toggleChatMode should switch from CHANNEL to GLOBAL`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + + val newMode = manager.toggleChatMode(playerId) + + assertEquals(ChatMode.GLOBAL, newMode) + assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId)) + } + + @Test + fun `toggleChatMode should persist changes`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.toggleChatMode(playerId) + + verify(atLeast = 1) { storage.queueAsyncSave(any()) } + } + + @Test + fun `removeChatMode should revert to default`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId)) + + manager.removeChatMode(playerId) + + assertEquals(ChatMode.GLOBAL, manager.getChatMode(playerId)) + } + + @Test + fun `removeChatMode should save to storage`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + manager.removeChatMode(playerId) + + verify(atLeast = 2) { storage.queueAsyncSave(any()) } // Once for set, once for remove + } + + @Test + fun `saveToDisk should synchronously save`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + manager.setChatMode(playerId, ChatMode.CHANNEL) + manager.saveToDisk() + + verify { storage.saveToDisk(any()) } + } + + @Test + fun `shutdown should save and shutdown storage`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + manager.shutdown() + + verify { storage.saveToDisk(any()) } + verify { storage.shutdown() } + } + + @Test + fun `manager should handle multiple players independently`() { + val (manager, _, _) = createChatModeManager() + manager.initialize() + + val player1 = UUID.randomUUID() + val player2 = UUID.randomUUID() + + manager.setChatMode(player1, ChatMode.CHANNEL) + manager.setChatMode(player2, ChatMode.GLOBAL) + + assertEquals(ChatMode.CHANNEL, manager.getChatMode(player1)) + assertEquals(ChatMode.GLOBAL, manager.getChatMode(player2)) + } + + @Test + fun `manager should support rapid mode changes`() { + val (manager, storage, _) = createChatModeManager() + manager.initialize() + + val playerId = UUID.randomUUID() + + // Toggle multiple times + manager.toggleChatMode(playerId) // GLOBAL -> CHANNEL + manager.toggleChatMode(playerId) // CHANNEL -> GLOBAL + manager.toggleChatMode(playerId) // GLOBAL -> CHANNEL + + assertEquals(ChatMode.CHANNEL, manager.getChatMode(playerId)) + verify(atLeast = 3) { storage.queueAsyncSave(any()) } + } + + @Test + fun `manager should handle empty initial data`() { + val (manager, _, logger) = createChatModeManager(ChatModeData()) + manager.initialize() + + assert(logger.infoMessages.any { it.contains("ChatModeManager initialized with 0 saved modes") }) + } + + @Test + fun `manager should restore modes after initialization`() { + val player1 = TestUtils.createTestUUID(1) + val player2 = TestUtils.createTestUUID(2) + val initialData = + ChatModeData( + modes = + mapOf( + player1 to ChatMode.CHANNEL, + player2 to ChatMode.GLOBAL, + ), + ) + + val (manager, _, _) = createChatModeManager(initialData) + manager.initialize() + + assertEquals(ChatMode.CHANNEL, manager.getChatMode(player1)) + assertEquals(ChatMode.GLOBAL, manager.getChatMode(player2)) + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt new file mode 100644 index 0000000..b07636b --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandlerTest.kt @@ -0,0 +1,134 @@ +package dev.m1sk9.lunaticChat.paper.chat.handler + +import dev.m1sk9.lunaticChat.paper.TestUtils +import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter +import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Tests for DirectMessageHandler. + * Validates message handling with dependency injection and conversion features. + */ +class DirectMessageHandlerTest { + private fun createHandler( + configuration: dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration? = null, + settingsManager: PlayerSettingsManager? = null, + romanjiConverter: RomanjiConverter? = null, + ): DirectMessageHandler { + val config = configuration ?: TestUtils.createTestConfiguration() + return DirectMessageHandler(config, settingsManager, romanjiConverter) + } + + @Test + fun `sendDirectMessage should return true on success`() { + val handler = createHandler() + val sender = TestUtils.createMockPlayer() + val recipient = TestUtils.createMockPlayer() + + val result = handler.sendDirectMessage(sender, recipient, "Test message") + + assertTrue(result) + } + + @Test + fun `sendDirectMessage should use custom message format from configuration`() { + val customConfig = + TestUtils.createTestConfiguration().copy( + messageFormat = + TestUtils + .createTestConfiguration() + .messageFormat + .copy( + directMessageFormat = "[DM] {sender} -> {recipient}: {message}", + ), + ) + + val handler = createHandler(configuration = customConfig) + val sender = TestUtils.createMockPlayer(name = "Alice") + val recipient = TestUtils.createMockPlayer(name = "Bob") + + val result = handler.sendDirectMessage(sender, recipient, "Test") + + assertTrue(result) + } + + @Test + fun `sendDirectMessage without conversion should send original message`() { + val settingsManager = mockk<PlayerSettingsManager>() + val senderSettings = + TestUtils.createTestPlayerSettings( + japaneseConversionEnabled = false, + ) + + every { settingsManager.getSettings(any()) } returns senderSettings + + val handler = createHandler(settingsManager = settingsManager) + val sender = TestUtils.createMockPlayer() + val recipient = TestUtils.createMockPlayer() + + val result = handler.sendDirectMessage(sender, recipient, "konnichiwa") + + assertTrue(result) + } + + @Test + fun `sendDirectMessage with conversion should handle conversion timeout gracefully`() { + val settingsManager = mockk<PlayerSettingsManager>() + val senderSettings = + TestUtils.createTestPlayerSettings( + japaneseConversionEnabled = true, + ) + val romanjiConverter = mockk<RomanjiConverter>() + + every { settingsManager.getSettings(any()) } returns senderSettings + // Simulate a slow conversion that would timeout + coEvery { romanjiConverter.convert(any()) } coAnswers { + kotlinx.coroutines.delay(2000) // Exceeds 1s timeout + "こんにちは" + } + + val handler = createHandler(settingsManager = settingsManager, romanjiConverter = romanjiConverter) + val sender = TestUtils.createMockPlayer() + val recipient = TestUtils.createMockPlayer() + + // Should not throw exception and should complete quickly (within timeout) + val result = handler.sendDirectMessage(sender, recipient, "konnichiwa") + + assertTrue(result) + } + + @Test + fun `handler can be created with injected configuration`() { + val config = TestUtils.createTestConfiguration(debug = true) + val handler = DirectMessageHandler(config, null, null) + + // Handler should accept configuration via constructor (DI pattern) + // This validates Issue #1 refactoring - ConfigManager DI + val sender = TestUtils.createMockPlayer() + val recipient = TestUtils.createMockPlayer() + val result = handler.sendDirectMessage(sender, recipient, "Test") + + assertTrue(result) + } + + @Test + fun `handler can be created with all dependencies`() { + val config = TestUtils.createTestConfiguration() + val settingsManager = mockk<PlayerSettingsManager>(relaxed = true) + val romanjiConverter = mockk<RomanjiConverter>(relaxed = true) + + every { settingsManager.getSettings(any()) } returns TestUtils.createTestPlayerSettings() + + val handler = DirectMessageHandler(config, settingsManager, romanjiConverter) + + val sender = TestUtils.createMockPlayer() + val recipient = TestUtils.createMockPlayer() + val result = handler.sendDirectMessage(sender, recipient, "Test") + + assertTrue(result) + } +} diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt new file mode 100644 index 0000000..3f936f6 --- /dev/null +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManagerTest.kt @@ -0,0 +1,288 @@ +package dev.m1sk9.lunaticChat.paper.config + +import dev.m1sk9.lunaticChat.paper.i18n.Language +import io.mockk.every +import io.mockk.mockk +import org.bukkit.configuration.file.FileConfiguration +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Tests for ConfigManager to ensure proper configuration loading and parsing. + */ +class ConfigManagerTest { + private fun createMockConfig(values: Map<String, Any> = emptyMap()): FileConfiguration { + val config = mockk<FileConfiguration>(relaxed = true) + + // Set up default values + every { config.getBoolean(any(), any()) } answers { + val key = firstArg<String>() + val default = secondArg<Boolean>() + (values[key] as? Boolean) ?: default + } + + every { config.getInt(any(), any()) } answers { + val key = firstArg<String>() + val default = secondArg<Int>() + (values[key] as? Int) ?: default + } + + every { config.getLong(any(), any()) } answers { + val key = firstArg<String>() + val default = secondArg<Long>() + (values[key] as? Long) ?: default + } + + every { config.getString(any(), any()) } answers { + val key = firstArg<String>() + val default = secondArg<String>() + (values[key] as? String) ?: default + } + + every { config.getString(any()) } answers { + val key = firstArg<String>() + values[key] as? String + } + + return config + } + + @Test + fun `loadConfiguration should load all default values`() { + val configManager = ConfigManager() + val mockConfig = createMockConfig() + + val configuration = configManager.loadConfiguration(mockConfig) + + assertNotNull(configuration) + assertTrue(configuration.features.quickReplies.enabled) + assertFalse(configuration.features.japaneseConversion.enabled) + assertFalse(configuration.features.channelChat.enabled) + assertFalse(configuration.debug) + assertFalse(configuration.checkForUpdates) + assertEquals(Language.EN, configuration.language) + } + + @Test + fun `loadConfiguration should load quick replies configuration`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "features.quickReplies.enabled" to false, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertFalse(configuration.features.quickReplies.enabled) + } + + @Test + fun `loadConfiguration should load Japanese conversion configuration`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "features.japaneseConversion.enabled" to true, + "features.japaneseConversion.cache.maxEntries" to 1000, + "features.japaneseConversion.cache.saveIntervalSeconds" to 600, + "features.japaneseConversion.cache.filePath" to "custom_cache.json", + "features.japaneseConversion.api.timeout" to 5000L, + "features.japaneseConversion.api.retryAttempts" to 3, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertTrue(configuration.features.japaneseConversion.enabled) + assertEquals(1000, configuration.features.japaneseConversion.cacheMaxEntries) + assertEquals(600, configuration.features.japaneseConversion.cacheSaveIntervalSeconds) + assertEquals("custom_cache.json", configuration.features.japaneseConversion.cacheFilePath) + assertEquals(5000L, configuration.features.japaneseConversion.apiTimeout) + assertEquals(3, configuration.features.japaneseConversion.apiRetryAttempts) + } + + @Test + fun `loadConfiguration should load channel chat configuration`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "features.channelChat.enabled" to true, + "features.channelChat.maxChannelsPerServer" to 20, + "features.channelChat.maxMembersPerChannel" to 100, + "features.channelChat.maxMembershipPerPlayer" to 10, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertTrue(configuration.features.channelChat.enabled) + assertEquals(20, configuration.features.channelChat.maxChannelsPerServer) + assertEquals(100, configuration.features.channelChat.maxMembersPerChannel) + assertEquals(10, configuration.features.channelChat.maxMembershipPerPlayer) + } + + @Test + fun `loadConfiguration should load message format configuration`() { + val configManager = ConfigManager() + val customDMFormat = "DM: {sender} -> {recipient}: {message}" + val customChannelFormat = "[{channel}] {sender}: {message}" + val mockConfig = + createMockConfig( + mapOf( + "messageFormat.directMessageFormat" to customDMFormat, + "messageFormat.channelMessageFormat" to customChannelFormat, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertEquals(customDMFormat, configuration.messageFormat.directMessageFormat) + assertEquals(customChannelFormat, configuration.messageFormat.channelMessageFormat) + } + + @Test + fun `loadConfiguration should load debug and update check settings`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "debug" to true, + "checkForUpdates" to true, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertTrue(configuration.debug) + assertTrue(configuration.checkForUpdates) + } + + @Test + fun `loadConfiguration should load custom settings file path`() { + val configManager = ConfigManager() + val customPath = "custom-player-settings.yaml" + val mockConfig = + createMockConfig( + mapOf( + "userSettingsFilePath" to customPath, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertEquals(customPath, configuration.userSettingsFilePath) + } + + @Test + fun `loadConfiguration should load Japanese language`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "language" to "ja", + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertEquals(Language.JA, configuration.language) + } + + @Test + fun `loadConfiguration should handle unknown language code`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "language" to "fr", // French not supported + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + // Should fall back to English + assertEquals(Language.EN, configuration.language) + } + + @Test + fun `loadConfiguration should use default values when keys are missing`() { + val configManager = ConfigManager() + val mockConfig = createMockConfig(emptyMap()) + + val configuration = configManager.loadConfiguration(mockConfig) + + // All features should have their defaults + assertTrue(configuration.features.quickReplies.enabled) + assertEquals(500, configuration.features.japaneseConversion.cacheMaxEntries) + assertEquals(300, configuration.features.japaneseConversion.cacheSaveIntervalSeconds) + assertEquals("conversion_cache.json", configuration.features.japaneseConversion.cacheFilePath) + assertEquals(3000L, configuration.features.japaneseConversion.apiTimeout) + assertEquals(2, configuration.features.japaneseConversion.apiRetryAttempts) + assertEquals(0, configuration.features.channelChat.maxChannelsPerServer) + assertEquals(0, configuration.features.channelChat.maxMembersPerChannel) + assertEquals(0, configuration.features.channelChat.maxMembershipPerPlayer) + } + + @Test + fun `loadConfiguration can be called multiple times`() { + val configManager = ConfigManager() + val mockConfig1 = + createMockConfig( + mapOf("debug" to true), + ) + val mockConfig2 = + createMockConfig( + mapOf("debug" to false), + ) + + val config1 = configManager.loadConfiguration(mockConfig1) + val config2 = configManager.loadConfiguration(mockConfig2) + + assertTrue(config1.debug) + assertFalse(config2.debug) + } + + @Test + fun `loadConfiguration should handle all features enabled`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "features.quickReplies.enabled" to true, + "features.japaneseConversion.enabled" to true, + "features.channelChat.enabled" to true, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertTrue(configuration.features.quickReplies.enabled) + assertTrue(configuration.features.japaneseConversion.enabled) + assertTrue(configuration.features.channelChat.enabled) + } + + @Test + fun `loadConfiguration should handle all features disabled`() { + val configManager = ConfigManager() + val mockConfig = + createMockConfig( + mapOf( + "features.quickReplies.enabled" to false, + "features.japaneseConversion.enabled" to false, + "features.channelChat.enabled" to false, + ), + ) + + val configuration = configManager.loadConfiguration(mockConfig) + + assertFalse(configuration.features.quickReplies.enabled) + assertFalse(configuration.features.japaneseConversion.enabled) + assertFalse(configuration.features.channelChat.enabled) + } +} |
