diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index 4b869b5c..f1a3c8fe 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -13,7 +13,18 @@ Use this skill when the task needs a real local server loop, not just code readi - Runtime target: `.NET 10` / `net10.0` - Environment: WSL Ubuntu, Java 21, tmux, python3 - Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}` -- Default validation target when the user does not specify a version: `1.21.11-Vanilla` +- Default validation target when the user does not specify a version: `1.21.11` + +## Console modes + +MCC supports two console modes selectable via `ConsoleMode` in `[Console.General]`: + +| Mode | Backend | Best for | +|------|---------|----------| +| `classic` | `ClassicConsoleBackend` (ConsoleInteractive) | Normal use, legacy CI/scripts, `FileInput` mode | +| `tui` | `TuiConsoleBackend` (Avalonia/Consolonia) | Full-screen TUI with scrollable log, command input, popup inventory | + +Both modes support the same commands and input/output through `ConsoleIO.Backend`. The mode is determined at startup from config; `BasicIO` CLI arg overrides to simple stdio. ## Core rules @@ -35,16 +46,16 @@ Interactive shell: ```bash source tools/mcc-env.sh -mc-start 1.21.11-Vanilla -mc-log 1.21.11-Vanilla 100 +mc-start 1.21.11 +mc-log 1.21.11 100 mc-rcon "op CursorBot" -mc-stop 1.21.11-Vanilla +mc-stop 1.21.11 ``` Non-interactive shell: ```bash -tools/start-server.sh 1.21.11-Vanilla +tools/start-server.sh 1.21.11 tools/mc-rcon.sh "op CursorBot" ``` @@ -55,17 +66,158 @@ export MCC_SERVERS=/home/anon/Minecraft/Servers source tools/mcc-env.sh ``` -## Recommended automation recipe +## One-step debug session (recommended) -Use this for reproducible local runs: +The `tools/mcc-debug.sh` script handles build, server startup, config preparation, and MCC launch in one step: -1. Source `tools/mcc-env.sh`. -2. Pick a concrete server directory, usually `1.21.11-Vanilla`. -3. Copy `MinecraftClient.ini` to a temp location and pin the account, version, and feature gates there. -4. Start the server and wait for `Done (` in the tmux log. -5. Launch MCC with `MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- ""`. -6. Drive MCC through `mcc_input.txt`. -7. Stop MCC and the server cleanly, then keep the temp logs. +```bash +source tools/mcc-env.sh + +# Classic mode with FileInput (script-driven debugging): +mcc-debug -v 1.21.11 --file-input + +# Classic mode interactive (attach via tmux): +mcc-debug -v 1.21.11 + +# TUI mode: +mcc-debug -v 1.21.11 -m tui + +# With debug messages enabled from start: +mcc-debug -v 1.21.11 --file-input --debug-on + +# Skip build (already built): +mcc-debug -v 1.21.11 --file-input --no-build +``` + +### What mcc-debug.sh does + +1. Builds MCC (unless `--no-build`) +2. Creates a temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled +3. Ensures server is running (starts if not, waits for `Done (`) +4. Launches MCC in the specified mode + +### After launch + +- **FileInput mode**: drive MCC via `mcc-cmd "debug state"` or `echo "debug state" >> mcc_input.txt` +- **Interactive/TUI mode**: attach with `tmux attach -t mcc-debug`, type commands directly +- **Logs**: `tail -f /tmp/mcc-debug/mcc-debug.log` (FileInput mode only; TUI/interactive mode outputs to tmux) +- **Server RCON**: `mc-rcon "op CursorBot"`, `mc-rcon "gamemode creative CursorBot"` + +## Debug commands (in-game) + +### `/debug [on|off]` + +Toggles debug logging. Now correctly syncs both `Settings.Config.Logging.DebugMessages` and `McClient.Log.DebugEnabled`. + +### `/debug state` + +Prints a one-shot summary of MCC's internal state: + +``` +=== MCC Debug State === +Server: localhost:25565 +Username: CursorBot +Protocol: 774 +GameMode: 1 +Health: 20.0 +Food: 20 +Location: 0.50, 80.00, 0.50 +TPS: 20.0 +Console: ClassicConsoleBackend (or TuiConsoleBackend) +Features: Terrain Inventory Entity +Debug: ON +Bots (3): AutoFishing, FileInputBot, ScriptScheduler +Players: 2 online +``` + +This works in both classic and TUI modes. + +## Classic mode debugging + +### Agent workflow (FileInput mode) + +For agents calling MCC commands programmatically: + +```bash +source tools/mcc-env.sh +mcc-debug -v 1.21.11 --file-input --no-build + +# Send commands: +mcc-cmd "debug state" +mcc-cmd "inventory player list" +mcc-cmd "entity" + +# Check results: +tail -20 /tmp/mcc-debug/mcc-debug.log + +# Stop: +mcc-cmd "quit" +mc-stop 1.21.11 +``` + +### Interactive workflow + +```bash +source tools/mcc-env.sh +mcc-debug -v 1.21.11 + +# In another terminal: +tmux attach -t mcc-debug +# Type commands directly in MCC console +``` + +## TUI mode debugging + +TUI mode runs Consolonia full-screen in a tmux session. Key differences: + +1. **No pipe/redirect**: TUI needs a real tty. Cannot `| tee` or redirect stdout. +2. **Log output is in-screen**: all output appears in the scrollable log area. +3. **Keyboard shortcuts**: PageUp/PageDown scroll, ESC exits. +4. **`/debug state`**: the primary way to inspect internal state since external log tailing is not available. +5. **Dialog windows**: `/inventui` opens as an overlay dialog instead of a separate screen. + +### Agent workflow for TUI mode + +```bash +source tools/mcc-env.sh +mcc-debug -v 1.21.11 -m tui --no-build + +# Cannot use mcc-cmd (no FileInput); must use tmux send-keys: +tmux send-keys -t mcc-debug "/debug state" Enter + +# Read TUI screen: +tmux capture-pane -t mcc-debug -p -S -30 + +# Stop: +tmux send-keys -t mcc-debug Escape +``` + +**Caveat with tmux send-keys and Consolonia**: When sending text containing `/`, the Enter key may need to be sent separately: +```bash +tmux send-keys -t mcc-debug "/inventory player list" +tmux send-keys -t mcc-debug Enter +``` + +## mcc-env.sh quick reference + +After `source tools/mcc-env.sh`: + +| Function | Description | +|----------|-------------| +| `mc-start VER` | Start MC server in tmux | +| `mc-stop VER` | Graceful stop via stdin pipe | +| `mc-log VER [N]` | Capture last N lines of server output | +| `mc-rcon "CMD"` | Send RCON command | +| `mc-kill VER` | Force-kill server tmux session | +| `mc-list` | List running MC server sessions | +| `mcc-build` | Build MCC | +| `mcc-run [PORT]` | Run MCC classic+FileInput on port | +| `mcc-tui [PORT]` | Run MCC TUI mode in tmux | +| `mcc-cmd "CMD"` | Append command to mcc_input.txt | +| `mcc-kill` | Kill MCC process and debug session | +| `mcc-debug [OPTS]` | One-step debug session (see above) | +| `mcc-log-mcc` | Tail MCC debug log | +| `mcc-state` | Send `debug state` and print last 30 log lines | ## Temporary config recipe @@ -84,26 +236,11 @@ sed -i \ "$CFG" ``` -Add extra `sed -i` edits only for the scenario you are testing. - -## Run MCC - -Direct temp-config launch: - +For TUI mode, also add: ```bash -cd "$MCC_REPO" -MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" 2>&1 +sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" ``` -Quick manual launch with the repo-root config: - -```bash -source tools/mcc-env.sh -mcc-run 25565 -``` - -`mcc-run` is fine for manual smoke tests. Use a temp config for repeatable automation. - ## Verify connection and a basic command MCC output should include: @@ -117,25 +254,39 @@ Server output should include: Basic command check: ```bash -echo "inventory player list" >> mcc_input.txt +mcc-cmd "inventory player list" ``` ## Typical debug loop 1. `source tools/mcc-env.sh` -2. `dotnet build MinecraftClient.sln -c Release` -3. start `1.21.11-Vanilla` -4. wait for `Done (` -5. launch MCC with a temp config -6. confirm the join in both logs -7. run one MCC command and one server command -8. inspect logs -9. stop both sides and iterate +2. `mcc-debug -v 1.21.11 --file-input` (or `-m tui`) +3. Confirm `Server was successfully joined` in log +4. `mcc-cmd "debug state"` to verify MCC state +5. Run test commands +6. Inspect log output +7. `mcc-cmd "quit"` and `mc-stop 1.21.11` +8. Edit code, rebuild, repeat ## Debugging tips +- **`/debug state` is your primary diagnostic tool** in both modes. Use it first to verify connection, mode, and feature flags. +- **`/debug on` now correctly enables debug logging** at runtime. Previous versions had a bug where `Log.DebugEnabled` was not synced. - Protocol mismatches usually show up as a version line such as `Server version : 1.21.11 (protocol vNNN)` before the failure. - If an early `mc-rcon` command fails, retry it before assuming the server setup is broken. - If a supposedly isolated run behaves strangely, check `tmux list-sessions` and kill stale `mc-*` sessions first. - Legacy `1.8` and `1.8.9` servers may need `use-native-transport=false` in `server.properties` on some Linux environments. - For timing-sensitive work, do not trust wall-clock intuition. Use a real server run and capture evidence from logs or test scripts. +- **TUI mode tip**: if the terminal becomes unresponsive after a crash, run `stty sane && reset` to restore it. +- **tmux capture trick**: `tmux capture-pane -t mcc-debug -p -S -50` captures the last 50 lines of a tmux session without attaching. + +## Tool files + +| File | Purpose | +|------|---------| +| `tools/mcc-env.sh` | Shell functions for server/MCC management | +| `tools/mcc-debug.sh` | One-step debug session launcher | +| `tools/mcc-log-tail.sh` | Log tailing for MCC and/or server | +| `tools/start-server.sh` | Server lifecycle in tmux | +| `tools/mc-rcon.sh` | RCON command sender | +| `tools/run-creative-e2e.sh` | Full creative mode end-to-end test | diff --git a/AGENTS.md b/AGENTS.md index fbed3799..40f216f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,6 +108,7 @@ Read `docs/guide/ai-assisted-development.md` before starting development work on - Prefer nullable-aware code, pattern matching, `ArgumentNullException.ThrowIfNull`, `Try*` APIs for expected failures, and `InvokeOnMainThread()` for cross-thread state changes. - Use modern C# 14 features. - Use provided skills proactively depending on the context, read their descriptions to determine when to use them. +- All user-facing text (log messages, command output, TUI labels, notifications, help text, error messages) **must** go through the translation system: add entries to `Translations.resx` + `Translations.Designer.cs`, then reference `Translations.key_name` in code. Never hardcode user-visible strings directly in `.cs` files. Use `string.Format(Translations.key, ...)` for parameterized messages. Translation keys follow dot-delimited naming: `..` (e.g. `cmd.inventory.tui_opened`, `tui.inventory.controls`). Pure technical identifiers (class names, protocol constants, color codes) are exempt. ### DON'T - Don't update only `MCVer2ProtocolVersion()` or only one palette file when adding a new Minecraft version. @@ -118,4 +119,5 @@ Read `docs/guide/ai-assisted-development.md` before starting development work on - Don't start background workers when `Update()` or delayed tasks are sufficient; if you must, stop them on unload/disconnect. - Don't leave movement locks, plugin channels, or dispatcher registrations behind. - Don't trust older docs over current code for supported versions or feature gates. When AGENTS.md, skills, and older docs disagree, prefer current code and current tool behavior, then update the stale source. +- Don't hardcode user-facing strings (messages, labels, help text) directly in source code; always use `Translations.*` resources so the text can be localized. - Never use "—" ("em dash"), unless specifically being instructed to do so! diff --git a/MinecraftClient/ClassicConsoleBackend.cs b/MinecraftClient/ClassicConsoleBackend.cs new file mode 100644 index 00000000..ac4912c9 --- /dev/null +++ b/MinecraftClient/ClassicConsoleBackend.cs @@ -0,0 +1,160 @@ +using System; + +namespace MinecraftClient +{ + /// + /// Console backend wrapping the ConsoleInteractive library (existing behavior). + /// + public class ClassicConsoleBackend : IConsoleBackend + { + public event EventHandler? MessageReceived; + public event EventHandler? OnInputChange; + + public bool DisplayUserInput + { + get => ConsoleInteractive.ConsoleReader.DisplayUesrInput; + set => ConsoleInteractive.ConsoleReader.DisplayUesrInput = value; + } + + public void Init() + { + ConsoleInteractive.ConsoleWriter.Init(); + } + + public void WriteLine(string text) + { + ConsoleInteractive.ConsoleWriter.WriteLine(text); + } + + public void WriteLineFormatted(string text) + { + ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text); + } + + public void BeginReadThread() + { + ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage; + ConsoleInteractive.ConsoleReader.OnInputChange += ForwardInputChange; + ConsoleInteractive.ConsoleReader.BeginReadThread(); + } + + public void StopReadThread() + { + ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleInteractive.ConsoleReader.MessageReceived -= ForwardMessage; + ConsoleInteractive.ConsoleReader.OnInputChange -= ForwardInputChange; + } + + public string RequestImmediateInput() + { + return ConsoleInteractive.ConsoleReader.RequestImmediateInput(); + } + + public string? ReadPassword() + { + ConsoleInteractive.ConsoleReader.SetInputVisible(false); + var input = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); + ConsoleInteractive.ConsoleReader.SetInputVisible(true); + return input; + } + + public void ClearInputBuffer() + { + ConsoleInteractive.ConsoleReader.ClearBuffer(); + } + + public void SetInputVisible(bool visible) + { + ConsoleInteractive.ConsoleReader.SetInputVisible(visible); + } + + public void SetBackreadBufferLimit(int limit) + { + ConsoleInteractive.ConsoleBuffer.SetBackreadBufferLimit(limit); + } + + public void Shutdown() + { + ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + } + + #region Suggestion forwarding for classic mode + + public void UpdateSuggestions( + ConsoleInteractive.ConsoleSuggestion.Suggestion[] suggestions, + Tuple range) + { + ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(suggestions, range); + } + + public void ClearSuggestions() + { + ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + } + + public void SetSuggestionColors( + string textColor, string textBgColor, + string hlTextColor, string hlTextBgColor, + string tooltipColor, string hlTooltipColor, + string arrowColor) + { + ConsoleInteractive.ConsoleSuggestion.SetColors( + textColor, textBgColor, + hlTextColor, hlTextBgColor, + tooltipColor, hlTooltipColor, + arrowColor); + } + + public bool EnableSuggestionColor + { + get => ConsoleInteractive.ConsoleSuggestion.EnableColor; + set => ConsoleInteractive.ConsoleSuggestion.EnableColor = value; + } + + public bool Enable24bitColor + { + get => ConsoleInteractive.ConsoleSuggestion.Enable24bitColor; + set => ConsoleInteractive.ConsoleSuggestion.Enable24bitColor = value; + } + + public bool UseBasicArrow + { + get => ConsoleInteractive.ConsoleSuggestion.UseBasicArrow; + set => ConsoleInteractive.ConsoleSuggestion.UseBasicArrow = value; + } + + public int SetMaxSuggestionLength(int length) + { + return ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionLength(length); + } + + public int SetMaxSuggestionCount(int count) + { + return ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionCount(count); + } + + public bool EnableWriterColor + { + get => ConsoleInteractive.ConsoleWriter.EnableColor; + set => ConsoleInteractive.ConsoleWriter.EnableColor = value; + } + + public bool UseVT100ColorCode + { + get => ConsoleInteractive.ConsoleWriter.UseVT100ColorCode; + set => ConsoleInteractive.ConsoleWriter.UseVT100ColorCode = value; + } + + #endregion + + private void ForwardMessage(object? sender, string e) + { + MessageReceived?.Invoke(sender, e); + } + + private void ForwardInputChange(object? sender, ConsoleInteractive.ConsoleReader.Buffer buffer) + { + OnInputChange?.Invoke(sender, new ConsoleInputBuffer(buffer.Text, buffer.CursorPosition)); + } + } +} diff --git a/MinecraftClient/Commands/Debug.cs b/MinecraftClient/Commands/Debug.cs index 0302ea05..92184f1d 100644 --- a/MinecraftClient/Commands/Debug.cs +++ b/MinecraftClient/Commands/Debug.cs @@ -1,13 +1,17 @@ -using Brigadier.NET; +using System; +using System.Linq; +using System.Text; +using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; +using MinecraftClient.Scripting; namespace MinecraftClient.Commands { public class Debug : Command { public override string CmdName { get { return "debug"; } } - public override string CmdUsage { get { return "debug [on|off]"; } } + public override string CmdUsage { get { return "debug [on|off|state]"; } } public override string CmdDesc { get { return Translations.cmd_debug_desc; } } public override void RegisterCommand(CommandDispatcher dispatcher) @@ -24,6 +28,8 @@ namespace MinecraftClient.Commands .Executes(r => SetDebugMode(r.Source, false, true))) .Then(l => l.Literal("off") .Executes(r => SetDebugMode(r.Source, false, false))) + .Then(l => l.Literal("state") + .Executes(r => ShowState(r.Source))) .Then(l => l.Literal("_help") .Executes(r => GetUsage(r.Source, string.Empty)) .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) @@ -42,15 +48,57 @@ namespace MinecraftClient.Commands private int SetDebugMode(CmdResult r, bool flip, bool mode = false) { + McClient handler = CmdResult.currentHandler!; + if (flip) Settings.Config.Logging.DebugMessages = !Settings.Config.Logging.DebugMessages; else Settings.Config.Logging.DebugMessages = mode; + handler.Log.DebugEnabled = Settings.Config.Logging.DebugMessages; + if (Settings.Config.Logging.DebugMessages) return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_debug_state_on); else return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_debug_state_off); } + + private int ShowState(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + var sb = new StringBuilder(); + + sb.AppendLine($"§e=== {Translations.cmd_debug_state_header} ==="); + sb.AppendLine($"§7{Translations.cmd_debug_state_server,-10}§f{handler.GetServerHost()}:{handler.GetServerPort()}"); + sb.AppendLine($"§7{Translations.cmd_debug_state_username,-10}§f{handler.GetUsername()}"); + sb.AppendLine($"§7{Translations.cmd_debug_state_protocol,-10}§f{handler.GetProtocolVersion()}"); + sb.AppendLine($"§7{Translations.cmd_debug_state_gamemode,-10}§f{handler.GetGamemode()}"); + sb.AppendLine($"§7{Translations.cmd_debug_state_health,-10}§f{handler.GetHealth():F1}"); + sb.AppendLine($"§7{Translations.cmd_debug_state_food,-10}§f{handler.GetSaturation()}"); + + var loc = handler.GetCurrentLocation(); + sb.AppendLine($"§7{Translations.cmd_debug_state_location,-10}§f{loc.X:F2}, {loc.Y:F2}, {loc.Z:F2}"); + + sb.AppendLine($"§7{Translations.cmd_debug_state_tps,-10}§f{handler.GetServerTPS():F1}"); + + sb.AppendLine($"§7{Translations.cmd_debug_state_console,-10}§f{(ConsoleIO.Backend?.GetType().Name ?? "null")}"); + + var features = new StringBuilder(); + features.Append(handler.GetTerrainEnabled() ? "§aTerrain " : "§8Terrain "); + features.Append(handler.GetInventoryEnabled() ? "§aInventory " : "§8Inventory "); + features.Append(handler.GetEntityHandlingEnabled() ? "§aEntity " : "§8Entity "); + sb.AppendLine($"§7{Translations.cmd_debug_state_features,-10}{features}"); + + sb.AppendLine($"§7{Translations.cmd_debug_state_debug,-10}§f{(Settings.Config.Logging.DebugMessages ? "§aON" : "§cOFF")}"); + + var bots = handler.GetLoadedChatBots(); + sb.AppendLine($"§7{Translations.cmd_debug_state_bots} ({bots.Count}): §f{string.Join(", ", bots.Select(b => b.GetType().Name))}"); + + var players = handler.GetOnlinePlayers(); + sb.AppendLine($"§7{Translations.cmd_debug_state_players,-10}§f{string.Format(Translations.cmd_debug_state_online, players.Length)}"); + + handler.Log.Info(sb.ToString()); + return r.SetAndReturn(CmdResult.Status.Done); + } } } diff --git a/MinecraftClient/Commands/Inventory.cs b/MinecraftClient/Commands/Inventory.cs index cc0aeac4..eeeee3d0 100644 --- a/MinecraftClient/Commands/Inventory.cs +++ b/MinecraftClient/Commands/Inventory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,6 +6,7 @@ using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; using MinecraftClient.Inventory; +using MinecraftClient.Tui; namespace MinecraftClient.Commands { @@ -22,6 +23,8 @@ namespace MinecraftClient.Commands .Executes(r => GetUsage(r.Source, string.Empty)) .Then(l => l.Literal("list") .Executes(r => GetUsage(r.Source, "list"))) + .Then(l => l.Literal("open") + .Executes(r => GetUsage(r.Source, "open"))) .Then(l => l.Literal("close") .Executes(r => GetUsage(r.Source, "close"))) .Then(l => l.Literal("click") @@ -59,6 +62,9 @@ namespace MinecraftClient.Commands .Then(l => l.Argument("Count", Arguments.Integer(0, 64)) .Executes(r => SearchItem(r.Source, MccArguments.GetItemType(r, "ItemType"), Arguments.GetInteger(r, "Count")))))) .Then(l => l.Argument("InventoryId", MccArguments.InventoryId()) + .Executes(r => DoOpenOrList(r.Source, Arguments.GetInteger(r, "InventoryId"))) + .Then(l => l.Literal("open") + .Executes(r => DoOpenTui(r.Source, Arguments.GetInteger(r, "InventoryId")))) .Then(l => l.Literal("close") .Executes(r => DoCloseAction(r.Source, Arguments.GetInteger(r, "InventoryId")))) .Then(l => l.Literal("list") @@ -113,6 +119,7 @@ namespace MinecraftClient.Commands return r.SetAndReturn(cmd switch { #pragma warning disable format // @formatter:off + "open" => Translations.cmd_inventory_help_open + usageStr + "/inventory open", "list" => Translations.cmd_inventory_help_list + usageStr + "/inventory > list", "close" => Translations.cmd_inventory_help_close + usageStr + "/inventory > close", "click" => Translations.cmd_inventory_help_click + usageStr + "/inventory > click [left|right|middle|shift|shiftright]\nDefault is left click", @@ -394,6 +401,55 @@ namespace MinecraftClient.Commands } + private int DoOpenOrList(CmdResult r, int inventoryId) + { + if (ConsoleIO.Backend is TuiConsoleBackend) + return DoOpenTui(r, inventoryId); + return DoListAction(r, inventoryId); + } + + private int DoOpenTui(CmdResult r, int inventoryId) + { + McClient handler = CmdResult.currentHandler!; + + if (!handler.GetInventoryEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + + if (ConsoleIO.Backend is not TuiConsoleBackend) + { + handler.Log.Warn(Translations.cmd_inventory_tui_only); + return r.SetAndReturn(CmdResult.Status.Fail); + } + + if (InventoryTuiHost.IsRunning) + { + handler.Log.Warn(Translations.cmd_inventory_tui_already_running); + return r.SetAndReturn(CmdResult.Status.Fail); + } + + var container = handler.GetInventory(inventoryId); + if (container == null) + { + string msg = string.Format(Translations.cmd_inventory_not_exist, inventoryId); + handler.Log.Warn(msg); + return r.SetAndReturn(CmdResult.Status.Fail, msg); + } + + handler.Log.Info(string.Format(Translations.cmd_inventory_tui_opening, inventoryId)); + + bool success = InventoryTuiHost.Launch(handler, inventoryId); + if (success) + { + handler.Log.Info(Translations.cmd_inventory_tui_opened); + return r.SetAndReturn(CmdResult.Status.Done); + } + else + { + handler.Log.Warn(Translations.cmd_inventory_tui_launch_failed); + return r.SetAndReturn(CmdResult.Status.Fail); + } + } + #region Methods for commands help private static string GetAvailableActions() diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs index 0c7987a5..485d5a90 100644 --- a/MinecraftClient/ConsoleIO.cs +++ b/MinecraftClient/ConsoleIO.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,6 +8,7 @@ using Brigadier.NET; using FuzzySharp; using MinecraftClient.CommandHandler; using MinecraftClient.Scripting; +using MinecraftClient.Tui; using static MinecraftClient.Settings; namespace MinecraftClient @@ -16,18 +17,23 @@ namespace MinecraftClient /// Allows simultaneous console input and output without breaking user input /// (Without having this annoying behaviour : User inp[Some Console output]ut) /// Provide some fancy features such as formatted output, text pasting and tab-completion. - /// By ORelio - (c) 2012-2018 - Available under the CDDL-1.0 license + /// By ORelio - (c) 2012-2018 - Available under the CDDL-1.0 License /// public static class ConsoleIO { private static IAutoComplete? autocomplete_engine; + /// + /// The active console backend. Set once during startup. + /// + public static IConsoleBackend Backend { get; set; } = null!; + /// /// Reset the IO mechanism and clear all buffers /// public static void Reset() { - ClearLineAndBuffer(); + Backend?.ClearInputBuffer(); } /// @@ -40,9 +46,8 @@ namespace MinecraftClient } /// - /// Determines whether to use interactive IO or basic IO. - /// Set to true to disable interactive command prompt and use the default Console.Read|Write() methods. - /// Color codes are printed as is when BasicIO is enabled. + /// Determines whether to use basic IO (legacy flag, kept for compatibility). + /// In the new architecture this is true when Backend is BasicConsoleBackend. /// public static bool BasicIO = false; @@ -59,7 +64,7 @@ namespace MinecraftClient /// /// Specify a generic log line prefix for WriteLogLine() /// - public static string LogPrefix = "§8[Log] "; + public static string LogPrefix = "§8[MCC] "; /// /// Read a password from the standard input @@ -68,13 +73,7 @@ namespace MinecraftClient { if (BasicIO) return Console.ReadLine(); - else - { - ConsoleInteractive.ConsoleReader.SetInputVisible(false); - var input = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); - ConsoleInteractive.ConsoleReader.SetInputVisible(true); - return input; - } + return Backend.ReadPassword(); } /// @@ -84,8 +83,7 @@ namespace MinecraftClient { if (BasicIO) return Console.ReadLine() ?? String.Empty; - else - return ConsoleInteractive.ConsoleReader.RequestImmediateInput(); + return Backend.RequestImmediateInput(); } /// @@ -109,7 +107,7 @@ namespace MinecraftClient if (BasicIO) Console.WriteLine(line); else - ConsoleInteractive.ConsoleWriter.WriteLine(line); + Backend.WriteLine(line); } /// @@ -153,7 +151,7 @@ namespace MinecraftClient return; } output.Append(str); - ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString()); + Backend.WriteLineFormatted(output.ToString()); } } @@ -177,10 +175,9 @@ namespace MinecraftClient private static void ClearLineAndBuffer() { if (BasicIO) return; - ConsoleInteractive.ConsoleReader.ClearBuffer(); + Backend.ClearInputBuffer(); } - #endregion internal static bool AutoCompleteDone = false; @@ -193,12 +190,37 @@ namespace MinecraftClient private static Task _latestTask = Task.CompletedTask; private static CancellationTokenSource? _cancellationTokenSource; - private static void MccAutocompleteHandler(ConsoleInteractive.ConsoleReader.Buffer buffer) + private static void SendSuggestions( + ConsoleInteractive.ConsoleSuggestion.Suggestion[] classicSugs, + Tuple range) + { + if (Backend is ClassicConsoleBackend classic) + { + classic.UpdateSuggestions(classicSugs, range); + } + else if (Backend is TuiConsoleBackend tui) + { + var tuiSugs = new CommandSuggestion[classicSugs.Length]; + for (int i = 0; i < classicSugs.Length; i++) + tuiSugs[i] = new CommandSuggestion(classicSugs[i].Text, classicSugs[i].Tooltip); + tui.UpdateSuggestions(tuiSugs, (range.Item1, range.Item2)); + } + } + + private static void DoClearSuggestions() + { + if (Backend is ClassicConsoleBackend classic) + classic.ClearSuggestions(); + else if (Backend is TuiConsoleBackend tui) + tui.ClearSuggestions(); + } + + private static void MccAutocompleteHandler(ConsoleInputBuffer buffer) { string fullCommand = buffer.Text; if (string.IsNullOrEmpty(fullCommand)) { - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + DoClearSuggestions(); return; } @@ -208,7 +230,7 @@ namespace MinecraftClient int offset = InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none ? 0 : 1; if (buffer.CursorPosition - offset < 0) { - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + DoClearSuggestions(); return; } _cancellationTokenSource?.Cancel(); @@ -232,7 +254,7 @@ namespace MinecraftClient foreach (var cmd in Commands) sugList.Add(new(cmd)); - ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList.ToArray(), new(offset, offset)); + SendSuggestions(sugList.ToArray(), new(offset, offset)); } else if (command.Length > 0 && command[0] == '/' && !command.Contains(' ')) { @@ -242,7 +264,7 @@ namespace MinecraftClient int index = 0; foreach (var sug in sorted) sugList[index++] = new(sug.Value); - ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList, new(offset, offset + command.Length)); + SendSuggestions(sugList, new(offset, offset + command.Length)); } else { @@ -257,7 +279,7 @@ namespace MinecraftClient int sugLen = suggestions.List.Count; if (sugLen == 0) { - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + DoClearSuggestions(); return; } @@ -278,7 +300,7 @@ namespace MinecraftClient foreach (var sug in sorted) sugList[index++] = new(sug.Value, dictionary[sug.Value] ?? string.Empty); - ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList, range); + SendSuggestions(sugList, range); } }, cts.Token); _latestTask = newTask; @@ -287,22 +309,68 @@ namespace MinecraftClient } else { - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + DoClearSuggestions(); return; } } - public static void AutocompleteHandler(object? sender, ConsoleInteractive.ConsoleReader.Buffer buffer) + public static void AutocompleteHandler(object? sender, ConsoleInputBuffer buffer) { if (Settings.Config.Console.CommandSuggestion.Enable) MccAutocompleteHandler(buffer); } + private static readonly string[] OfflineCommands = ["quit", "exit", "connect", "reco", "help"]; + + public static void OfflineAutocompleteHandler(object? sender, ConsoleInputBuffer buffer) + { + if (!Settings.Config.Console.CommandSuggestion.Enable) + return; + + string fullCommand = buffer.Text; + if (string.IsNullOrEmpty(fullCommand)) + { + DoClearSuggestions(); + return; + } + + var InternalCmdChar = Config.Main.Advanced.InternalCmdChar; + int offset = 0; + if (InternalCmdChar != MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none) + { + if (fullCommand[0] != InternalCmdChar.ToChar()) + { + DoClearSuggestions(); + return; + } + offset = 1; + } + + string command = fullCommand[offset..]; + if (command.Contains(' ')) + { + DoClearSuggestions(); + return; + } + + var sugList = new List(); + foreach (string cmd in OfflineCommands) + { + if (command.Length == 0 || cmd.StartsWith(command, StringComparison.OrdinalIgnoreCase)) + sugList.Add(new(cmd)); + } + + if (sugList.Count > 0) + SendSuggestions(sugList.ToArray(), new(offset, offset + command.Length)); + else + DoClearSuggestions(); + } + public static void CancelAutocomplete() { _cancellationTokenSource?.Cancel(); _latestTask = Task.CompletedTask; - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + DoClearSuggestions(); AutoCompleteDone = false; AutoCompleteResult = Array.Empty(); diff --git a/MinecraftClient/IConsoleBackend.cs b/MinecraftClient/IConsoleBackend.cs new file mode 100644 index 00000000..b4d52ebf --- /dev/null +++ b/MinecraftClient/IConsoleBackend.cs @@ -0,0 +1,71 @@ +using System; + +namespace MinecraftClient +{ + /// + /// Input buffer state passed with OnInputChange events. + /// + public readonly struct ConsoleInputBuffer + { + public string Text { get; } + public int CursorPosition { get; } + + public ConsoleInputBuffer(string text, int cursorPosition) + { + Text = text; + CursorPosition = cursorPosition; + } + } + + /// + /// Backend-independent suggestion item used by the TUI autocomplete popup. + /// Mirrors the shape of ConsoleInteractive.ConsoleSuggestion.Suggestion + /// without requiring a dependency on the ConsoleInteractive assembly. + /// + public readonly struct CommandSuggestion + { + public string Text { get; } + public string Tooltip { get; } + + public CommandSuggestion(string text, string tooltip = "") + { + Text = text; + Tooltip = tooltip; + } + } + + /// + /// Abstraction over the console I/O backend. + /// Implementations: ClassicConsoleBackend (ConsoleInteractive), TuiConsoleBackend (Avalonia/Consolonia), BasicConsoleBackend (stdio). + /// + public interface IConsoleBackend + { + void Init(); + + void WriteLine(string text); + + void WriteLineFormatted(string text); + + void BeginReadThread(); + + void StopReadThread(); + + event EventHandler? MessageReceived; + + event EventHandler? OnInputChange; + + string RequestImmediateInput(); + + string? ReadPassword(); + + void ClearInputBuffer(); + + bool DisplayUserInput { get; set; } + + void SetInputVisible(bool visible); + + void SetBackreadBufferLimit(int limit); + + void Shutdown(); + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index e1449079..4d82a1d4 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -260,9 +260,9 @@ namespace MinecraftClient Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString())); cmdprompt = new CancellationTokenSource(); - ConsoleInteractive.ConsoleReader.BeginReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler; + ConsoleIO.Backend.BeginReadThread(); + ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler; } else { @@ -304,9 +304,9 @@ namespace MinecraftClient } else if (InternalConfig.InteractiveMode) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleIO.Backend.StopReadThread(); + ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler; Program.HandleFailure(); } @@ -324,9 +324,9 @@ namespace MinecraftClient // kick messages and Ignore_Kick_Message is false, or retry limit reached) if (InternalConfig.InteractiveMode) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleIO.Backend.StopReadThread(); + ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler; Program.HandleFailure(); } @@ -383,6 +383,11 @@ namespace MinecraftClient UpdateKeepAlive(); Log.Info($"Successfully transferred connection and logged in to {newHost}:{newPort}."); + + cmdprompt = new CancellationTokenSource(); + ConsoleIO.Backend.BeginReadThread(); + ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler; } else { @@ -426,9 +431,9 @@ namespace MinecraftClient } else if (InternalConfig.InteractiveMode) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleIO.Backend.StopReadThread(); + ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler; Program.HandleFailure(); } @@ -683,6 +688,8 @@ namespace MinecraftClient /// public void Disconnect() { + instance = null; + DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, "")); botsOnHold.Clear(); @@ -715,6 +722,8 @@ namespace MinecraftClient /// public void OnConnectionLost(ChatBot.DisconnectReason reason, string message) { + instance = null; + ConsoleIO.CancelAutocomplete(); handler.Dispose(); @@ -775,9 +784,9 @@ namespace MinecraftClient if (!will_restart) { - ConsoleInteractive.ConsoleReader.StopReadThread(); - ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived; - ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler; + ConsoleIO.Backend.StopReadThread(); + ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived; + ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler; Program.HandleFailure(null, false, reason); } } @@ -803,6 +812,14 @@ namespace MinecraftClient return; } + /// + /// Get the console message handler delegate for re-attaching after TUI mode. + /// + public EventHandler GetConsoleMessageHandler() + { + return ConsoleReaderOnMessageReceived; + } + /// /// Allows the user to send chat messages, commands, and leave the server. /// Process text from the MCC command prompt on the main thread. diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 59bde3af..5df50933 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -32,6 +32,7 @@ + diff --git a/MinecraftClient/Physics/BlockShapes.cs b/MinecraftClient/Physics/BlockShapes.cs index b44c6816..535c1ee4 100644 --- a/MinecraftClient/Physics/BlockShapes.cs +++ b/MinecraftClient/Physics/BlockShapes.cs @@ -78,7 +78,7 @@ namespace MinecraftClient.Physics using var stream = assembly.GetManifestResourceStream("BlockShapeData.json"); if (stream is null) { - ConsoleInteractive.ConsoleWriter.WriteLineFormatted("§e[Physics] BlockShapeData.json not found as embedded resource"); + ConsoleIO.WriteLineFormatted("§e[Physics] BlockShapeData.json not found as embedded resource"); return; } using var doc = JsonDocument.Parse(stream); @@ -130,7 +130,7 @@ namespace MinecraftClient.Physics } catch (Exception ex) { - ConsoleInteractive.ConsoleWriter.WriteLineFormatted($"§e[Physics] Failed to load BlockShapeData.json: {ex.Message}"); + ConsoleIO.WriteLineFormatted($"§e[Physics] Failed to load BlockShapeData.json: {ex.Message}"); } } diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 3bb3fbe5..46b4e19e 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -64,7 +64,8 @@ namespace MinecraftClient static void Main(string[] args) { // [SENTRY] Initialize Sentry SDK only if the DSN is not empty - if (SentryDSN != string.Empty) { + if (SentryDSN != string.Empty) + { _sentrySdk = SentrySdk.Init(options => { options.Dsn = SentryDSN; @@ -73,7 +74,7 @@ namespace MinecraftClient options.TracesSampleRate = 1.0; options.SendDefaultPii = false; }); - + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => { SentrySdk.CaptureException((Exception)eventArgs.ExceptionObject); @@ -115,7 +116,10 @@ namespace MinecraftClient } if (!ConsoleIO.BasicIO) - ConsoleInteractive.ConsoleWriter.Init(); + { + ConsoleIO.Backend = new ClassicConsoleBackend(); + ConsoleIO.Backend.Init(); + } ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); @@ -164,20 +168,24 @@ namespace MinecraftClient // Only show the Sentry message if the DSN is not empty // as Sentry will not be initialized if the DSN is empty - if (SentryDSN != string.Empty) { + if (SentryDSN != string.Empty) + { ConsoleIO.WriteLine(Translations.mcc_sentry_logging); } } else if (!loadSucceed) { - ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleIO.Backend?.StopReadThread(); string command = " "; while (command.Length > 0) { ConsoleIO.WriteLine(string.Empty); ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString())); - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); - command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim(); + if (ConsoleIO.Backend is Tui.TuiConsoleBackend) + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString())); + else + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + command = ConsoleIO.ReadLine().Trim(); if (command.Length > 0) { if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' ' @@ -210,11 +218,31 @@ namespace MinecraftClient ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); WriteBackSettings(true); // format } - + if (!Config.Main.Advanced.EnableSentry) _sentrySdk?.Dispose(); } + // Switch to TUI mode if configured (must happen after config load) + if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) + { + ConsoleIO.Backend?.Shutdown(); + var tuiBackend = new Tui.TuiConsoleBackend(); + ConsoleIO.Backend = tuiBackend; + tuiBackend.RunTuiMainLoop(args); + return; + } + + ContinueAfterTuiInit(args); + } + + /// + /// Continues MCC startup after console mode has been determined. + /// Called directly from Main for classic/basic mode, or from a background + /// thread for TUI mode (after the Avalonia UI loop has started). + /// + internal static void ContinueAfterTuiInit(string[] args) + { //Other command-line arguments if (args.Length >= 1) { @@ -668,7 +696,7 @@ namespace MinecraftClient { // [SENTRY] SentrySdk.CaptureException(e); - + ConsoleIO.WriteLine(e.Message); ConsoleIO.WriteLine(e.StackTrace ?? ""); HandleFailure(); // Other error @@ -723,11 +751,15 @@ namespace MinecraftClient /// Optional, keep account and server settings public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false) { - ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleIO.Backend.StopReadThread(); new Thread(new ThreadStart(delegate { if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); } - if (offlinePrompt is not null) { offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); } + if (offlinePrompt is not null) + { + ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler; + offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); + } if (delaySeconds > 0) { ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds)); @@ -742,11 +774,15 @@ namespace MinecraftClient public static void DoExit(int exitcode = 0) { WriteBackSettings(); - ConsoleInteractive.ConsoleSuggestion.ClearSuggestions(); + ConsoleIO.Backend?.Shutdown(); ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath)); if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); } - if (offlinePrompt is not null) { offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); } + if (offlinePrompt is not null) + { + ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler; + offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); + } if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); } Environment.Exit(exitcode); } @@ -771,10 +807,12 @@ namespace MinecraftClient if (!String.IsNullOrEmpty(errorMessage)) { ConsoleIO.Reset(); - try { + try + { while (Console.KeyAvailable) Console.ReadKey(true); - } catch { } + } + catch { } ConsoleIO.WriteLine(errorMessage); if (disconnectReason.HasValue) @@ -789,7 +827,7 @@ namespace MinecraftClient if (versionError) { ConsoleIO.WriteLine(Translations.mcc_server_version); - InternalConfig.MinecraftVersion = ConsoleInteractive.ConsoleReader.RequestImmediateInput(); + InternalConfig.MinecraftVersion = ConsoleIO.ReadLine(); if (InternalConfig.MinecraftVersion != "") { useMcVersionOnce = true; @@ -798,14 +836,16 @@ namespace MinecraftClient } } - if (disconnectReason.HasValue) { + if (disconnectReason.HasValue) + { if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!)) return; //AutoRelog is triggering a restart of the client, don't turn on the offline prompt } - + if (offlinePrompt is null) { - ConsoleInteractive.ConsoleReader.StopReadThread(); + ConsoleIO.Backend.StopReadThread(); + ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler; var cancellationTokenSource = new CancellationTokenSource(); offlinePrompt = new(new Thread(new ThreadStart(delegate @@ -814,7 +854,10 @@ namespace MinecraftClient string command = " "; ConsoleIO.WriteLine(string.Empty); ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString())); - ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); + if (ConsoleIO.Backend is Tui.TuiConsoleBackend) + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString())); + else + ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true); while (!cancellationTokenSource.IsCancellationRequested) { @@ -826,7 +869,7 @@ namespace MinecraftClient if (cancellationTokenSource.IsCancellationRequested) return; - command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim(); + command = ConsoleIO.ReadLine().Trim(); if (command.Length > 0) { string message = ""; diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 43d81d95..143d1b26 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1392,6 +1392,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Console mode: "classic" for the standard terminal, "tui" for a pseudo-graphical full-screen interface.. + /// + internal static string Console_General_ConsoleMode { + get { + return ResourceManager.GetString("Console.General.ConsoleMode", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it.. /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 6735295a..06421374 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -1,4 +1,4 @@ - +