using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Brigadier.NET; using FuzzySharp; using MinecraftClient.CommandHandler; using MinecraftClient.Scripting; using MinecraftClient.Tui; using static MinecraftClient.Settings; 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 /// 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() { Backend?.ClearInputBuffer(); } /// /// Set an auto-completion engine for TAB autocompletion. /// /// Engine implementing the IAutoComplete interface public static void SetAutoCompleteEngine(IAutoComplete engine) { autocomplete_engine = engine; } /// /// 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; /// /// Determines whether not to print color codes in BasicIO mode. /// public static bool BasicIO_NoColor = false; /// /// Determine whether WriteLineFormatted() should prepend lines with timestamps by default. /// public static bool EnableTimestamps = false; /// /// Specify a generic log line prefix for WriteLogLine() /// public static string LogPrefix = "§8[MCC] "; /// /// Read a password from the standard input /// public static string? ReadPassword() { if (BasicIO) return Console.ReadLine(); return Backend.ReadPassword(); } /// /// Read a line from the standard input /// public static string ReadLine() { if (BasicIO) return Console.ReadLine() ?? String.Empty; return Backend.RequestImmediateInput(); } /// /// Debug routine: print all keys pressed in the console /// public static void DebugReadInput() { ConsoleKeyInfo k; while (true) { k = Console.ReadKey(true); Console.WriteLine("Key: {0}\tChar: {1}\tModifiers: {2}", k.Key, k.KeyChar, k.Modifiers); } } /// /// Write a string to the standard output with a trailing newline /// public static void WriteLine(string line) { if (BasicIO || Backend is null) Console.WriteLine(line); else Backend.WriteLine(line); } /// /// Write a Minecraft-Like formatted string to the standard output, using §c color codes /// for more info /// /// String to write /// If false, space are printed instead of newlines /// /// If false, no timestamp is prepended. /// If true, "hh-mm-ss" timestamp will be prepended. /// If unspecified, value is retrieved from EnableTimestamps. /// public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null) { StringBuilder output = new(); if (!String.IsNullOrEmpty(str)) { displayTimestamp ??= EnableTimestamps; if (displayTimestamp.Value) { int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second; output.Append(String.Format("{0}:{1}:{2} ", hour.ToString("00"), minute.ToString("00"), second.ToString("00"))); } if (!acceptnewlines) { str = str.Replace('\n', ' '); } if (BasicIO || Backend is null) { if (BasicIO_NoColor) { output.Append(ChatBot.GetVerbatim(str)); } else { output.Append(str); } Console.WriteLine(output.ToString()); return; } output.Append(str); Backend.WriteLineFormatted(output.ToString()); } } /// /// Write a prefixed log line. Prefix is set in LogPrefix. /// /// Text of the log line /// Allow line breaks public static void WriteLogLine(string text, bool acceptnewlines = true) { if (!acceptnewlines) text = text.Replace('\n', ' '); WriteLineFormatted(LogPrefix + text, acceptnewlines); } #region Subfunctions /// /// Clear all text inside the input prompt /// private static void ClearLineAndBuffer() { if (BasicIO) return; Backend.ClearInputBuffer(); } #endregion internal static bool AutoCompleteDone = false; internal static string[] AutoCompleteResult = Array.Empty(); private static HashSet Commands = new(); private static string[] CommandsFromAutoComplete = Array.Empty(); private static string[] CommandsFromDeclareCommands = Array.Empty(); private static Task _latestTask = Task.CompletedTask; private static CancellationTokenSource? _cancellationTokenSource; 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)) { DoClearSuggestions(); return; } var InternalCmdChar = Config.Main.Advanced.InternalCmdChar; if (InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none || fullCommand[0] == InternalCmdChar.ToChar()) { int offset = InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none ? 0 : 1; if (buffer.CursorPosition - offset < 0) { DoClearSuggestions(); return; } _cancellationTokenSource?.Cancel(); var cts = new CancellationTokenSource(); _cancellationTokenSource = cts; Task newTask = UpdateSuggestionsAsync(fullCommand, offset, buffer.CursorPosition, cts.Token); _latestTask = newTask; _ = ObserveAutocompleteTaskAsync(newTask, cts); } else { DoClearSuggestions(); return; } } private static async Task UpdateSuggestionsAsync(string fullCommand, int offset, int cursorPosition, CancellationToken cancellationToken) { string command = fullCommand[offset..]; if (command.Length == 0) { List suggestionList = new() { new("/") }; var childs = McClient.dispatcher.GetRoot().Children; if (childs is not null) { foreach (var child in childs) suggestionList.Add(new(child.Name)); } foreach (var cmd in Commands) suggestionList.Add(new(cmd)); if (cancellationToken.IsCancellationRequested) return; SendSuggestions(suggestionList.ToArray(), new(offset, offset)); return; } if (command[0] == '/' && !command.Contains(' ')) { var sorted = Process.ExtractSorted(command[1..], Commands); var suggestionList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sorted.Count()]; int index = 0; foreach (var suggestion in sorted) suggestionList[index++] = new(suggestion.Value); if (cancellationToken.IsCancellationRequested) return; SendSuggestions(suggestionList, new(offset, offset + command.Length)); return; } CommandDispatcher? dispatcher = McClient.dispatcher; if (dispatcher is null) return; ParseResults parse = dispatcher.Parse(command, CmdResult.Empty); Brigadier.NET.Suggestion.Suggestions suggestions = await dispatcher.GetCompletionSuggestions(parse, cursorPosition - offset); if (cancellationToken.IsCancellationRequested) return; int suggestionCount = suggestions.List.Count; if (suggestionCount == 0) { DoClearSuggestions(); return; } Dictionary tooltips = new(); foreach (var suggestion in suggestions.List) tooltips.Add(suggestion.Text, suggestion.Tooltip?.String); Tuple range = new(suggestions.Range.Start + offset, suggestions.Range.End + offset); var sortedSuggestions = Process.ExtractSorted(fullCommand[range.Item1..range.Item2], tooltips.Keys); if (cancellationToken.IsCancellationRequested) return; var suggestionListWithTooltips = new ConsoleInteractive.ConsoleSuggestion.Suggestion[suggestionCount]; int suggestionIndex = 0; foreach (var suggestion in sortedSuggestions) suggestionListWithTooltips[suggestionIndex++] = new(suggestion.Value, tooltips[suggestion.Value] ?? string.Empty); SendSuggestions(suggestionListWithTooltips, range); } private static async Task ObserveAutocompleteTaskAsync(Task task, CancellationTokenSource cancellationTokenSource) { try { await task; } catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested) { } catch (Exception e) { if (Settings.Config.Logging.DebugMessages) WriteLogLine(e.ToString(), acceptnewlines: true); DoClearSuggestions(); } finally { if (ReferenceEquals(_cancellationTokenSource, cancellationTokenSource)) _cancellationTokenSource = null; cancellationTokenSource.Dispose(); } } 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; DoClearSuggestions(); AutoCompleteDone = false; AutoCompleteResult = Array.Empty(); CommandsFromAutoComplete = Array.Empty(); CommandsFromDeclareCommands = Array.Empty(); } private static void MergeCommands() { Commands.Clear(); foreach (string cmd in CommandsFromAutoComplete) Commands.Add('/' + cmd); foreach (string cmd in CommandsFromDeclareCommands) Commands.Add('/' + cmd); } public static void OnAutoCompleteDone(int transactionId, string[] result) { AutoCompleteResult = result; if (transactionId == 0) { CommandsFromAutoComplete = result; MergeCommands(); } AutoCompleteDone = true; } public static void OnDeclareMinecraftCommand(string[] rootCommands) { CommandsFromDeclareCommands = rootCommands; MergeCommands(); } public static void InitCommandList(CommandDispatcher dispatcher) { autocomplete_engine!.AutoComplete("/"); } } /// /// Interface for TAB autocompletion /// Allows to use any object which has an AutoComplete() method using the IAutocomplete interface /// public interface IAutoComplete { /// /// Provide a list of auto-complete strings based on the provided input behing the cursor /// /// Text behind the cursor, e.g. "my input comm" /// List of auto-complete words, e.g. ["command", "comment"] int AutoComplete(string BehindCursor); } }