platform-paper - Paper / Folia プラグイン本体
platform-paper はプラグインの本体です.
Bukkit / Paper / Folia API・Adventure・Brigadier・Plugin Messaging といったプラットフォーム API との橋渡しに徹する層で,ドメインモデルやアルゴリズム・プロトコルは engine に委譲します.
paper 側は「Bukkit / Folia という現実」を吸収するアダプタとして働き,engine の純粋なモデルをプラットフォームの都合 (スケジューラ・スレッド・イベント) に接続するのが役割です.
エントリポイントと DI (Service Container)
外部 DI フレームワークを使わず,手動 DI でサービスを組み立てます.「構築の責務」と「保持の責務」を分離しているのが要点です.
LunaticChat(JavaPlugin+Listener) — プラグインのエントリポイントServiceInitializer— サービスの構築・初期化順序・shutdown を担当ServiceContainer— 構築済みサービスを保持するイミュータブルなdata classPluginCoroutineScope—SupervisorJob+Dispatchers.Default.UpdateCheckerなどの非ブロッキング実行に使う
ライフサイクル
onEnable の流れは次の通りです.
saveDefaultConfig()→ConfigManagerでLunaticChatConfigurationを生成HttpClient(CIO)とPluginCoroutineScopeを初期化ServiceInitializer.initialize()→ServiceContainerを受け取る- コマンドから使う公開プロパティへサービスを移し替え
schedulePeriodicTasks()→registerCommands()→registerEventListeners()checkForUpdatesが有効ならUpdateCheckerを起動
onDisable は pluginScope.cancel() → serviceInitializer.shutdown() の順で,設定・キャッシュ・チャンネル・ログ・Velocity 接続を順に閉じます.
ServiceContainer と ServiceInitializer
ServiceContainer は,常時利用可能なサービス (languageManager / playerSettingsManager / directMessageHandler) を非 null,機能ゲート対象 (channelManager / velocityConnectionManager など) を nullable フィールド (デフォルト null) として保持します.null-assertion (!!) をコードから排除する狙いです.
ServiceInitializer.initialize() は依存順にサービスを生成します.
LanguageManager(コマンドより前,全機能の前提)PlayerSettingsManager(DM 通知などに常時必要)- Japanese conversion (optional)
- Channel 群 —
ChannelManager/ChannelMembershipManager/ChannelMessageHandler/ChannelNotificationHandler,ログ有効時はChannelMessageLogger(optional) DirectMessageHandler(settings・romaji・language に依存)- Velocity integration (optional)
- 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 許容性で表現しています.
設計思想の全体像は 設計概要 を参照してください.
コマンドフレームワーク (アノテーション駆動 + 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 でフックします.
処理の流れ:
- メッセージを plain text 化し,先頭の
!(グローバル強制プレフィックス) を判定する !のみで本文が空なら,イベントをキャンセルして終了する (空メッセージを流さない)- 送信者の設定でローマ字変換が有効なら
convertWithRomajiを通す 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
実装ノート
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]プレフィックス付きの AdventureComponentを生成し,{braces}プレースホルダを正規表現で検出して色分けする
converter (paper 側) — engine 連携
paper 側は「キャッシュ管理・タイムアウト・Bukkit スケジューリング」というプラットフォーム都合を担い,変換アルゴリズムと API 通信は engine に委譲します.
RomanjiConverter— 2 段変換のオーケストレータ.単語ごとに キャッシュ確認 → engineKanaConverterでローマ字→ひらがな → engineGoogleIMEClientでひらがな→漢字.API 失敗時はひらがなにフォールバックConversionCache— engineCacheDataを 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 を使い,未設定はデフォルト trueYamlPlayerSettingsStorage— KAML でplayer-settings.yamlを read/write.読み込み失敗時はバックアップから復旧,5 秒デバウンス保存UpdateChecker— GitHub Releases API を Ktor で叩き semver 比較.結果は sealedUpdateCheckResultSoundCollector— 通知音の AdventureSound定数と Player 拡張関数PermissionCollector—@PermissionDsl++LunaticChatPermissionNode演算子で権限を集める DSL.requirePermissionは engine のRequirePermissionExceptionを投げる