diff --git a/MinecraftClient/Dialogs/DialogFormatter.cs b/MinecraftClient/Dialogs/DialogFormatter.cs
index f3001aaa..d7fe9ffa 100644
--- a/MinecraftClient/Dialogs/DialogFormatter.cs
+++ b/MinecraftClient/Dialogs/DialogFormatter.cs
@@ -18,7 +18,7 @@ public static class DialogFormatter
{
StringBuilder builder = new();
builder.AppendLine(string.Format(Translations.dialog_render_header, instance.Revision, instance.Phase, instance.Definition.DisplayTitle()));
- builder.AppendLine(string.Format(Translations.dialog_render_type, instance.Definition.Type));
+ builder.AppendLine(string.Format(Translations.dialog_render_type, DisplayType(instance.Definition.Type)));
foreach (var body in instance.Definition.Body.Where(static body => !string.IsNullOrWhiteSpace(body.Text)))
builder.AppendLine(string.Format(Translations.dialog_render_body, body.Text));
@@ -47,6 +47,19 @@ public static class DialogFormatter
return builder.ToString();
}
+ public static string DisplayType(string rawType)
+ {
+ return rawType switch
+ {
+ "minecraft:notice" => Translations.dialog_type_notice,
+ "minecraft:confirmation" => Translations.dialog_type_confirmation,
+ "minecraft:multi_action" => Translations.dialog_type_multi_action,
+ "minecraft:dialog_list" => Translations.dialog_type_dialog_list,
+ "minecraft:server_links" => Translations.dialog_type_server_links,
+ _ => string.IsNullOrEmpty(rawType) ? Translations.dialog_type_unknown : rawType
+ };
+ }
+
private static string DescribeInput(DialogInput input)
{
return input.Kind switch
diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs
index 01936daf..fcc8bcad 100644
--- a/MinecraftClient/Resources/Translations/Translations.Designer.cs
+++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs
@@ -8077,5 +8077,29 @@ namespace MinecraftClient {
get { return ResourceManager.GetString("dialog.action_desc_unknown", resourceCulture); }
}
+ internal static string dialog_type_notice {
+ get { return ResourceManager.GetString("dialog.type.notice", resourceCulture); }
+ }
+
+ internal static string dialog_type_confirmation {
+ get { return ResourceManager.GetString("dialog.type.confirmation", resourceCulture); }
+ }
+
+ internal static string dialog_type_multi_action {
+ get { return ResourceManager.GetString("dialog.type.multi_action", resourceCulture); }
+ }
+
+ internal static string dialog_type_dialog_list {
+ get { return ResourceManager.GetString("dialog.type.dialog_list", resourceCulture); }
+ }
+
+ internal static string dialog_type_server_links {
+ get { return ResourceManager.GetString("dialog.type.server_links", resourceCulture); }
+ }
+
+ internal static string dialog_type_unknown {
+ get { return ResourceManager.GetString("dialog.type.unknown", resourceCulture); }
+ }
+
}
}
diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx
index a0fa7014..90c2f04a 100644
--- a/MinecraftClient/Resources/Translations/Translations.resx
+++ b/MinecraftClient/Resources/Translations/Translations.resx
@@ -3034,4 +3034,22 @@ see item details.
unknown
+
+ Notice
+
+
+ Confirmation
+
+
+ Multi-action
+
+
+ Dialog list
+
+
+ Server links
+
+
+ Unknown
+
diff --git a/docs/guide/usage.md b/docs/guide/usage.md
index 5d4d0768..8b84e9bb 100644
--- a/docs/guide/usage.md
+++ b/docs/guide/usage.md
@@ -1647,6 +1647,120 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+dialog
+
+- **Description:**
+
+ Browse and interact with dialogs sent by the server. Dialogs are popups with a title, body text, and buttons. TUI mode gives the best experience -- use `/dialog open` to view the dialog in a full-screen overlay.
+
+ When a server shows a dialog, MCC prints:
+
+ ```
+ [MCC] Server showed custom dialog: Server Notice. Use dialog show.
+ ```
+
+ Run `/dialog show` to see the full dialog. Buttons show up like this:
+
+ ```
+ Actions:
+ [1] OK (close)
+ [2] Visit (command)
+ [3] Rules (show dialog)
+ ```
+
+ The text in parentheses tells you what the button does: `(close)` closes the dialog, `(command)` sends a chat command, `(show dialog)` opens another dialog.
+
+- **Dialog types:**
+
+ Dialogs come in a few shapes. You might see these in the output of `/dialog show`:
+
+ **Notice** -- A popup with a title, optional body, and one button.
+
+ ```
+ Type: Notice
+
+ Welcome to the server!
+
+ Body: Read the rules before playing.
+
+ Actions:
+ [1] OK (close)
+ ```
+
+ **Confirmation** -- A choice between two buttons.
+
+ ```
+ Type: Confirmation
+
+ Reset your progress?
+
+ Actions:
+ [1] Yes (close)
+ [2] No (close)
+ ```
+
+ **Multi-action** -- A grid of buttons.
+
+ ```
+ Type: Multi-action
+
+ Choose a destination
+
+ Actions:
+ [1] Spawn (close)
+ [2] Shop (close)
+ [3] Arena (close)
+ ```
+
+ **Dialog list** -- A list of sub-dialogs. Clicking one opens another dialog.
+
+ ```
+ Type: Dialog list
+
+ Help Topics
+
+ Actions:
+ [1] Rules (show dialog)
+ [2] Commands (show dialog)
+ ```
+
+ **Server links** -- Shows the server's configured links as buttons. May be empty.
+
+ ```
+ Type: Server links
+
+ Server Links
+ ```
+
+- **Usage:**
+
+ ```
+ /dialog
+ /dialog show
+ /dialog open
+ /dialog click
+ /dialog click-label
+
debug
diff --git a/tools/run-dialog-test.sh b/tools/run-dialog-test.sh
new file mode 100755
index 00000000..12aad1b1
--- /dev/null
+++ b/tools/run-dialog-test.sh
@@ -0,0 +1,238 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+source "$REPO_ROOT/tools/mcc-env.sh"
+source "$REPO_ROOT/.skills/mcc-integration-testing/scripts/common.sh"
+
+usage() {
+ cat <<'EOF'
+Usage: tools/run-dialog-test.sh
+
+Integration test for MCC dialog system against a real local server.
+Tests all 5 dialog types, button actions, cancel/dismiss, and body content.
+
+Examples:
+ tools/run-dialog-test.sh 26.1
+ tools/run-dialog-test.sh 1.21.11
+EOF
+}
+
+MC_VERSION="${1:-}"
+if [[ -z "$MC_VERSION" ]]; then
+ usage >&2
+ exit 1
+fi
+
+SESSION_NAME="dialog-test-${MC_VERSION//[^a-zA-Z0-9]/_}"
+TEST_ROOT="${TMPDIR:-/tmp}/mcc-dialog-test/${MC_VERSION//\//_}"
+CFG="$TEST_ROOT/custom.ini"
+MCC_LOG="$TEST_ROOT/mcc-output.log"
+INPUT_FILE="$TEST_ROOT/mcc_input.txt"
+SERVER_PORT="25565"
+PASS=0
+FAIL=0
+
+cleanup() {
+ tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
+}
+trap cleanup EXIT
+
+header() {
+ echo ""
+ echo "===== $* ====="
+}
+
+# Strip ANSI escape codes for grep matching
+ansi_strip() {
+ sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
+}
+
+assert_log() {
+ local label="$1"
+ local pattern="$2"
+ local timeout="${3:-5}"
+ local elapsed=0
+ while (( elapsed < timeout )); do
+ if [[ -f "$MCC_LOG" ]] && ansi_strip < "$MCC_LOG" | grep -Fq "$pattern" 2>/dev/null; then
+ echo " PASS: $label"
+ PASS=$((PASS + 1))
+ return 0
+ fi
+ sleep 1
+ ((elapsed += 1))
+ done
+ echo " FAIL: $label (expected: '$pattern')"
+ FAIL=$((FAIL + 1))
+}
+
+wait_for_pattern() {
+ local file="$1"
+ local pattern="$2"
+ local timeout="${3:-60}"
+ local elapsed=0
+ while (( elapsed < timeout )); do
+ if [[ -f "$file" ]] && ansi_strip < "$file" | grep -Fq "$pattern" 2>/dev/null; then
+ return 0
+ fi
+ sleep 1
+ ((elapsed += 1))
+ done
+ return 1
+}
+
+write_input() {
+ echo "$1" >> "$INPUT_FILE"
+ sleep 1
+}
+
+assert_dialog_shown() {
+ assert_log "$1 received" "Server showed custom dialog: $2" 10
+}
+
+# ---- Setup ----
+
+mkdir -p "$TEST_ROOT"
+rm -f "$MCC_LOG" "$INPUT_FILE"
+
+echo "[Dialog Test] Version: $MC_VERSION, Session: $SESSION_NAME"
+echo "[Dialog Test] Log: $MCC_LOG"
+
+# Ensure server is running
+if ! server_running "$MC_VERSION"; then
+ echo "[Setup] Starting server..."
+ bash "$REPO_ROOT/tools/start-server.sh" "$MC_VERSION" 2>&1 | tail -1
+ wait_for_server_ready "$MC_VERSION" 120
+fi
+
+echo "[Setup] Server ready."
+
+# Prepare temp config
+echo "[Setup] Preparing MCC config..."
+bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
+ "$CFG" "$MC_VERSION" "MCCBot" >/dev/null
+
+# Disable packet debug (fix in-place to avoid dup sections)
+sed_in_place \
+ -e '/^\[Debug\]/,/^\s*$/d' \
+ "$CFG"
+
+cat >> "$CFG" < "$INPUT_FILE"
+tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
+sleep 1
+
+cd "$REPO_ROOT"
+# FileInputBot ignores config and uses MCC_INPUT_FILE env var only
+INPUT_FILE_ABS="$(realpath "$INPUT_FILE")"
+tmux new-session -d -s "$SESSION_NAME" \
+ "bash -c 'export MCC_FILE_INPUT=1; export MCC_INPUT_FILE=\"$INPUT_FILE_ABS\"; exec dotnet run --no-build --project MinecraftClient -c Release -- \"$CFG\" \"MCCBot\" \"-\" \"localhost:$SERVER_PORT\"' > '$MCC_LOG' 2>&1"
+
+echo "[Setup] Waiting for MCC to join..."
+if ! wait_for_pattern "$MCC_LOG" "Server was successfully joined" 90; then
+ echo "ERROR: MCC did not join the server. Check $MCC_LOG"
+ tail -10 "$MCC_LOG" | ansi_strip
+ exit 1
+fi
+echo "[Setup] MCC joined."
+sleep 2
+
+# ---- Tests ----
+
+header "1. Notice Dialog"
+mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"Notice Title"}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "notice dialog" "Notice Title"
+write_input "dialog show"
+assert_log "notice type" "Type: Notice" 10
+assert_log "notice OK button" "OK (close)" 3
+
+header "2. Confirmation Dialog"
+mc-rcon 'dialog show MCCBot {type:"minecraft:confirmation", title:{text:"Confirm?"}, yes:{label:{text:"Yes"}}, no:{label:{text:"No"}}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "confirmation" "Confirm?"
+write_input "dialog show"
+assert_log "confirmation type" "Type: Confirmation" 10
+assert_log "yes button" "Yes (close)" 3
+assert_log "no button" "No (close)" 3
+
+header "3. Multi-Action Dialog"
+mc-rcon 'dialog show MCCBot {type:"minecraft:multi_action", title:{text:"Choose"}, actions:[{label:{text:"Alpha"}}, {label:{text:"Beta"}}, {label:{text:"Gamma"}}]}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "multi_action" "Choose"
+write_input "dialog show"
+assert_log "multi_action type" "Type: Multi-action" 10
+assert_log "multi_action button 1" "Alpha (close)" 3
+assert_log "multi_action button 2" "Beta (close)" 3
+assert_log "multi_action button 3" "Gamma (close)" 3
+
+header "4. Dialog-List Dialog"
+mc-rcon 'dialog show MCCBot {type:"minecraft:dialog_list", title:{text:"List"}, dialogs:[{type:"minecraft:notice", title:{text:"Sub One"}}, {type:"minecraft:notice", title:{text:"Sub Two"}}]}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "dialog_list" "List"
+write_input "dialog show"
+assert_log "dialog_list type" "Type: Dialog list" 10
+assert_log "dialog_list sub 1" "Sub One (show dialog)" 3
+assert_log "dialog_list sub 2" "Sub Two (show dialog)" 3
+
+header "5. Server-Links Dialog"
+mc-rcon 'dialog show MCCBot {type:"minecraft:server_links", title:{text:"Links"}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "server_links" "Links"
+write_input "dialog show"
+assert_log "server_links type" "Type: Server links" 10
+
+header "6. Body Content"
+mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"With Body"}, body:[{type:"minecraft:plain_message", contents:{text:"Hello from body"}}]}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "body dialog" "With Body"
+write_input "dialog show"
+assert_log "body content" "Hello from body" 10
+
+header "7. Custom run_command Action"
+mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"Run Cmd"}, action:{label:{text:"/list"}, action:{type:"minecraft:run_command", command:"/list"}}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "command action" "Run Cmd"
+write_input "dialog show"
+assert_log "command action button" "/list (command)" 10
+write_input "dialog click 1"
+assert_log "command executed" "There are " 10
+
+header "8. show_dialog Action (nested)"
+mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"First"}, action:{label:{text:"Next"}, action:{type:"minecraft:show_dialog", dialog:{type:"minecraft:notice", title:{text:"Second"}}}}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "first dialog" "First"
+write_input "dialog click 1"
+assert_dialog_shown "nested dialog" "Second"
+
+header "9. Dialog Cancel"
+mc-rcon 'dialog show MCCBot {type:"minecraft:notice", title:{text:"Cancel Me"}}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "cancel test dialog" "Cancel Me"
+write_input "dialog cancel"
+assert_log "cancel closed dialog" "Dialog action closed locally" 10
+
+header "10. Dialog Click-Label"
+mc-rcon 'dialog show MCCBot {type:"minecraft:multi_action", title:{text:"Label Test"}, actions:[{label:{text:"Pick Me"}}, {label:{text:"Leave Me"}}]}' 2>&1 | ansi_strip | grep -v "^$"
+assert_dialog_shown "click-label dialog" "Label Test"
+write_input "dialog click-label Pick Me"
+assert_log "click-label worked" "Dialog action closed locally" 10
+
+# ---- Results ----
+
+echo ""
+echo "=========================================="
+echo " Dialog Integration Test Results"
+echo "=========================================="
+echo " PASS: $PASS"
+echo " FAIL: $FAIL"
+echo "------------------------------------------"
+
+if [[ $FAIL -gt 0 ]]; then
+ echo "FAILURES DETECTED. Full log: $MCC_LOG"
+ echo "Last 20 lines:"
+ ansi_strip < "$MCC_LOG" | tail -20
+ exit 1
+else
+ echo "ALL TESTS PASSED."
+ exit 0
+fi