対象バージョン: Paper 1.21.7 以降(Minecraft 1.21.6 で追加された Dialog 機能の Paper API ラッパー) APIステータス: Experimental(将来変更される可能性あり) 情報源: https://docs.papermc.io/paper/dev/dialogs/ / PaperMC Javadoc
Dialog(ダイアログ)は Minecraft 1.21.6 で追加されたゲーム内メニュー機能であり、サーバーからクライアントにカスタムUIを送信できる。Paper は 1.21.7 でこの機能に対する開発者向けAPIを提供開始した。
ダイアログは コンフィギュレーションフェーズ(プレイヤー接続時)と通常のゲームプレイ中の両方で表示可能なため、ルール同意画面、入力フォーム、情報表示など幅広い用途に使える。
io.papermc.paper.dialog
├── Dialog // ダイアログ本体のインターフェース
io.papermc.paper.registry.data.dialog
├── DialogBase // ダイアログの基本設定(タイトル、ボディ、入力等)
├── DialogBase.Builder // DialogBase のビルダー
├── DialogBase.DialogAfterAction // ダイアログ閉じた後のアクション(enum)
├── ActionButton // アクションボタン
├── ActionButton.Builder // ActionButton のビルダー
├── DialogRegistryEntry // レジストリエントリ
├── DialogRegistryEntry.Builder // レジストリエントリのビルダー
io.papermc.paper.registry.data.dialog.body
├── DialogBody // ダイアログ本文(テキストやアイテム表示)
io.papermc.paper.registry.data.dialog.input
├── DialogInput // ユーザー入力(bool, text, singleOption, numberRange)
io.papermc.paper.registry.data.dialog.type
├── DialogType // ダイアログ種別(sealed interface)
├── NoticeType // 通知型
├── ConfirmationType // 確認型(Yes/No)
├── DialogListType // ダイアログリスト型
├── MultiActionType // 複数アクション型
├── ServerLinksType // サーバーリンク型
io.papermc.paper.registry.data.dialog.action
├── DialogAction // アクション定義(sealed interface)
├── DialogAction.CommandTemplateAction // コマンドテンプレート実行
├── DialogAction.CustomClickAction // カスタムクリックアクション
├── DialogAction.StaticAction // 静的クリックイベント
├── DialogActionCallback // コールバック関数インターフェース
io.papermc.paper.dialog
├── DialogResponseView // ダイアログ入力値の読み取りビュー
io.papermc.paper.registry.keys
├── DialogKeys // 組み込みダイアログのキー定数
/dialog show <players> <dialog>
// Audience#showDialog を使用(Adventure API)
player.showDialog(dialog);
// PlayerConfigurationConnection 経由でも可能(コンフィギュレーションフェーズ)
connection.getAudience().showDialog(dialog);// 推奨: ダイアログのみ閉じる(開いているインベントリは維持される)
audience.closeDialog();
// 代替: ダイアログ含む全画面を閉じる(インベントリも閉じる)
player.closeInventory();ダイアログには必ず base(基本設定) と type(種別) が必要。
通常のサーバー稼働中に作成する方法。
Dialog dialog = Dialog.create(builder -> builder.empty()
.base(DialogBase.builder(Component.text("タイトル"))
// オプション設定...
.build()
)
.type(DialogType.notice())
);
player.showDialog(dialog);プラグインのブートストラッパーでレジストリに登録する方法。コマンドからの参照やコード内での再利用に便利。
// PluginBootstrapper 内
@Override
public void bootstrap(BootstrapContext context) {
context.getLifecycleManager().registerEventHandler(
RegistryEvents.DIALOG.compose()
.newHandler(event -> event.registry().register(
DialogKeys.create(Key.key("myplugin:my_dialog")),
builder -> builder
.base(DialogBase.builder(Component.text("タイトル")).build())
.type(DialogType.notice())
))
);
}Dialog dialog = RegistryAccess.registryAccess()
.getRegistry(RegistryKey.DIALOG)
.get(Key.key("myplugin:my_dialog"));DialogBase.builder(Component title) で作成開始。
| メソッド | 型 | 説明 |
|---|---|---|
canCloseWithEscape(boolean) |
boolean |
Escキーで閉じられるか(デフォルト: true) |
externalTitle(Component) |
Component or null |
このダイアログを開くボタンに表示されるタイトル |
afterAction(DialogAfterAction) |
DialogAfterAction |
ダイアログ閉じた後のアクション |
pause(boolean) |
boolean |
シングルプレイ時にゲームを一時停止するか |
body(List<? extends DialogBody>) |
List<DialogBody> |
ダイアログ本文(テキスト・アイテム) |
inputs(List<? extends DialogInput>) |
List<DialogInput> |
入力フィールド一覧 |
DialogBase.builder(Component.text("ルールへの同意", NamedTextColor.LIGHT_PURPLE))
.canCloseWithEscape(false)
.body(List.of(
DialogBody.plainMessage(Component.text("サーバールールに同意しますか?")),
DialogBody.plainMessage(Component.text("詳細はWebサイトをご確認ください。"))
))
.inputs(List.of(
DialogInput.text("name", Component.text("お名前")),
DialogInput.bool("agree", Component.text("同意する"))
))
.build()ダイアログ内にコンテンツを表示するための要素。複数指定可能。
| ファクトリメソッド | 説明 |
|---|---|
DialogBody.plainMessage(Component) |
テキストメッセージを表示 |
DialogBody.item(ItemStack) |
アイテムを表示 |
ユーザーからの入力を受け取るためのフィールド。各入力は key(文字列キー)で識別される。
| ファクトリメソッド | 説明 | UIイメージ |
|---|---|---|
DialogInput.bool(String key, Component label) |
チェックボックス(true/false) | トグルスイッチ |
DialogInput.text(String key, Component label) |
テキスト入力フィールド | 文字列入力欄 |
DialogInput.singleOption(String key, Component label, List<...> options) |
択一選択ボタン | 複数ボタンから1つ選択 |
DialogInput.numberRange(String key, Component label, float min, float max) |
数値スライダー | スライダーUI |
DialogInput.numberRange() は追加のビルダーメソッドを持つ:
DialogInput.numberRange("level", Component.text("レベル"), 0f, 100f)
.step(1f) // ステップ値
.initial(0f) // 初期値
.width(300) // 表示幅
.labelFormat("%s: %s") // ラベルフォーマット
.build()入力値は DialogResponseView 経由で取得する:
DialogResponseView view = event.getDialogResponseView();
if (view != null) {
// numberRange の値取得
int level = view.getFloat("level").intValue();
float exp = view.getFloat("experience").floatValue();
// text の値取得(キーを指定)
// bool の値取得(キーを指定)
}DialogType は sealed interface で、ダイアログ下部のボタン構成を決定する。
| 種別 | ファクトリメソッド | 説明 |
|---|---|---|
| Notice | DialogType.notice() |
OKボタンのみ。情報表示用 |
| Notice (カスタム) | DialogType.notice(ActionButton button) |
カスタムボタン1つ |
| Confirmation | DialogType.confirmation(ActionButton yes, ActionButton no) |
Yes/Noの2ボタン |
| Multi Action | DialogType.multiAction(List<ActionButton> actions) |
複数ボタン |
| Dialog List | DialogType.dialogList(RegistrySet<Dialog> dialogs) |
他のダイアログへのリンク一覧 |
| Server Links | DialogType.serverLinks(ActionButton exit, int columns, int buttonWidth) |
サーバーリンク表示 |
// Notice(シンプル)
DialogType.notice()
// Confirmation
DialogType.confirmation(
ActionButton.builder(Component.text("同意する", TextColor.color(0xAEFFC1)))
.tooltip(Component.text("クリックして同意"))
.action(DialogAction.customClick(Key.key("myplugin:agree"), null))
.build(),
ActionButton.builder(Component.text("拒否する", TextColor.color(0xFFA0B1)))
.tooltip(Component.text("クリックして拒否"))
.action(DialogAction.customClick(Key.key("myplugin:disagree"), null))
.build()
)
// Multi Action
DialogType.multiAction(List.of(
ActionButton.create(Component.text("選択肢A"), null, 100, someAction),
ActionButton.create(Component.text("選択肢B"), null, 100, anotherAction),
ActionButton.create(Component.text("選択肢C"), null, 100, yetAnotherAction)
))// ビルダーパターン
ActionButton.builder(Component.text("ラベル"))
.tooltip(Component.text("ツールチップ"))
.action(dialogAction) // DialogAction or null
.build()
// ファクトリメソッド
ActionButton.create(
Component.text("ラベル"), // label
Component.text("ツールチップ"), // tooltip (nullable)
100, // width (1-1024)
dialogAction // action (nullable; null = 閉じるのみ)
)ボタンの action を null にすると、クリック時にダイアログを閉じるだけで他の処理は行われない。
DialogAction は sealed interface で3つの実装を持つ。
サーバーに PlayerCustomClickEvent を送信する。最も汎用的。
// Key + NBTペイロード方式(イベントリスナーで処理)
DialogAction.customClick(Key.key("myplugin:action_id"), null)
// コールバック方式(ラムダで直接処理)
DialogAction.customClick(
(DialogResponseView view, Audience audience) -> {
// ここで入力値を処理
if (audience instanceof Player player) {
// プレイヤーへの処理
}
},
ClickCallback.Options.builder()
.uses(1) // 使用回数(デフォルト: 1)
.lifetime(ClickCallback.DEFAULT_LIFETIME) // 有効期間(デフォルト: 12時間)
.build()
)入力値をコマンドに埋め込んで実行する。$(variable_name) 形式で DialogInput のキーを参照する。
// 例: DialogInput で "level" と "name" というキーの入力がある場合
DialogAction.commandTemplate("give $(name) diamond $(level)")Adventure の ClickEvent を実行する。
DialogAction.staticAction(ClickEvent.openUrl("https://example.com"))DialogAction.customClick(Key, BinaryTagHolder) で設定したアクションのクリックを処理する。
@EventHandler
void onCustomClick(PlayerCustomClickEvent event) {
Key key = event.getIdentifier();
// コンフィギュレーションフェーズの場合
if (event.getCommonConnection() instanceof PlayerConfigurationConnection configConn) {
UUID uuid = configConn.getProfile().getId();
// ...
}
// ゲームプレイ中の場合
if (event.getCommonConnection() instanceof PlayerGameConnection gameConn) {
Player player = gameConn.getPlayer();
// ...
}
// 入力値の取得
DialogResponseView view = event.getDialogResponseView();
if (view != null) {
// view.getFloat("key"), view.getString("key") 等で値を取得
}
}PlayerCustomClickEvent では event.getPlayer() を直接使えない。代わりに event.getCommonConnection() を適切な型にキャストする必要がある:
- ゲームプレイ中:
PlayerGameConnectionにキャスト →getPlayer()で Player を取得 - コンフィギュレーション中:
PlayerConfigurationConnectionにキャスト →getProfile()で GameProfile を取得
Paper には3つの組み込みダイアログが存在する:
| 定数 | キー | 説明 |
|---|---|---|
Dialog.SERVER_LINKS |
DialogKeys.SERVER_LINKS |
サーバーリンク一覧 |
Dialog.QUICK_ACTIONS |
DialogKeys.QUICK_ACTIONS |
クイックアクション |
Dialog.CUSTOM_OPTIONS |
DialogKeys.CUSTOM_OPTIONS |
カスタムオプション |
サーバーリンクの追加:
ServerLinks links = Bukkit.getServer().getServerLinks();
// links に対してリンクを追加するコンフィギュレーションフェーズでプレイヤーの同意を待つパターン。
@NullMarked
public class JoinConfirmListener implements Listener {
private final Map<UUID, CompletableFuture<Boolean>> awaitingResponse = new ConcurrentHashMap<>();
@EventHandler
void onConfigure(AsyncPlayerConnectionConfigureEvent event) {
Dialog dialog = RegistryAccess.registryAccess()
.getRegistry(RegistryKey.DIALOG)
.get(Key.key("myplugin:join_confirm"));
if (dialog == null) return;
PlayerConfigurationConnection connection = event.getConnection();
UUID uuid = connection.getProfile().getId();
if (uuid == null) return;
CompletableFuture<Boolean> response = new CompletableFuture<>();
response.completeOnTimeout(false, 1, TimeUnit.MINUTES);
awaitingResponse.put(uuid, response);
Audience audience = connection.getAudience();
audience.showDialog(dialog);
// response.join() でブロッキング待機
if (!response.join()) {
audience.closeDialog();
connection.disconnect(Component.text("同意が得られませんでした。"));
}
awaitingResponse.remove(uuid);
}
@EventHandler
void onCustomClick(PlayerCustomClickEvent event) {
if (!(event.getCommonConnection() instanceof PlayerConfigurationConnection configConn)) return;
UUID uuid = configConn.getProfile().getId();
if (uuid == null) return;
Key key = event.getIdentifier();
if (key.equals(Key.key("myplugin:agree"))) {
complete(uuid, true);
} else if (key.equals(Key.key("myplugin:disagree"))) {
complete(uuid, false);
}
}
@EventHandler
void onDisconnect(PlayerConnectionCloseEvent event) {
awaitingResponse.remove(event.getPlayerUniqueId());
}
private void complete(UUID uuid, boolean value) {
CompletableFuture<Boolean> future = awaitingResponse.get(uuid);
if (future != null) future.complete(value);
}
}ゲームプレイ中にプレイヤーから入力を受け取るパターン。
Dialog inputDialog = Dialog.create(builder -> builder.empty()
.base(DialogBase.builder(Component.text("経験値設定"))
.inputs(List.of(
DialogInput.numberRange("level", Component.text("レベル", NamedTextColor.GREEN), 0f, 100f)
.step(1f).initial(0f).width(300).build(),
DialogInput.numberRange("exp", Component.text("経験値%", NamedTextColor.GREEN), 0f, 100f)
.step(1f).initial(0f).width(300)
.labelFormat("%s: %s%%")
.build()
))
.build()
)
.type(DialogType.confirmation(
ActionButton.create(
Component.text("確定", TextColor.color(0xAEFFC1)),
Component.text("入力を確定します"),
100,
DialogAction.customClick(
(view, audience) -> {
int level = view.getFloat("level").intValue();
float exp = view.getFloat("exp").floatValue();
if (audience instanceof Player player) {
player.setLevel(level);
player.setExp(exp / 100f);
player.sendMessage(Component.text("設定しました!"));
}
},
ClickCallback.Options.builder().uses(1).build()
)
),
ActionButton.create(
Component.text("キャンセル", TextColor.color(0xFFA0B1)),
null, 100, null // null action = ダイアログを閉じるだけ
)
))
);
player.showDialog(inputDialog);サーバー側イベントリスナー不要で、入力値をコマンドに直接埋め込む方式。
Dialog commandDialog = Dialog.create(builder -> builder.empty()
.base(DialogBase.builder(Component.text("テレポート先の座標"))
.inputs(List.of(
DialogInput.numberRange("x", Component.text("X座標"), -1000f, 1000f)
.step(1f).initial(0f).width(300).build(),
DialogInput.numberRange("z", Component.text("Z座標"), -1000f, 1000f)
.step(1f).initial(0f).width(300).build()
))
.build()
)
.type(DialogType.confirmation(
ActionButton.create(
Component.text("テレポート"), null, 100,
// $(x), $(z) は DialogInput のキーに対応する
DialogAction.commandTemplate("tp @s $(x) 64 $(z)")
),
ActionButton.create(Component.text("キャンセル"), null, 100, null)
))
);// Dialog 本体
import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.dialog.DialogResponseView;
// Dialog 構成要素
import io.papermc.paper.registry.data.dialog.DialogBase;
import io.papermc.paper.registry.data.dialog.ActionButton;
import io.papermc.paper.registry.data.dialog.body.DialogBody;
import io.papermc.paper.registry.data.dialog.input.DialogInput;
// Dialog タイプ
import io.papermc.paper.registry.data.dialog.type.DialogType;
// Dialog アクション
import io.papermc.paper.registry.data.dialog.action.DialogAction;
import io.papermc.paper.registry.data.dialog.action.DialogActionCallback;
// レジストリ関連
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.event.RegistryEvents;
import io.papermc.paper.registry.keys.DialogKeys;
// イベント
import io.papermc.paper.event.player.PlayerCustomClickEvent;
import org.bukkit.event.player.AsyncPlayerConnectionConfigureEvent;
import io.papermc.paper.event.player.PlayerConnectionCloseEvent;
// コネクション
import io.papermc.paper.connection.PlayerConfigurationConnection;
import io.papermc.paper.connection.PlayerGameConnection;
// Adventure (Component, Key 等)
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.event.ClickCallback;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.key.Key;- Dialog は base と type の両方が必須: どちらか欠けるとエラーになる。
- Escキーで閉じさせたくない場合:
canCloseWithEscape(false)を明示的に設定する。 - コンフィギュレーションフェーズでの使用:
AsyncPlayerConnectionConfigureEvent内でCompletableFutureを使ってプレイヤーの応答を待つ。response.join()でブロッキング待機する。 - レジストリ登録はブートストラッパーで行う: 通常のプラグイン
onEnableではなく、PluginBootstrap#bootstrapで行う。 - PlayerCustomClickEvent でのプレイヤー取得:
event.getPlayer()ではなくevent.getCommonConnection()をPlayerGameConnectionにキャストしてgetPlayer()を呼ぶ。 - action が null のボタン: ダイアログを閉じるだけの動作になる。意図的に使用可能。
- コールバック方式の有効期間: デフォルトで12時間、
ClickCallback.Optionsで制御可能。 completeOnTimeoutの活用: コンフィギュレーションフェーズでの待機にタイムアウトを設定して、無応答プレイヤーを適切に処理する。