diff options
Diffstat (limited to 'website/src/ja/docs')
| -rw-r--r-- | website/src/ja/docs/developers/architecture.md | 87 | ||||
| -rw-r--r-- | website/src/ja/docs/developers/engine.md | 97 | ||||
| -rw-r--r-- | website/src/ja/docs/developers/introduction.md | 37 | ||||
| -rw-r--r-- | website/src/ja/docs/developers/platform-paper.md | 189 | ||||
| -rw-r--r-- | website/src/ja/docs/developers/platform-velocity.md | 92 | ||||
| -rw-r--r-- | website/src/ja/docs/developers/resource.md | 60 |
6 files changed, 562 insertions, 0 deletions
diff --git a/website/src/ja/docs/developers/architecture.md b/website/src/ja/docs/developers/architecture.md new file mode 100644 index 0000000..cd3965a --- /dev/null +++ b/website/src/ja/docs/developers/architecture.md @@ -0,0 +1,87 @@ +--- +layout: doc +--- + +# 設計概要 + +LunaticChat は Paper/Folia におけるDM・チャンネルチャット・ローマ字変換に加え,Velocity プロキシ配下での**サーバー間グローバルチャット中継**を提供します. + +このページはアーキテクチャの全体像と,モジュールをまたぐ横断的な設計判断をまとめます.各モジュールの詳細は配下のページを参照してください. + +## モジュール構成 + +Gradle マルチモジュール構成で,共有カーネルとプラットフォーム実装を分離しています. + +依存の向きは一方向で,`platform-paper` と `platform-velocity` の両方が `engine` に依存し,`engine` は下流に何も依存しません.どちらのプラットフォームも protocol を「所有」せず,中立な `engine` に対して対等な peer として依存します. + +| モジュール | 役割 | +|-----------|------| +| `engine` | プラットフォーム非依存のコア (ドメインモデル・プロトコル・変換・例外・権限) | +| `platform-paper` | Paper / Folia プラグイン本体 | +| `platform-velocity` | Velocity プロキシプラグイン (サーバー間チャット中継) | +| `dokka` | API ドキュメント集約専用 (Kotlin ソースなし) | + +### なぜ engine を切り出すのか + +engine は**共有カーネル (Shared Kernel)** です.中身は 2 系統に分かれます. + +#### (a) 両者が合意しなければならない契約 + +Paper と Velocity が同一定義でないと壊れるもの. + +- `protocol` — 両プロセス間の通信契約 (同一定義でないと通信不能) +- `chat` / `channel`, `settings` — 永続化スキーマ (`@Serializable`) +- `exception` — ドメインエラーの共通語彙 +- `permission`, `command` — 権限ノード文字列とコマンド結果の中立抽象 + +#### (b) プラットフォーム非依存の純ロジック + +どこに置いてもよいが,純粋な再利用可能のロジックを中立コアに寄せたもの. + +- `converter` — ローマ字変換の純アルゴリズム (Trie) +外部 API クライアント + +(a) を engine に一元化する最大の狙いは,**ワイヤ契約の「単一の真実源」を作ること**です.Paper と Velocity は別々にビルド・デプロイ・バージョニングされる 2 つの成果物であり,protocol を両モジュールに複製すれば必ず drift します.engine に 1 つだけ置けば,契約の不一致が「本番での実行時ミスマッチ」ではなく「コンパイルエラー / スナップショットテスト失敗」として早期に顕在化します. + +engine は Bukkit / Velocity API に依存せず,Adventure / Brigadier も「型・値の意味」だけを借りて本体依存を避けています (`compileOnly` の Adventure,Brigadier に依存せず `Int` を返す `toBrigadierResult()`).これにより engine は Minecraft サーバーを立てずに pure-JVM でテストでき,プラットフォーム都合 (Folia のスケジューラ等) は platform 側に隔離されます. + +## プロトコルバージョンによる互換管理 + +Paper–Velocity 間の互換性は,プラグインのバージョンではなく engine が持つ **`ProtocolVersion`** だけで決まります.これが LunaticChat のマルチプラットフォーム設計の要です. + +- 互換判定は MAJOR 完全一致 & リモート MINOR ∈ `[MIN_SUPPORTED_MINOR, MINOR]` となり, PATCH は無視 +- 後方互換は JSON の `ignoreUnknownKeys` + デフォルト値付きフィールド +- `ProtocolBackwardCompatibilityTest` が JSON スナップショットで後方互換を機械的に検証 + +互換をプラグインバージョンから切り離した帰結として,**Paper と Velocity を独立にバージョニングでき,更新頻度の異なる 2 プラットフォームをそれぞれのペースでリリースできます**.さらにプロトコルのバンプレベルごとに更新順序が定義され (PATCH=任意 / MINOR=Velocity 先 / MAJOR=同時),これがローリングアップデートを可能にします. + +詳細は [engine - 共通カーネル](/ja/docs/developers/engine) を参照してください. + +## Service Container パターン + Feature Gating + +`platform-paper` は外部 DI フレームワークを使わず,手動 DI で機能を組み立てます. + +- `ServiceInitializer` が構築・初期化順序・shutdown を担当 +- `ServiceContainer` (イミュータブルな data class) がサービスを保持 +- **無効な機能はサービスが `null`** になり,型で機能の有無が表現される +- コマンド・リスナー・SettingHandler の登録が `null` 判定で条件分岐する + +要は「config フラグ → `ServiceInitializer` が nullable なサービスを生成 → `ServiceContainer` の nullable フィールド → 登録処理が `null` 判定で分岐」という一本の流れです.機能が無効なら対応するサービスが型のうえで「存在しない」ことになり,そのコードパスは最初から構築されません.外部フレームワークを持ち込まず,機能トグルとライフサイクル管理を Kotlin の型と null 許容性だけで表現しているのが特徴です. + +詳細は [platform-paper - Paper / Folia プラグイン本体](/ja/docs/developers/platform-paper) を参照してください. + +## 横断的な設計特徴 + +1. **engine / platform の関心分離** — platform はプラットフォーム API との橋渡しに徹し,ドメインモデル・アルゴリズム・プロトコルは engine が持ちます.platform 側は「Bukkit / Velocity という現実」を吸収するアダプタ層です. +2. **プロトコルバージョンによる互換管理** — 互換は `ProtocolVersion` だけで決まります. +3. **Service Container + Feature Gating** — 機能の有無を型で表現します. +4. **アノテーション駆動コマンド** — `@Command` / `@Permission` / `@PlayerOnly` を Kotlin リフレクションで読み Brigadier ツリーへマッピングします.コマンドの定義とメタデータ (権限・エイリアス) が同じ場所に宣言的に並びます. +5. **Folia 互換性** — 非同期処理は `asyncScheduler` と `PluginCoroutineScope` (SupervisorJob) で行い,Bukkit API 呼び出しは `scheduler.runTask` でメインスレッドへ戻します.リージョンスレッド化された Folia でも壊れないよう,スレッド境界を明示的に扱います. +6. **永続化の使い分け** — 言語/プレイヤー設定=KAML(YAML),チャンネル/変換キャッシュ=kotlinx.serialization JSON,チャンネルログ=NDJSON.いずれも「メモリキャッシュ+非同期保存 (デバウンス/キュー)+shutdown 同期保存」の共通パターンに従います. +7. **DM/チャンネル=ローカル,グローバル=プロキシ経由** — チャットの種類でルーティングが分かれ,グローバルチャットだけが Velocity を経由します.中継は「送信元サーバー除外」+「messageId による重複排除」の二段でループを防ぎます. + +## 各モジュールの詳細についてはこちら + +- [engine - 共通カーネル](/ja/docs/developers/engine) +- [platform-paper - Paper / Folia プラグイン本体](/ja/docs/developers/platform-paper) +- [platform-velocity - Velocity プラグイン本体 (プロキシ中継)](/ja/docs/developers/platform-velocity) +- [ビルド・リリース・バージョニング](/ja/docs/developers/resource) diff --git a/website/src/ja/docs/developers/engine.md b/website/src/ja/docs/developers/engine.md new file mode 100644 index 0000000..fb9ee9e --- /dev/null +++ b/website/src/ja/docs/developers/engine.md @@ -0,0 +1,97 @@ +--- +layout: doc +--- + +# engine - 共通カーネル + +`engine` はプラットフォーム非依存のコアモジュールです. + +Paper と Velocity が共有する契約 (protocol・スキーマ・語彙) と,プラットフォームに依存しない純ロジック (変換アルゴリズム) を集約した**共有カーネル**として位置づけられます. + +Bukkit / Velocity API に依存せず,Adventure / Brigadier も「型・値の意味」だけを借りるにとどめ,本体依存を持ちません.そのため Minecraft サーバーを立てずに pure-JVM でテストできます. + +engine を切り出す意図の全体像は [設計概要](/ja/docs/developers/architecture#なぜ-engine-を切り出すのか) を参照してください. + +## protocol — Paper ↔ Velocity 通信 + +Paper と Velocity は別プロセスの成果物であり,プラグインメッセージで通信します.その**ワイヤ契約を両者が同一定義で共有するため**に protocol は engine に置かれています.片方だけが定義を変えれば通信は壊れるので,唯一の定義を engine に持たせ,不一致をコンパイル時・テスト時に検出できるようにしています. + +`sealed interface PluginMessage` を頂点とする 5 種類のメッセージがあります. + +| 種別 | 方向 | 主なフィールド | +|------|------|---------------| +| `Handshake` | Paper→Velocity | `pluginVersion`, `protocol` 各要素 | +| `HandshakeResponse` | Velocity→Paper | `compatible`, `velocityVersion`, `error?`, `protocol` 各要素 | +| `StatusRequest` | Paper→Velocity | (フィールドなし) | +| `StatusResponse` | Velocity→Paper | `velocityVersion`, `protocolVersion`, `online` | +| `GlobalChatMessage` | Paper↔Velocity↔Paper | `messageId`, `serverName`, `playerId`, `playerName`, `message`, `timestamp` | + +`GlobalChatMessage` の `messageId` は中継ループでの重複表示を防ぐための一意 ID です.また protocol 層では UUID を素の `String` として運びます (settings/channel 層の `UUID` 型+カスタムシリアライザとは対照的に,移送を単純化する狙い) . + +### ワイヤフォーマット + +- `[subChannel: UTF][messageJson: UTF]` — `DataOutputStream.writeUTF` で「サブチャネル名」「JSON 本文」の 2 つを書き出す,Minecraft のプラグインメッセージで扱いやすい `ByteArray` 形式 +- JSON は kotlinx-serialization.`Json { ignoreUnknownKeys = true }` で,新バージョンが増やした未知フィールドを旧バージョンが受け取っても壊れない (前方互換の土台) +- サブチャネル: `handshake` / `handshake_response` / `status_request` / `status_response` / `global_chat` + +### バージョニング戦略 (`ProtocolVersion`) + +Paper–Velocity の互換性は,プラグインバージョンではなく `ProtocolVersion` だけで判定します.SemVer に沿って,変更の性質ごとにバンプするレベルとデプロイ順が決まります. + +| レベル | いつ上げる | デプロイ順 | +|--------|-----------|-----------| +| PATCH | デフォルト付き任意フィールド追加 / 無視可能な新サブチャネル | 任意 | +| MINOR | 必須フィールド追加 / 欠けると機能低下するサブチャネル | Velocity → Paper | +| MAJOR | フィールド/サブチャネルの削除・改名,ワイヤ形式変更 | 全同時 | + +互換判定は「**MAJOR 完全一致 & リモート MINOR ∈ `[MIN_SUPPORTED_MINOR, MINOR]`,PATCH は無視**」で行っています.これにより `MIN_SUPPORTED_MINOR` を引き上げることで,古い MINOR の受け入れを段階的に打ち切れます.新しいメッセージやフィールドを追加したときは `ProtocolBackwardCompatibilityTest` に JSON スナップショットを足し,旧フォーマットが読み続けられることを機械的に保証します. + +この設計の帰結として Paper と Velocity を独立にリリースできます.詳しくは [ビルド・リリース・バージョニング](/ja/docs/developers/resource#独立バージョニング) を参照してください. + +## converter — ローマ字→日本語変換 + +converter は Paper↔Velocity の契約ではなく (Velocity はローマ字変換をしない),**プラットフォームに依存しない純ロジックだから** engine に置かれています.3 段構成です. + +- `KanaConverter` (`object`) — **Trie** でローマ字→ひらがなに変換.`sealed class TrieNode { Leaf, Branch }` の不変構造で,4 文字 (`xtsu`→っ) 〜1 文字 (`a`→あ) を網羅.`isValidRomaji()` で変換前検証,`toHiragana()` は最長一致+促音処理を行う純アルゴリズム +- `GoogleIMEClient` — Ktor `HttpClient` を DI で受け取り,Google IME (`langpair=ja-Hira|ja`) でひらがな→漢字仮名交じりに変換.レスポンスの各セグメント第 1 候補を連結する +- `CacheData` (`@Serializable`) — 変換結果 (`version` + `entries: Map`) の永続化スキーマ.コストの高い IME 変換をキャッシュするための器で,キャッシュ本体のロジックは paper 側にある + +## chat/channel — チャンネルのドメインモデル + +チャンネルの永続化スキーマは,保存を行う paper 側と,将来的な共有可能性を見据えて engine 側に `@Serializable` なモデルとして置かれています. + +- `Channel` — `init` でバリデーション (`id` は `^[a-zA-Z0-9_-]{3,30}$`,`name` は空白不可) +- `ChannelData` — 永続化ルート.`version` フィールドでスキーマ進化に対応 +- `ChannelMember` / `ChannelRole` — メンバーとロール.ロールは `OWNER` / `MODERATOR` / `MEMBER` の 3 階層 +- `ChannelContext` — 非 Serializable な実行時集約 DTO (`channel` + `members` を操作に渡すビュー) +- `ChannelMessageLogEntry` — NDJSON・日次ローテーション・Grafana Loki 互換を想定したログエントリ + +チャンネル数・メンバー数・所属数などの上限は,engine には**例外の「語彙」だけ**を置き,具体的な閾値は config (paper 側) が注入します.「上限があること」と「上限がいくつか」を分離する設計です. + +## settings — プレイヤー設定と UUID シリアライズ + +永続用と実行用でモデルを分けています. + +- `PlayerSettingsData` — YAML 永続化のルート.3 種の設定を UUID→Boolean のマップで保持 +- `PlayerChatSettings` — 1 プレイヤー単位のフラットモデル (全設定デフォルト true).全体マップから射影した実行時ビュー + +UUID シリアライザが 2 つあるのは用途が違うためです. + +`UUIDSerializer` (descriptor 名 `"UUID"`) は汎用で channel や `PlayerChatSettings.uuid` に,`UUIDASStringSerializer` (descriptor 名 `"UUIDAsString"`) は YAML 互換のため `PlayerSettingsData` の**マップキー**に使います.`kotlinx.serialization` が UUID を標準サポートしないため自前実装しています. + +## exception — 共通の例外語彙 + +ドメインエラーを Paper / Velocity 双方で同じ型として扱えるよう,例外を engine に集約しています.共通の封印基底は持たず,`Exception` を直接継承するフラット構造 (23 種) です.存在/参照系・状態系・制限系・権限/BAN・KICK 系に分類でき,多くが `playerId` / `channelId` / `limit` をコンストラクタで受けてメッセージを自前生成します.基底を持たないため,呼び出し側は個別に catch する前提です. + +## permission / command — 中立抽象 + +Bukkit / Velocity どちらの API にも渡せる中立表現として,権限とコマンド結果を engine に置いています. + +- `LunaticChatPermissionNode` — `sealed class` + `object` サブクラスで権限を型安全に列挙.文字列ノードは両プラットフォームの permission API に渡せ,`when` で網羅性チェックも効く +- `CommandResult` — `sealed class` (`Success` / `SuccessWithMessage` / `Failure` / `InvalidUsage`).メッセージは Adventure `Component`,`toBrigadierResult()` は Brigadier 本体に依存せず成功=1/失敗=0 という「戻り値の意味」だけを表現する + +## 関連 + +- [設計概要](/ja/docs/developers/architecture) +- [platform-paper - Paper / Folia プラグイン本体](/ja/docs/developers/platform-paper) +- [platform-velocity - Velocity プラグイン本体](/ja/docs/developers/platform-velocity) diff --git a/website/src/ja/docs/developers/introduction.md b/website/src/ja/docs/developers/introduction.md new file mode 100644 index 0000000..79782d1 --- /dev/null +++ b/website/src/ja/docs/developers/introduction.md @@ -0,0 +1,37 @@ +--- +layout: doc +--- + +# はじめに + +このガイドは LunaticChat の設計・アーキテクチャを解説する開発者ガイドになります. + +プレイヤー・サーバ管理者は [ドキュメント/リファレンス](/ja/docs/getting-started) を参照してください. + +::: tip 対象バージョン + +このガイドの設計・アーキテクチャは [Paper/Folia: v1.2.2](https://github.com/m1sk9/LunaticChat/releases/tag/paper%2Fv1.2.2), [Velocity: v1.1.0](https://github.com/m1sk9/LunaticChat/releases/tag/velocity%2Fv1.1.0) 時点で記述しています. + +::: + +## モジュール構成 + +LunaticChat のモジュール構成は次の通りです. + +設計の全体像は [設計/アーキテクチャ](/ja/docs/developers/architecture) を参照してください. + +| モジュール | 役割 | +|-----------|------| +| `engine` | プラットフォーム非依存のコア | +| `platform-paper` | Paper / Folia プラグイン本体 | +| `platform-velocity` | Velocity プロキシプラグイン | + +## ガイド一覧 + +- [設計/アーキテクチャ](/ja/docs/developers/architecture) — アーキテクチャの全体像 + - [engine - 共通カーネル](/ja/docs/developers/engine) + - [platform-paper - Paper / Folia プラグイン本体](/ja/docs/developers/platform-paper) + - [platform-velocity - Velocity プラグイン本体 (プロキシ中継)](/ja/docs/developers/platform-velocity) +- [ビルド・リリース・バージョニング](/ja/docs/developers/resource) — リリースフローとバージョニング + +- [LunaChat の後継 "LunaticChat" を開発した話 - m1sk9 (Zenn)](https://zenn.dev/m1sk9/articles/adb6c0a7fa7bd2) — Null安全やコルーチン活用、キャッシュシステム導入など (外部サイト) diff --git a/website/src/ja/docs/developers/platform-paper.md b/website/src/ja/docs/developers/platform-paper.md new file mode 100644 index 0000000..bbbeb0c --- /dev/null +++ b/website/src/ja/docs/developers/platform-paper.md @@ -0,0 +1,189 @@ +--- +layout: doc +--- + +# platform-paper - Paper / Folia プラグイン本体 + +`platform-paper` はプラグインの本体です. + +Bukkit / Paper / Folia API・Adventure・Brigadier・Plugin Messaging といったプラットフォーム API との橋渡しに徹する層で,ドメインモデルやアルゴリズム・プロトコルは [engine](/ja/docs/developers/engine) に委譲します. + +paper 側は「Bukkit / Folia という現実」を吸収するアダプタとして働き,engine の純粋なモデルをプラットフォームの都合 (スケジューラ・スレッド・イベント) に接続するのが役割です. + +## エントリポイントと DI (Service Container) + +外部 DI フレームワークを使わず,手動 DI でサービスを組み立てます.**「構築の責務」と「保持の責務」を分離**しているのが要点です. + +- `LunaticChat` (`JavaPlugin` + `Listener`) — プラグインのエントリポイント +- `ServiceInitializer` — サービスの構築・初期化順序・shutdown を担当 +- `ServiceContainer` — 構築済みサービスを保持するイミュータブルな `data class` +- `PluginCoroutineScope` — `SupervisorJob` + `Dispatchers.Default`.`UpdateChecker` などの非ブロッキング実行に使う + +### ライフサイクル + +`onEnable` の流れは次の通りです. + +1. `saveDefaultConfig()` → `ConfigManager` で `LunaticChatConfiguration` を生成 +2. `HttpClient(CIO)` と `PluginCoroutineScope` を初期化 +3. `ServiceInitializer.initialize()` → `ServiceContainer` を受け取る +4. コマンドから使う公開プロパティへサービスを移し替え +5. `schedulePeriodicTasks()` → `registerCommands()` → `registerEventListeners()` +6. `checkForUpdates` が有効なら `UpdateChecker` を起動 + +`onDisable` は `pluginScope.cancel()` → `serviceInitializer.shutdown()` の順で,設定・キャッシュ・チャンネル・ログ・Velocity 接続を順に閉じます. + +### ServiceContainer と ServiceInitializer + +`ServiceContainer` は,常時利用可能なサービス (`languageManager` / `playerSettingsManager` / `directMessageHandler`) を非 null,機能ゲート対象 (`channelManager` / `velocityConnectionManager` など) を nullable フィールド (デフォルト null) として保持します.null-assertion (`!!`) をコードから排除する狙いです. + +`ServiceInitializer.initialize()` は依存順にサービスを生成します. + +1. `LanguageManager` (コマンドより前,全機能の前提) +2. `PlayerSettingsManager` (DM 通知などに常時必要) +3. Japanese conversion (optional) +4. Channel 群 — `ChannelManager` / `ChannelMembershipManager` / `ChannelMessageHandler` / `ChannelNotificationHandler`,ログ有効時は `ChannelMessageLogger` (optional) +5. `DirectMessageHandler` (settings・romaji・language に依存) +6. Velocity integration (optional) +7. Cross-server chat (velocity 有効 かつ `crossServerGlobalChat` かつ velocity manager 非 null のときのみ) + +### Feature Gating + +機能トグルの実装本体はこの `initialize()` です.Japanese conversion / Channel 群 / Velocity integration / Cross-server chat は,**config フラグが true のときだけサービスを生成し,それ以外は `null`** にします. + +``` +config フラグ + → ServiceInitializer が nullable なサービスを生成 + → ServiceContainer の nullable フィールドに格納 + → コマンド・リスナー・SettingHandler の登録が null 判定で条件分岐 +``` + +無効な機能はサービスが型のうえで「存在しない」ことになり,そのコードパスは最初から構築されません.機能の有無を Kotlin の null 許容性で表現しています. + +設計思想の全体像は [設計概要](/ja/docs/developers/architecture#service-container-パターン-feature-gating) を参照してください. + +## コマンドフレームワーク (アノテーション駆動 + Brigadier) + +コマンドの定義とメタデータ (権限・エイリアス・プレイヤー限定) を同じ場所に宣言的に並べ,**Kotlin リフレクションで読み取って Brigadier ツリーへマッピング**します. + +### アノテーション + +- `@Command(name, aliases, description)` — コマンド名・エイリアス・説明 +- `@Permission(KClass<out LunaticChatPermissionNode>)` — 必要権限 (engine の権限ノードを型で指定) +- `@PlayerOnly` — プレイヤー専用マーカー + +### LunaticCommand + +全コマンドの抽象基底です.クラスに付いたアノテーションを lazy に読み取り,`buildWithChecks()` がサブクラスの `buildCommand()` を包んで共通処理を差し込みます. + +- `@Deprecated` が付いていれば,実行時にエラーメッセージを返すハンドラへ差し替える +- `@Permission` があれば Brigadier の `.requires { source.sender.hasPermission(perm) }` を付与する +- `handleResult()` が engine の `CommandResult` を Adventure メッセージ送信+`toBrigadierResult()` の `Int` へ変換する +- `withAliases()` は Brigadier ノードを複製してエイリアスノードを生成,`applyMethodPermission()` は**メソッドレベル**の `@Permission` を反映する + +### CommandRegistry + +`register` / `registerAll` でコマンドを蓄積し,`initialize()` で Paper の `LifecycleEvents.COMMANDS` にハンドラを登録します.実際の Brigadier ツリー構築 (`buildWithChecks().build()`) はこのライフサイクルイベント内で行われます. + +### 規約: ルートとネストサブコマンド + +- **ルートコマンド** — クラスに `@Command` を付ける +- **ネストサブコマンド** — `@Command` を付けず,`build()` メソッド+メソッドレベル `@Permission` + `applyMethodPermission("build", …)` で権限を適用する + +### コマンド階層 + +| コマンド | エイリアス | 登録条件 | +|---------|-----------|---------| +| `lc` (→ settings / status / channel) | `lunaticchat` | 常時 | +| `channel` (14 サブコマンド) | `ch` | channelChat 有効時 | +| `tell` | `t` / `msg` / `m` / `w` / `whisper` | 常時 | +| `reply` | `r` | quickReplies 有効時 | +| `lcv` (→ status) | `lunaticvelocity` | velocity 有効時 | + +`settings` は `SettingKey.values()` を回して各キーに on/off/status ノードを動的生成し,`SettingHandlerRegistry` に委譲します.設定を増やすのは「SettingKey 追加 → Handler 実装 → Registry 登録」の 3 ステップです. + +## チャット処理 + +### ルーティング (PlayerChatListener) + +チャットの振り分けはここが担い,**「ローカル (チャンネル) か,グローバル (プロキシ経由の可能性) か」** を決めます.`AsyncChatEvent` を `EventPriority.HIGHEST, ignoreCancelled = true` でフックします. + +処理の流れ: + +1. メッセージを plain text 化し,先頭の `!` (グローバル強制プレフィックス) を判定する +2. `!` のみで本文が空なら,イベントをキャンセルして終了する (空メッセージを流さない) +3. 送信者の設定でローマ字変換が有効なら `convertWithRomaji` を通す +4. `channelManager.getPlayerChannel()` でアクティブチャンネルの有無を判定する + +分岐: + +- **アクティブチャンネルあり かつ `!` なし** → `event.isCancelled = true` + `viewers().clear()` + `message(empty)` で通常チャットを止め,`ChannelMessageHandler.sendChannelMessage()` に流す (サーバーローカル完結) +- **それ以外** (チャンネル未所属 or `!` プレフィックス) → `handleGlobalChat()`.velocity cross-server が有効なら `CrossServerChatManager.sendGlobalMessage()` へ送りつつ通常チャットも表示,無効なら通常チャットのみ + +### ダイレクトメッセージ (DirectMessageHandler) + +`/tell`・`/reply` の状態を管理します.`lastMessager` / `lastRecipient` の 2 つの `ConcurrentHashMap` で返信先を追跡し,`getReplyTarget()` は「自分に送ってきた人 → 自分が送った人」の優先順でオンラインのプレイヤーを返します. + +`sendDirectMessage()` は,送信者設定に応じたローマ字変換 → spy プレイヤーへの hover 付き配信 (送受信者は除外) → 送受信者への整形メッセージ送信+通知音 (設定依存) を行います.メッセージには `/tell <sender>` を補完する `ClickEvent.suggestCommand` が付きます. + +### チャンネルチャット (ChannelMessageHandler) + +`sendChannelMessage()` は `channelManager.getPlayerChannelContext()` でアクティブチャンネルを解決し (無ければ何もしない),spy 配信 (送信者とメンバーを除外) → チャンネルメンバー全員への配信+受信者通知音 → ログ有効時は engine の `ChannelMessageLogEntry.create()` で NDJSON ログ,という順で処理します. + +チャンネルの状態管理そのものは `chat/channel` パッケージが担います. + +- `ChannelManager` — チャンネルの単一の真実源.`channelsCache` / `membersCache` / `activeChannels` の `ConcurrentHashMap` で状態を持ち,CRUD は `kotlin.Result` を返して失敗時に engine 例外を包む.config の上限 (0 = 無制限) を検査する +- `ChannelMembershipManager` — 入退室・切替・ロールのビジネスロジック.`joinChannel()` は 存在 / 既アクティブ / BAN / private-invite / 既メンバー / 所属上限 を順に検査する +- `ChannelStorage` — `ChannelData` を JSON (`channels.json`) で永続化 +- `ChannelMessageLogger` — NDJSON の非同期ロガー.日次ローテーション+サイズ上限+保持日数超過ファイルの定期削除 + +## リスナー登録 + +- `EventListenerRegistry` (`object`) — `SpyPermissionManager` と `PlayerPresenceListener` は常時,`PlayerChatListener` は channel / velocity cross-server / romaji のいずれかが有効なときだけ登録する (ここも Feature Gating) +- `PlayerPresenceListener` — Join でアップデート通知・nightly 警告・アクティブチャンネル復元通知,Quit で DM 参照クリア+アクティブチャンネル解除+設定保存 +- `SpyPermissionManager` (`object : Listener`) — `Spy` 権限保持者を join/quit でキャッシュし,DM・チャンネルハンドラが参照する + +## config + +- `ConfigManager` — メイン `config.yml` を **Bukkit の `FileConfiguration`** からドット記法で読み,`LunaticChatConfiguration` を手組みする (この経路は KAML ではない点に注意) +- 機能デフォルト: `quickReplies=true`, `japaneseConversion=false`, `channelChat=false`, `velocityIntegration=false` +- `config/key` 以下に `FeaturesConfig` / `ChannelChatFeatureConfig` / `JapaneseConversionFeatureConfig` / `VelocityIntegrationConfig` / `QuickRepliesFeatureConfig` / `MessageFormatConfig` / `ChannelMessageLoggingConfig` + +::: warning 実装ノート +`ChannelChatFeatureConfig.messageLogging` は `ConfigManager` でロードされず,デフォルト値 (enabled=true, retention=30, 100MB) 固定になっています.意図的な仕様か要確認 — 修正するか,仕様として明記するかを決める必要があります. +::: + +## i18n + +- `Language` (enum) — `EN` / `JA`.未知コードは EN にフォールバック +- `LanguageManager` — 起動時に `resources/languages/` を KAML でロードし,ネストした YAML をドット記法 (`toggle.on` 等) にフラット化する.`getMessage(key, placeholders)` は 選択言語 → EN フォールバック で解決し `{placeholder}` を置換,未発見はキー自身を返す.EN が無ければ致命エラー +- `MessageFormatter` (`object`) — `[LC]` プレフィックス付きの Adventure `Component` を生成し,`{braces}` プレースホルダを正規表現で検出して色分けする + +## converter (paper 側) — engine 連携 + +paper 側は「キャッシュ管理・タイムアウト・Bukkit スケジューリング」というプラットフォーム都合を担い,変換アルゴリズムと API 通信は engine に委譲します. + +- `RomanjiConverter` — 2 段変換のオーケストレータ.単語ごとに キャッシュ確認 → engine `KanaConverter` でローマ字→ひらがな → engine `GoogleIMEClient` でひらがな→漢字.API 失敗時はひらがなにフォールバック +- `ConversionCache` — engine `CacheData` を JSON 永続化.メモリキャッシュ+デバウンス保存 (`maxEntries` 超過時の退避は ConcurrentHashMap の順不同により実質ランダム,との FIXME あり) +- `RomajiConversionHelper` — `convertWithRomaji()`.`runBlocking` + `withTimeoutOrNull` (既定 1000ms) で同期呼び出しし,成功時 `"元文 §e(変換)"`,失敗/タイムアウト時は原文を返す + +## velocity 連携 (Paper 側視点) + +engine の protocol を使い,Bukkit の Plugin Messaging Channel (`lunaticchat:main`) でプロキシと通信します.実際のクロスサーバールーティングは Velocity 側が担い,paper は「送出・受信・重複排除・整形表示」を担当します. + +- `VelocityConnectionManager` (`PluginMessageListener`) — `ConnectionState` (DISCONNECTED / HANDSHAKING / CONNECTED / FAILED) を管理.ハンドシェイクは engine の `PluginMessage.Handshake` を encode して送信し,5 秒でタイムアウトする.循環依存回避のため `CrossServerChatManager` は後入れ (setter injection) +- ハンドシェイクは**最初のプレイヤー参加を契機に一度だけ** (`AtomicBoolean`) 実行される.参加の 1 秒後に `asyncScheduler` でスケジュールし,結果は `HandshakeResult.Success` / `Error` で受ける +- `CrossServerChatManager` — グローバルチャットの送出・受信・**重複排除**.送信時に生成した `messageId` を即キャッシュ登録して自サーバーでのエコーを防ぎ (一段目),受信時は `messageId` の重複排除キャッシュ (TTL 60s,`cacheSize` 超過で古い順に掃除) で二重表示を防ぐ.Bukkit API 呼び出しは `scheduler.runTask` でメインスレッドに戻す + +## settings / common + +- `PlayerSettingsManager` — 3 種のブール設定を `ConcurrentHashMap` で管理.engine の DTO を使い,未設定はデフォルト true +- `YamlPlayerSettingsStorage` — KAML で `player-settings.yaml` を read/write.読み込み失敗時はバックアップから復旧,5 秒デバウンス保存 +- `UpdateChecker` — GitHub Releases API を Ktor で叩き semver 比較.結果は sealed `UpdateCheckResult` +- `SoundCollector` — 通知音の Adventure `Sound` 定数と Player 拡張関数 +- `PermissionCollector` — `@PermissionDsl` + `+LunaticChatPermissionNode` 演算子で権限を集める DSL.`requirePermission` は engine の `RequirePermissionException` を投げる + +## 関連 + +- [設計概要](/ja/docs/developers/architecture) +- [engine - 共通カーネル](/ja/docs/developers/engine) +- [platform-velocity - Velocity プラグイン本体](/ja/docs/developers/platform-velocity) diff --git a/website/src/ja/docs/developers/platform-velocity.md b/website/src/ja/docs/developers/platform-velocity.md new file mode 100644 index 0000000..46f5ac0 --- /dev/null +++ b/website/src/ja/docs/developers/platform-velocity.md @@ -0,0 +1,92 @@ +--- +layout: doc +--- + +# platform-velocity - Velocity プラグイン本体 (プロキシ中継) + +`platform-velocity` はサーバー間グローバルチャットの**中継**だけを担う薄い層です. + +実体は `LunaticChat` / `BuildInfo` / `messaging/` の 2 ファイルのみで,コマンドクラスは持ちません.`/lcv` は Velocity に関する機能ですが,コマンドの実装は [platform-paper](/ja/docs/developers/platform-paper) 側にある点に注意してください. + +::: tip なぜ Velocity 側は薄いのか +protocol の定義は engine が持ち,チャットの状態 (チャンネル・DM・設定) はすべて Paper 側にあります.Velocity に残る責務は「受け取ったグローバルチャットを他サーバーへ配る」ことだけなので,この層は意図的に薄く保たれています.どちらのプラットフォームも protocol を所有せず,engine に対して対等に依存する構図です (詳細は [設計概要](/ja/docs/developers/architecture#なぜ-engine-を切り出すのか)). +::: + +## ライフサイクル + +- `LunaticChat` (`@Plugin`) — Guice の `@Inject` コンストラクタで `ProxyServer` / `Logger` / `PluginContainer` を受け取る +- `@Plugin` アノテーションの `version` は `"0.0.0"` 固定で**実行時には使われない**.実バージョンは `velocity-plugin.json` から `PluginContainer.description.version` 経由で取得する (見つからなければ起動を止める) +- `@Subscribe onProxyInitialization` で `CrossServerChatRelay` を生成 → それを注入して `PluginMessageHandler` を生成し `initialize()` +- `@Subscribe onProxyShutdown` で `messageHandler.shutdown()` + +## メッセージ受信とディスパッチ + +`PluginMessageHandler` がチャンネル `lunaticchat:main` の受信を捌きます.`initialize()` で `channelRegistrar.register(CHANNEL)` とイベント購読を行います. + +`@Subscribe onPluginMessage` の処理: + +1. `event.identifier != CHANNEL` なら無視する +2. 送信元が `ServerConnection` でなければ警告して破棄する +3. `PluginMessageCodec.decode()` の結果を `when` で分岐する +4. `Handshake` → 互換判定して `HandshakeResponse` 返送 / `StatusRequest` → `StatusResponse` 返送 / `GlobalChatMessage` → 中継へ委譲 / それ以外 (Velocity 発の応答型) → 警告のみ + +### 信頼境界: クライアント由来メッセージの拒否 + +送信元が `ServerConnection` かどうかのチェックは,単なる型ガードではなく**信頼境界**です. + +Velocity のプラグインメッセージはバックエンドサーバーだけでなくクライアントからも届き得ます.ここでバックエンド接続以外を弾くことで,**クライアントがグローバルチャットや偽ハンドシェイクを直接注入することを防いでいます**.中継されるのは信頼できるサーバー接続から来たメッセージだけです. + +## ハンドシェイク処理 + +`Handshake` を受け取ると engine の `ProtocolVersion.isCompatible(major, minor)` で互換性を判定します. + +- **互換** — `compatible=true` の `HandshakeResponse` を返送 +- **非互換** — Paper 側・Velocity 側のバージョンを載せた error 文字列とともに `compatible=false` を返送 + +`HandshakeResponse` / `StatusResponse` には Velocity 自身の `ProtocolVersion` (`MAJOR` / `MINOR` / `PATCH`) を必ず載せるため,Paper 側はレスポンスから相手のプロトコルを知ることができます. + +## サーバー間中継 + +`CrossServerChatRelay.relayGlobalMessage(message, sourceServer)` が中継の核心です. + +``` +server.allServers + .filter { it != sourceServer } // 送信元を除外 + .forEach { it.sendPluginMessage(CHANNEL, encoded) } +``` + +**送信元サーバーを除外**して残り全バックエンドへブロードキャストします (エコー防止の一段目) .中継件数はログに出ます. + +### 中継されるもの / ローカルに留まるもの + +中継の範囲を最小に絞っているのが設計上のポイントです. + +- Velocity が他サーバーへ中継するのは **`GlobalChatMessage` のみ** +- `Handshake` / `HandshakeResponse` / `StatusRequest` / `StatusResponse` は Velocity ↔ 単一 Paper の間で完結し,転送しない +- **DM・チャンネルチャットはそもそも Velocity へ送られない** (Paper 内でローカル完結する) + +### エコー / ループ防止の二段構え + +グローバルチャットが中継ループで多重表示されないよう,2 箇所で防いでいます. + +1. **Velocity 側** — 送信元サーバーを除外してブロードキャスト +2. **Paper 側** — `messageId` による重複排除 LRU キャッシュ (TTL 60s).送信側は生成直後に自分の `messageId` を登録して自サーバーでのエコーも防ぐ + +## 通信フロー + +1. Paper がプレイヤー接続を契機に `Handshake` (自プロトコルバージョン) を送信する +2. Velocity が `ProtocolVersion.isCompatible` で判定し `HandshakeResponse` を返送する → 互換なら Paper 側は `CONNECTED` +3. プレイヤーがグローバルチャット (チャンネル未所属 or `!` プレフィックス) を送信 → Paper が `GlobalChatMessage` (新規 `messageId`) を Velocity へ送信し,送信元では通常チャットを表示する +4. Velocity が送信元以外の全バックエンドへ中継する +5. 各 Paper が受信 → `messageId` で dedup → `crossServerGlobalChatFormat` で整形して全プレイヤーへ配信する + +## 実装ノート + +- `PluginMessageHandler` の `plugin` パラメータ型が `Any` なのは,Velocity の `EventManager.register()` が `Object` を取るためです (API 自体が型安全でないので,ジェネリクス化しても実益が薄いとの判断). +- クロスサーバーチャットを動かすには,Velocity の `velocity.toml` で `bungee-plugin-message-channel=true` (プラグインメッセージ有効) が必要です. + +## 関連 + +- [設計概要](/ja/docs/developers/architecture) +- [engine - 共通カーネル](/ja/docs/developers/engine) — protocol の詳細 +- [platform-paper - Paper / Folia プラグイン本体](/ja/docs/developers/platform-paper) — Paper 側の対向実装 diff --git a/website/src/ja/docs/developers/resource.md b/website/src/ja/docs/developers/resource.md new file mode 100644 index 0000000..2e241b7 --- /dev/null +++ b/website/src/ja/docs/developers/resource.md @@ -0,0 +1,60 @@ +--- +layout: doc +--- + +# ビルド・リリース・バージョニング + +Gradle マルチモジュール構成で,engine を共有しつつ Paper / Velocity を独立した成果物としてビルド・リリースします. + +## ビルド構成 + +- ルート `build.gradle.kts` — Kotlin 2.4.0 + serialization / Shadow / ktlint / dokka を管理.JVM ターゲットは **JVM_25**.テストは JUnit Platform + jacoco で,共通テスト依存を全モジュールへ注入 +- `engine` — コアライブラリ (serialization / coroutines / ktor) を `api()` で公開しプラットフォームへ伝播.Adventure は `compileOnly`.Shadow を持たない純ライブラリ +- `platform-paper` — `version = paperVersion`.`api(project(":engine"))`.paper-api を `compileOnly`,KAML + kotlin-reflect を `implementation`.成果物は **`LunaticChat-<ver>.jar`** (classifier なし,`jar` は無効化) +- `platform-velocity` — `version = velocityVersion`.`api(project(":engine"))`.velocity-api を `compileOnly` + `annotationProcessor` (`@Plugin` 処理用).成果物は **`LunaticChat-<ver>-velocity.jar`** (classifier で区別) +- `dokka` — engine/paper/velocity を集約し HTML に README を include + +両プラットフォームの `processResources` は git short hash と `isNightly` から `version` / `gitCommitHash` / `channel` を算出し,`paper-plugin.yml` / `velocity-plugin.json` と `build-info.properties` にトークン展開します. + +## 独立バージョニング + +```properties +# gradle.properties +paperVersion=1.2.2 +velocityVersion=1.1.0 +``` + +Paper と Velocity は別々のバージョン番号を持ち,独立にリリースできます.**互換性を数値バージョンではなく engine 共有の [`ProtocolVersion`](/ja/docs/developers/engine#バージョニング戦略-protocolversion) で保証している**ため,更新頻度の異なる 2 プラットフォームをそれぞれのペースでバンプ・公開できるからです.ワイヤ形式は JSON + `ignoreUnknownKeys` で前方互換,プロトコルの MAJOR 一致 + MINOR 範囲チェックで後方互換をコントロールします. + +## リリースワークフロー + +タグのパターンでリリース対象が切り替わります. + +| ワークフロー | トリガタグ | ビルド対象 | バージョン検証 | +|-------------|-----------|-----------|---------------| +| `release.yaml` | `v*` | Paper + Velocity 両方 | gradle.properties から両バージョン抽出 | +| `release-paper.yaml` | `paper/v*` | Paper のみ | タグと `paperVersion` の一致を必須検証 | +| `release-velocity.yaml` | `velocity/v*` | Velocity のみ | タグと `velocityVersion` の一致を必須検証 | + +- 共通フロー: `validate` (既存リリースの重複チェック) → `build` (mise + Gradle setup, `shadowJar`) → `release` (`gh release create --draft` + Modrinth 公開) +- 個別ワークフロー (paper / velocity) はタグと `gradle.properties` の厳密一致を要求する点が `release.yaml` と異なる +- Modrinth の game-versions は Paper=`26.1.x` (loader: paper, folia),Velocity=`1.21.x` + `26.1.x` (loader: velocity) + +## CI + +`ci.yaml` は main への push / PR / 手動実行で動きます. + +- `build_plugin` — ktlintCheck → test + jacocoTestReport → Codecov アップロード → nightly shadowJar (`-PisNightly=true`) → artifact 保持 +- `build_dokka` / `deploy_dokka` — Dokka 生成 → GitHub Pages デプロイ (main push のみ) +- `build_docs` — `website/` を bun で format/lint/build → Cloudflare Workers (wrangler) へデプロイ (main push のみ) + +## 開発環境 + +- `mise.toml` — bun / java zulu-25 (`JVM_25` と整合) +- `x` — bash 製のデバッグサーバースクリプト.`./x <action> <platform> [--stable]` で start/stop/log/clean/rcon/help.`--stable` 省略時は nightly ビルド.`velocity` 指定時は **1 Velocity + 2 Paper** を立ち上げ,サーバー間チャット中継を実地検証できる +- `docker/` — paper / velocity / folia の 3 環境に `compose.yaml` (`itzg/minecraft-server:java25` 等).velocity.toml は `bungee-plugin-message-channel=true` でプラグインメッセージを有効化 + +## 関連 + +- [設計概要](/ja/docs/developers/architecture) +- [はじめに](/ja/docs/developers/introduction) |
