From b33ee7e4a1ed9e1441e06bd4c67cc82618c8cb47 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Thu, 26 Mar 2026 23:20:10 +0800 Subject: [PATCH 01/76] Refactor exit handling and command input in TUI - Improved the exit process by ensuring the backend shutdown is called after handling offline prompts. - Enhanced command input history management by encapsulating text setting logic in a dedicated method to prevent event handler interference. - Adjusted the background color in the color parser for better visibility. - Increased the sleep duration in the TUI exit guard thread to ensure a smoother shutdown process. --- MinecraftClient/Program.cs | 123 +++++++++++------------ MinecraftClient/Tui/MainTuiView.cs | 19 +++- MinecraftClient/Tui/McColorParser.cs | 2 + MinecraftClient/Tui/TuiConsoleBackend.cs | 2 +- 4 files changed, 79 insertions(+), 67 deletions(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 46b4e19e..4bcbf5be 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -774,16 +774,20 @@ namespace MinecraftClient public static void DoExit(int exitcode = 0) { WriteBackSettings(); - 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) { ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler; - offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); + offlinePrompt.Item2.Cancel(); + if (Thread.CurrentThread != offlinePrompt.Item1) + offlinePrompt.Item1.Join(1000); + offlinePrompt = null; + ConsoleIO.Reset(); } if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); } + ConsoleIO.Backend?.Shutdown(); Environment.Exit(exitcode); } @@ -804,15 +808,18 @@ namespace MinecraftClient /// If set, the error message will be processed by the AutoRelog bot public static void HandleFailure(string? errorMessage = null, bool versionError = false, ChatBot.DisconnectReason? disconnectReason = null) { - if (!String.IsNullOrEmpty(errorMessage)) + if (!string.IsNullOrEmpty(errorMessage)) { ConsoleIO.Reset(); - try + if (ConsoleIO.Backend is not Tui.TuiConsoleBackend) { - while (Console.KeyAvailable) - Console.ReadKey(true); + try + { + while (Console.KeyAvailable) + Console.ReadKey(true); + } + catch { } } - catch { } ConsoleIO.WriteLine(errorMessage); if (disconnectReason.HasValue) @@ -864,65 +871,57 @@ namespace MinecraftClient if (exitThread) return; - while (command.Length > 0) + command = ConsoleIO.ReadLine().Trim(); + + if (command.Length == 0) { - if (cancellationTokenSource.IsCancellationRequested) - return; - - command = ConsoleIO.ReadLine().Trim(); - if (command.Length > 0) - { - string message = ""; - - if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' ' - && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) - command = command[1..]; - - if (command.StartsWith("reco")) - { - message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command)); - if (message == "") - { - exitThread = true; - break; - } - } - else if (command.StartsWith("connect")) - { - message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command)); - if (message == "") - { - exitThread = true; - break; - } - } - else if (command.StartsWith("exit") || command.StartsWith("quit")) - { - message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command)); - } - else if (command.StartsWith("help")) - { - ConsoleIO.WriteLineFormatted("§8MCC: " + - Config.Main.Advanced.InternalCmdChar.ToLogString() + - new Commands.Reco().GetCmdDescTranslated()); - ConsoleIO.WriteLineFormatted("§8MCC: " + - Config.Main.Advanced.InternalCmdChar.ToLogString() + - new Commands.Connect().GetCmdDescTranslated()); - } - else - ConsoleIO.WriteLineFormatted(string.Format(Translations.icmd_unknown, command.Split(' ')[0])); - - if (message != "") - ConsoleIO.WriteLineFormatted("§8MCC: " + message); - } - else - { + if (ConsoleIO.Backend is not Tui.TuiConsoleBackend) Commands.Exit.DoExit(Config.AppVar.ExpandVars(command)); - } + continue; } - if (exitThread) - return; + string message = ""; + + if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' ' + && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + command = command[1..]; + + if (command.StartsWith("reco")) + { + message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command)); + if (message == "") + { + exitThread = true; + continue; + } + } + else if (command.StartsWith("connect")) + { + message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command)); + if (message == "") + { + exitThread = true; + continue; + } + } + else if (command.StartsWith("exit") || command.StartsWith("quit")) + { + message = Commands.Exit.DoExit(Config.AppVar.ExpandVars(command)); + } + else if (command.StartsWith("help")) + { + ConsoleIO.WriteLineFormatted("§8MCC: " + + Config.Main.Advanced.InternalCmdChar.ToLogString() + + new Commands.Reco().GetCmdDescTranslated()); + ConsoleIO.WriteLineFormatted("§8MCC: " + + Config.Main.Advanced.InternalCmdChar.ToLogString() + + new Commands.Connect().GetCmdDescTranslated()); + } + else + ConsoleIO.WriteLineFormatted(string.Format(Translations.icmd_unknown, command.Split(' ')[0])); + + if (message != "") + ConsoleIO.WriteLineFormatted("§8MCC: " + message); } })), cancellationTokenSource); offlinePrompt.Item1.Start(); diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 02f2d142..bfd79e04 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -494,10 +494,21 @@ namespace MinecraftClient.Tui } string historyText = _commandHistory[_historyIndex]; - _commandInput.Text = historyText; - _commandInput.CaretIndex = historyText.Length; - Dispatcher.UIThread.Post(() => _commandInput.CaretIndex = historyText.Length, - DispatcherPriority.Input); + SetCommandText(historyText); + } + + private void SetCommandText(string text) + { + _commandInput.TextChanged -= OnCommandTextChanged; + try + { + _commandInput.Text = text; + _commandInput.CaretIndex = text.Length; + } + finally + { + _commandInput.TextChanged += OnCommandTextChanged; + } } #endregion diff --git a/MinecraftClient/Tui/McColorParser.cs b/MinecraftClient/Tui/McColorParser.cs index 70751b6e..c46b0adf 100644 --- a/MinecraftClient/Tui/McColorParser.cs +++ b/MinecraftClient/Tui/McColorParser.cs @@ -47,6 +47,8 @@ namespace MinecraftClient.Tui return tb; } + tb.Background = Brushes.Black; + IBrush currentColor = Brushes.White; bool bold = false; bool italic = false; diff --git a/MinecraftClient/Tui/TuiConsoleBackend.cs b/MinecraftClient/Tui/TuiConsoleBackend.cs index 95cc4606..64d5af86 100644 --- a/MinecraftClient/Tui/TuiConsoleBackend.cs +++ b/MinecraftClient/Tui/TuiConsoleBackend.cs @@ -256,7 +256,7 @@ namespace MinecraftClient.Tui new Thread(() => { - Thread.Sleep(500); + Thread.Sleep(1000); Environment.Exit(0); }) { Name = "TUI-Exit-Guard", IsBackground = true }.Start(); } From fe7ab9f373aed23dc24aad5e91c54c19e4d460a1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 27 Mar 2026 00:29:01 +0800 Subject: [PATCH 02/76] Implement startup state management and enhance configuration loading - Introduced a new `StartupState` class to encapsulate the state collected before the console backend initialization. - Updated the configuration loading process to handle legacy upgrades and provide detailed feedback on configuration status. - Enhanced the TUI backend to utilize the new startup state for improved initialization flow. - Refactored the `LoadFromFile` method in `Settings` to return a structured result, improving error handling and clarity. --- MinecraftClient/Program.cs | 290 ++++++++++++++--------- MinecraftClient/Settings.cs | 39 ++- MinecraftClient/Tui/TuiConsoleBackend.cs | 19 +- 3 files changed, 219 insertions(+), 129 deletions(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 4bcbf5be..759e69ed 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -58,6 +58,17 @@ namespace MinecraftClient // Setting this string to an empty string will disable Sentry private const string SentryDSN = ""; + /// + /// Snapshot of all state collected before the console backend is initialized. + /// Passed to once the backend is ready. + /// + internal sealed class StartupState + { + public Settings.ConfigLoadResult ConfigResult { get; init; } + public bool NewlyGenerated { get; init; } + public bool SentryEnabled { get; init; } + } + /// /// The main entry point of Minecraft Console Client /// @@ -103,7 +114,6 @@ namespace MinecraftClient Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); }); - //Setup ConsoleIO ConsoleIO.LogPrefix = "§8[MCC] "; if (args.Length >= 1 && args[^1] == "BasicIO" || args.Length >= 1 && args[^1] == "BasicIO-NoColor") { @@ -115,133 +125,178 @@ namespace MinecraftClient args = args.Where(o => !Object.ReferenceEquals(o, args[^1])).ToArray(); } + //Debug input ? + if (args.Length == 1 && args[0] == "--keyboard-debug") + { + if (!ConsoleIO.BasicIO) + { + ConsoleIO.Backend = new ClassicConsoleBackend(); + ConsoleIO.Backend.Init(); + } + ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); + ConsoleIO.DebugReadInput(); + } + + // --- Load config as early as possible (no printing yet) --- + Settings.ConfigLoadResult configResult; + bool newlyGenerated = false; + + if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") + { + configResult = Settings.LoadFromFile(args[0]); + settingsIniPath = args[0]; + + List args_tmp = args.ToList(); + args_tmp.RemoveAt(0); + args = args_tmp.ToArray(); + } + else if (File.Exists("MinecraftClient.ini")) + { + configResult = Settings.LoadFromFile("MinecraftClient.ini"); + } + else + { + configResult = new Settings.ConfigLoadResult { Success = true, NeedWriteDefault = true }; + newlyGenerated = true; + } + + if (configResult.NeedWriteDefault) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + WriteBackSettings(false); + } + else if (configResult.Success) + { + WriteBackSettings(true); + } + + if (!Config.Main.Advanced.EnableSentry) + _sentrySdk?.Dispose(); + + var startupState = new StartupState + { + ConfigResult = configResult, + NewlyGenerated = newlyGenerated, + SentryEnabled = SentryDSN != string.Empty, + }; + + // --- Determine console mode and initialize backend --- + if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) + { + ConsoleIO.Backend?.Shutdown(); + var tuiBackend = new Tui.TuiConsoleBackend(); + ConsoleIO.Backend = tuiBackend; + tuiBackend.RunTuiMainLoop(args, startupState); + return; + } + + // Classic mode: init backend, then print and process startup state. if (!ConsoleIO.BasicIO) { ConsoleIO.Backend = new ClassicConsoleBackend(); ConsoleIO.Backend.Init(); } - ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - - //Build information to facilitate processing of bug reports - if (BuildInfo is not null) - ConsoleIO.WriteLineFormatted("§8" + BuildInfo); - - //Debug input ? - if (args.Length == 1 && args[0] == "--keyboard-debug") - { - ConsoleIO.WriteLine("Keyboard debug mode: Press any key to display info"); - ConsoleIO.DebugReadInput(); - } - - //Process ini configuration file - { - bool loadSucceed, needWriteDefaultSetting, newlyGenerated = false; - if (args.Length >= 1 && File.Exists(args[0]) && Settings.ToLowerIfNeed(Path.GetExtension(args[0])) == ".ini") - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile(args[0]); - settingsIniPath = args[0]; - - //remove ini configuration file from arguments array - List args_tmp = args.ToList(); - args_tmp.RemoveAt(0); - args = args_tmp.ToArray(); - } - else if (File.Exists("MinecraftClient.ini")) - { - (loadSucceed, needWriteDefaultSetting) = Settings.LoadFromFile("MinecraftClient.ini"); - } - else - { - loadSucceed = true; - needWriteDefaultSetting = true; - newlyGenerated = true; - } - - if (needWriteDefaultSetting) - { - Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(false); - if (newlyGenerated) - ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); - ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); - - // 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) - { - ConsoleIO.WriteLine(Translations.mcc_sentry_logging); - } - } - else if (!loadSucceed) - { - 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())); - 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() != ' ' - && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) - command = command[1..]; - - if (command.StartsWith("exit") || command.StartsWith("quit")) - { - return; - } - else if (command.StartsWith("new")) - { - Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(true); - ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath)); - return; - } - } - else - { - return; - } - } - return; - } - else - { - //Load external translation file. Should be called AFTER settings loaded - if (!Config.Main.Advanced.Language.StartsWith("en")) - 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); + if (!ProcessStartupState(startupState)) return; - } - ContinueAfterTuiInit(args); + RunStartupSequence(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). + /// Prints the application banner and processes the startup state collected before + /// the console backend was ready. Called once from classic mode or from TUI after + /// the view is initialized. /// - internal static void ContinueAfterTuiInit(string[] args) + /// True if startup can continue; false if config load failed and user chose to exit. + internal static bool ProcessStartupState(StartupState state) + { + ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); + if (BuildInfo is not null) + ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + + var cfg = state.ConfigResult; + + if (cfg.NeedWriteDefault) + { + if (cfg.IsLegacyUpgrade) + { + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); + ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_backup_old_config, cfg.LegacyBackupPath)); + } + + if (state.NewlyGenerated) + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_settings_generated); + + ConsoleIO.WriteLine(Translations.mcc_run_with_default_settings); + + if (state.SentryEnabled) + ConsoleIO.WriteLine(Translations.mcc_sentry_logging); + } + else if (!cfg.Success) + { + ConsoleIO.WriteLineFormatted("§c" + Translations.config_load_fail); + if (cfg.ErrorMessage is not null) + ConsoleIO.WriteLine(cfg.ErrorMessage); + HandleConfigLoadFailure(); + return false; + } + else + { + if (!Config.Main.Advanced.Language.StartsWith("en")) + ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); + } + + return true; + } + + /// + /// Handles a failed config load by prompting the user to fix or regenerate the config file. + /// + internal static void HandleConfigLoadFailure() + { + 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())); + 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() != ' ' + && command[0] == Config.Main.Advanced.InternalCmdChar.ToChar()) + command = command[1..]; + + if (command.StartsWith("exit") || command.StartsWith("quit")) + { + return; + } + else if (command.StartsWith("new")) + { + Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); + WriteBackSettings(true); + ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_gen_new_config, settingsIniPath)); + return; + } + } + else + { + return; + } + } + } + + /// + /// Runs the main startup sequence: CLI argument processing, auth, and connection. + /// Called from Main() for classic/basic mode, or from TuiConsoleBackend on a + /// background thread after the Avalonia UI loop has started. + /// + internal static void RunStartupSequence(string[] args) { //Other command-line arguments if (args.Length >= 1) @@ -732,7 +787,8 @@ namespace MinecraftClient /// public static void ReloadSettings(bool keepAccountAndServerSettings = false) { - if (Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings).Item1) + var result = Settings.LoadFromFile(settingsIniPath, keepAccountAndServerSettings); + if (result.Success) ConsoleIO.WriteLine(string.Format(Translations.config_load, settingsIniPath)); } diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 79e314d0..a3368c8e 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -136,7 +136,22 @@ namespace MinecraftClient } - public static Tuple LoadFromFile(string filepath, bool keepAccountAndServerSettings = false) + /// + /// Structured result returned by . + /// + public readonly struct ConfigLoadResult + { + public bool Success { get; init; } + public bool NeedWriteDefault { get; init; } + /// True when a pre-TOML legacy config was detected, backed up, and a fresh default is needed. + public bool IsLegacyUpgrade { get; init; } + /// Non-null when the load failed due to a parse/IO error (not a legacy upgrade). + public string? ErrorMessage { get; init; } + /// Path where the old config was backed up (legacy upgrade case). + public string? LegacyBackupPath { get; init; } + } + + public static ConfigLoadResult LoadFromFile(string filepath, bool keepAccountAndServerSettings = false) { bool keepAccountSettings = InternalConfig.KeepAccountSettings; bool keepServerSettings = InternalConfig.KeepServerSettings; @@ -157,21 +172,27 @@ namespace MinecraftClient Thread.CurrentThread.CurrentCulture = Program.ActualCulture; try { - // The old configuration file has been backed up as A. string configString = File.ReadAllText(filepath); if (configString.Contains("Some settings missing here after an upgrade?")) { string newFilePath = Path.ChangeExtension(filepath, ".old.ini"); File.Copy(filepath, newFilePath, true); - ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); - ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.mcc_backup_old_config, newFilePath)); - return new(false, true); + return new ConfigLoadResult + { + Success = false, + NeedWriteDefault = true, + IsLegacyUpgrade = true, + LegacyBackupPath = newFilePath + }; } } catch { } - ConsoleIO.WriteLineFormatted("§c" + Translations.config_load_fail); - ConsoleIO.WriteLine(ex.GetFullMessage()); - return new(false, false); + return new ConfigLoadResult + { + Success = false, + NeedWriteDefault = false, + ErrorMessage = ex.GetFullMessage() + }; } finally { @@ -180,7 +201,7 @@ namespace MinecraftClient if (!keepServerSettings) InternalConfig.KeepServerSettings = false; } - return new(true, false); + return new ConfigLoadResult { Success = true, NeedWriteDefault = false }; } public static void WriteToFile(string filepath, bool backupOldFile) diff --git a/MinecraftClient/Tui/TuiConsoleBackend.cs b/MinecraftClient/Tui/TuiConsoleBackend.cs index 64d5af86..bee59751 100644 --- a/MinecraftClient/Tui/TuiConsoleBackend.cs +++ b/MinecraftClient/Tui/TuiConsoleBackend.cs @@ -25,14 +25,18 @@ namespace MinecraftClient.Tui internal static TuiConsoleBackend? Instance { get; private set; } + private Program.StartupState? _pendingStartupState; + private readonly ManualResetEventSlim _viewReady = new(false); + /// /// Initializes the Avalonia app and starts the main UI loop. /// This blocks the calling thread until the TUI exits. /// Before blocking, it starts MCC's remaining initialization on a background thread. /// - public void RunTuiMainLoop(string[] args) + internal void RunTuiMainLoop(string[] args, Program.StartupState startupState) { Instance = this; + _pendingStartupState = startupState; AppDomain.CurrentDomain.ProcessExit += (_, _) => RestoreTerminalState(); @@ -46,7 +50,7 @@ namespace MinecraftClient.Tui new Thread(() => { - Thread.Sleep(500); + _viewReady.Wait(); ContinueMccStartup(args); }) { Name = "MCC-Main", IsBackground = true }.Start(); @@ -113,7 +117,15 @@ namespace MinecraftClient.Tui { try { - Program.ContinueAfterTuiInit(args); + var instance = Instance; + if (instance?._pendingStartupState is { } state) + { + instance._pendingStartupState = null; + if (!Program.ProcessStartupState(state)) + return; + } + + Program.RunStartupSequence(args); } catch (Exception ex) { @@ -124,6 +136,7 @@ namespace MinecraftClient.Tui internal void SetView(MainTuiView view) { _view = view; + _viewReady.Set(); } internal MainTuiView? GetView() => _view; From ac1d5c37a606fcf12c71f356a4e550487af0217f Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 27 Mar 2026 00:29:25 +0800 Subject: [PATCH 03/76] Refactor tab cycling logic in TUI command input handling - Simplified the condition for disabling tab cycling by checking only for the Tab key. - Improved clarity in the command key down event handling for better user experience. --- MinecraftClient/Tui/MainTuiView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index bfd79e04..397f1475 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -272,7 +272,7 @@ namespace MinecraftClient.Tui private void OnCommandKeyDown(object? sender, KeyEventArgs e) { - if (_tabCycling && e.Key is not (Key.Tab or Key.Up or Key.Down or Key.Escape)) + if (_tabCycling && e.Key is not Key.Tab) _tabCycling = false; bool ctrl = (e.KeyModifiers & KeyModifiers.Control) != 0; From cac9e5c625db64fa6cd52bfff973e1cf4aa194ed Mon Sep 17 00:00:00 2001 From: breadbyte <14045257+breadbyte@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:08:05 +0800 Subject: [PATCH 04/76] skipci update ConsoleInteractive library --- ConsoleInteractive | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ConsoleInteractive b/ConsoleInteractive index f6306528..a9afc0df 160000 --- a/ConsoleInteractive +++ b/ConsoleInteractive @@ -1 +1 @@ -Subproject commit f63065282a4bd64758e7e8d30f232e3dc8ce2622 +Subproject commit a9afc0df4ce79450b76acedffa1b549449cf69cb From a7a356675645db4cdb20d60c7bf2484c3886676d Mon Sep 17 00:00:00 2001 From: breadbyte <14045257+breadbyte@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:05:20 +0800 Subject: [PATCH 05/76] Update build-and-release.yml Fixes long commit message >256 chars breaking releases Cleanup build code and consolidate variables Make skip ci more user-friendly by including all variations of skip-ci and ci-skip Add PublishSingleFile flag --- .github/workflows/build-and-release.yml | 76 +++++++++++++++---------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index dba44918..e0baaa91 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -9,19 +9,25 @@ on: env: PROJECT: "MinecraftClient" target-version: "net10.0" - compile-flags: "--self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded" + dotnet-version: "10.0.x" + compile-flags: "--self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true" jobs: determine-build: runs-on: ubuntu-slim - if: >- - ${{ - !contains(github.event.head_commit.message, 'skipci') && - !contains(github.event.pull_request.title, 'skipci') - }} + outputs: + skip: ${{ steps.check-skip.outputs.skip }} steps: - - name: dummy action - run: "echo 'dummy action that checks if the build is to be skipped, if it is, this action does not run to break the entire build action'" + - name: Check skip CI + id: check-skip + run: | + MSG="${{ github.event.head_commit.message }}" + LOWER=$(echo "$MSG" | tr '[:upper:]' '[:lower:]') + if echo "$LOWER" | grep -qE 'skip.?ci|ci.?skip'; then + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi fetch-translations: strategy: @@ -29,7 +35,7 @@ jobs: runs-on: ubuntu-latest needs: determine-build # Translations will only be fetched in the MCCTeam repository, since it needs crowdin secrets. - if: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }} + if: ${{ needs.determine-build.outputs.skip != 'true' && github.repository == 'MCCTeam/Minecraft-Console-Client' }} timeout-minutes: 15 steps: @@ -77,8 +83,8 @@ jobs: create-tag: runs-on: ubuntu-slim timeout-minutes: 5 # Wait 5 minutes in case of network issues/etc - needs: [determine-build] - if: ${{ needs.determine-build.result == 'success' }} + needs: determine-build + if: ${{ needs.determine-build.outputs.skip != 'true' }} steps: - id: make-tag run: | @@ -111,9 +117,9 @@ jobs: build: runs-on: ubuntu-latest # Check if we're not skipping build, tag is created, and translations successfully fetched (or skipped) - if: ${{ needs.determine-build.result == 'success' && - needs.create-tag.result == 'success' && - (needs.fetch-translations.result == 'success' || needs.fetch-translations.result == 'skipped') + if: ${{ needs.determine-build.outputs.skip != 'true' && + needs.create-tag.result == 'success' && + (needs.fetch-translations.result == 'success' || needs.fetch-translations.result == 'skipped') }} needs: [determine-build, fetch-translations, create-tag] timeout-minutes: 15 @@ -130,7 +136,6 @@ jobs: - name: Get Current Date run: | - echo date=$(date +'%Y%m%d') >> $GITHUB_ENV echo date_dashed=$(date -u +'%Y-%m-%d') >> $GITHUB_ENV - name: Restore Translations (if available) @@ -140,33 +145,34 @@ jobs: key: "translation-${{ github.sha }}" restore-keys: "translation-" - - name: Setup Environment Variables (early) - run: | - echo project-path=${{ github.workspace }}/${{ env.PROJECT }} >> $GITHUB_ENV - echo file-ext=${{ (startsWith(matrix.target, 'win') && '.exe') || '' }} >> $GITHUB_ENV - - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + dotnet-version: ${{ env.dotnet-version }} - name: Setup Environment Variables run: | - echo target-out-path=${{ env.project-path }}/bin/Release/${{ env.target-version }}/${{ matrix.target }}/publish/ >> $GITHUB_ENV - echo assembly-info=${{ env.project-path }}/Properties/AssemblyInfo.cs >> $GITHUB_ENV - echo build-version-info=${{ needs.create-tag.outputs.build-tag }} >> $GITHUB_ENV - echo commit=$(echo ${{ github.sha }} | cut -c 1-7) >> $GITHUB_ENV + PROJECT_PATH=${{ github.workspace }}/${{ env.PROJECT }} + FILE_EXT=${{ (startsWith(matrix.target, 'win') && '.exe') || '' }} + TARGET_OUT_PATH=$PROJECT_PATH/bin/Release/${{ env.target-version }}/${{ matrix.target }}/publish/ + + echo "project-path=$PROJECT_PATH" >> $GITHUB_ENV + echo "file-ext=$FILE_EXT" >> $GITHUB_ENV + echo "target-out-path=$TARGET_OUT_PATH" >> $GITHUB_ENV + echo "assembly-info=$PROJECT_PATH/Properties/AssemblyInfo.cs" >> $GITHUB_ENV + echo "build-version-info=${{ needs.create-tag.outputs.build-tag }}" >> $GITHUB_ENV + echo "commit=$(echo ${{ github.sha }} | cut -c 1-7)" >> $GITHUB_ENV - - name: Setup Environment Variables (late) + - name: Setup Binaries Path run: | echo built-executable-path=${{ env.target-out-path }}${{ env.PROJECT }}${{ env.file-ext }} >> $GITHUB_ENV - - name: Set Version Info and Sentry Project (if applicable) + - name: Set Version Info run: | echo '' >> ${{ env.assembly-info }} - echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env.date_dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }} + echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env._dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }} - - name: Inject Sentry DSN + - name: Inject Sentry DSN (if applicable) if: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }} run: | grep -q 'SentryDSN = "";' ${{ env.project-path }}/Program.cs || { echo "SentryDSN pattern not found in Program.cs"; exit 1; } @@ -198,6 +204,16 @@ jobs: with: path: artifacts/ merge-multiple: true + + - name: Truncate commit message for release name + id: release-name + run: | + RAW="${{ github.event.head_commit.message }}" + # Take only the first line (subject), then truncate to safe length + SUBJECT=$(echo "$RAW" | head -n 1) + MAX=220 # leave room for tag prefix + ": " + TRUNCATED="${SUBJECT:0:$MAX}" + echo "name=${{ needs.create-tag.outputs.build-tag }}: $TRUNCATED" >> $GITHUB_OUTPUT - name: Create Release uses: ncipollo/release-action@v1.14.0 @@ -205,7 +221,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} artifacts: "artifacts/**/*" tag: ${{ needs.create-tag.outputs.build-tag }} - name: '${{ needs.create-tag.outputs.build-tag }}: ${{ github.event.head_commit.message }}' + name: ${{ steps.release-name.outputs.name }} generateReleaseNotes: true artifactErrorsFailBuild: true allowUpdates: true From 20f536188ae4758d14a09f5d9912058e9d8b3ad3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:36:21 +0000 Subject: [PATCH 06/76] Initial plan From 4993498d52257f5406494d50d81bb15da5e6fcd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:45:45 +0000 Subject: [PATCH 07/76] docs: remove empty warning, rename Tips to Notes, add MCC.js recommendation Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/8f04f3c4-cedd-427c-ad3c-c2488363fe6f Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/guide/chat-bots.md | 58 +++++++++++++++--------------- docs/guide/configuration.md | 28 +++++++-------- docs/guide/creating-bots.md | 8 ++--- docs/guide/installation.md | 62 ++++++++++++++++---------------- docs/guide/usage.md | 66 +++++++++++++++++----------------- docs/guide/websocket/README.md | 8 +++++ 6 files changed, 117 insertions(+), 113 deletions(-) diff --git a/docs/guide/chat-bots.md b/docs/guide/chat-bots.md index dc747a86..512f1664 100644 --- a/docs/guide/chat-bots.md +++ b/docs/guide/chat-bots.md @@ -15,7 +15,7 @@ redirectFrom: **Minecraft Console Client** has a number of default built in Chat Bots (Scripts/Plugins) which allow for various types of automation. -

Tip

+

Note

**Settings refer to settings in the [configuration file](configuration.md)** @@ -80,7 +80,7 @@ redirectFrom: #### `Beep_Enabled` -

Tip

+

Note

**This might not work depending on your system or a console (terminal emulator).** @@ -243,7 +243,7 @@ redirectFrom: #### `Use_Terrain_Handling` -

Tip

+

Note

**You need to enable [Terrain Handling](configuration.md#terrainandmovements) in the settings and it's recommended to put the bot into an enclosure not to wander off. (Recommended size 5x5x5)** @@ -273,7 +273,7 @@ redirectFrom: #### `Walk_Retries` -

Tip

+

Note

**This happens on each trigger of the task, so it does not permanently switch to alternative method.** @@ -289,7 +289,7 @@ redirectFrom: ## Auto Attack -

Tip

+

Note

**You need to have [inventoryhandling](configuration.md#inventoryhandling) and [entityhandling](configuration.md#entityhandling) enabled in order for this bot to work.** @@ -445,7 +445,7 @@ redirectFrom: ## Auto Craft -

Tip

+

Note

**You need to have [inventoryhandling](configuration.md#inventoryhandling) enabled in order for basic crafting in the inventory to work, in addition if you want to use a crafting table, you need to enable [terrainandmovements](configuration.md#terrainandmovements) in order for bot to be able to reach the crafting table.** @@ -530,7 +530,7 @@ redirectFrom: ### Defining a recipe -

Tip

+

Note

**If you're using `table` you need to set the `CraftingTable` setting.** @@ -630,13 +630,13 @@ redirectFrom: Automatically digs block on specified locations. -

Tip

+

Note

**You need to have [inventoryhandling](configuration.md#inventoryhandling) and [terrainandmovements](configuration.md#terrainandmovements) enabled in order for this bot to work.**
-

Tip

+

Note

**Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead.** @@ -806,7 +806,7 @@ redirectFrom: Automatically drop items you don't need from the inventory. -

Tip

+

Note

**You need to have [inventoryhandling](configuration.md#inventoryhandling) enabled in order for this bot to work** @@ -859,7 +859,7 @@ redirectFrom: #### `Items` -

Tip

+

Note

**All item types can be found [here](https://mccteam.github.io/r/item/#L12).** @@ -885,7 +885,7 @@ redirectFrom: Automatically eat food when your Hunger value is low. -

Tip

+

Note

**You need to have [inventoryhandling](configuration.md#inventoryhandling) enabled in order for this bot to work** @@ -928,19 +928,19 @@ redirectFrom: Automatically catch fish using a fishing rod. -

Tip

+

Note

**You need to have [entityhandling](configuration.md#entityhandling) enabled in order for this bot to work.**
-

Tip

+

Note

**To use the automatic rod switching and durability check feature, you need to enable [inventoryhandling](configuration.md#inventoryhandling).**
-

Tip

+

Note

**Note: To adjust the position or angle after catching a fish, you need to enable [terrainandmovements](configuration.md#terrainandmovements).** @@ -1227,7 +1227,7 @@ redirectFrom: #### `Retries` -

Tip

+

Note

**This might get you banned by the server owners.** @@ -1304,7 +1304,7 @@ redirectFrom: #### `Matches_File` -

Tip

+

Note

**This file is not created by default, we recommend making a clone of the [`sample-matches.ini`](https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/config/sample-matches.ini) and changing it according to your needs.** @@ -1330,7 +1330,7 @@ redirectFrom: #### `Match_Colors` -

Tip

+

Note

**This feature uses the `§` symbol for color matching** @@ -1847,7 +1847,7 @@ redirectFrom: ## Farmer -

Tip

+

Note

**You need to have [Terrain And Movements](configuration.md#terrainandmovements) and [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this bot to work.** @@ -1965,13 +1965,13 @@ redirectFrom: This bot enables you to make a bot follow a specific player. -

Tip

+

Note

**The bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you, it's similar to making animals follow you when you're holding food in your hand. This is due to a slow pathfinding algorithm, we're working on getting a better one. You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, this might clog the thread for terrain handling) and thus slow the bot even more.**
-

Tip

+

Note

**You need to have [terrainandmovements](configuration.md#terrainandmovements) and [entityhandling](configuration.md#entityhandling) enabled in order for this bot to work.** @@ -2030,7 +2030,7 @@ redirectFrom: Also set `enabled` to `true`, then, add your username in the `botowners` INI setting, and finally, connect to the server and use `/tell start` to start the game. -

Tip

+

Note

**If the bot does not respond to bot owners, see the [Detecting chat messages](https://github.com/MCCTeam/Minecraft-Console-Client/tree/master/MinecraftClient/config#detecting-chat-messages) section.** @@ -2065,7 +2065,7 @@ redirectFrom: #### `FileWords_EN` -

Tip

+

Note

**This settings file is for English and is not created by the default** @@ -2081,7 +2081,7 @@ redirectFrom: #### `FileWords_FR` -

Tip

+

Note

**This settings file is for French and is not created by the default** @@ -2339,9 +2339,9 @@ redirectFrom: - **Default:** `false` - #### `Rasize_Rendered_Image` + #### `Resize_Rendered_Image` -

Tip

+

Note

**The bigger the size, the less is the quality.** @@ -2369,7 +2369,7 @@ redirectFrom: #### `Resize_To` -

Tip

+

Note

**Might be a bit slow on less powerful systems when rendering a lot of maps. Lower down the resolution if you have any performance issues. If your system is not that powerful and can't handle it, use external tools for upscaling and resizing.** @@ -2397,7 +2397,7 @@ redirectFrom:
-

Tip

+

Note

**Sometimes when the client connects, the [Discord Bridge](#discord-bridge) will be loaded a tiny bit after. Rendered map images are queued up and sent in order as soon as the [Discord Bridge](#discord-bridge) is ready and connected.** @@ -2527,7 +2527,7 @@ redirectFrom:
-

Tip

+

Note

**Please note that due to technical limitations, the client player (you) will not be shown in the replay file** diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 527776a0..3f8e5b8b 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -11,10 +11,6 @@ redirectFrom: By default, MCC stores its settings in `MinecraftClient.ini`, which is created the first time you run the program. You can also pass a custom configuration file path as the first argument when starting MCC. See [Usage](usage.md#quick-usage-of-mcc-with-examples) for examples. -

Warning

- -
- ## Notes - Some less common settings are not repeated here. The generated config file contains inline descriptions for every setting. @@ -126,7 +122,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } This setting defines the account type: `mojang`, `microsoft`, or `yggdrasil`. -

Tip

+

Note

**Use `microsoft` for normal Microsoft accounts. `yggdrasil` is for custom authlib/Yggdrasil servers.** @@ -391,7 +387,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } MinecraftVersion = "1.18.2" ``` -

Tip

+

Note

**Current code support is `1.4.6` through `26.1`.** @@ -413,7 +409,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `no` -

Tip

+

Note

**Force-enabling only works for MC 1.13 +** @@ -429,7 +425,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `mcc` -

Tip

+

Note

**For playing on Hypixel you need to use `vanilla`** @@ -523,7 +519,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `false` -

Tip

+

Note

**Sometimes the latest versions might not support this straight away, since Mojang often makes changes to this.** @@ -561,7 +557,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `false` -

Tip

+

Note

**Sometimes the latest versions might not support this straight away, since Mojang often makes changes to this.** @@ -615,7 +611,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `true` -

Tip

+

Note

**Only works on Windows XP-8 or Windows 10 with old console** @@ -661,7 +657,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `false` -

Tip

+

Note

**Make sure the spawn point is safe** @@ -966,7 +962,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `.*` -

Tip

+

Note

**Not filtering anything by default** @@ -984,7 +980,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `.*` -

Tip

+

Note

**Not filtering anything by default** @@ -1022,7 +1018,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `console-log.txt` -

Tip

+

Note

**%username% and %serverip% will be substituted with your username and the IP address of the server you are connected to. So you can use something like: `console-log-%username%-%serverip%.txt`** @@ -1062,7 +1058,7 @@ Coordinate = { x = 145, y = 64, z = 2045 } To define a variable/setting, simply make a new line with the following format under the `[AppVar.VarStirng]` section: -

Tip

+

Note

**`%username%`, `%login%`, `%serverip%`, `%serverport%`, `%datetime%`, `%players%` are reserved read-only variables** diff --git a/docs/guide/creating-bots.md b/docs/guide/creating-bots.md index a7f27188..e374f273 100644 --- a/docs/guide/creating-bots.md +++ b/docs/guide/creating-bots.md @@ -13,7 +13,7 @@ title: Creating Chat Bots ## Notes -

Tip

+

Note

**This page covers the basics of the Chat Bot API. For the full surface area, read [ChatBot.cs](https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Scripting/ChatBot.cs) and the example scripts linked below.** @@ -41,7 +41,7 @@ More in-depth: This introduction assumes that you have the basic knowledge of C#. -

Tip

+

Note

**In this page, "Chat Bot" and "Script" are used interchangeably.** @@ -124,7 +124,7 @@ MCC.LoadBot(new YourChatBotClassNameHere()); The **Script Metadata** section also lets you include namespaces and DLL references with `//using ` and `//dll `. -

Tip

+

Note

**Avoid adding whitespace between `//` and keywords** @@ -176,7 +176,7 @@ When the Chat Bot is initialized for the first time, the `Initialize` method is Use it to initialize state such as dictionaries or cached values. -

Tip

+

Note

**For allocating resources like a database connection, we recommend allocating them in `AfterGameJoined` and freeing them in `OnDisconnect`** diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 38e46f4e..612cf37c 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -38,7 +38,7 @@ Requirements: - [Git](https://www.git-scm.com/) - [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download) or [Visual Studio](https://visualstudio.microsoft.com/) configured for C# app development -::: tip +::: note If you want to modify the code and you are new to C# or programming in general, the tutorials listed in [Creating Bots](creating-bots.md#requirements) are a good starting point. ::: @@ -129,7 +129,7 @@ If the publish step succeeds, the published binary `MinecraftClient.exe` will be
Linux and macOS build instructions -

Tip

+

Note

**If you're using Linux we will assume that you should be able to install git on your own. If you don't know how, search it up for your distribution, it should be easy. (Debian based distros: `apt install git`, Arch based: `pacman -S git`)** @@ -187,7 +187,7 @@ git clone https://github.com/MCCTeam/Minecraft-Console-Client.git --recursive dotnet publish MinecraftClient.sln -f net10.0 -r linux-x64 --self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded ``` -

Tip

+

Note

**If you are using Linux on ARM, 32-bit, RHEL-based distributions, or Musl, [pick the appropriate RID](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids) for your platform and replace `-r linux-x64` with it, for example `-r linux-arm64`.** @@ -199,7 +199,7 @@ git clone https://github.com/MCCTeam/Minecraft-Console-Client.git --recursive dotnet publish MinecraftClient.sln -f net10.0 -r osx-x64 --self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded ``` -

Tip

+

Note

**If you are not using an Intel Mac, [pick the appropriate RID](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#macos-rids) for your processor and replace `-r osx-x64` with it, for example `-r osx-arm64`.** @@ -228,7 +228,7 @@ Requirements: - Git - Docker -

Tip

+

Note

**This section is for more advanced users, if you do not know how to install git or docker, you can take a look at other sections for Git, and search on how to install Docker on your system.** @@ -316,19 +316,19 @@ docker-compose down It is possible to run Minecraft Console Client on Android through Termux and Ubuntu, but it requires a manual setup with a lot of commands, so be careful not to skip any steps. Depending on your technical background, internet speed, and device speed, this can take anywhere from 10 to 20 minutes or more. -

Tip

+

Note

**This section gets a bit technical. If you run into issues, open a discussion on our GitHub repository page.**
-

Tip

+

Note

**You're required to have some bare basic knowledge of Linux, if you do not know anything about it, watch [this video](https://www.youtube.com/watch?v=SkB-eRCzWIU) to get familiar with basic commands.**
-

Tip

+

Note

**Here we're installing everything on the root account for simplicity sake, if you want to make a user account, make sure you update the command which reference the `/root` directory with your home directory.** @@ -351,7 +351,7 @@ It is possible to run Minecraft Console Client on Android through Termux and Ubu **GitHub releases:** Go to [the latest Termux GitHub release](https://github.com/termux/termux-app/releases/latest/), download the APK file whose name contains `universal` (e.g. `termux-app_v...-debug_universal.apk`), and install it. -

Tip

+

Note

**If your file manager does not let you install APK files, install and use `File Manager +` and grant it permission to install third-party applications when asked.** @@ -373,7 +373,7 @@ Open Termux and run the following commands one at a time, in order: 2. `pkg upgrade` 3. `pkg install proot-distro` -

Tip

+

Note

**If you are asked to press Y/N during the update or upgrade step, enter Y and press Enter.** @@ -391,7 +391,7 @@ Once the installation finishes, start Ubuntu with: proot-distro login ubuntu ``` -

Tip

+

Note

**Every time you open Termux after it has been closed, run this command to get back into Ubuntu.** @@ -470,7 +470,7 @@ wget -O MinecraftClient \ | cut -d '"' -f 4)" ``` -

Tip

+

Note

**If you have a 32-bit ARM device, replace `linux-arm64` with `linux-arm` in the command above.** @@ -517,7 +517,7 @@ Also, here are some linux tutorials for people who are new to it: ## Run on a VPS -

Tip

+

Note

**This is a newer section. If you spot a mistake, please report it by opening an issue in our [GitHub repository](https://github.com/MCCTeam/Minecraft-Console-Client).** @@ -548,7 +548,7 @@ Here is a [YouTube video](https://youtu.be/42fwh_1KP_o) that explains it in more Download and install [Git Bash](https://git-scm.com/downloads). -

Tip

+

Note

**Make sure to allow the installation to add it to the context menu** @@ -602,7 +602,7 @@ Some of the reliable and cheap hosting providers (sorted for price/performance): **Minimum price**: `2.50 EUR / month` -

Tip

+

Note

**If Ubuntu 24.04 LTS is not in the dropdown when ordering, you may need to reinstall later or ask support to do it.** @@ -648,7 +648,7 @@ You also may want to search for better deals.
-

Tip

+

Note

**If you're not banned, sometimes fetching the keys can take some time, try giving it a minute or two, if it still hangs, hit some keys to refresh the screen, or try restarting and running again. If it still happens, use tmux instead of screen.** @@ -665,7 +665,7 @@ Once you're done, you can continue to [Setting up the Amazon VPS](#setting-up-an
AWS EC2 setup steps -

Tip

+

Note

**Skip this section if you're not using AWS. Go to [Initial VPS setup](#initial-vps-setup)** @@ -673,7 +673,7 @@ Once you're done, you can continue to [Setting up the Amazon VPS](#setting-up-an When you register and open the `AWS Console`, click on the Search field on the top of the page and search for: `EC2` -

Tip

+

Note

**Make sure to select the region closest to you for the minimal latency** @@ -713,7 +713,7 @@ For the **Network settings** check the following checkboxes on: - `Allow HTTPs traffic from the internet` - `Allow HTTP traffic from the internet` -

Tip

+

Note

**The SSH traffic from Anywhere is not the best thing for security, you might want to enter IP addresses of your devices from which you want to access the VPS manually.** @@ -737,13 +737,13 @@ In order to login with SSH, you are going to use the following command: ssh -i ubuntu@ ``` -

Tip

+

Note

**`<` and `>` are not typed, that is just a notation for a placeholder!**
-

Tip

+

Note

**`ubuntu` is a default root account username for Ubuntu on AWS!** @@ -766,7 +766,7 @@ Now you can continue to [Creating a new user](#creating-a-new-user)
Non-AWS VPS login steps -

Tip

+

Note

**This section if for those who do not use AWS, if you use AWS skip it** @@ -784,7 +784,7 @@ If you're on Windows open `Git Bash`, on mac OS and Linux open a `Terminal` and ssh @ ``` -

Tip

+

Note

**If you're given a custom port other than `22` by your host, you should add `-p ` before the username (eg. `ssh -p @`) or `:` after the ip (eg. `ssh @:`)** @@ -815,7 +815,7 @@ Once you've logged in to your VPS you need to create a new user and give it SSH In this tutorial we will be using `mcc` as a name for the user account that will be running the MCC. -

Tip

+

Note

**You may be wondering why we're creating a separate user account and making it be accessible over SSH only. This is for security reasons, if you do not want to do this, you're free to skip it, but be careful.** @@ -833,13 +833,13 @@ Now we need to give it a password, execute the following command, type the passw sudo passwd mcc ``` -

Tip

+

Note

**When you're typing a password it will not be displayed on the screen, but you're typing it for real.**
-

Tip

+

Note

**Make sure you have a strong password!** @@ -993,7 +993,7 @@ Example: ssh -i MCC_Key mcc@3.71.108.69 ``` -

Tip

+

Note

**If you've changed the `Port`, make sure you add a `-p ` option after the `-i ` option (eg. `ssh -i MCC_Key -p 8973 mcc@3.71.108.69`)!** @@ -1012,7 +1012,7 @@ Now you can install the .NET 10 SDK and MCC.
.NET SDK installation on VPS -

Tip

+

Note

**If your VPS has an ARM CPU, follow [this](#installing-net-on-arm) part of the documentation and then return to section after this one.** @@ -1073,7 +1073,7 @@ If it was successful, you can now install MCC. Now that you have the .NET SDK and a user account, install the `screen` utility. You will need it if you want MCC to keep running after you close the SSH session. -

Tip

+

Note

**There is also a Docker method, if you're using Docker, you do not need the `screen` program.** @@ -1107,13 +1107,13 @@ To start a screen, type: screen -S mcc ``` -

Tip

+

Note

**`mcc` here is the name of the screen, you can use whatever you like, but if you've used a different name, make sure you use that one instead of the `mcc` in the following commands.**
-

Tip

+

Note

**You need to make a screen only once, however if you reboot your VPS, you need to start it on each reboot.** diff --git a/docs/guide/usage.md b/docs/guide/usage.md index e57a23e1..7a9248d4 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -106,7 +106,7 @@ MCC also supports a few maintenance and debugging switches such as `--upgrade`, ### Quick usage of MCC with examples -

Tip

+

Note

**On Linux and macOS, you need to type: `./MinecraftClient` instead of `MinecraftClient.exe`** @@ -120,7 +120,7 @@ MinecraftClient.exe --section.setting=value [--other settings] MinecraftClient.exe [--other settings] ``` -

Tip

+

Note

**Microsoft accounts use the OAuth 2.0 device code flow and do not require a password on the command line. MCC will display a code and a URL for you to sign in through your browser (with full 2FA support). You can simply omit the password or use `""` as a placeholder.** @@ -198,7 +198,7 @@ From chat prompt, commands must by default be prepended with a slash, eg. `/quit In scripts and remote control, no slash is needed to perform the command, eg. `quit`. -

Tip

+

Note

**Some commands may not be documented yet or are defined in description of Chat Bots, use `/help` to list them all, or you can contribute to this page.** @@ -261,7 +261,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
blockinfo -

Tip

+

Note

**You need to have [Terrain And Movements](configuration.md#terrainandmovements) enabled in order for this to work.** @@ -321,7 +321,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Change your selected slot in the hotbar. -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.** @@ -348,7 +348,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
-

Tip

+

Note

**You need a terminal with emoji support, like Powershell 7, Windows Terminal or Alacritty, if you do not want emoji support and want to use cmd or powershell 5, disable emojis with: [`enableemoji`](configuration.md#enableemoji)** @@ -400,7 +400,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Drop all items of a specific type from your inventory. -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.** @@ -412,7 +412,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /dropitem ``` -

Tip

+

Note

**All item types can be found [here](https://mccteam.github.io/r/item/#L12).** @@ -429,7 +429,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
enchant -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.** @@ -460,7 +460,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Attack an entity, use an entity or get a list of entities around you. -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) and [Entity Handling](configuration.md#entityhandling) enabled in order for this to work.** @@ -480,7 +480,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /entity ``` -

Tip

+

Note

**All entity types can be found [here](https://mccteam.github.io/r/entity/#L15).** @@ -509,7 +509,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Also the instance of MCC is available with `MCC.`. -

Tip

+

Note

**All local variables are treated as strings in the app, when comparing their values, you can use ` == ""`, or better use [`.Equals`](https://www.programiz.com/csharp-programming/library/string/equals) method** @@ -528,7 +528,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /execif 'test == "Something"' "send Success!" ``` -

Tip

+

Note

**You can use single quote (`'`) to wrap your expression if the expression contains double quote (`"`)** @@ -606,7 +606,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /reco [account] ``` -

Tip

+

Note

**`[account]` is an account alias defined in accounts file, for more info check out [accountlist](configuration.html#accountlist)** @@ -621,7 +621,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Reloads the active configuration file and chat bots. -

Tip

+

Note

**Some settings are not reloaded because they are used before client initialization. Settings passed on the command line also override file values.** @@ -648,13 +648,13 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /connect [account] ``` -

Tip

+

Note

**`` is either a server IP or a server alias defined in servers file, for more info check out [serverlist](configuration.html#serverlist)**
-

Tip

+

Note

**`[account]` is an account alias defined in accounts file, for more info check out [accountlist](configuration.html#accountlist)** @@ -824,7 +824,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Use item in the hand, this can be used to do a right click on items which open menus on servers. -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.** @@ -859,13 +859,13 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q - shulker - loom -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) and [Terrain and Movements](configuration.md#terrainandmovements) enabled in order for this to work.**
-

Tip

+

Note

**Not all inventories have a GUI representation in an ASCII art format.** @@ -898,21 +898,21 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Make the bot follow a player. -

Tip

+

Note

**This command is available only when the [Follow Player](chat-bots.md#follow-player) chat bot is enabled.**
-

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.**
-

Tip

+

Note

- **You need to have [Enity Handling](configuration.md#entityhandling) enabled in order for this to work.** + **You need to have [Entity Handling](configuration.md#entityhandling) enabled in order for this to work.**
@@ -980,7 +980,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Used for moving when terrain and movements feature is enabled. -

Tip

+

Note

**You need to have [Terrain and Movements](configuration.md#terrainandmovements) enabled in order for this to work.** @@ -1100,7 +1100,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Used for inventory manipulation. -

Tip

+

Note

**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this to work.** @@ -1116,7 +1116,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Inventory has slots and each one of them has an id. -

Tip

+

Note

**This command DOES NOT physically open a container (eg. chest), for that you need to use [`useblock`](#useblock) command first.** @@ -1134,7 +1134,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /inventory > [action parameters] | /inventory | /inventory [amount] ``` -

Tip

+

Note

**player and container can be simplified with p and c accordingly** @@ -1157,7 +1157,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /inventory > [left|right|middle|Shift|ShiftRight] ``` -

Tip

+

Note

**The default click is left click** @@ -1175,7 +1175,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /inventory drop ``` -

Tip

+

Note

**To drop all items from a slot, you can use: `all`** @@ -1187,7 +1187,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /inventory creativegive ``` -

Tip

+

Note

**To find item types, check out [this list](https://mccteam.github.io/r/item/#L12)** @@ -1261,7 +1261,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /inventory creativegive 36 diamondblock 64 ``` -

Tip

+

Note

**To find item types, check out [this list](https://mccteam.github.io/r/item/#L12)** @@ -1297,7 +1297,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q Show commands help. -

Tip

+

Note

**Use "/send /help" for server help** diff --git a/docs/guide/websocket/README.md b/docs/guide/websocket/README.md index a8a6c9d8..1f4af2f1 100644 --- a/docs/guide/websocket/README.md +++ b/docs/guide/websocket/README.md @@ -117,6 +117,14 @@ These are useful if your client needs a name-to-ID lookup for the current MCC ve - [Commands](Commands.md) - full list of available commands - [Events](Events.md) - full list of emitted events +

⭐ Reference Implementation: MCC.js

+ +[MCC.js](https://github.com/milutinke/MCC.js) is a Node.js/TypeScript library built for this bot. It handles authentication, JSON serialization, event subscriptions, and typed command wrappers out of the box. + +If you're writing a client in JavaScript or TypeScript, start there. + +
+ ## Compatibility - Requires any MCC version that supports `/script` (standalone MCCScript 1.0 bots). From 4b9e0e93aa8accf7e93916c1c5af907b6f5d2a33 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 27 Mar 2026 22:47:20 +0800 Subject: [PATCH 08/76] Fix: cursor locate at the beginning when pressing Up to view history --- MinecraftClient/Tui/MainTuiView.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 397f1475..7f140d0c 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -509,6 +509,17 @@ namespace MinecraftClient.Tui { _commandInput.TextChanged += OnCommandTextChanged; } + + Dispatcher.UIThread.Post(() => + { + var endKeyEvent = new KeyEventArgs + { + RoutedEvent = KeyDownEvent, + Key = Key.End, + Source = _commandInput, + }; + _commandInput.RaiseEvent(endKeyEvent); + }, DispatcherPriority.Input); } #endregion From f6446b198db7a48bf4331e8605d0a66398251138 Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 27 Mar 2026 15:47:27 +0100 Subject: [PATCH 09/76] Implemented effects support, as a command, chat notification and added in TUI mode --- MinecraftClient/Commands/EffectsCommand.cs | 67 ++++++ MinecraftClient/Inventory/EffectData.cs | 194 ++++++++++++++++++ MinecraftClient/Mapping/Entity.cs | 8 + MinecraftClient/McClient.cs | 90 +++++++- .../Protocol/Handlers/Protocol18.cs | 21 +- .../Protocol/IMinecraftComHandler.cs | 13 ++ .../ConfigComments/ConfigComments.Designer.cs | 29 ++- .../ConfigComments/ConfigComments.resx | 3 + .../Translations/Translations.Designer.cs | 144 +++++++++++++ .../Resources/Translations/Translations.resx | 50 ++++- MinecraftClient/Scripting/ChatBot.cs | 7 + MinecraftClient/Settings.cs | 3 + MinecraftClient/Tui/MainTuiView.cs | 89 ++++++++ docs/guide/configuration.md | 10 + docs/guide/usage.md | 15 ++ 15 files changed, 728 insertions(+), 15 deletions(-) create mode 100644 MinecraftClient/Commands/EffectsCommand.cs create mode 100644 MinecraftClient/Inventory/EffectData.cs diff --git a/MinecraftClient/Commands/EffectsCommand.cs b/MinecraftClient/Commands/EffectsCommand.cs new file mode 100644 index 00000000..700764aa --- /dev/null +++ b/MinecraftClient/Commands/EffectsCommand.cs @@ -0,0 +1,67 @@ +using System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class EffectsCommand : Command + { + public override string CmdName { get { return "effects"; } } + public override string CmdUsage { get { return "effects"; } } + public override string CmdDesc { get { return Translations.cmd_effects_desc; } } + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ShowEffects(r.Source)) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private int ShowEffects(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetEntityHandlingEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedEntity); + + var effects = handler.GetPlayerEffects() + .Values + .Where(effectData => !effectData.IsExpired) + .OrderBy(effectData => effectData.Effect) + .ToArray(); + + if (effects.Length == 0) + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_effects_none); + + StringBuilder response = new(); + response.AppendLine(Translations.cmd_effects_header); + foreach (var effectData in effects) + { + response.AppendLine(string.Format(Translations.cmd_effects_entry, + effectData.GetDisplayName(), effectData.GetRemainingDurationText())); + } + + return r.SetAndReturn(CmdResult.Status.Done, response.ToString().TrimEnd()); + } + } +} diff --git a/MinecraftClient/Inventory/EffectData.cs b/MinecraftClient/Inventory/EffectData.cs new file mode 100644 index 00000000..c13e4312 --- /dev/null +++ b/MinecraftClient/Inventory/EffectData.cs @@ -0,0 +1,194 @@ +namespace MinecraftClient.Inventory; + +using System; +using System.Collections.Generic; +using System.Linq; +using MinecraftClient.Protocol; +using MinecraftClient.Protocol.Message; + +/// +/// Represents an active status effect on an entity +/// +public class EffectData +{ + /// + /// The type of effect + /// + public Effects Effect { get; set; } + + /// + /// Effect amplifier (level - 1, e.g., 0 = level I, 1 = level II) + /// + public int Amplifier { get; set; } + + /// + /// Duration in ticks (20 ticks = 1 second). -1 for infinite. + /// + public int Duration { get; set; } + + /// + /// Effect flags (ambient, show particles, show icon) + /// + public byte Flags { get; set; } + + /// + /// Time when the effect was applied + /// + public DateTime StartTime { get; set; } + + public EffectData(Effects effect, int amplifier, int duration, byte flags) + { + Effect = effect; + Amplifier = amplifier; + Duration = duration; + Flags = flags; + StartTime = DateTime.UtcNow; + } + + /// + /// Check if this is an infinite duration effect + /// + public bool IsInfinite => Duration == -1 || Duration == int.MaxValue; + + /// + /// Check if the effect has expired + /// + public bool IsExpired + { + get + { + if (IsInfinite) return false; + return GetElapsedTicks() >= Duration; + } + } + + /// + /// Get remaining duration in ticks + /// + public int RemainingTicks + { + get + { + if (IsInfinite) return -1; + return Math.Max(0, Duration - GetElapsedTicks()); + } + } + + /// + /// Get remaining duration in seconds + /// + public int RemainingSeconds + { + get + { + if (IsInfinite) return -1; + return (RemainingTicks + 19) / 20; + } + } + + /// + /// Get the translated effect name from Minecraft translations + /// + public string GetTranslatedName() + { + var key = $"effect.minecraft.{Effect.ToString().ToUnderscoreCase()}"; + var translated = ChatParser.TranslateString(key); + return string.IsNullOrEmpty(translated) ? Effect.ToString() : translated; + } + + /// + /// Get the translated effect name with level when applicable + /// + public string GetDisplayName() + { + string translatedName = GetTranslatedName(); + if (Amplifier <= 0) + return translatedName; + + return string.Format(Translations.effect_name_with_amplifier, translatedName, + EnchantmentMapping.ConvertLevelToRomanNumbers(Amplifier + 1)); + } + + /// + /// Get the translated effect name prefixed with the best-fit indefinite article + /// + public string GetDisplayNameWithArticle() + { + string displayName = GetDisplayName(); + char? firstLetter = displayName + .TrimStart() + .FirstOrDefault(char.IsLetter); + + if (firstLetter is null) + return displayName; + + string article = "AEIOUaeiou".Contains(firstLetter.Value) + ? Translations.effect_article_an + : Translations.effect_article_a; + return $"{article} {displayName}"; + } + + /// + /// Get the configured short duration label for the remaining time + /// + public string GetRemainingDurationText() + { + return FormatShortDuration(RemainingSeconds); + } + + /// + /// Get the configured short duration label for the initial effect duration + /// + public string GetInitialDurationText() + { + if (IsInfinite) + return Translations.effect_duration_unlimited; + + int durationSeconds = (Duration + 19) / 20; + return FormatShortDuration(durationSeconds); + } + + /// + /// Format a duration for compact UI output + /// + /// Duration in seconds, -1 for unlimited + public static string FormatShortDuration(int seconds) + { + if (seconds < 0) + return Translations.effect_duration_short_unlimited; + + if (seconds < 60) + return string.Format(Translations.effect_duration_short_seconds, seconds); + + int minutes = seconds / 60; + int remainingSeconds = seconds % 60; + if (seconds < 3600) + { + return remainingSeconds == 0 + ? string.Format(Translations.effect_duration_short_minutes, minutes) + : string.Format(Translations.effect_duration_short_minutes_seconds, minutes, remainingSeconds); + } + + int hours = seconds / 3600; + int remainingMinutes = (seconds % 3600) / 60; + return remainingMinutes == 0 + ? string.Format(Translations.effect_duration_short_hours, hours) + : string.Format(Translations.effect_duration_short_hours_minutes, hours, remainingMinutes); + } + + private int GetElapsedTicks() + { + return (int)((DateTime.UtcNow - StartTime).TotalMilliseconds / 50); + } +} + +/// +/// Extension method for converting PascalCase to snake_case +/// +public static class StringExtensions +{ + public static string ToUnderscoreCase(this string str) + { + return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower(); + } +} diff --git a/MinecraftClient/Mapping/Entity.cs b/MinecraftClient/Mapping/Entity.cs index 3d1dd67e..33b250f0 100644 --- a/MinecraftClient/Mapping/Entity.cs +++ b/MinecraftClient/Mapping/Entity.cs @@ -99,6 +99,11 @@ namespace MinecraftClient.Mapping /// public Dictionary Equipment; + /// + /// Active status effects on this entity + /// + public Dictionary ActiveEffects { get; private set; } + /// /// Create a new entity based on Entity ID, Entity Type and location /// @@ -112,6 +117,7 @@ namespace MinecraftClient.Mapping Location = location; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); } @@ -128,6 +134,7 @@ namespace MinecraftClient.Mapping Location = location; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); Yaw = yaw * (1F / 256) * 360; // to angle in 360 degree Pitch = pitch * (1F / 256) * 360; @@ -151,6 +158,7 @@ namespace MinecraftClient.Mapping Name = name; Health = 1.0f; Equipment = new Dictionary(); + ActiveEffects = new Dictionary(); Item = new Item(ItemType.Air, 0, null); Yaw = yaw * (1F / 256) * 360; // to angle in 360 degree Pitch = pitch * (1F / 256) * 360; diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 4d82a1d4..e76b9b80 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -102,6 +102,9 @@ namespace MinecraftClient private int playerLevel; private int playerTotalExperience; private byte CurrentSlot = 0; + + // player effects + private readonly Dictionary playerEffects = new(); // Sneaking public bool IsSneaking { get; set; } = false; @@ -141,6 +144,16 @@ namespace MinecraftClient public bool GetIsSupportPreviewsChat() { return isSupportPreviewsChat; } public float GetHealth() { return playerHealth; } public int GetSaturation() { return playerFoodSaturation; } + + /// + /// Get the player's active effects + /// + /// Dictionary of active effects + public Dictionary GetPlayerEffects() + { + return new Dictionary(playerEffects); + } + public int GetLevel() { return playerLevel; } public int GetTotalExperience() { return playerTotalExperience; } public byte GetCurrentSlot() { return CurrentSlot; } @@ -616,6 +629,26 @@ namespace MinecraftClient SendRespawnPacket(); } + // Check for expired effects + if (playerEffects.Count > 0) + { + var expiredEffects = playerEffects + .Where(e => e.Value.IsExpired) + .Select(e => e.Key) + .ToList(); + + foreach (var effect in expiredEffects) + { + if (!playerEffects.Remove(effect, out var effectData)) + continue; + + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_expired, effectData.GetDisplayName())); + + if (entities.TryGetValue(playerEntityID, out var playerEntity)) + playerEntity.ActiveEffects.Remove(effect); + } + } + lock (threadTasksLock) { while (threadTasks.Count > 0) @@ -3394,8 +3427,61 @@ namespace MinecraftClient /// public void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec) { - if (entities.ContainsKey(entityid)) - DispatchBotEvent(bot => bot.OnEntityEffect(entities[entityid], effect, amplifier, duration, flags)); + Entity? entity = null; + if (entities.TryGetValue(entityid, out var trackedEntity)) + { + entity = trackedEntity; + } + + var effectData = new EffectData(effect, amplifier, duration, flags); + entity?.ActiveEffects[effect] = effectData; + + if (entityid == playerEntityID) + { + playerEffects.TryGetValue(effect, out var previousPlayerEffect); + playerEffects[effect] = effectData; + + bool shouldAnnounceEffectGain = previousPlayerEffect is null + || previousPlayerEffect.Amplifier != amplifier + || (effectData.IsInfinite && !previousPlayerEffect.IsInfinite) + || (!effectData.IsInfinite && duration > previousPlayerEffect.RemainingTicks + 20); + + if (shouldAnnounceEffectGain) + { + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_gained, + effectData.GetDisplayNameWithArticle(), effectData.GetInitialDurationText())); + } + } + + if (entity is not null) + DispatchBotEvent(bot => bot.OnEntityEffect(entity, effect, amplifier, duration, flags)); + } + + /// + /// Called when an entity has an effect removed + /// + /// Entity ID + /// Effect that was removed + public void OnRemoveEntityEffect(int entityid, Effects effect) + { + Entity? entity = null; + EffectData? removedEffectData = null; + + if (entities.TryGetValue(entityid, out var trackedEntity)) + { + entity = trackedEntity; + if (entity.ActiveEffects.Remove(effect, out var entityEffectData)) + removedEffectData = entityEffectData; + } + + if (entityid == playerEntityID && playerEffects.Remove(effect, out var playerEffectData)) + removedEffectData ??= playerEffectData; + + if (entityid == playerEntityID && removedEffectData is not null) + ConsoleIO.WriteLine(string.Format(Translations.bot_effect_expired, removedEffectData.GetDisplayName())); + + if (entity is not null) + DispatchBotEvent(bot => bot.OnRemoveEntityEffect(entity, effect)); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index b9128587..be0914bc 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2513,11 +2513,12 @@ namespace MinecraftClient.Protocol.Handlers { var entityId = dataTypes.ReadNextVarInt(packetData); var effectId = protocolVersion >= MC_1_18_2_Version - ? dataTypes.ReadNextVarInt(packetData) + ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); - if (Enum.TryParse(effectId.ToString(), out Effects effect)) + if (Enum.IsDefined(typeof(Effects), effectId)) { + var effect = (Effects)effectId; var amplifier = dataTypes.ReadNextByte(packetData); var duration = dataTypes.ReadNextVarInt(packetData); var flags = dataTypes.ReadNextByte(packetData); @@ -2536,6 +2537,22 @@ namespace MinecraftClient.Protocol.Handlers } } + break; + case PacketTypesIn.RemoveEntityEffect: + if (handler.GetEntityHandlingEnabled()) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + var effectId = protocolVersion >= MC_1_18_2_Version + ? dataTypes.ReadNextVarInt(packetData) + 1 + : dataTypes.ReadNextByte(packetData); + + if (Enum.IsDefined(typeof(Effects), effectId)) + { + var effect = (Effects)effectId; + handler.OnRemoveEntityEffect(entityId, effect); + } + } + break; case PacketTypesIn.DestroyEntities: if (handler.GetEntityHandlingEnabled()) diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 7edb83cd..94fe0590 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -434,6 +434,19 @@ namespace MinecraftClient.Protocol /// factorCodec void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary? factorCodec); + /// + /// Called when an entity has an effect removed + /// + /// Entity ID + /// Effect that was removed + void OnRemoveEntityEffect(int entityid, Effects effect); + + /// + /// Get the player's active effects + /// + /// Dictionary of active effects + Dictionary GetPlayerEffects(); + /// /// Called when Soreboard Objective /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 143d1b26..66213cc9 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1843,16 +1843,25 @@ namespace MinecraftClient { /// /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. /// - internal static string Main_Advanced_show_inventory_layout { - get { - return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to System messages for server ops.. - /// - internal static string Main_Advanced_show_system_messages { + internal static string Main_Advanced_show_inventory_layout { + get { + return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show full effect names and levels in the TUI status bar instead of compact effect icons only.. + /// + internal static string Main_Advanced_show_effect_names_in_tui { + get { + return ResourceManager.GetString("Main.Advanced.show_effect_names_in_tui", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to System messages for server ops.. + /// + internal static string Main_Advanced_show_system_messages { get { return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); } diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 06421374..6d91740b 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -705,6 +705,9 @@ Usage examples: "/tell <mybot> connect Server1", "/connect Server2" Show inventory layout as ASCII art in inventory command. + + Show full effect names and levels in the TUI status bar instead of compact effect icons only. + System messages for server ops. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index d20d36af..d76a3bed 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -3522,6 +3522,42 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to list your currently active effects.. + /// + internal static string cmd_effects_desc { + get { + return ResourceManager.GetString("cmd.effects.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to - {0} ({1}). + /// + internal static string cmd_effects_entry { + get { + return ResourceManager.GetString("cmd.effects.entry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Active effects:. + /// + internal static string cmd_effects_header { + get { + return ResourceManager.GetString("cmd.effects.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No active effects.. + /// + internal static string cmd_effects_none { + get { + return ResourceManager.GetString("cmd.effects.none", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display Health and Food saturation.. /// @@ -6520,5 +6556,113 @@ namespace MinecraftClient { return ResourceManager.GetString("tui.inventory.item_count", resourceCulture); } } + + /// + /// Looks up a localized string similar to You're now under {0} effect (Duration: {1}).. + /// + internal static string bot_effect_gained { + get { + return ResourceManager.GetString("bot.effect.gained", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Effect {0} has expired. + /// + internal static string bot_effect_expired { + get { + return ResourceManager.GetString("bot.effect.expired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlimited. + /// + internal static string effect_duration_unlimited { + get { + return ResourceManager.GetString("effect.duration.unlimited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to a. + /// + internal static string effect_article_a { + get { + return ResourceManager.GetString("effect.article.a", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to an. + /// + internal static string effect_article_an { + get { + return ResourceManager.GetString("effect.article.an", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}h. + /// + internal static string effect_duration_short_hours { + get { + return ResourceManager.GetString("effect.duration.short.hours", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}h {1}m. + /// + internal static string effect_duration_short_hours_minutes { + get { + return ResourceManager.GetString("effect.duration.short.hours_minutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}m. + /// + internal static string effect_duration_short_minutes { + get { + return ResourceManager.GetString("effect.duration.short.minutes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}m {1}s. + /// + internal static string effect_duration_short_minutes_seconds { + get { + return ResourceManager.GetString("effect.duration.short.minutes_seconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0}s. + /// + internal static string effect_duration_short_seconds { + get { + return ResourceManager.GetString("effect.duration.short.seconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ∞. + /// + internal static string effect_duration_short_unlimited { + get { + return ResourceManager.GetString("effect.duration.short.unlimited", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1}. + /// + internal static string effect_name_with_amplifier { + get { + return ResourceManager.GetString("effect.name.with_amplifier", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 5eb2fa76..10b2ef3a 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1237,6 +1237,18 @@ Change EnableEmoji=false in the settings if the display is confusing. follow <player name|stop> [-f] (Use -f to enable un-safe walking) + + list your currently active effects. + + + - {0} ({1}) + + + Active effects: + + + No active effects. + Display Health and Food saturation. @@ -2299,4 +2311,40 @@ see item details. {0} items - \ No newline at end of file + + You're now under {0} effect (Duration: {1}). + + + Effect {0} has expired + + + Unlimited + + + a + + + an + + + {0}h + + + {0}h {1}m + + + {0}m + + + {0}m {1}s + + + {0}s + + + + + + {0} {1} + + diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 586a6fa8..f62e1377 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -333,6 +333,13 @@ namespace MinecraftClient.Scripting /// effect flags public virtual void OnEntityEffect(Entity entity, Effects effect, int amplifier, int duration, byte flags) { } + /// + /// Called when an entity has an effect removed (expired or cleared) + /// + /// Entity + /// Effect that was removed + public virtual void OnRemoveEntityEffect(Entity entity, Effects effect) { } + /// /// Called when a scoreboard objective updated /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index a3368c8e..e77514da 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -817,6 +817,9 @@ namespace MinecraftClient [TomlInlineComment("$Main.Advanced.show_inventory_layout$")] public bool ShowInventoryLayout = true; + [TomlInlineComment("$Main.Advanced.show_effect_names_in_tui$")] + public bool ShowEffectNamesInTUI = false; + [TomlInlineComment("$Main.Advanced.terrain_and_movements$")] public bool TerrainAndMovements = false; diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 397f1475..6a4079a9 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -9,6 +10,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; +using MinecraftClient.Inventory; namespace MinecraftClient.Tui { @@ -866,6 +868,45 @@ namespace MinecraftClient.Tui Foreground = new SolidColorBrush(Color.FromRgb(220, 190, 100)), }); + // Add effects display + var effects = client.GetPlayerEffects().Values + .Where(effectData => !effectData.IsExpired) + .OrderBy(effectData => effectData.Effect) + .ToArray(); + if (effects.Length > 0) + { + bool showEffectNamesInTui = Settings.Config.Main.Advanced.ShowEffectNamesInTUI; + + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(" | ") + { + Foreground = Brushes.Gray, + }); + + bool first = true; + foreach (var effectData in effects) + { + if (!first) + { + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(", ") + { + Foreground = Brushes.Gray, + }); + } + first = false; + + var color = GetEffectIconAndColor(effectData.Effect).Color; + var displayText = showEffectNamesInTui + ? effectData.GetDisplayName() + : GetCompactEffectLabel(effectData); + displayText = $"{displayText} ({effectData.GetRemainingDurationText()})"; + + _statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(displayText) + { + Foreground = color, + }); + } + } + _statusBar.IsVisible = true; } @@ -884,6 +925,54 @@ namespace MinecraftClient.Tui return sb.ToString(); } + private static string GetCompactEffectLabel(EffectData effectData) + { + var icon = GetEffectIconAndColor(effectData.Effect).Icon; + return effectData.Amplifier > 0 + ? $"{icon}{effectData.Amplifier + 1}" + : icon; + } + + private static (string Icon, IBrush Color) GetEffectIconAndColor(Effects effect) + { + return effect switch + { + Effects.Speed => ("⚡", new SolidColorBrush(Color.FromRgb(135, 206, 235))), + Effects.Slowness => ("🐢", new SolidColorBrush(Color.FromRgb(139, 139, 139))), + Effects.Haste => ("⛏", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + Effects.MiningFatigue => ("🔨", new SolidColorBrush(Color.FromRgb(64, 64, 64))), + Effects.Strength => ("⚔", new SolidColorBrush(Color.FromRgb(255, 99, 71))), + Effects.InstantHealth => ("❤", new SolidColorBrush(Color.FromRgb(255, 182, 193))), + Effects.InstantDamage => ("💀", new SolidColorBrush(Color.FromRgb(139, 0, 0))), + Effects.JumpBoost => ("🦘", new SolidColorBrush(Color.FromRgb(50, 205, 50))), + Effects.Nausea => ("💫", new SolidColorBrush(Color.FromRgb(85, 107, 47))), + Effects.Regeneration => ("✨", new SolidColorBrush(Color.FromRgb(255, 105, 180))), + Effects.Resistance => ("🛡", new SolidColorBrush(Color.FromRgb(112, 128, 144))), + Effects.FireResistance => ("🔥", new SolidColorBrush(Color.FromRgb(255, 140, 0))), + Effects.WaterBreathing => ("🐟", new SolidColorBrush(Color.FromRgb(0, 191, 255))), + Effects.Invisibility => ("👻", new SolidColorBrush(Color.FromRgb(200, 200, 200))), + Effects.Blindness => ("🕶", new SolidColorBrush(Color.FromRgb(50, 50, 50))), + Effects.NightVision => ("👁", new SolidColorBrush(Color.FromRgb(0, 255, 127))), + Effects.Hunger => ("🍔", new SolidColorBrush(Color.FromRgb(139, 69, 19))), + Effects.Weakness => ("💪", new SolidColorBrush(Color.FromRgb(128, 128, 128))), + Effects.Poison => ("☠", new SolidColorBrush(Color.FromRgb(75, 0, 130))), + Effects.Wither => ("🥀", new SolidColorBrush(Color.FromRgb(0, 0, 0))), + Effects.HealthBoost => ("💖", new SolidColorBrush(Color.FromRgb(255, 20, 147))), + Effects.Absorption => ("💛", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + Effects.Saturation => ("🍖", new SolidColorBrush(Color.FromRgb(255, 165, 0))), + Effects.Glowing => ("💡", new SolidColorBrush(Color.FromRgb(255, 255, 150))), + Effects.Levitation => ("🎈", new SolidColorBrush(Color.FromRgb(147, 112, 219))), + Effects.Luck => ("🍀", new SolidColorBrush(Color.FromRgb(50, 205, 50))), + Effects.BadLuck => ("🐈‍⬛", new SolidColorBrush(Color.FromRgb(128, 0, 0))), + Effects.SlowFalling => ("🪶", new SolidColorBrush(Color.FromRgb(255, 182, 193))), + Effects.ConduitPower => ("🐡", new SolidColorBrush(Color.FromRgb(0, 255, 255))), + Effects.DolphinsGrace => ("🐬", new SolidColorBrush(Color.FromRgb(135, 206, 235))), + Effects.BadOmen => ("🏴", new SolidColorBrush(Color.FromRgb(0, 100, 0))), + Effects.HerooftheVillage => ("🎉", new SolidColorBrush(Color.FromRgb(255, 215, 0))), + _ => ("✦", new SolidColorBrush(Color.FromRgb(200, 200, 200))), + }; + } + #endregion #region Overlay diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 527776a0..76336fba 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -507,6 +507,16 @@ Coordinate = { x = 145, y = 64, z = 2045 } - **Default:** `true` +#### `ShowEffectNamesInTUI` + +- **Description:** + + This setting lets you show full effect names and levels in the TUI status bar instead of the compact icon-only effect display. + +- **Type:** `boolean` + +- **Default:** `false` + #### `TerrainAndMovements` - **Description:** diff --git a/docs/guide/usage.md b/docs/guide/usage.md index e57a23e1..6c8e284c 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -453,6 +453,21 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+effects + +- **Description:** + + Lists the status effects currently applied to your player. + +- **Usage:** + + ``` + /effects + ``` + +
+
entity From 0c9ff137b2b22055af56cb22e9ec6ce443e768ae Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 27 Mar 2026 16:43:53 +0100 Subject: [PATCH 10/76] Fixed shovel not being able to be used on dirt --- MinecraftClient/Commands/UseItem.cs | 32 ++++++++++++++++++- .../Protocol/Handlers/Protocol18.cs | 18 +++++++++-- .../Translations/Translations.Designer.cs | 2 +- .../Resources/Translations/Translations.resx | 4 +-- docs/guide/usage.md | 8 ++++- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/MinecraftClient/Commands/UseItem.cs b/MinecraftClient/Commands/UseItem.cs index 4a0fe1f6..40993c7a 100644 --- a/MinecraftClient/Commands/UseItem.cs +++ b/MinecraftClient/Commands/UseItem.cs @@ -1,6 +1,8 @@ using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; +using MinecraftClient.Inventory; +using MinecraftClient.Mapping; using static MinecraftClient.CommandHandler.CmdResult; namespace MinecraftClient.Commands @@ -8,7 +10,7 @@ namespace MinecraftClient.Commands class UseItem : Command { public override string CmdName { get { return "useitem"; } } - public override string CmdUsage { get { return "useitem"; } } + public override string CmdUsage { get { return "useitem [x] [y] [z]"; } } public override string CmdDesc { get { return Translations.cmd_useitem_desc; } } public override void RegisterCommand(CommandDispatcher dispatcher) @@ -21,6 +23,8 @@ namespace MinecraftClient.Commands dispatcher.Register(l => l.Literal(CmdName) .Executes(r => DoUseItem(r.Source)) + .Then(l => l.Argument("Location", MccArguments.Location()) + .Executes(r => DoUseItemAtLocation(r.Source, MccArguments.GetLocation(r, "Location")))) .Then(l => l.Literal("_help") .Executes(r => GetUsage(r.Source, string.Empty)) .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) @@ -43,8 +47,34 @@ namespace MinecraftClient.Commands if (!handler.GetInventoryEnabled()) return r.SetAndReturn(Status.FailNeedInventory); + if (handler.GetTerrainEnabled()) + { + const double maxDistance = 4.5; + var raycast = RaycastHelper.RaycastBlock(handler, maxDistance, false); + if (raycast.Item1 && raycast.Item3.Type != Material.Air) + { + handler.PlaceBlock(raycast.Item2, Direction.Up, lookAtBlock: true); + handler.DoAnimation((int)Hand.MainHand); + return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use); + } + } + handler.UseItemOnHand(); return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use); } + + private int DoUseItemAtLocation(CmdResult r, Location block) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetTerrainEnabled()) + return r.SetAndReturn(Status.FailNeedTerrain); + + Location current = handler.GetCurrentLocation(); + block = block.ToAbsolute(current).ToFloor(); + handler.PlaceBlock(block, Direction.Up, lookAtBlock: true); + handler.DoAnimation((int)Hand.MainHand); + return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use); + } + } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index b9128587..6508b05f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -4546,6 +4546,7 @@ namespace MinecraftClient.Protocol.Handlers try { var packet = new List(); + var (cursorX, cursorY, cursorZ) = GetFaceHitCursor(face); switch (protocolVersion) { @@ -4581,9 +4582,9 @@ namespace MinecraftClient.Protocol.Handlers break; } - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorX - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorY - packet.AddRange(dataTypes.GetFloat(0.5f)); // cursorZ + packet.AddRange(dataTypes.GetFloat(cursorX)); // cursorX + packet.AddRange(dataTypes.GetFloat(cursorY)); // cursorY + packet.AddRange(dataTypes.GetFloat(cursorZ)); // cursorZ if(protocolVersion >= MC_1_14_Version) packet.Add(0); // insideBlock = false @@ -4611,6 +4612,17 @@ namespace MinecraftClient.Protocol.Handlers } } + private static (float x, float y, float z) GetFaceHitCursor(Direction face) => face switch + { + Direction.Up => (0.5f, 1.0f, 0.5f), + Direction.Down => (0.5f, 0.0f, 0.5f), + Direction.North => (0.5f, 0.5f, 0.0f), + Direction.South => (0.5f, 0.5f, 1.0f), + Direction.West => (0.0f, 0.5f, 0.5f), + Direction.East => (1.0f, 0.5f, 0.5f), + _ => (0.5f, 0.5f, 0.5f), + }; + public bool SendHeldItemChange(short slot) { try diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index d20d36af..26e79c38 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -4445,7 +4445,7 @@ namespace MinecraftClient { } /// - /// Looks up a localized string similar to Use (left click) an item on the hand. + /// Looks up a localized string similar to Use the item in your hand, optionally on a specific block. /// internal static string cmd_useitem_desc { get { diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 5eb2fa76..9eab9558 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1506,7 +1506,7 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Useblock at ({0:0.0}, {1:0.0}, {2:0.0}) {3}. - Use (left click) an item on the hand + Use the item in your hand, optionally on a specific block Used an item @@ -2299,4 +2299,4 @@ see item details. {0} items - \ No newline at end of file + diff --git a/docs/guide/usage.md b/docs/guide/usage.md index e57a23e1..243b1e10 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -822,7 +822,7 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q - **Description:** - Use item in the hand, this can be used to do a right click on items which open menus on servers. + Use the item in your hand, including use-on-block actions like shovel flattening.

Tip

@@ -842,6 +842,12 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q /useitem ``` + Use the item on a specific block: + + ``` + /useitem + ``` +
From c42133797edcd76bc208e03600d8f46d298966cf Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 00:30:53 +0800 Subject: [PATCH 11/76] Update build-and-release.yml to enhance translation fetching logic - Modified the condition for fetching translations to include checks for CROWDIN_PROJECT_ID and CROWDIN_PERSONAL_TOKEN. - Added environment variables for CROWDIN credentials to streamline the translation process. --- .github/workflows/build-and-release.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index e0baaa91..8901321d 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -34,11 +34,12 @@ jobs: fail-fast: true runs-on: ubuntu-latest needs: determine-build - # Translations will only be fetched in the MCCTeam repository, since it needs crowdin secrets. - if: ${{ needs.determine-build.outputs.skip != 'true' && github.repository == 'MCCTeam/Minecraft-Console-Client' }} + + if: ${{ needs.determine-build.outputs.skip != 'true' }} + timeout-minutes: 15 - - steps: + + steps: - name: Check cache uses: actions/cache/restore@v3 id: cache-check @@ -52,12 +53,13 @@ jobs: if: steps.cache-check.outputs.cache-hit != 'true' uses: actions/checkout@v3 with: - fetch-depth: 0 - submodules: 'true' + fetch-depth: 0 + submodules: 'true' - name: Download translations from crowdin uses: crowdin/github-action@v1.6.0 - if: steps.cache-check.outputs.cache-hit != 'true' + # Translations will only be fetched when Crowdin tokens are available. + if: ${{ steps.cache-check.outputs.cache-hit != 'true' && secrets.CROWDIN_PROJECT_ID && secrets.CROWDIN_TOKEN }} with: upload_sources: false upload_translations: false From da330a158ee55dc79ee06a91c9c41619b8084849 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 00:36:02 +0800 Subject: [PATCH 12/76] Enhance Crowdin integration in build-and-release.yml - Added a step to check for the availability of Crowdin secrets before downloading translations. - Updated the condition for fetching translations to rely on the new check for Crowdin credentials. - Corrected the assembly configuration to use the correct date format for builds. --- .github/workflows/build-and-release.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 8901321d..9cde2ae2 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -56,10 +56,21 @@ jobs: fetch-depth: 0 submodules: 'true' + - name: Check Crowdin secrets + id: crowdin-check + run: | + if [ -z "$CROWDIN_PROJECT_ID" ] || [ -z "$CROWDIN_PERSONAL_TOKEN" ]; then + echo "available=false" >> $GITHUB_OUTPUT + else + echo "available=true" >> $GITHUB_OUTPUT + fi + env: + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }} + - name: Download translations from crowdin uses: crowdin/github-action@v1.6.0 - # Translations will only be fetched when Crowdin tokens are available. - if: ${{ steps.cache-check.outputs.cache-hit != 'true' && secrets.CROWDIN_PROJECT_ID && secrets.CROWDIN_TOKEN }} + if: steps.cache-check.outputs.cache-hit != 'true' && steps.crowdin-check.outputs.available == 'true' with: upload_sources: false upload_translations: false @@ -172,7 +183,7 @@ jobs: - name: Set Version Info run: | echo '' >> ${{ env.assembly-info }} - echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env._dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }} + echo "[assembly: AssemblyConfiguration(\"GitHub build ${{ github.run_number }}, built on ${{ env.date_dashed }} from commit ${{ env.commit }}\")]" >> ${{ env.assembly-info }} - name: Inject Sentry DSN (if applicable) if: ${{ github.repository == 'MCCTeam/Minecraft-Console-Client' }} From ef8e41196b2e8e13a983a777bf0343afebdb99c5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 00:40:05 +0800 Subject: [PATCH 13/76] Update Crowdin GitHub Action version in build-and-release.yml - Upgraded the Crowdin GitHub Action from v1.6.0 to v1.20.4 to leverage the latest features and improvements. --- .github/workflows/build-and-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 9cde2ae2..97147eb8 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -69,7 +69,7 @@ jobs: CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }} - name: Download translations from crowdin - uses: crowdin/github-action@v1.6.0 + uses: crowdin/github-action@v1.20.4 if: steps.cache-check.outputs.cache-hit != 'true' && steps.crowdin-check.outputs.available == 'true' with: upload_sources: false From 19a60fd85c0305268ae1ca5a8e40d0589820f182 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 00:43:28 +0800 Subject: [PATCH 14/76] Update Crowdin GitHub Action version in build-and-release.yml - Upgraded the Crowdin GitHub Action from v1.20.4 to v2.4.0 to incorporate the latest updates and improvements. --- .github/workflows/build-and-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 97147eb8..359bd388 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -69,7 +69,7 @@ jobs: CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }} - name: Download translations from crowdin - uses: crowdin/github-action@v1.20.4 + uses: crowdin/github-action@v2.4.0 if: steps.cache-check.outputs.cache-hit != 'true' && steps.crowdin-check.outputs.available == 'true' with: upload_sources: false From 22f46d14a941f78932539cfaa0446d73dc5ad33b Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 02:16:04 +0800 Subject: [PATCH 15/76] Fix negative location error --- MinecraftClient/Protocol/Handlers/DataTypes.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 473757c1..78456c2d 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -1715,16 +1715,15 @@ namespace MinecraftClient.Protocol.Handlers public byte[] GetLocation(Location location) { byte[] locationBytes; + ulong x = (ulong)(int)Math.Floor(location.X) & 0x3FFFFFF; + ulong y = (ulong)(int)Math.Floor(location.Y) & 0xFFF; + ulong z = (ulong)(int)Math.Floor(location.Z) & 0x3FFFFFF; if (protocolversion >= Protocol18Handler.MC_1_14_Version) { - locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) | - ((((ulong)location.Z) & 0x3FFFFFF) << 12) | - (((ulong)location.Y) & 0xFFF)); + locationBytes = BitConverter.GetBytes((x << 38) | (z << 12) | y); } else - locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) | - ((((ulong)location.Y) & 0xFFF) << 26) | - (((ulong)location.Z) & 0x3FFFFFF)); + locationBytes = BitConverter.GetBytes((x << 38) | (y << 26) | z); Array.Reverse(locationBytes); //Endianness return locationBytes; From ef133f3d6d5ec38602460be55fc211be8e71b731 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 02:16:31 +0800 Subject: [PATCH 16/76] Enhance container handling for Minecraft protocol updates - Updated the Container class to include a protocol version parameter for accurate container type mapping. - Modified the GetContainerType method to account for changes in container types introduced in Minecraft 1.20.4. - Added new container types, including Crafter, to the ContainerType enum. - Adjusted ContainerTypeExtensions to reflect the new container mappings and ensure compatibility with the updated protocol. --- MinecraftClient/Inventory/Container.cs | 74 +++++++++++++++---- MinecraftClient/Inventory/ContainerType.cs | 3 +- .../Inventory/ContainerTypeExtensions.cs | 9 ++- .../Protocol/Handlers/Protocol18.cs | 2 +- 4 files changed, 69 insertions(+), 19 deletions(-) diff --git a/MinecraftClient/Inventory/Container.cs b/MinecraftClient/Inventory/Container.cs index 98908655..f2258fee 100644 --- a/MinecraftClient/Inventory/Container.cs +++ b/MinecraftClient/Inventory/Container.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace MinecraftClient.Inventory { @@ -91,10 +91,11 @@ namespace MinecraftClient.Inventory /// Container ID /// Container Type /// Container Title - public Container(int id, int typeID, string title) + /// Protocol version for version-specific mapping + public Container(int id, int typeID, string title, int protocolVersion = 0) { ID = id; - Type = GetContainerType(typeID); + Type = GetContainerType(typeID, protocolVersion); Title = title; Items = new(); Properties = new(); @@ -131,22 +132,62 @@ namespace MinecraftClient.Inventory /// Get container type from Type ID /// /// Container Type ID + /// Protocol version (menu registry changed across versions) /// Container Type - public static ContainerType GetContainerType(int typeID) + public static ContainerType GetContainerType(int typeID, int protocolVersion = 0) { - // https://wiki.vg/Inventory didn't state the inventory ID, assume that list start with 0 + // MC 1.20.4 (protocol 765) added crafter_3x3 at index 7, shifting all subsequent IDs by +1. + // Registry order from decompiled MenuType.java: + // 1.14-1.20.2: generic_9x1..generic_3x3(6), anvil(7), beacon(8), ... stonecutter(22) + // 1.20.4+: generic_9x1..generic_3x3(6), crafter_3x3(7), anvil(8), beacon(9), ... stonecutter(24) + if (protocolVersion >= 765) + { + return typeID switch + { +#pragma warning disable format // @formatter:off + 0 => ContainerType.Generic_9x1, + 1 => ContainerType.Generic_9x2, + 2 => ContainerType.Generic_9x3, + 3 => ContainerType.Generic_9x4, + 4 => ContainerType.Generic_9x5, + 5 => ContainerType.Generic_9x6, + 6 => ContainerType.Generic_3x3, + 7 => ContainerType.Crafter, + 8 => ContainerType.Anvil, + 9 => ContainerType.Beacon, + 10 => ContainerType.BlastFurnace, + 11 => ContainerType.BrewingStand, + 12 => ContainerType.Crafting, + 13 => ContainerType.Enchantment, + 14 => ContainerType.Furnace, + 15 => ContainerType.Grindstone, + 16 => ContainerType.Hopper, + 17 => ContainerType.Lectern, + 18 => ContainerType.Loom, + 19 => ContainerType.Merchant, + 20 => ContainerType.ShulkerBox, + 21 => ContainerType.SmightingTable, + 22 => ContainerType.Smoker, + 23 => ContainerType.Cartography, + 24 => ContainerType.Stonecutter, + _ => ContainerType.Unknown, +#pragma warning restore format // @formatter:on + }; + } + return typeID switch { - 0 => ContainerType.Generic_9x1, - 1 => ContainerType.Generic_9x2, - 2 => ContainerType.Generic_9x3, - 3 => ContainerType.Generic_9x4, - 4 => ContainerType.Generic_9x5, - 5 => ContainerType.Generic_9x6, - 6 => ContainerType.Generic_3x3, - 7 => ContainerType.Anvil, - 8 => ContainerType.Beacon, - 9 => ContainerType.BlastFurnace, +#pragma warning disable format // @formatter:off + 0 => ContainerType.Generic_9x1, + 1 => ContainerType.Generic_9x2, + 2 => ContainerType.Generic_9x3, + 3 => ContainerType.Generic_9x4, + 4 => ContainerType.Generic_9x5, + 5 => ContainerType.Generic_9x6, + 6 => ContainerType.Generic_3x3, + 7 => ContainerType.Anvil, + 8 => ContainerType.Beacon, + 9 => ContainerType.BlastFurnace, 10 => ContainerType.BrewingStand, 11 => ContainerType.Crafting, 12 => ContainerType.Enchantment, @@ -160,7 +201,8 @@ namespace MinecraftClient.Inventory 20 => ContainerType.Smoker, 21 => ContainerType.Cartography, 22 => ContainerType.Stonecutter, - _ => ContainerType.Unknown, + _ => ContainerType.Unknown, +#pragma warning restore format // @formatter:on }; } diff --git a/MinecraftClient/Inventory/ContainerType.cs b/MinecraftClient/Inventory/ContainerType.cs index 76d05416..e82878fe 100644 --- a/MinecraftClient/Inventory/ContainerType.cs +++ b/MinecraftClient/Inventory/ContainerType.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Inventory +namespace MinecraftClient.Inventory { // For MC 1.14 after ONLY public enum ContainerType @@ -10,6 +10,7 @@ Generic_9x5, Generic_9x6, Generic_3x3, + Crafter, Anvil, Beacon, BlastFurnace, diff --git a/MinecraftClient/Inventory/ContainerTypeExtensions.cs b/MinecraftClient/Inventory/ContainerTypeExtensions.cs index 4fe16373..4644e553 100644 --- a/MinecraftClient/Inventory/ContainerTypeExtensions.cs +++ b/MinecraftClient/Inventory/ContainerTypeExtensions.cs @@ -1,4 +1,4 @@ -namespace MinecraftClient.Inventory +namespace MinecraftClient.Inventory { public static class ContainerTypeExtensions { @@ -13,9 +13,14 @@ { #pragma warning disable format // @formatter:off ContainerType.PlayerInventory => 46, + ContainerType.Generic_9x1 => 45, + ContainerType.Generic_9x2 => 54, ContainerType.Generic_9x3 => 63, + ContainerType.Generic_9x4 => 72, + ContainerType.Generic_9x5 => 81, ContainerType.Generic_9x6 => 90, ContainerType.Generic_3x3 => 45, + ContainerType.Crafter => 45, ContainerType.Crafting => 46, ContainerType.BlastFurnace => 39, ContainerType.Furnace => 39, @@ -27,6 +32,7 @@ ContainerType.Anvil => 39, ContainerType.Hopper => 41, ContainerType.ShulkerBox => 63, + ContainerType.SmightingTable => 39, ContainerType.Loom => 40, ContainerType.Stonecutter => 38, ContainerType.Lectern => 37, @@ -52,6 +58,7 @@ ContainerType.Generic_9x3 => AsciiArt.Container_Generic_9x3, ContainerType.Generic_9x6 => AsciiArt.Container_Generic_9x6, ContainerType.Generic_3x3 => AsciiArt.Container_Generic_3x3, + ContainerType.Crafter => AsciiArt.Container_Generic_3x3, ContainerType.Crafting => AsciiArt.Container_Crafting, ContainerType.BlastFurnace => AsciiArt.Container_Furnace, ContainerType.Furnace => AsciiArt.Container_Furnace, diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 48568f0f..b6cdcd05 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2319,7 +2319,7 @@ namespace MinecraftClient.Protocol.Handlers var windowId = dataTypes.ReadNextVarInt(packetData); var windowType = dataTypes.ReadNextVarInt(packetData); var title = dataTypes.ReadNextChat(packetData); - Container inventory = new(windowId, windowType, ChatParser.ParseText(title)); + Container inventory = new(windowId, windowType, ChatParser.ParseText(title), protocolVersion); handler.OnInventoryOpen(windowId, inventory); } } From 0194380fbc1899a234f838882b64f054c59d0c10 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 02:17:09 +0800 Subject: [PATCH 17/76] TUI support for more container --- MinecraftClient/Commands/Inventory.cs | 2 +- MinecraftClient/McClient.cs | 9 + .../Translations/Translations.Designer.cs | 135 ++++ .../Resources/Translations/Translations.resx | 45 ++ MinecraftClient/Tui/BrewingStandView.cs | 152 ++++ MinecraftClient/Tui/ContainerViewBase.cs | 730 ++++++++++++++++++ MinecraftClient/Tui/ContainerViewModel.cs | 271 +++++++ MinecraftClient/Tui/CraftingView.cs | 112 +++ MinecraftClient/Tui/EnchantingTableView.cs | 198 +++++ MinecraftClient/Tui/FurnaceView.cs | 133 ++++ MinecraftClient/Tui/GridContainerView.cs | 31 + MinecraftClient/Tui/GrindstoneView.cs | 126 +++ MinecraftClient/Tui/HopperView.cs | 30 + MinecraftClient/Tui/InventoryApp.cs | 9 +- MinecraftClient/Tui/InventoryMainView.cs | 665 +--------------- MinecraftClient/Tui/InventoryTuiHost.cs | 29 +- MinecraftClient/Tui/InventoryViewModel.cs | 190 +---- 17 files changed, 2045 insertions(+), 822 deletions(-) create mode 100644 MinecraftClient/Tui/BrewingStandView.cs create mode 100644 MinecraftClient/Tui/ContainerViewBase.cs create mode 100644 MinecraftClient/Tui/ContainerViewModel.cs create mode 100644 MinecraftClient/Tui/CraftingView.cs create mode 100644 MinecraftClient/Tui/EnchantingTableView.cs create mode 100644 MinecraftClient/Tui/FurnaceView.cs create mode 100644 MinecraftClient/Tui/GridContainerView.cs create mode 100644 MinecraftClient/Tui/GrindstoneView.cs create mode 100644 MinecraftClient/Tui/HopperView.cs diff --git a/MinecraftClient/Commands/Inventory.cs b/MinecraftClient/Commands/Inventory.cs index 5936afd0..cc90f185 100644 --- a/MinecraftClient/Commands/Inventory.cs +++ b/MinecraftClient/Commands/Inventory.cs @@ -435,7 +435,7 @@ namespace MinecraftClient.Commands return r.SetAndReturn(CmdResult.Status.Fail, msg); } - if (container.Type != ContainerType.PlayerInventory) + if (!Tui.ContainerViewBase.HasTuiSupport(container.Type)) { handler.Log.Warn(string.Format(Translations.cmd_inventory_tui_unsupported_container, inventoryId)); return r.SetAndReturn(CmdResult.Status.Fail); diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index e76b9b80..938a85bf 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -3124,6 +3124,13 @@ namespace MinecraftClient Log.Info(string.Format(Translations.extra_inventory_open, inventoryID, inventory.Title)); Log.Info(Translations.extra_inventory_interact); DispatchBotEvent(bot => bot.OnInventoryOpen(inventoryID)); + + if (ConsoleIO.Backend is Tui.TuiConsoleBackend + && Tui.ContainerViewBase.HasTuiSupport(inventory.Type) + && Tui.InventoryTuiHost.CanLaunch) + { + Tui.InventoryTuiHost.Launch(this, inventoryID); + } } } @@ -3146,6 +3153,8 @@ namespace MinecraftClient Log.Info(string.Format(Translations.extra_inventory_close, inventoryID)); DispatchBotEvent(bot => bot.OnInventoryClose(inventoryID)); } + + Tui.InventoryTuiHost.NotifyInventoryClosed(inventoryID); } /// diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index cce4eda2..3a2c4bc9 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -6530,6 +6530,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Durability. + /// + internal static string tui_inventory_durability { + get { + return ResourceManager.GetString("tui.inventory.durability", resourceCulture); + } + } + /// /// Looks up a localized string similar to Container not found. /// @@ -6664,5 +6673,131 @@ namespace MinecraftClient { return ResourceManager.GetString("effect.name.with_amplifier", resourceCulture); } } + + /// + /// Looks up a localized string similar to Container. + /// + internal static string tui_container_label { + get { + return ResourceManager.GetString("tui.container.label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input. + /// + internal static string tui_furnace_input { + get { + return ResourceManager.GetString("tui.furnace.input", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fuel. + /// + internal static string tui_furnace_fuel { + get { + return ResourceManager.GetString("tui.furnace.fuel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Output. + /// + internal static string tui_furnace_output { + get { + return ResourceManager.GetString("tui.furnace.output", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item. + /// + internal static string tui_enchanting_item { + get { + return ResourceManager.GetString("tui.enchanting.item", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lapis. + /// + internal static string tui_enchanting_lapis { + get { + return ResourceManager.GetString("tui.enchanting.lapis", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enchant Options. + /// + internal static string tui_enchanting_options { + get { + return ResourceManager.GetString("tui.enchanting.options", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Option {0}. + /// + internal static string tui_enchanting_option_slot { + get { + return ResourceManager.GetString("tui.enchanting.option_slot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fuel. + /// + internal static string tui_brewing_fuel { + get { + return ResourceManager.GetString("tui.brewing.fuel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ingredient. + /// + internal static string tui_brewing_ingredient { + get { + return ResourceManager.GetString("tui.brewing.ingredient", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bottle {0}. + /// + internal static string tui_brewing_bottle { + get { + return ResourceManager.GetString("tui.brewing.bottle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input 1. + /// + internal static string tui_grindstone_input1 { + get { + return ResourceManager.GetString("tui.grindstone.input1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Input 2. + /// + internal static string tui_grindstone_input2 { + get { + return ResourceManager.GetString("tui.grindstone.input2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Crafting. + /// + internal static string tui_crafting_grid { + get { + return ResourceManager.GetString("tui.crafting.grid", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 45662bd5..68202894 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2302,6 +2302,9 @@ see item details. Slot #{0} Count: {1} + + Durability + Container not found @@ -2347,4 +2350,46 @@ see item details. {0} {1} + + Container + + + Input + + + Fuel + + + Output + + + Item + + + Lapis + + + Enchant Options + + + Option {0} + + + Fuel + + + Ingredient + + + Bottle {0} + + + Input 1 + + + Input 2 + + + Crafting + \ No newline at end of file diff --git a/MinecraftClient/Tui/BrewingStandView.cs b/MinecraftClient/Tui/BrewingStandView.cs new file mode 100644 index 00000000..6a00071b --- /dev/null +++ b/MinecraftClient/Tui/BrewingStandView.cs @@ -0,0 +1,152 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class BrewingStandView : ContainerViewBase + { + private readonly BrewingViewModel _brewVm; + + public BrewingStandView(McClient handler, int windowId) + : base(new BrewingViewModel(handler, windowId)) + { + _brewVm = (BrewingViewModel)_vm; + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return 3 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var panel = new StackPanel + { + Spacing = 0, + HorizontalAlignment = HorizontalAlignment.Center, + }; + + var topRow = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Spacing = 0, + }; + + var fuelCol = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + fuelCol.Children.Add(new TextBlock + { + Text = Translations.tui_brewing_fuel, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + fuelCol.Children.Add(CreateSlotCell(_brewVm.FuelSlot, 0, 0)); + topRow.Children.Add(fuelCol); + + topRow.Children.Add(new Border { Width = 2 }); + + var ingredientCol = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + ingredientCol.Children.Add(new TextBlock + { + Text = Translations.tui_brewing_ingredient, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + ingredientCol.Children.Add(CreateSlotCell(_brewVm.IngredientSlot, 0, 1)); + topRow.Children.Add(ingredientCol); + + panel.Children.Add(topRow); + + panel.Children.Add(new TextBlock + { + Text = "\u25bc", + Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)), + HorizontalAlignment = HorizontalAlignment.Center, + }); + + var bottleRow = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Spacing = 0, + }; + for (int i = 0; i < 3; i++) + { + var bottlePanel = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + bottlePanel.Children.Add(new TextBlock + { + Text = string.Format(Translations.tui_brewing_bottle, i + 1), + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + bottlePanel.Children.Add(CreateSlotCell(_brewVm.BottleSlots[i], 1, i)); + bottleRow.Children.Add(bottlePanel); + } + + panel.Children.Add(bottleRow); + + return panel; + } + } + + public class BrewingViewModel : ContainerViewModel + { + public ObservableCollection BottleSlots { get; } = new(); + public SlotViewModel IngredientSlot { get; private set; } = null!; + public SlotViewModel FuelSlot { get; private set; } = null!; + + public BrewingViewModel(McClient handler, int windowId) + : base(handler, windowId, ContainerType.BrewingStand) + { + IngredientSlot = SlotMap[3]; + FuelSlot = SlotMap[4]; + } + + protected override void InitializeSlots() + { + SlotMap.Clear(); + + for (int i = 0; i <= 2; i++) + { + var slot = new SlotViewModel(i); + BottleSlots.Add(slot); + SlotMap[i] = slot; + } + + SlotMap[3] = new SlotViewModel(3); + SlotMap[4] = new SlotViewModel(4); + + for (int i = 5; i <= 31; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 32; i <= 40; i++) + { + int hotbarIdx = i - 32; + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + } +} diff --git a/MinecraftClient/Tui/ContainerViewBase.cs b/MinecraftClient/Tui/ContainerViewBase.cs new file mode 100644 index 00000000..2a5b7668 --- /dev/null +++ b/MinecraftClient/Tui/ContainerViewBase.cs @@ -0,0 +1,730 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public abstract class ContainerViewBase : UserControl + { + protected static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40)); + protected static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55)); + protected static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75)); + protected static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90)); + protected static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140)); + protected static readonly IBrush BrName = Brushes.White; + protected static readonly IBrush BrCount = Brushes.Yellow; + protected static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80)); + protected static readonly IBrush BrEquipLbl = Brushes.DarkCyan; + protected static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60)); + protected static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80)); + protected static readonly IBrush BrHeldItemBorder = Brushes.Yellow; + + protected int _slotW; + protected int _slotH; + protected int _nameMaxLen; + protected int _nameLines; + protected int _termW; + + protected readonly ContainerViewModel _vm; + protected TextBlock _titleText = null!; + protected Border _infoDetailBorder = null!; + protected TextBlock _infoDetailText = null!; + protected TextBlock _cursorItemText = null!; + protected TextBlock _helpText = null!; + + protected TextBlock[] _hotbarIndicators = new TextBlock[9]; + protected int _currentHotbarSlot = -1; + + protected Border? _lastHoveredSlotBorder; + + protected Canvas _overlayCanvas = null!; + protected Border _heldItemFloater = null!; + protected TextBlock _heldItemFloaterName = null!; + protected TextBlock _heldItemFloaterCount = null!; + + protected ScrollViewer _chatScrollViewer = null!; + protected ObservableCollection? _chatLines; + protected int _lastTermW; + protected int _lastTermH; + protected bool _chatScrollToBottom = true; + + protected ContainerViewBase(ContainerViewModel vm) + { + _vm = vm; + _currentHotbarSlot = vm.Handler.GetCurrentSlot(); + + _chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50) + ?? new ObservableCollection(); + } + + protected void Initialize() + { + RebuildUi(); + } + + protected abstract int GetTotalSlotRows(); + + protected abstract Control BuildContainerSpecificArea(); + + protected virtual void OnContainerDataChanged() { } + + protected virtual void RebuildUi() + { + int termH; + try + { + _termW = System.Console.WindowWidth; + termH = System.Console.WindowHeight; + } + catch + { + _termW = 120; + termH = 40; + } + + _lastTermW = _termW; + _lastTermH = termH; + + int availW = _termW - 26; + _slotW = Math.Clamp(availW / 9, 8, 18); + _nameMaxLen = _slotW; + + int totalRows = GetTotalSlotRows(); + int overhead = 4; + int chatMinH = 1; + _slotH = Math.Clamp((termH - overhead - chatMinH) / totalRows, 2, 5); + _nameLines = _slotH; + + _vm.SetSlotDisplayParams(_nameMaxLen, _nameLines); + + _lastHoveredSlotBorder = null; + + _titleText = new TextBlock + { + FontWeight = FontWeight.Bold, + Foreground = Brushes.Cyan, + HorizontalAlignment = HorizontalAlignment.Center, + }; + + _infoDetailText = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Foreground = Brushes.White, + }; + + _infoDetailBorder = new Border + { + Background = Brushes.Transparent, + Padding = new Thickness(0), + Child = _infoDetailText, + }; + + _cursorItemText = new TextBlock + { + Foreground = Brushes.Yellow, + FontWeight = FontWeight.Bold, + TextWrapping = TextWrapping.Wrap, + }; + + _helpText = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)), + Text = Translations.tui_inventory_controls_help, + }; + + _heldItemFloaterName = new TextBlock + { + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + TextWrapping = TextWrapping.Wrap, + }; + _heldItemFloaterCount = new TextBlock + { + Foreground = BrCount, + FontWeight = FontWeight.Bold, + }; + _heldItemFloater = new Border + { + Background = BrHeldItemBg, + BorderBrush = BrHeldItemBorder, + BorderThickness = new Thickness(1), + Padding = new Thickness(1, 0), + IsVisible = false, + MaxWidth = 24, + Child = new StackPanel + { + Children = { _heldItemFloaterName, _heldItemFloaterCount }, + }, + }; + + _overlayCanvas = new Canvas { IsHitTestVisible = false }; + _overlayCanvas.Children.Add(_heldItemFloater); + + var chatLines = _chatLines!; + chatLines.CollectionChanged += (_, _) => + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + var sv = _chatScrollViewer; + if (sv.Extent.Height > sv.Viewport.Height) + sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height); + }, Avalonia.Threading.DispatcherPriority.Background); + }; + var chatItemsControl = new ItemsControl + { + ItemsSource = chatLines, + Focusable = false, + ItemTemplate = new FuncDataTemplate((s, _) => + new TextBlock + { + Text = s, + Foreground = Brushes.Gray, + Padding = new Thickness(0), + Margin = new Thickness(0), + TextWrapping = TextWrapping.Wrap, + }), + }; + _chatScrollViewer = new ScrollViewer + { + Content = chatItemsControl, + Background = Brushes.Black, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + VerticalScrollBarVisibility = ScrollBarVisibility.Hidden, + Padding = new Thickness(0), + }; + + _hotbarIndicators = new TextBlock[9]; + + Content = BuildRootLayout(); + UpdateTitle(); + UpdateInfoPanel(); + + _chatScrollToBottom = true; + _chatScrollViewer.ScrollChanged += OnChatScrollChanged; + } + + private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e) + { + if (!_chatScrollToBottom) return; + var sv = _chatScrollViewer; + if (sv.Extent.Height > sv.Viewport.Height) + { + sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height); + _chatScrollToBottom = false; + } + } + + protected virtual Control BuildRootLayout() + { + var inventoryArea = BuildMainArea(); + DockPanel.SetDock(_titleText, Dock.Top); + DockPanel.SetDock(inventoryArea, Dock.Top); + + var mainContent = new DockPanel + { + Children = { _titleText, inventoryArea, _chatScrollViewer } + }; + + return new Panel + { + Background = Brushes.Black, + Children = { mainContent, _overlayCanvas } + }; + } + + protected virtual Control BuildMainArea() + { + var infoPanel = BuildInfoPanel(); + DockPanel.SetDock(infoPanel, Dock.Right); + + return new DockPanel + { + Children = { infoPanel, BuildInventoryPanel() } + }; + } + + protected virtual Control BuildInventoryPanel() + { + var root = new StackPanel + { + Spacing = 0, + HorizontalAlignment = HorizontalAlignment.Center, + }; + + root.Children.Add(BuildContainerSpecificArea()); + root.Children.Add(BuildSeparator()); + root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9)); + root.Children.Add(BuildHotbarSection()); + + return new Border + { + BorderThickness = new Thickness(1), + BorderBrush = Brushes.Gray, + Child = root, + }; + } + + protected Control BuildSeparator() + { + return new Border + { + Height = 1, + Background = Brushes.Transparent, + Margin = new Thickness(0, 0, 0, 0), + }; + } + + protected Control BuildInfoPanel() + { + return new Border + { + BorderThickness = new Thickness(1), + BorderBrush = Brushes.Gray, + Padding = new Thickness(1), + Width = 24, + Child = new StackPanel + { + Children = + { + new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan }, + _infoDetailBorder, + new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) }, + _cursorItemText, + new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) }, + _helpText, + } + } + }; + } + + protected Control BuildHotbarSection() + { + var panel = new StackPanel { Spacing = 0 }; + + var numberRow = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + }; + for (int i = 0; i < 9; i++) + { + bool active = i == _currentHotbarSlot; + string label = active ? $"{i + 1} \u25bc" : $" {i + 1} "; + + var tb = new TextBlock + { + Text = label, + Width = _slotW, + TextAlignment = TextAlignment.Center, + Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan, + FontWeight = FontWeight.Bold, + }; + _hotbarIndicators[i] = tb; + numberRow.Children.Add(tb); + } + panel.Children.Add(numberRow); + panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9)); + return panel; + } + + protected Control BuildSlotGrid(ObservableCollection slots, int columns) + { + var grid = new Grid(); + int rows = (slots.Count + columns - 1) / columns; + + for (int r = 0; r < rows; r++) + grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + for (int c = 0; c < columns; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + + for (int i = 0; i < slots.Count; i++) + { + int row = i / columns; + int col = i % columns; + var cell = CreateSlotCell(slots[i], row, col); + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + grid.Children.Add(cell); + } + + return grid; + } + + protected static IBrush GetSlotBg(bool isEmpty, int row, int col) + { + bool isA = (row + col) % 2 == 0; + return isEmpty + ? (isA ? BrSlotEmptyA : BrSlotEmptyB) + : (isA ? BrSlotFillA : BrSlotFillB); + } + + protected Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0) + { + var nameTb = new TextBlock + { + TextWrapping = TextWrapping.Wrap, + Padding = new Thickness(0), + Margin = new Thickness(0), + VerticalAlignment = VerticalAlignment.Top, + }; + + var countTb = new TextBlock + { + Foreground = BrCount, + FontWeight = FontWeight.Bold, + Padding = new Thickness(0), + Margin = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Bottom, + }; + + ApplySlotVisual(slot, nameTb, countTb); + + int r = row, c = col; + var border = new Border + { + Width = _slotW, + Height = _slotH, + Background = GetSlotBg(slot.IsEmpty, r, c), + Child = new Panel + { + Children = { nameTb, countTb }, + }, + Tag = (slot, r, c), + }; + + border.PointerPressed += OnSlotPointerPressed; + border.PointerEntered += OnSlotPointerEnter; + border.PointerExited += OnSlotPointerExit; + border.PointerMoved += OnSlotPointerMoved; + + slot.PropertyChanged += (_, _) => + { + ApplySlotVisual(slot, nameTb, countTb); + border.Background = GetSlotBg(slot.IsEmpty, r, c); + }; + + return border; + } + + protected static void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb) + { + if (slot.IsEmpty) + { + nameTb.Text = ""; + nameTb.Foreground = BrDim; + countTb.Text = ""; + } + else + { + nameTb.Text = slot.ItemDisplayText; + nameTb.Foreground = BrName; + countTb.Text = slot.CountDisplay; + } + } + + protected TextBlock MakeLabel(string text) + { + return new TextBlock + { + Text = text, + Foreground = BrEquipLbl, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(1, 0, 0, 0), + FontWeight = FontWeight.Bold, + }; + } + + #region Pointer / Keyboard interaction + + private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int)) + return; + + SetHover(border, slot); + + var point = e.GetCurrentPoint(border); + bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0; + + WindowActionType action; + if (point.Properties.IsRightButtonPressed) + action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick; + else + action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick; + + _vm.PerformAction(slot.SlotId, action); + UpdateInfoPanel(); + UpdateHeldItemFloater(e); + OnContainerDataChanged(); + e.Handled = true; + } + + private void OnSlotPointerEnter(object? sender, PointerEventArgs e) + { + if (sender is Border b && b.Tag is (SlotViewModel slot, int, int)) + { + SetHover(b, slot); + UpdateHeldItemFloater(e); + } + } + + private void OnSlotPointerMoved(object? sender, PointerEventArgs e) + { + if (sender is Border b && b.Tag is (SlotViewModel slot, int, int)) + { + SetHover(b, slot); + UpdateHeldItemFloater(e); + } + } + + private void OnSlotPointerExit(object? sender, PointerEventArgs e) + { + if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col)) + b.Background = GetSlotBg(slot.IsEmpty, row, col); + } + + protected void SetHover(Border border, SlotViewModel slot) + { + if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border) + { + if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc)) + _lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc); + } + + _lastHoveredSlotBorder = border; + border.Background = BrSlotHover; + _vm.HoveredSlot = slot; + UpdateInfoPanel(); + } + + protected void UpdateHeldItemFloater(PointerEventArgs e) + { + if (!_vm.HasCursorItem) + { + _heldItemFloater.IsVisible = false; + return; + } + + _heldItemFloaterName.Text = _vm.CursorItemInfo; + _heldItemFloaterCount.Text = ""; + + try + { + var pos = e.GetPosition(_overlayCanvas); + double left = pos.X + 2; + double remainingW = _termW - left - 2; + int maxW = Math.Max(8, (int)remainingW); + _heldItemFloater.MaxWidth = maxW; + Canvas.SetLeft(_heldItemFloater, left); + Canvas.SetTop(_heldItemFloater, pos.Y); + } + catch + { + _heldItemFloater.MaxWidth = 24; + Canvas.SetLeft(_heldItemFloater, 0); + Canvas.SetTop(_heldItemFloater, 0); + } + + _heldItemFloater.IsVisible = true; + } + + protected void UpdateInfoPanel() + { + _infoDetailText.Text = _vm.HoveredSlotDetailText; + + bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty; + _infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent; + + if (_vm.HasCursorItem) + { + _cursorItemText.Text = _vm.CursorItemInfo; + _cursorItemText.Foreground = Brushes.Yellow; + } + else + { + _cursorItemText.Text = Translations.tui_inventory_cursor_empty; + _cursorItemText.Foreground = BrDim; + _heldItemFloater.IsVisible = false; + } + } + + protected void UpdateTitle() + { + _titleText.Text = _vm.Title; + } + + protected void CloseInventory() + { + if (_vm.WindowId != 0) + _vm.Handler.CloseInventory(_vm.WindowId); + + if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend) + tuiBackend.GetView()?.HideOverlay(); + else + (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + + switch (e.Key) + { + case Key.Escape: + case Key.E: + CloseInventory(); + e.Handled = true; + break; + + case Key.C: + if ((e.KeyModifiers & KeyModifiers.Shift) != 0 && + _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty) + { + _vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick); + UpdateInfoPanel(); + } + e.Handled = true; + break; + + case Key.Q: + if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty) + { + var action = (e.KeyModifiers & KeyModifiers.Control) != 0 + ? WindowActionType.DropItemStack + : WindowActionType.DropItem; + _vm.PerformAction(_vm.HoveredSlot.SlotId, action); + UpdateInfoPanel(); + } + e.Handled = true; + break; + + case Key.R: + _vm.RefreshFromContainer(); + _currentHotbarSlot = _vm.Handler.GetCurrentSlot(); + UpdateHotbarIndicators(); + UpdateInfoPanel(); + OnContainerDataChanged(); + e.Handled = true; + break; + } + } + + protected void UpdateHotbarIndicators() + { + for (int i = 0; i < 9; i++) + { + bool active = i == _currentHotbarSlot; + _hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} "; + _hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan; + } + } + + #endregion + + #region Lifecycle + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Focusable = true; + Focus(); + AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); + SizeChanged += OnViewSizeChanged; + } + + private void OnTunnelKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + CloseInventory(); + e.Handled = true; + } + } + + private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e) + { + int newW, newH; + try + { + newW = System.Console.WindowWidth; + newH = System.Console.WindowHeight; + } + catch { return; } + + if (newW == _lastTermW && newH == _lastTermH) return; + + _vm.RefreshFromContainer(); + _currentHotbarSlot = _vm.Handler.GetCurrentSlot(); + RebuildUi(); + Focus(); + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + Focusable = true; + } + + #endregion + + public static bool HasTuiSupport(ContainerType type) + { + return type switch + { + ContainerType.PlayerInventory => true, + ContainerType.Generic_9x1 => true, + ContainerType.Generic_9x2 => true, + ContainerType.Generic_9x3 => true, + ContainerType.Generic_9x4 => true, + ContainerType.Generic_9x5 => true, + ContainerType.Generic_9x6 => true, + ContainerType.Generic_3x3 => true, + ContainerType.Crafter => true, + ContainerType.ShulkerBox => true, + ContainerType.Crafting => true, + ContainerType.Furnace => true, + ContainerType.BlastFurnace => true, + ContainerType.Smoker => true, + ContainerType.Enchantment => true, + ContainerType.BrewingStand => true, + ContainerType.Hopper => true, + ContainerType.Grindstone => true, + _ => false, + }; + } + + public static ContainerViewBase CreateView(ContainerType type, McClient handler, int windowId) + { + return type switch + { + ContainerType.PlayerInventory => new PlayerInventoryView(handler, windowId), + ContainerType.Generic_9x3 or ContainerType.ShulkerBox => new GridContainerView(handler, windowId, type, 3, 9), + ContainerType.Generic_9x6 => new GridContainerView(handler, windowId, type, 6, 9), + ContainerType.Generic_3x3 or ContainerType.Crafter + => new GridContainerView(handler, windowId, type, 3, 3), + ContainerType.Generic_9x1 => new GridContainerView(handler, windowId, type, 1, 9), + ContainerType.Generic_9x2 => new GridContainerView(handler, windowId, type, 2, 9), + ContainerType.Generic_9x4 => new GridContainerView(handler, windowId, type, 4, 9), + ContainerType.Generic_9x5 => new GridContainerView(handler, windowId, type, 5, 9), + ContainerType.Crafting => new CraftingView(handler, windowId), + ContainerType.Furnace or ContainerType.BlastFurnace or ContainerType.Smoker + => new FurnaceView(handler, windowId, type), + ContainerType.Enchantment => new EnchantingTableView(handler, windowId), + ContainerType.BrewingStand => new BrewingStandView(handler, windowId), + ContainerType.Hopper => new HopperView(handler, windowId), + ContainerType.Grindstone => new GrindstoneView(handler, windowId), + _ => throw new ArgumentException($"No TUI view for {type}"), + }; + } + } +} diff --git a/MinecraftClient/Tui/ContainerViewModel.cs b/MinecraftClient/Tui/ContainerViewModel.cs new file mode 100644 index 00000000..1effcdae --- /dev/null +++ b/MinecraftClient/Tui/ContainerViewModel.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using MinecraftClient.Inventory; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; + +namespace MinecraftClient.Tui +{ + public class ContainerViewModel : INotifyPropertyChanged + { + private SlotViewModel? _hoveredSlot; + private string _title = ""; + private string _statusText = ""; + private string _cursorItemInfo = ""; + private bool _hasCursorItem; + + public McClient Handler { get; } + public int WindowId { get; } + public ContainerType ContainerType { get; } + + public ObservableCollection ContainerSlots { get; } = new(); + public ObservableCollection MainInventorySlots { get; } = new(); + public ObservableCollection HotbarSlots { get; } = new(); + + public string Title + { + get => _title; + set { _title = value; OnPropertyChanged(); } + } + + public string StatusText + { + get => _statusText; + set { _statusText = value; OnPropertyChanged(); } + } + + public string CursorItemInfo + { + get => _cursorItemInfo; + set { _cursorItemInfo = value; OnPropertyChanged(); } + } + + public bool HasCursorItem + { + get => _hasCursorItem; + set { _hasCursorItem = value; OnPropertyChanged(); } + } + + public SlotViewModel? HoveredSlot + { + get => _hoveredSlot; + set + { + if (_hoveredSlot != null) + _hoveredSlot.IsHovered = false; + _hoveredSlot = value; + if (_hoveredSlot != null) + _hoveredSlot.IsHovered = true; + OnPropertyChanged(); + OnPropertyChanged(nameof(HoveredSlotDetailText)); + } + } + + public string HoveredSlotDetailText + { + get + { + if (_hoveredSlot == null) + return Translations.tui_inventory_hover_hint; + + if (_hoveredSlot.IsEmpty) + return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}"; + + var sb = new StringBuilder(); + sb.AppendLine(_hoveredSlot.ItemTypeName); + sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount)); + + var item = _hoveredSlot.RawItem; + if (item != null) + AppendItemExtras(sb, item); + + return sb.ToString().TrimEnd(); + } + } + + protected Dictionary SlotMap { get; } = new(); + + public ContainerViewModel(McClient handler, int windowId, ContainerType containerType) + { + Handler = handler; + WindowId = windowId; + ContainerType = containerType; + + InitializeSlots(); + RefreshFromContainer(); + } + + public void SetSlotDisplayParams(int maxWidth, int maxLines) + { + foreach (var kvp in SlotMap) + { + kvp.Value.NameMaxWidth = maxWidth; + kvp.Value.NameMaxLines = maxLines; + } + RefreshFromContainer(); + } + + protected virtual void InitializeSlots() + { + SlotMap.Clear(); + + int slotCount = ContainerType.SlotCount(); + if (slotCount == 0) return; + + int playerInvStart = slotCount - 36; + + for (int i = 0; i < playerInvStart; i++) + { + var slot = new SlotViewModel(i); + ContainerSlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = playerInvStart; i < playerInvStart + 27; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = playerInvStart + 27; i < slotCount; i++) + { + int hotbarIdx = i - (playerInvStart + 27); + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + + public virtual void RefreshFromContainer() + { + Inventory.Container? container = Handler.GetInventory(WindowId); + if (container == null) + { + StatusText = Translations.tui_inventory_container_not_found; + return; + } + + Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title); + + foreach (var kvp in SlotMap) + { + Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null; + kvp.Value.Update(item); + } + + UpdateCursorItem(container); + int itemCount = 0; + foreach (var kvp in container.Items) + { + if (kvp.Key >= 0 && !kvp.Value.IsEmpty) + itemCount++; + } + StatusText = string.Format(Translations.tui_inventory_item_count, itemCount); + + OnPropertyChanged(nameof(HoveredSlotDetailText)); + } + + protected void UpdateCursorItem(Inventory.Container _) + { + var playerInv = Handler.GetInventory(0); + if (playerInv != null && playerInv.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty) + { + CursorItemInfo = FormatItemDetail(cursorItem); + HasCursorItem = true; + } + else + { + CursorItemInfo = ""; + HasCursorItem = false; + } + } + + protected static string FormatItemDetail(Item item) + { + var sb = new StringBuilder(); + sb.AppendLine($"x{item.Count} {item.GetTypeString()}"); + AppendItemExtras(sb, item); + if (sb.Length > 0 && sb[sb.Length - 1] == '\n') + sb.Remove(sb.Length - 1, 1); + return sb.ToString(); + } + + private static void AppendItemExtras(StringBuilder sb, Item item) + { + int damage = item.Damage; + if (damage != 0) + { + int maxDamage = item.Components?.OfType().FirstOrDefault()?.MaxDamage ?? 0; + if (maxDamage > 0) + sb.AppendLine($"{Translations.tui_inventory_durability}: {maxDamage - damage}/{maxDamage}"); + else + sb.AppendLine($"{Translations.cmd_inventory_damage}: {damage}"); + } + + try + { + var enchList = item.EnchantmentList; + if (enchList is not null) + { + bool isFirstEnchantment = true; + foreach (var ench in enchList) + { + string name = EnchantmentMapping.GetEnchantmentName(ench.Type); + string level = EnchantmentMapping.ConvertLevelToRomanNumbers(ench.Level); + if (isFirstEnchantment) + { + isFirstEnchantment = false; + sb.Append($"{name} {level}"); + } + else + { + sb.Append($" | {name} {level}"); + } + } + } + else if (item.NBT is not null && + (item.NBT.TryGetValue("Enchantments", out object? enchantments) || + item.NBT.TryGetValue("StoredEnchantments", out enchantments))) + { + bool isFirstEnchantment = true; + foreach (Dictionary enchantment in (object[])enchantments) + { + short level = (short)enchantment["lvl"]; + string id = ((string)enchantment["id"]).Replace(':', '.'); + string name = Protocol.Message.ChatParser.TranslateString("enchantment." + id) ?? id; + string levelStr = Protocol.Message.ChatParser.TranslateString("enchantment.level." + level) ?? level.ToString(); + if (isFirstEnchantment) + { + isFirstEnchantment = false; + sb.Append($"{name} {levelStr}"); + } + else + { + sb.Append($" | {name} {levelStr}"); + } + } + } + } + catch { } + } + + public bool PerformAction(int slotId, WindowActionType action) + { + bool result = Handler.DoWindowAction(WindowId, slotId, action); + RefreshFromContainer(); + return result; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + } +} diff --git a/MinecraftClient/Tui/CraftingView.cs b/MinecraftClient/Tui/CraftingView.cs new file mode 100644 index 00000000..dbbfc508 --- /dev/null +++ b/MinecraftClient/Tui/CraftingView.cs @@ -0,0 +1,112 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class CraftingView : ContainerViewBase + { + private readonly CraftingViewModel _craftVm; + + public CraftingView(McClient handler, int windowId) + : base(new CraftingViewModel(handler, windowId)) + { + _craftVm = (CraftingViewModel)_vm; + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return 3 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var row = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + }; + + var gridPanel = new StackPanel { Spacing = 0 }; + gridPanel.Children.Add(new TextBlock + { + Text = Translations.tui_crafting_grid, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + gridPanel.Children.Add(BuildSlotGrid(_craftVm.CraftingGridSlots, 3)); + row.Children.Add(gridPanel); + + row.Children.Add(new TextBlock + { + Text = " \u2192 ", + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + VerticalAlignment = VerticalAlignment.Center, + }); + + var outPanel = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + outPanel.Children.Add(new TextBlock + { + Text = Translations.tui_inventory_output, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + outPanel.Children.Add(CreateSlotCell(_craftVm.OutputSlot, 0, 0)); + row.Children.Add(outPanel); + + return row; + } + } + + public class CraftingViewModel : ContainerViewModel + { + public ObservableCollection CraftingGridSlots { get; } = new(); + public SlotViewModel OutputSlot { get; private set; } = null!; + + public CraftingViewModel(McClient handler, int windowId) + : base(handler, windowId, ContainerType.Crafting) + { + OutputSlot = SlotMap[0]; + } + + protected override void InitializeSlots() + { + SlotMap.Clear(); + + var output = new SlotViewModel(0); + SlotMap[0] = output; + + for (int i = 1; i <= 9; i++) + { + var slot = new SlotViewModel(i); + CraftingGridSlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 10; i <= 36; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 37; i <= 45; i++) + { + int hotbarIdx = i - 37; + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + } +} diff --git a/MinecraftClient/Tui/EnchantingTableView.cs b/MinecraftClient/Tui/EnchantingTableView.cs new file mode 100644 index 00000000..c1386d7a --- /dev/null +++ b/MinecraftClient/Tui/EnchantingTableView.cs @@ -0,0 +1,198 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class EnchantingTableView : ContainerViewBase + { + private readonly EnchantingViewModel _enchantVm; + private readonly TextBlock[] _enchantNameLabels = new TextBlock[3]; + private readonly TextBlock[] _enchantCostLabels = new TextBlock[3]; + + public EnchantingTableView(McClient handler, int windowId) + : base(new EnchantingViewModel(handler, windowId)) + { + _enchantVm = (EnchantingViewModel)_vm; + Initialize(); + } + + private void RefreshEnchantOptions() + { + var container = _vm.Handler.GetInventory(_vm.WindowId); + if (container == null) return; + + int protocolVersion = _vm.Handler.GetProtocolVersion(); + + for (int i = 0; i < 3; i++) + { + if (_enchantNameLabels[i] == null) continue; + + short levelReq = container.Properties.TryGetValue(i, out var lr) ? lr : (short)0; + short enchantId = container.Properties.TryGetValue(i + 4, out var eid) ? eid : (short)-1; + short enchantLevel = container.Properties.TryGetValue(i + 7, out var el) ? el : (short)0; + + if (levelReq > 0 && enchantId >= 0) + { + try + { + var enchant = EnchantmentMapping.GetEnchantmentById(protocolVersion, enchantId); + string name = EnchantmentMapping.GetEnchantmentName(enchant); + string roman = EnchantmentMapping.ConvertLevelToRomanNumbers(enchantLevel); + _enchantNameLabels[i].Text = $"{name} {roman}"; + _enchantCostLabels[i].Text = $" ({levelReq})"; + } + catch + { + _enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1); + _enchantCostLabels[i].Text = levelReq > 0 ? $" ({levelReq})" : ""; + } + } + else + { + _enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1); + _enchantCostLabels[i].Text = ""; + } + } + } + + protected override void OnContainerDataChanged() + { + RefreshEnchantOptions(); + } + + protected override int GetTotalSlotRows() + { + return 3 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Spacing = 0, + }; + + var slotsCol = new StackPanel + { + Spacing = 0, + VerticalAlignment = VerticalAlignment.Center, + }; + + slotsCol.Children.Add(new TextBlock + { + Text = Translations.tui_enchanting_item, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + slotsCol.Children.Add(CreateSlotCell(_enchantVm.ItemSlot, 0, 0)); + + slotsCol.Children.Add(new TextBlock + { + Text = Translations.tui_enchanting_lapis, + Foreground = new SolidColorBrush(Color.FromRgb(60, 80, 200)), + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + slotsCol.Children.Add(CreateSlotCell(_enchantVm.LapisSlot, 1, 0)); + + panel.Children.Add(slotsCol); + + var optionsCol = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(2, 0, 0, 0), + }; + + optionsCol.Children.Add(new TextBlock + { + Text = Translations.tui_enchanting_options, + Foreground = Brushes.Magenta, + FontWeight = FontWeight.Bold, + }); + + int optionWidth = System.Math.Max(_slotW * 4, 30); + + for (int i = 0; i < 3; i++) + { + var nameLabel = new TextBlock + { + Text = string.Format(Translations.tui_enchanting_option_slot, i + 1), + Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)), + TextWrapping = TextWrapping.NoWrap, + }; + _enchantNameLabels[i] = nameLabel; + + var costLabel = new TextBlock + { + Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)), + FontWeight = FontWeight.Bold, + VerticalAlignment = VerticalAlignment.Center, + }; + _enchantCostLabels[i] = costLabel; + + var content = new DockPanel(); + DockPanel.SetDock(costLabel, Dock.Right); + content.Children.Add(costLabel); + content.Children.Add(nameLabel); + + optionsCol.Children.Add(new Border + { + Background = new SolidColorBrush(Color.FromRgb(55, 50, 40)), + MinWidth = optionWidth, + MinHeight = _slotH, + Padding = new Thickness(1, 0), + Child = content, + }); + } + + RefreshEnchantOptions(); + + panel.Children.Add(optionsCol); + + return panel; + } + } + + public class EnchantingViewModel : ContainerViewModel + { + public SlotViewModel ItemSlot { get; private set; } = null!; + public SlotViewModel LapisSlot { get; private set; } = null!; + + public EnchantingViewModel(McClient handler, int windowId) + : base(handler, windowId, ContainerType.Enchantment) + { + ItemSlot = SlotMap[0]; + LapisSlot = SlotMap[1]; + } + + protected override void InitializeSlots() + { + SlotMap.Clear(); + + SlotMap[0] = new SlotViewModel(0); + SlotMap[1] = new SlotViewModel(1); + + for (int i = 2; i <= 28; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 29; i <= 37; i++) + { + int hotbarIdx = i - 29; + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + } +} diff --git a/MinecraftClient/Tui/FurnaceView.cs b/MinecraftClient/Tui/FurnaceView.cs new file mode 100644 index 00000000..cde8054c --- /dev/null +++ b/MinecraftClient/Tui/FurnaceView.cs @@ -0,0 +1,133 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class FurnaceView : ContainerViewBase + { + private readonly FurnaceViewModel _furnaceVm; + + public FurnaceView(McClient handler, int windowId, ContainerType type) + : base(new FurnaceViewModel(handler, windowId, type)) + { + _furnaceVm = (FurnaceViewModel)_vm; + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return 3 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Spacing = 0, + }; + + var leftCol = new StackPanel + { + Spacing = 0, + VerticalAlignment = VerticalAlignment.Center, + }; + + leftCol.Children.Add(new TextBlock + { + Text = Translations.tui_furnace_input, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + leftCol.Children.Add(CreateSlotCell(_furnaceVm.InputSlot, 0, 0)); + + leftCol.Children.Add(new TextBlock + { + Text = "\u2592\u2592\u2592", + Foreground = new SolidColorBrush(Color.FromRgb(180, 100, 40)), + HorizontalAlignment = HorizontalAlignment.Center, + }); + + leftCol.Children.Add(new TextBlock + { + Text = Translations.tui_furnace_fuel, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + leftCol.Children.Add(CreateSlotCell(_furnaceVm.FuelSlot, 1, 0)); + + panel.Children.Add(leftCol); + + panel.Children.Add(new TextBlock + { + Text = " \u2192 ", + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + VerticalAlignment = VerticalAlignment.Center, + }); + + var rightCol = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + rightCol.Children.Add(new TextBlock + { + Text = Translations.tui_furnace_output, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + rightCol.Children.Add(CreateSlotCell(_furnaceVm.OutputSlot, 0, 1)); + + panel.Children.Add(rightCol); + + return panel; + } + } + + public class FurnaceViewModel : ContainerViewModel + { + public SlotViewModel InputSlot { get; private set; } = null!; + public SlotViewModel FuelSlot { get; private set; } = null!; + public SlotViewModel OutputSlot { get; private set; } = null!; + + public FurnaceViewModel(McClient handler, int windowId, ContainerType type) + : base(handler, windowId, type) + { + InputSlot = SlotMap[0]; + FuelSlot = SlotMap[1]; + OutputSlot = SlotMap[2]; + } + + protected override void InitializeSlots() + { + SlotMap.Clear(); + + SlotMap[0] = new SlotViewModel(0); + SlotMap[1] = new SlotViewModel(1); + SlotMap[2] = new SlotViewModel(2); + + for (int i = 3; i <= 29; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 30; i <= 38; i++) + { + int hotbarIdx = i - 30; + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + } +} diff --git a/MinecraftClient/Tui/GridContainerView.cs b/MinecraftClient/Tui/GridContainerView.cs new file mode 100644 index 00000000..c2da293b --- /dev/null +++ b/MinecraftClient/Tui/GridContainerView.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class GridContainerView : ContainerViewBase + { + private readonly int _gridRows; + private readonly int _gridCols; + + public GridContainerView(McClient handler, int windowId, ContainerType type, int rows, int cols) + : base(new ContainerViewModel(handler, windowId, type)) + { + _gridRows = rows; + _gridCols = cols; + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return _gridRows + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + return BuildSlotGrid(_vm.ContainerSlots, _gridCols); + } + } +} diff --git a/MinecraftClient/Tui/GrindstoneView.cs b/MinecraftClient/Tui/GrindstoneView.cs new file mode 100644 index 00000000..04a283a6 --- /dev/null +++ b/MinecraftClient/Tui/GrindstoneView.cs @@ -0,0 +1,126 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class GrindstoneView : ContainerViewBase + { + private readonly GrindstoneViewModel _grindVm; + + public GrindstoneView(McClient handler, int windowId) + : base(new GrindstoneViewModel(handler, windowId)) + { + _grindVm = (GrindstoneViewModel)_vm; + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return 2 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var row = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Center, + Spacing = 0, + }; + + var inputCol = new StackPanel + { + Spacing = 0, + VerticalAlignment = VerticalAlignment.Center, + }; + + inputCol.Children.Add(new TextBlock + { + Text = Translations.tui_grindstone_input1, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + inputCol.Children.Add(CreateSlotCell(_grindVm.Input1Slot, 0, 0)); + + inputCol.Children.Add(new TextBlock + { + Text = Translations.tui_grindstone_input2, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + inputCol.Children.Add(CreateSlotCell(_grindVm.Input2Slot, 1, 0)); + + row.Children.Add(inputCol); + + row.Children.Add(new TextBlock + { + Text = "=>", + Foreground = Brushes.White, + FontWeight = FontWeight.Bold, + VerticalAlignment = VerticalAlignment.Center, + Padding = new Thickness(1, 0), + }); + + var outCol = new StackPanel + { + VerticalAlignment = VerticalAlignment.Center, + }; + outCol.Children.Add(new TextBlock + { + Text = Translations.tui_inventory_output, + Foreground = BrEquipLbl, + FontWeight = FontWeight.Bold, + HorizontalAlignment = HorizontalAlignment.Center, + }); + outCol.Children.Add(CreateSlotCell(_grindVm.OutputSlot, 0, 1)); + row.Children.Add(outCol); + + return row; + } + } + + public class GrindstoneViewModel : ContainerViewModel + { + public SlotViewModel Input1Slot { get; private set; } = null!; + public SlotViewModel Input2Slot { get; private set; } = null!; + public SlotViewModel OutputSlot { get; private set; } = null!; + + public GrindstoneViewModel(McClient handler, int windowId) + : base(handler, windowId, ContainerType.Grindstone) + { + Input1Slot = SlotMap[0]; + Input2Slot = SlotMap[1]; + OutputSlot = SlotMap[2]; + } + + protected override void InitializeSlots() + { + SlotMap.Clear(); + + SlotMap[0] = new SlotViewModel(0); + SlotMap[1] = new SlotViewModel(1); + SlotMap[2] = new SlotViewModel(2); + + for (int i = 3; i <= 29; i++) + { + var slot = new SlotViewModel(i); + MainInventorySlots.Add(slot); + SlotMap[i] = slot; + } + + for (int i = 30; i <= 38; i++) + { + int hotbarIdx = i - 30; + var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); + HotbarSlots.Add(slot); + SlotMap[i] = slot; + } + } + } +} diff --git a/MinecraftClient/Tui/HopperView.cs b/MinecraftClient/Tui/HopperView.cs new file mode 100644 index 00000000..bb69b2ae --- /dev/null +++ b/MinecraftClient/Tui/HopperView.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using MinecraftClient.Inventory; + +namespace MinecraftClient.Tui +{ + public class HopperView : ContainerViewBase + { + public HopperView(McClient handler, int windowId) + : base(new ContainerViewModel(handler, windowId, ContainerType.Hopper)) + { + Initialize(); + } + + protected override int GetTotalSlotRows() + { + return 1 + 3 + 1; + } + + protected override Control BuildContainerSpecificArea() + { + var grid = BuildSlotGrid(_vm.ContainerSlots, 5); + return new StackPanel + { + HorizontalAlignment = HorizontalAlignment.Center, + Children = { grid }, + }; + } + } +} diff --git a/MinecraftClient/Tui/InventoryApp.cs b/MinecraftClient/Tui/InventoryApp.cs index ac71bb39..ea866300 100644 --- a/MinecraftClient/Tui/InventoryApp.cs +++ b/MinecraftClient/Tui/InventoryApp.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Consolonia.Themes; +using MinecraftClient.Inventory; namespace MinecraftClient.Tui { @@ -16,9 +17,15 @@ namespace MinecraftClient.Tui { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + var handler = InventoryTuiHost.ActiveHandler!; + var windowId = InventoryTuiHost.ActiveWindowId; + var container = handler.GetInventory(windowId); + var containerType = container?.Type ?? ContainerType.PlayerInventory; + var view = ContainerViewBase.CreateView(containerType, handler, windowId); + desktop.MainWindow = new Window { - Content = new InventoryMainView(), + Content = view, Title = "MCC Inventory" }; } diff --git a/MinecraftClient/Tui/InventoryMainView.cs b/MinecraftClient/Tui/InventoryMainView.cs index 7cd95974..d141870d 100644 --- a/MinecraftClient/Tui/InventoryMainView.cs +++ b/MinecraftClient/Tui/InventoryMainView.cs @@ -1,304 +1,39 @@ -using System; -using System.Collections.ObjectModel; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; -using MinecraftClient.Inventory; namespace MinecraftClient.Tui { - public class InventoryMainView : UserControl + public class PlayerInventoryView : ContainerViewBase { - private static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40)); - private static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55)); - private static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75)); - private static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90)); - private static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140)); - private static readonly IBrush BrName = Brushes.White; - private static readonly IBrush BrCount = Brushes.Yellow; - private static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80)); - private static readonly IBrush BrEquipLbl = Brushes.DarkCyan; - private static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60)); - private static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80)); - private static readonly IBrush BrHeldItemBorder = Brushes.Yellow; - - private int _slotW; - private int _slotH; - private int _nameMaxLen; - private int _nameLines; + private readonly PlayerInventoryViewModel _playerVm; private int _topGap; - private int _termW; - private readonly InventoryViewModel _vm; - private TextBlock _titleText = null!; - private Border _infoDetailBorder = null!; - private TextBlock _infoDetailText = null!; - private TextBlock _cursorItemText = null!; - private TextBlock _helpText = null!; - - private TextBlock[] _hotbarIndicators = new TextBlock[9]; - private int _currentHotbarSlot = -1; - - private Border? _lastHoveredSlotBorder; - - private Canvas _overlayCanvas = null!; - private Border _heldItemFloater = null!; - private TextBlock _heldItemFloaterName = null!; - private TextBlock _heldItemFloaterCount = null!; - - private ScrollViewer _chatScrollViewer = null!; - private ObservableCollection? _chatLines; - private int _lastTermW; - private int _lastTermH; - - public InventoryMainView() + public PlayerInventoryView(McClient handler, int windowId) + : base(new PlayerInventoryViewModel(handler, windowId)) { - var handler = InventoryTuiHost.ActiveHandler - ?? throw new InvalidOperationException("No active McClient"); - int windowId = InventoryTuiHost.ActiveWindowId; - - _vm = new InventoryViewModel(handler, windowId); - _currentHotbarSlot = handler.GetCurrentSlot(); - - _chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50) - ?? new ObservableCollection(); - - RebuildUi(); + _playerVm = (PlayerInventoryViewModel)_vm; + Initialize(); } - private void RebuildUi() + protected override int GetTotalSlotRows() { - int termH; - try - { - _termW = System.Console.WindowWidth; - termH = System.Console.WindowHeight; - } - catch - { - _termW = 120; - termH = 40; - } - - _lastTermW = _termW; - _lastTermH = termH; - - int availW = _termW - 26; - _slotW = Math.Clamp(availW / 9, 8, 18); - _nameMaxLen = _slotW; - - int topUsedW = _slotW * 4 + 8 + _slotW * 2 + 4 + _slotW; - _topGap = Math.Max(2, (_slotW * 9 - topUsedW) / 2); - - _slotH = Math.Clamp((termH - 8) / 6, 2, 5); - _nameLines = _slotH; - - _vm.SetSlotDisplayParams(_nameMaxLen, _nameLines); - - _lastHoveredSlotBorder = null; - - _titleText = new TextBlock - { - FontWeight = FontWeight.Bold, - Foreground = Brushes.Cyan, - HorizontalAlignment = HorizontalAlignment.Center, - }; - - _infoDetailText = new TextBlock - { - TextWrapping = TextWrapping.Wrap, - Foreground = Brushes.White, - }; - - _infoDetailBorder = new Border - { - Background = Brushes.Transparent, - Padding = new Thickness(0), - Child = _infoDetailText, - }; - - _cursorItemText = new TextBlock - { - Foreground = Brushes.Yellow, - FontWeight = FontWeight.Bold, - TextWrapping = TextWrapping.Wrap, - }; - - _helpText = new TextBlock - { - TextWrapping = TextWrapping.Wrap, - Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)), - Text = Translations.tui_inventory_controls_help, - }; - - _heldItemFloaterName = new TextBlock - { - Foreground = Brushes.White, - FontWeight = FontWeight.Bold, - TextWrapping = TextWrapping.Wrap, - }; - _heldItemFloaterCount = new TextBlock - { - Foreground = BrCount, - FontWeight = FontWeight.Bold, - }; - _heldItemFloater = new Border - { - Background = BrHeldItemBg, - BorderBrush = BrHeldItemBorder, - BorderThickness = new Thickness(1), - Padding = new Thickness(1, 0), - IsVisible = false, - MaxWidth = 24, - Child = new StackPanel - { - Children = { _heldItemFloaterName, _heldItemFloaterCount }, - }, - }; - - _overlayCanvas = new Canvas { IsHitTestVisible = false }; - _overlayCanvas.Children.Add(_heldItemFloater); - - var chatLines = _chatLines!; - chatLines.CollectionChanged += (_, _) => - { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - var sv = _chatScrollViewer; - if (sv.Extent.Height > sv.Viewport.Height) - sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height); - }, Avalonia.Threading.DispatcherPriority.Background); - }; - var chatItemsControl = new ItemsControl - { - ItemsSource = chatLines, - Focusable = false, - ItemTemplate = new FuncDataTemplate((s, _) => - new TextBlock - { - Text = s, - Foreground = Brushes.Gray, - Padding = new Thickness(0), - Margin = new Thickness(0), - TextWrapping = TextWrapping.Wrap, - }), - }; - _chatScrollViewer = new ScrollViewer - { - Content = chatItemsControl, - Background = Brushes.Black, - HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, - VerticalScrollBarVisibility = ScrollBarVisibility.Hidden, - Padding = new Thickness(0), - }; - - _hotbarIndicators = new TextBlock[9]; - - Content = BuildRootLayout(); - UpdateTitle(); - UpdateInfoPanel(); - - _chatScrollToBottom = true; - _chatScrollViewer.ScrollChanged += OnChatScrollChanged; + return 6; } - private bool _chatScrollToBottom = true; - - private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e) + protected override void RebuildUi() { - if (!_chatScrollToBottom) return; - var sv = _chatScrollViewer; - if (sv.Extent.Height > sv.Viewport.Height) - { - sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height); - _chatScrollToBottom = false; - } + int availW = 0; + try { availW = System.Console.WindowWidth - 26; } catch { availW = 94; } + int slotW = System.Math.Clamp(availW / 9, 8, 18); + int topUsedW = slotW * 4 + 8 + slotW * 2 + 4 + slotW; + _topGap = System.Math.Max(2, (slotW * 9 - topUsedW) / 2); + + base.RebuildUi(); } - private Control BuildRootLayout() - { - // Layout (top-down): - // Title - // [InfoPanel(right)] [InventoryGrid(left)] <-- inventory area - // ChatScrollViewer (full width, fills remaining) - - var inventoryArea = BuildMainArea(); - DockPanel.SetDock(_titleText, Dock.Top); - DockPanel.SetDock(inventoryArea, Dock.Top); - - var mainContent = new DockPanel - { - Children = { _titleText, inventoryArea, _chatScrollViewer } - }; - - return new Panel - { - Background = Brushes.Black, - Children = { mainContent, _overlayCanvas } - }; - } - - private Control BuildMainArea() - { - var infoPanel = BuildInfoPanel(); - DockPanel.SetDock(infoPanel, Dock.Right); - - return new DockPanel - { - Children = { infoPanel, BuildInventoryPanel() } - }; - } - - private Control BuildInfoPanel() - { - return new Border - { - BorderThickness = new Thickness(1), - BorderBrush = Brushes.Gray, - Padding = new Thickness(1), - Width = 24, - Child = new StackPanel - { - Children = - { - new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan }, - _infoDetailBorder, - new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) }, - _cursorItemText, - new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) }, - _helpText, - } - } - }; - } - - private Control BuildInventoryPanel() - { - var root = new StackPanel - { - Spacing = 0, - HorizontalAlignment = HorizontalAlignment.Center, - }; - - root.Children.Add(BuildTopSection()); - root.Children.Add(new Border { Height = 1 }); - root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9)); - root.Children.Add(BuildHotbarSection()); - - return new Border - { - BorderThickness = new Thickness(1), - BorderBrush = Brushes.Gray, - Child = root, - }; - } - - private Control BuildTopSection() + protected override Control BuildContainerSpecificArea() { var row = new StackPanel { @@ -318,7 +53,7 @@ namespace MinecraftClient.Tui FontWeight = FontWeight.Bold, HorizontalAlignment = HorizontalAlignment.Center, }); - offPanel.Children.Add(CreateSlotCell(_vm.OffhandSlot, 0, 0)); + offPanel.Children.Add(CreateSlotCell(_playerVm.OffhandSlot, 0, 0)); row.Children.Add(offPanel); var equipGrid = new Grid @@ -332,7 +67,7 @@ namespace MinecraftClient.Tui var lbl = MakeLabel(label); Grid.SetRow(lbl, r); Grid.SetColumn(lbl, gc); equipGrid.Children.Add(lbl); - var btn = CreateSlotCell(_vm.EquipmentSlots[eqIdx], r, gc / 2); + var btn = CreateSlotCell(_playerVm.EquipmentSlots[eqIdx], r, gc / 2); Grid.SetRow(btn, r); Grid.SetColumn(btn, gc + 1); equipGrid.Children.Add(btn); } @@ -354,7 +89,7 @@ namespace MinecraftClient.Tui for (int ci = 0; ci < 4; ci++) { int cr = ci / 2, cc = ci % 2; - var cs = CreateSlotCell(_vm.CraftingInputSlots[ci], cr, cc); + var cs = CreateSlotCell(_playerVm.CraftingInputSlots[ci], cr, cc); Grid.SetRow(cs, cr); Grid.SetColumn(cs, cc); craftGrid.Children.Add(cs); @@ -382,7 +117,7 @@ namespace MinecraftClient.Tui FontWeight = FontWeight.Bold, HorizontalAlignment = HorizontalAlignment.Center, }); - craftOutPanel.Children.Add(CreateSlotCell(_vm.CraftingOutputSlot, 0, 1)); + craftOutPanel.Children.Add(CreateSlotCell(_playerVm.CraftingOutputSlot, 0, 1)); Grid.SetRow(craftOutPanel, 0); Grid.SetColumn(craftOutPanel, 3); Grid.SetRowSpan(craftOutPanel, 2); craftGrid.Children.Add(craftOutPanel); @@ -390,363 +125,5 @@ namespace MinecraftClient.Tui row.Children.Add(craftGrid); return row; } - - private Control BuildHotbarSection() - { - var panel = new StackPanel { Spacing = 0 }; - - var numberRow = new StackPanel - { - Orientation = Orientation.Horizontal, - HorizontalAlignment = HorizontalAlignment.Center, - }; - for (int i = 0; i < 9; i++) - { - bool active = i == _currentHotbarSlot; - string label = active ? $"{i + 1} \u25bc" : $" {i + 1} "; - - var tb = new TextBlock - { - Text = label, - Width = _slotW, - TextAlignment = TextAlignment.Center, - Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan, - FontWeight = FontWeight.Bold, - }; - _hotbarIndicators[i] = tb; - numberRow.Children.Add(tb); - } - panel.Children.Add(numberRow); - panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9)); - return panel; - } - - private TextBlock MakeLabel(string text) - { - return new TextBlock - { - Text = text, - Foreground = BrEquipLbl, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(1, 0, 0, 0), - FontWeight = FontWeight.Bold, - }; - } - - private Control BuildSlotGrid(ObservableCollection slots, int columns) - { - var grid = new Grid(); - int rows = (slots.Count + columns - 1) / columns; - - for (int r = 0; r < rows; r++) - grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); - for (int c = 0; c < columns; c++) - grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); - - for (int i = 0; i < slots.Count; i++) - { - int row = i / columns; - int col = i % columns; - var cell = CreateSlotCell(slots[i], row, col); - Grid.SetRow(cell, row); - Grid.SetColumn(cell, col); - grid.Children.Add(cell); - } - - return grid; - } - - private static IBrush GetSlotBg(bool isEmpty, int row, int col) - { - bool isA = (row + col) % 2 == 0; - return isEmpty - ? (isA ? BrSlotEmptyA : BrSlotEmptyB) - : (isA ? BrSlotFillA : BrSlotFillB); - } - - private Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0) - { - var nameTb = new TextBlock - { - TextWrapping = TextWrapping.Wrap, - Padding = new Thickness(0), - Margin = new Thickness(0), - VerticalAlignment = VerticalAlignment.Top, - }; - - var countTb = new TextBlock - { - Foreground = BrCount, - FontWeight = FontWeight.Bold, - Padding = new Thickness(0), - Margin = new Thickness(0), - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Bottom, - }; - - ApplySlotVisual(slot, nameTb, countTb); - - int r = row, c = col; - var border = new Border - { - Width = _slotW, - Height = _slotH, - Background = GetSlotBg(slot.IsEmpty, r, c), - Child = new Panel - { - Children = { nameTb, countTb }, - }, - Tag = (slot, r, c), - }; - - border.PointerPressed += OnSlotPointerPressed; - border.PointerEntered += OnSlotPointerEnter; - border.PointerExited += OnSlotPointerExit; - border.PointerMoved += OnSlotPointerMoved; - - slot.PropertyChanged += (_, _) => - { - ApplySlotVisual(slot, nameTb, countTb); - border.Background = GetSlotBg(slot.IsEmpty, r, c); - }; - - return border; - } - - private void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb) - { - if (slot.IsEmpty) - { - nameTb.Text = ""; - nameTb.Foreground = BrDim; - countTb.Text = ""; - } - else - { - nameTb.Text = slot.ItemDisplayText; - nameTb.Foreground = BrName; - countTb.Text = slot.CountDisplay; - } - } - - private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int)) - return; - - SetHover(border, slot); - - var point = e.GetCurrentPoint(border); - bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0; - - WindowActionType action; - if (point.Properties.IsRightButtonPressed) - action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick; - else - action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick; - - _vm.PerformAction(slot.SlotId, action); - UpdateInfoPanel(); - UpdateHeldItemFloater(e); - e.Handled = true; - } - - private void OnSlotPointerEnter(object? sender, PointerEventArgs e) - { - if (sender is Border b && b.Tag is (SlotViewModel slot, int, int)) - { - SetHover(b, slot); - UpdateHeldItemFloater(e); - } - } - - private void OnSlotPointerMoved(object? sender, PointerEventArgs e) - { - if (sender is Border b && b.Tag is (SlotViewModel slot, int, int)) - { - SetHover(b, slot); - UpdateHeldItemFloater(e); - } - } - - private void OnSlotPointerExit(object? sender, PointerEventArgs e) - { - if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col)) - b.Background = GetSlotBg(slot.IsEmpty, row, col); - } - - private void SetHover(Border border, SlotViewModel slot) - { - if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border) - { - if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc)) - _lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc); - } - - _lastHoveredSlotBorder = border; - border.Background = BrSlotHover; - _vm.HoveredSlot = slot; - UpdateInfoPanel(); - } - - private void UpdateHeldItemFloater(PointerEventArgs e) - { - if (!_vm.HasCursorItem) - { - _heldItemFloater.IsVisible = false; - return; - } - - _heldItemFloaterName.Text = _vm.CursorItemInfo; - _heldItemFloaterCount.Text = ""; - - try - { - var pos = e.GetPosition(_overlayCanvas); - double left = pos.X + 2; - double remainingW = _termW - left - 2; - int maxW = Math.Max(8, (int)remainingW); - _heldItemFloater.MaxWidth = maxW; - Canvas.SetLeft(_heldItemFloater, left); - Canvas.SetTop(_heldItemFloater, pos.Y); - } - catch - { - _heldItemFloater.MaxWidth = 24; - Canvas.SetLeft(_heldItemFloater, 0); - Canvas.SetTop(_heldItemFloater, 0); - } - - _heldItemFloater.IsVisible = true; - } - - private void UpdateInfoPanel() - { - _infoDetailText.Text = _vm.HoveredSlotDetailText; - - bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty; - _infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent; - - if (_vm.HasCursorItem) - { - _cursorItemText.Text = _vm.CursorItemInfo; - _cursorItemText.Foreground = Brushes.Yellow; - } - else - { - _cursorItemText.Text = Translations.tui_inventory_cursor_empty; - _cursorItemText.Foreground = BrDim; - _heldItemFloater.IsVisible = false; - } - } - - private void UpdateTitle() - { - _titleText.Text = _vm.Title; - } - - private void CloseInventory() - { - if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend) - tuiBackend.GetView()?.HideOverlay(); - else - (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - - switch (e.Key) - { - case Key.Escape: - case Key.E: - CloseInventory(); - e.Handled = true; - break; - - case Key.C: - if ((e.KeyModifiers & KeyModifiers.Shift) != 0 && - _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty) - { - _vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick); - UpdateInfoPanel(); - } - e.Handled = true; - break; - - case Key.Q: - if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty) - { - var action = (e.KeyModifiers & KeyModifiers.Control) != 0 - ? WindowActionType.DropItemStack - : WindowActionType.DropItem; - _vm.PerformAction(_vm.HoveredSlot.SlotId, action); - UpdateInfoPanel(); - } - e.Handled = true; - break; - - case Key.R: - _vm.RefreshFromContainer(); - _currentHotbarSlot = _vm.Handler.GetCurrentSlot(); - UpdateHotbarIndicators(); - UpdateInfoPanel(); - e.Handled = true; - break; - } - } - - private void UpdateHotbarIndicators() - { - for (int i = 0; i < 9; i++) - { - bool active = i == _currentHotbarSlot; - _hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} "; - _hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan; - } - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - Focusable = true; - Focus(); - AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); - SizeChanged += OnViewSizeChanged; - } - - private void OnTunnelKeyDown(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Escape) - { - CloseInventory(); - e.Handled = true; - } - } - - private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e) - { - int newW, newH; - try - { - newW = System.Console.WindowWidth; - newH = System.Console.WindowHeight; - } - catch { return; } - - if (newW == _lastTermW && newH == _lastTermH) return; - - _vm.RefreshFromContainer(); - _currentHotbarSlot = _vm.Handler.GetCurrentSlot(); - RebuildUi(); - Focus(); - } - - protected override void OnGotFocus(GotFocusEventArgs e) - { - base.OnGotFocus(e); - Focusable = true; - } } } diff --git a/MinecraftClient/Tui/InventoryTuiHost.cs b/MinecraftClient/Tui/InventoryTuiHost.cs index c67c1892..f740ffd4 100644 --- a/MinecraftClient/Tui/InventoryTuiHost.cs +++ b/MinecraftClient/Tui/InventoryTuiHost.cs @@ -22,6 +22,30 @@ namespace MinecraftClient.Tui public static bool IsRunning => _isRunning; + /// + /// Called by McClient.OnInventoryClose when the server closes a container. + /// If the closed window matches the active TUI window, auto-close the TUI. + /// + public static void NotifyInventoryClosed(int windowId) + { + if (!_isRunning || windowId != ActiveWindowId) + return; + + if (ConsoleIO.Backend is TuiConsoleBackend) + { + Dispatcher.UIThread.Post(() => + { + var view = TuiConsoleBackend.Instance?.GetView(); + view?.HideOverlay(); + }); + } + else + { + (Avalonia.Application.Current?.ApplicationLifetime + as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime)?.Shutdown(); + } + } + /// /// Whether the TUI can be launched (classic mode has a one-shot limit). /// @@ -89,7 +113,10 @@ namespace MinecraftClient.Tui var view = TuiConsoleBackend.Instance?.GetView(); if (view != null) { - var content = new InventoryMainView(); + var container = ActiveHandler!.GetInventory(ActiveWindowId); + var content = ContainerViewBase.CreateView( + container?.Type ?? ContainerType.PlayerInventory, + ActiveHandler, ActiveWindowId); view.ShowOverlay(content, () => { ActiveHandler = null; diff --git a/MinecraftClient/Tui/InventoryViewModel.cs b/MinecraftClient/Tui/InventoryViewModel.cs index bfb383b2..3b29928f 100644 --- a/MinecraftClient/Tui/InventoryViewModel.cs +++ b/MinecraftClient/Tui/InventoryViewModel.cs @@ -1,152 +1,48 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Text; using MinecraftClient.Inventory; namespace MinecraftClient.Tui { - public class InventoryViewModel : INotifyPropertyChanged + public class PlayerInventoryViewModel : ContainerViewModel { - private SlotViewModel? _hoveredSlot; - private string _title = ""; - private string _statusText = ""; - private string _cursorItemInfo = ""; - private bool _hasCursorItem; - - public McClient Handler { get; } - public int WindowId { get; } - public ObservableCollection EquipmentSlots { get; } = new(); public ObservableCollection CraftingInputSlots { get; } = new(); public SlotViewModel CraftingOutputSlot { get; } - public ObservableCollection MainInventorySlots { get; } = new(); - public ObservableCollection HotbarSlots { get; } = new(); public SlotViewModel OffhandSlot { get; } - public string Title + public PlayerInventoryViewModel(McClient handler, int windowId) + : base(handler, windowId, ContainerType.PlayerInventory) { - get => _title; - set { _title = value; OnPropertyChanged(); } + CraftingOutputSlot = SlotMap[0]; + OffhandSlot = SlotMap[45]; } - public string StatusText + protected override void InitializeSlots() { - get => _statusText; - set { _statusText = value; OnPropertyChanged(); } - } + SlotMap.Clear(); - public string CursorItemInfo - { - get => _cursorItemInfo; - set { _cursorItemInfo = value; OnPropertyChanged(); } - } - - public bool HasCursorItem - { - get => _hasCursorItem; - set { _hasCursorItem = value; OnPropertyChanged(); } - } - - public SlotViewModel? HoveredSlot - { - get => _hoveredSlot; - set - { - if (_hoveredSlot != null) - _hoveredSlot.IsHovered = false; - _hoveredSlot = value; - if (_hoveredSlot != null) - _hoveredSlot.IsHovered = true; - OnPropertyChanged(); - OnPropertyChanged(nameof(HoveredSlotDetailText)); - } - } - - /// - /// Multi-line detail text for the hovered slot. - /// - public string HoveredSlotDetailText - { - get - { - if (_hoveredSlot == null) - return Translations.tui_inventory_hover_hint; - - if (_hoveredSlot.IsEmpty) - return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}"; - - var sb = new StringBuilder(); - sb.AppendLine(_hoveredSlot.ItemTypeName); - sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount)); - - string fullInfo = _hoveredSlot.FullInfo; - if (!string.IsNullOrEmpty(fullInfo)) - { - string[] parts = fullInfo.Split(" | "); - for (int i = 1; i < parts.Length; i++) - sb.AppendLine(parts[i].Trim()); - } - - return sb.ToString().TrimEnd(); - } - } - - private Dictionary _slotMap = new(); - private int _nameMaxLen = 9; - private int _nameMaxLines = 1; - - public InventoryViewModel(McClient handler, int windowId) - { - Handler = handler; - WindowId = windowId; - - CraftingOutputSlot = new SlotViewModel(0); - OffhandSlot = new SlotViewModel(45); - - InitializeSlots(); - RefreshFromContainer(); - } - - public void SetSlotDisplayParams(int maxWidth, int maxLines) - { - _nameMaxLen = maxWidth; - _nameMaxLines = maxLines; - foreach (var kvp in _slotMap) - { - kvp.Value.NameMaxWidth = maxWidth; - kvp.Value.NameMaxLines = maxLines; - } - RefreshFromContainer(); - } - - private void InitializeSlots() - { - _slotMap.Clear(); - - _slotMap[0] = CraftingOutputSlot; + var craftOut = new SlotViewModel(0); + SlotMap[0] = craftOut; for (int i = 1; i <= 4; i++) { var slot = new SlotViewModel(i); CraftingInputSlots.Add(slot); - _slotMap[i] = slot; + SlotMap[i] = slot; } for (int i = 5; i <= 8; i++) { var slot = new SlotViewModel(i); EquipmentSlots.Add(slot); - _slotMap[i] = slot; + SlotMap[i] = slot; } for (int i = 9; i <= 35; i++) { var slot = new SlotViewModel(i); MainInventorySlots.Add(slot); - _slotMap[i] = slot; + SlotMap[i] = slot; } for (int i = 36; i <= 44; i++) @@ -154,67 +50,11 @@ namespace MinecraftClient.Tui int hotbarIdx = i - 36; var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx); HotbarSlots.Add(slot); - _slotMap[i] = slot; + SlotMap[i] = slot; } - _slotMap[45] = OffhandSlot; - } - - public void RefreshFromContainer() - { - Inventory.Container? container = Handler.GetInventory(WindowId); - if (container == null) - { - StatusText = Translations.tui_inventory_container_not_found; - return; - } - - Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title); - - foreach (var kvp in _slotMap) - { - Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null; - kvp.Value.Update(item); - } - - UpdateCursorItem(container); - int itemCount = 0; - foreach (var kvp in container.Items) - { - if (kvp.Key >= 0 && !kvp.Value.IsEmpty) - itemCount++; - } - StatusText = string.Format(Translations.tui_inventory_item_count, itemCount); - - OnPropertyChanged(nameof(HoveredSlotDetailText)); - } - - private void UpdateCursorItem(Inventory.Container container) - { - if (container.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty) - { - CursorItemInfo = $"x{cursorItem.Count} {cursorItem.GetTypeString()}"; - HasCursorItem = true; - } - else - { - CursorItemInfo = ""; - HasCursorItem = false; - } - } - - public bool PerformAction(int slotId, WindowActionType action) - { - bool result = Handler.DoWindowAction(WindowId, slotId, action); - RefreshFromContainer(); - return result; - } - - public event PropertyChangedEventHandler? PropertyChanged; - - private void OnPropertyChanged([CallerMemberName] string? name = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + var offhand = new SlotViewModel(45); + SlotMap[45] = offhand; } } } From 2d677e0f0d400de5cdb729563931c6344f697313 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 23:17:08 +0800 Subject: [PATCH 18/76] Fix skill frontmatter validation --- .skills/csharp-best-practices/SKILL.md | 1 - .skills/csharp-dotnet-cli-optimization/SKILL.md | 1 - .skills/csharp-optimization/SKILL.md | 1 - .skills/humanizer/SKILL.md | 1 - .skills/writing-skills/SKILL.md | 4 ---- 5 files changed, 8 deletions(-) diff --git a/.skills/csharp-best-practices/SKILL.md b/.skills/csharp-best-practices/SKILL.md index 8eb731ef..27c67ad4 100644 --- a/.skills/csharp-best-practices/SKILL.md +++ b/.skills/csharp-best-practices/SKILL.md @@ -3,7 +3,6 @@ name: csharp-best-practices description: > C# 14 / .NET 10 coding conventions, idiomatic patterns, and performance best practices for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code. -version: 0.4.0 --- # C# 14 / .NET 10 Best Practices diff --git a/.skills/csharp-dotnet-cli-optimization/SKILL.md b/.skills/csharp-dotnet-cli-optimization/SKILL.md index 71acf832..7691da85 100644 --- a/.skills/csharp-dotnet-cli-optimization/SKILL.md +++ b/.skills/csharp-dotnet-cli-optimization/SKILL.md @@ -31,7 +31,6 @@ metadata: - slow - hang - deadlock -version: 0.2.0 --- # C#/.NET CLI Optimization diff --git a/.skills/csharp-optimization/SKILL.md b/.skills/csharp-optimization/SKILL.md index 060b4d6b..9caf92f9 100644 --- a/.skills/csharp-optimization/SKILL.md +++ b/.skills/csharp-optimization/SKILL.md @@ -7,7 +7,6 @@ metadata: category: technique triggers: performance, allocations, GC, hot path, latency, throughput, memory pressure, optimize, slow, freeze, lag spike, packet processing speed -version: 0.2.0 --- # C# Performance Optimization for MCC diff --git a/.skills/humanizer/SKILL.md b/.skills/humanizer/SKILL.md index 45e2cb0c..9609cb69 100644 --- a/.skills/humanizer/SKILL.md +++ b/.skills/humanizer/SKILL.md @@ -1,6 +1,5 @@ --- name: humanizer -version: 2.1.1 description: | Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's diff --git a/.skills/writing-skills/SKILL.md b/.skills/writing-skills/SKILL.md index c00da178..514e2c4f 100644 --- a/.skills/writing-skills/SKILL.md +++ b/.skills/writing-skills/SKILL.md @@ -1,10 +1,6 @@ --- name: writing-skills description: "Use when creating, updating, or improving agent skills." -category: meta -risk: unknown -source: community -date_added: "2026-02-27" --- # Writing Skills (Excellence) From f0fda8ce9ff4fed325c4ee6f53917d7ac4ad0a34 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sat, 28 Mar 2026 23:38:56 +0800 Subject: [PATCH 19/76] Update base_path in crowdin.yml to use relative path --- crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin.yml b/crowdin.yml index 5e80b8bb..89813ca1 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,6 @@ "project_id_env": "CROWDIN_PROJECT_ID" "api_token_env": "CROWDIN_PERSONAL_TOKEN" -"base_path": "/" +"base_path": "./" "preserve_hierarchy": true "base_url": "https://api.crowdin.com" From 9cada9d19d5a574dee17e2abe4c6ffa868ce5748 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 00:54:34 +0800 Subject: [PATCH 20/76] Refactor ConsoleIO and Program settings handling - Updated ConsoleIO to allow writing to the console when Backend is null, improving error handling. - Simplified settings write-back logic in Program.cs to ensure default settings are written correctly based on configuration results. --- MinecraftClient/ConsoleIO.cs | 4 ++-- MinecraftClient/Program.cs | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs index 485d5a90..8a77aaee 100644 --- a/MinecraftClient/ConsoleIO.cs +++ b/MinecraftClient/ConsoleIO.cs @@ -104,7 +104,7 @@ namespace MinecraftClient /// public static void WriteLine(string line) { - if (BasicIO) + if (BasicIO || Backend is null) Console.WriteLine(line); else Backend.WriteLine(line); @@ -137,7 +137,7 @@ namespace MinecraftClient { str = str.Replace('\n', ' '); } - if (BasicIO) + if (BasicIO || Backend is null) { if (BasicIO_NoColor) { diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 759e69ed..c0e7710d 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -161,14 +161,7 @@ namespace MinecraftClient } if (configResult.NeedWriteDefault) - { Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(false); - } - else if (configResult.Success) - { - WriteBackSettings(true); - } if (!Config.Main.Advanced.EnableSentry) _sentrySdk?.Dispose(); @@ -219,6 +212,8 @@ namespace MinecraftClient if (cfg.NeedWriteDefault) { + WriteBackSettings(false); + if (cfg.IsLegacyUpgrade) { ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); @@ -243,6 +238,8 @@ namespace MinecraftClient } else { + WriteBackSettings(true); + if (!Config.Main.Advanced.Language.StartsWith("en")) ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); } From 83f14a64f879246ef94a1b3175e2c3759a45c949 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 01:31:31 +0800 Subject: [PATCH 21/76] Add tryout command for TUI onboarding Add a new /tryout command as an extensible entry point for recommended feature trials, with /tryout tui as the first action. The command updates [Console.General] ConsoleMode to "tui", writes the config back immediately, and explains both how to revert the change and that a restart is required. Also show a startup recommendation in classic console mode when stdin is interactive, so users can discover the TUI experience without manually editing MinecraftClient.ini. All user-facing text is routed through the translation resources. --- MinecraftClient/Commands/Tryout.cs | 63 +++++++++++++++++++ MinecraftClient/Program.cs | 14 +++++ .../Translations/Translations.Designer.cs | 54 ++++++++++++++++ .../Resources/Translations/Translations.resx | 20 +++++- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 MinecraftClient/Commands/Tryout.cs diff --git a/MinecraftClient/Commands/Tryout.cs b/MinecraftClient/Commands/Tryout.cs new file mode 100644 index 00000000..8ae0829b --- /dev/null +++ b/MinecraftClient/Commands/Tryout.cs @@ -0,0 +1,63 @@ +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig; + +namespace MinecraftClient.Commands +{ + public class Tryout : Command + { + public override string CmdName => "tryout"; + public override string CmdUsage => "tryout [list|tui]"; + public override string CmdDesc => Translations.cmd_tryout_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ListTryouts(r.Source)) + .Then(l => l.Literal("list") + .Executes(r => ListTryouts(r.Source))) + .Then(l => l.Literal("tui") + .Executes(r => EnableTuiMode(r.Source))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r) + { + return r.SetAndReturn(GetCmdDescTranslated()); + } + + private int ListTryouts(CmdResult r) + { + return r.SetAndReturn(string.Join('\n', + GetCmdDescTranslated(), + string.Empty, + Translations.cmd_tryout_list_header, + $" - {Translations.cmd_tryout_list_tui}")); + } + + private int EnableTuiMode(CmdResult r) + { + var previousMode = Settings.Config.Console.General.ConsoleMode; + if (previousMode == ConsoleModeType.tui) + { + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_tryout_tui_already_enabled); + } + + Settings.Config.Console.General.ConsoleMode = ConsoleModeType.tui; + Program.WriteBackSettings(); + + return r.SetAndReturn(CmdResult.Status.Done, + string.Format(Translations.cmd_tryout_tui_enabled, previousMode, ConsoleModeType.tui)); + } + } +} diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index c0e7710d..18d8ce88 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -193,6 +193,7 @@ namespace MinecraftClient if (!ProcessStartupState(startupState)) return; + MaybePrintClassicModeTuiRecommendation(); RunStartupSequence(args); } @@ -247,6 +248,19 @@ namespace MinecraftClient return true; } + private static void MaybePrintClassicModeTuiRecommendation() + { + if (ConsoleIO.BasicIO + || Config.Console.General.ConsoleMode != ConsoleModeType.classic + || Console.IsInputRedirected) + { + return; + } + + char cmdChar = Config.Main.Advanced.InternalCmdChar.ToChar(); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_console_mode_tui_recommendation, cmdChar)); + } + /// /// Handles a failed config load by prompting the user to fix or regenerate the config file. /// diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 3a2c4bc9..242752e6 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -3558,6 +3558,51 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to quickly enable recommended features.. + /// + internal static string cmd_tryout_desc { + get { + return ResourceManager.GetString("cmd.tryout.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available quick actions:. + /// + internal static string cmd_tryout_list_header { + get { + return ResourceManager.GetString("cmd.tryout.list.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to tui: set [Console.General] ConsoleMode = "tui" for the next restart.. + /// + internal static string cmd_tryout_list_tui { + get { + return ResourceManager.GetString("cmd.tryout.list.tui", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect.. + /// + internal static string cmd_tryout_tui_already_enabled { + get { + return ResourceManager.GetString("cmd.tryout.tui.already_enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change.. + /// + internal static string cmd_tryout_tui_enabled { + get { + return ResourceManager.GetString("cmd.tryout.tui.enabled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display Health and Food saturation.. /// @@ -5534,6 +5579,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.. + /// + internal static string mcc_console_mode_tui_recommendation { + get { + return ResourceManager.GetString("mcc.console_mode_tui_recommendation", resourceCulture); + } + } + /// /// Looks up a localized string similar to To sign in, open {0} in your browser and enter the code: {1}. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 68202894..b028064d 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1249,6 +1249,21 @@ Change EnableEmoji=false in the settings if the display is confusing. No active effects. + + try a recommended feature. + + + Available tryouts: + + + tui: set [Console.General] ConsoleMode = "tui" for the next restart. + + + [Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect. + + + Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change. + Display Health and Food saturation. @@ -1863,6 +1878,9 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Connecting to {0}... + + Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart. + To sign in, open {0} in your browser and enter the code: §e{1} @@ -2392,4 +2410,4 @@ see item details. Crafting - \ No newline at end of file + From b757215dbb2146b3948a9484c6837b95789560d5 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 01:33:59 +0800 Subject: [PATCH 22/76] Temporarily disable classic mode TUI recommendation Commented out the MaybePrintClassicModeTuiRecommendation function call to prevent its execution until the related issue is resolved. Reference to the issue is included for tracking purposes. --- MinecraftClient/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 18d8ce88..946a4d85 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -193,7 +193,9 @@ namespace MinecraftClient if (!ProcessStartupState(startupState)) return; - MaybePrintClassicModeTuiRecommendation(); + // Wait for this issue to be fixed before enabling it: https://github.com/Consolonia/Consolonia/issues/602 + // MaybePrintClassicModeTuiRecommendation(); + RunStartupSequence(args); } From 157d90273b202ff490422e39fa374f5cefcb264e Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 01:57:21 +0800 Subject: [PATCH 23/76] Handle TUI startup failures with classic fallback --- MinecraftClient/Program.cs | 27 ++++++++++++++++--- .../Translations/Translations.Designer.cs | 27 +++++++++++++++++++ .../Resources/Translations/Translations.resx | 9 +++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 946a4d85..8c1e7644 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -177,9 +177,16 @@ namespace MinecraftClient if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) { ConsoleIO.Backend?.Shutdown(); - var tuiBackend = new Tui.TuiConsoleBackend(); - ConsoleIO.Backend = tuiBackend; - tuiBackend.RunTuiMainLoop(args, startupState); + try + { + var tuiBackend = new Tui.TuiConsoleBackend(); + ConsoleIO.Backend = tuiBackend; + tuiBackend.RunTuiMainLoop(args, startupState); + } + catch (Exception ex) + { + HandleTuiStartupFailure(ex); + } return; } @@ -199,6 +206,20 @@ namespace MinecraftClient RunStartupSequence(args); } + private static void HandleTuiStartupFailure(Exception exception) + { + Config.Console.General.ConsoleMode = ConsoleModeType.classic; + WriteBackSettings(enableBackup: false); + + ConsoleIO.Backend = new ClassicConsoleBackend(); + ConsoleIO.Backend.Init(); + + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_tui_startup_failed); + ConsoleIO.WriteLine(exception.ToString()); + ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_report_issue); + ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_tui_startup_fallback_classic); + } + /// /// Prints the application banner and processes the startup state collected before /// the console backend was ready. Called once from classic mode or from TUI after diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 242752e6..96419311 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -5587,6 +5587,33 @@ namespace MinecraftClient { return ResourceManager.GetString("mcc.console_mode_tui_recommendation", resourceCulture); } } + + /// + /// Looks up a localized string similar to MCC encountered a problem while starting TUI mode.. + /// + internal static string mcc_tui_startup_failed { + get { + return ResourceManager.GetString("mcc.tui_startup_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC.. + /// + internal static string mcc_tui_startup_fallback_classic { + get { + return ResourceManager.GetString("mcc.tui_startup_fallback_classic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please report this issue to the MCC Team.. + /// + internal static string mcc_report_issue { + get { + return ResourceManager.GetString("mcc.report_issue", resourceCulture); + } + } /// /// Looks up a localized string similar to To sign in, open {0} in your browser and enter the code: {1}. diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index b028064d..c3ecd406 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1881,6 +1881,15 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart. + + MCC encountered a problem while starting TUI mode. + + + As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC. + + + Please report this issue to the MCC Team. + To sign in, open {0} in your browser and enter the code: §e{1} From 6631180f8a1ef0b901b8554daf1fc4cf64f6cdb3 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 18:47:40 +0800 Subject: [PATCH 24/76] Minimap support --- .skills/mcc-version-adaptation/SKILL.md | 39 +- MinecraftClient/Commands/Minimap.cs | 256 ++ MinecraftClient/MinecraftClient.csproj | 2 + .../Protocol/Handlers/DataTypes.cs | 4 +- .../ConfigComments/ConfigComments.resx | 33 + .../Translations/Translations.Designer.cs | 153 + .../Resources/Translations/Translations.resx | 51 + MinecraftClient/Settings.cs | 47 + MinecraftClient/Tui/MainTuiView.cs | 125 +- MinecraftClient/Tui/MinimapBlockColors.json | 3552 +++++++++++++++++ MinecraftClient/Tui/MinimapColorMap.cs | 168 + MinecraftClient/Tui/MinimapControl.cs | 677 ++++ .../Tui/MinimapEntityCategories.json | 167 + .../Tui/MinimapEntityClassifier.cs | 178 + tools/README.md | 47 +- tools/gen_block_color_map.py | 268 ++ tools/gen_entity_category_map.py | 200 + 17 files changed, 5961 insertions(+), 6 deletions(-) create mode 100644 MinecraftClient/Commands/Minimap.cs create mode 100644 MinecraftClient/Tui/MinimapBlockColors.json create mode 100644 MinecraftClient/Tui/MinimapColorMap.cs create mode 100644 MinecraftClient/Tui/MinimapControl.cs create mode 100644 MinecraftClient/Tui/MinimapEntityCategories.json create mode 100644 MinecraftClient/Tui/MinimapEntityClassifier.cs create mode 100644 tools/gen_block_color_map.py create mode 100644 tools/gen_entity_category_map.py diff --git a/.skills/mcc-version-adaptation/SKILL.md b/.skills/mcc-version-adaptation/SKILL.md index ce21cf04..195b5592 100644 --- a/.skills/mcc-version-adaptation/SKILL.md +++ b/.skills/mcc-version-adaptation/SKILL.md @@ -215,7 +215,42 @@ The JSON maps block names (snake_case) → collision shape IDs → AABB coordina **Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc//blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`. -## Step 9: Compile and Verify +## Step 9: Update Minimap Block Color Map + +Regenerate the block-to-MapColor mapping used by the TUI minimap. This maps each block's `Material` enum to the RGB color from Minecraft's official `MapColor` table. + +```bash +python3 $MCC_REPO/tools/gen_block_color_map.py $MCC_REPO/MinecraftOfficial/-decompiled +# e.g. python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +``` + +Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). + +The script parses `MapColor.java`, `DyeColor.java`, and `Blocks.java` from the decompiled source to extract each block's assigned map color. Blocks not matched to a known `Material` enum value are skipped. + +**When to update**: Whenever new blocks are added or existing blocks change their `mapColor()` assignment. If only items or entities changed, this step can be skipped. + +## Step 10: Update Minimap Entity Categories + +Regenerate the entity-to-MobCategory mapping used by the TUI minimap for classifying entities as hostile, passive, neutral, or non-living. + +```bash +python3 $MCC_REPO/tools/gen_entity_category_map.py $MCC_REPO/MinecraftOfficial/-decompiled +# e.g. python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +``` + +Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). + +The script parses `EntityType.java` to extract each entity's `MobCategory` assignment, then maps Minecraft's categories to MCC minimap categories: +- `MONSTER` -> hostile (with neutral overrides for conditionally hostile mobs like Enderman, Spider, Wolf) +- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive +- `MISC` -> non_living (with passive overrides for Villager, WanderingTrader, ZombieHorse) + +The script maintains manual override lists for "neutral" mobs (attack only when provoked) since Minecraft has no machine-readable flag for this behavior. Review and update the `NEUTRAL_OVERRIDES` and `PASSIVE_OVERRIDES` sets in the script when new conditionally-hostile or misclassified mobs are added. + +**When to update**: Whenever new entity types are added. If only blocks or items changed, this step can be skipped. + +## Step 11: Compile and Verify ```bash dotnet build $MCC_REPO/MinecraftClient.sln -c Release @@ -274,3 +309,5 @@ All scripts are in `$MCC_REPO/tools/`. See `tools/README.md` for detailed usage. | `gen_entity_palette.py` | Generate EntityPalette C# | registries.json | | `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source | | `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data | +| `gen_block_color_map.py` | Generate minimap block color JSON | Decompiled source (MapColor/DyeColor/Blocks) | +| `gen_entity_category_map.py` | Generate minimap entity category JSON | Decompiled source (EntityType.java) | diff --git a/MinecraftClient/Commands/Minimap.cs b/MinecraftClient/Commands/Minimap.cs new file mode 100644 index 00000000..897c88ae --- /dev/null +++ b/MinecraftClient/Commands/Minimap.cs @@ -0,0 +1,256 @@ +using System; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.Tui; +using Avalonia.Threading; +using static MinecraftClient.CommandHandler.CmdResult; + +namespace MinecraftClient.Commands +{ + class Minimap : Command + { + public override string CmdName => "minimap"; + public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right]"; + public override string CmdDesc => Translations.cmd_minimap_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => DoToggle(r.Source)) + .Then(l => l.Literal("on") + .Executes(r => DoOn(r.Source))) + .Then(l => l.Literal("off") + .Executes(r => DoOff(r.Source))) + .Then(l => l.Literal("zoom") + .Executes(r => DoZoomInfo(r.Source)) + .Then(l => l.Literal("in") + .Executes(r => DoZoomIn(r.Source))) + .Then(l => l.Literal("out") + .Executes(r => DoZoomOut(r.Source))) + .Then(l => l.Argument("level", Arguments.Integer(MinimapControl.MinZoom, MinimapControl.MaxZoom)) + .Executes(r => DoZoomSet(r.Source, Arguments.GetInteger(r, "level"))))) + .Then(l => l.Literal("names") + .Executes(r => DoNamesInfo(r.Source)) + .Then(l => l.Literal("all_on") + .Executes(r => DoNamesAll(r.Source, true))) + .Then(l => l.Literal("all_off") + .Executes(r => DoNamesAll(r.Source, false))) + .Then(l => l.Literal("players") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Player)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, false)))) + .Then(l => l.Literal("hostile") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Hostile)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, false)))) + .Then(l => l.Literal("neutral") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Neutral)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, false)))) + .Then(l => l.Literal("passive") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Passive)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, false))))) + .Then(l => l.Literal("position") + .Executes(r => DoPositionInfo(r.Source)) + .Then(l => l.Literal("top_left") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.top_left))) + .Then(l => l.Literal("top_right") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.top_right))) + .Then(l => l.Literal("center") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.center))) + .Then(l => l.Literal("bottom_left") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_left))) + .Then(l => l.Literal("bottom_right") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_right)))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string _) => + r.SetAndReturn(GetCmdDescTranslated()); + + private static MainTuiView? GetTuiView(CmdResult r) + { + if (ConsoleIO.Backend is not TuiConsoleBackend) + { + r.SetAndReturn(Status.Fail, Translations.cmd_minimap_tui_only); + return null; + } + return TuiConsoleBackend.Instance?.GetView(); + } + + private static int DoToggle(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + bool wasVisible = view.IsMinimapVisible; + Dispatcher.UIThread.Post(() => view.ToggleMinimap()); + string msg = wasVisible + ? Translations.cmd_minimap_disabled + : Translations.cmd_minimap_enabled; + return r.SetAndReturn(Status.Done, msg); + } + + private static int DoOn(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.ShowMinimap()); + return r.SetAndReturn(Status.Done, Translations.cmd_minimap_enabled); + } + + private static int DoOff(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.HideMinimap()); + return r.SetAndReturn(Status.Done, Translations.cmd_minimap_disabled); + } + + private static int DoZoomInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int current = view.GetMinimapZoom(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_zoom_current, current, MinimapControl.MaxZoom)); + } + + private static int DoZoomIn(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int newLevel = Math.Max(view.GetMinimapZoom() - 1, MinimapControl.MinZoom); + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel)); + } + + private static int DoZoomOut(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int newLevel = Math.Min(view.GetMinimapZoom() + 1, MinimapControl.MaxZoom); + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel)); + } + + private static int DoZoomSet(CmdResult r, int level) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(level)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, level)); + } + + private static int DoNamesInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var nc = view.GetMinimapNameConfig(); + string status = string.Format(Translations.cmd_minimap_names_status, + BoolStr(nc.Players), BoolStr(nc.Hostile), BoolStr(nc.Neutral), BoolStr(nc.Passive)); + return r.SetAndReturn(Status.Done, status); + } + + private static int DoNamesAll(CmdResult r, bool on) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => + { + view.GetMinimapNameConfig().SetAll(on); + view.SyncMinimapNameConfig(); + }); + string msg = on ? Translations.cmd_minimap_names_all_on : Translations.cmd_minimap_names_all_off; + return r.SetAndReturn(Status.Done, msg); + } + + private static int DoNamesCatInfo(CmdResult r, MobCategory cat) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var nc = view.GetMinimapNameConfig(); + bool val = cat switch + { + MobCategory.Player => nc.Players, + MobCategory.Hostile => nc.Hostile, + MobCategory.Neutral => nc.Neutral, + MobCategory.Passive => nc.Passive, + _ => false, + }; + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_names_cat, cat, BoolStr(val))); + } + + private static int DoNamesCatSet(CmdResult r, MobCategory cat, bool on) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => + { + var nc = view.GetMinimapNameConfig(); + switch (cat) + { + case MobCategory.Player: nc.Players = on; break; + case MobCategory.Hostile: nc.Hostile = on; break; + case MobCategory.Neutral: nc.Neutral = on; break; + case MobCategory.Passive: nc.Passive = on; break; + } + view.SyncMinimapNameConfig(); + }); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_names_cat_set, cat, BoolStr(on))); + } + + private static int DoPositionInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var pos = view.GetMinimapPosition(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_position_current, pos)); + } + + private static int DoPositionSet(CmdResult r, MinimapPosition pos) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapPosition(pos)); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_position_set, pos)); + } + + private static string BoolStr(bool v) => v ? "ON" : "OFF"; + } +} diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 5df50933..372d153a 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -20,6 +20,8 @@ + + diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 78456c2d..669ba5b3 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -664,8 +664,10 @@ namespace MinecraftClient.Protocol.Handlers } } - return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch, + var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch, data); + entity.UUID = entityUUID; + return entity; } /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 6d91740b..d4b82e42 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -933,6 +933,39 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be Set to false to opt-out of Sentry error logging. + + Settings for the TUI minimap overlay that shows terrain and entities. + + + Whether the minimap is visible on startup in TUI mode. + + + Blocks per pixel, 1-16. 1 = closest (1:1), 16 = farthest (16 blocks per pixel). + + + Map width in pixels (characters). Range 10-120, default 40. + + + Map height in pixels (must be even, uses half-block chars). Range 4-80, default 40. + + + Minimap position: "top_left", "top_right", "center", "bottom_left", or "bottom_right". + + + Show player names on the minimap. + + + Show hostile mob names on the minimap. + + + Show neutral mob names on the minimap. + + + Show passive mob names on the minimap. + + + Minimap refresh interval in milliseconds (200-5000, default 1000). + Yggdrasil authlib multi-user selection. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 96419311..071bee44 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -6880,5 +6880,158 @@ namespace MinecraftClient { return ResourceManager.GetString("tui.crafting.grid", resourceCulture); } } + + /// + /// Looks up a localized string similar to Toggle the TUI minimap overlay, or adjust its zoom level.. + /// + internal static string cmd_minimap_desc { + get { + return ResourceManager.GetString("cmd.minimap.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap enabled.. + /// + internal static string cmd_minimap_enabled { + get { + return ResourceManager.GetString("cmd.minimap.enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap disabled.. + /// + internal static string cmd_minimap_disabled { + get { + return ResourceManager.GetString("cmd.minimap.disabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap zoom set to {0}:1 (blocks per pixel).. + /// + internal static string cmd_minimap_zoom_set { + get { + return ResourceManager.GetString("cmd.minimap.zoom_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current minimap zoom: {0}:1 blocks/px (range 1-{1}).. + /// + internal static string cmd_minimap_zoom_current { + get { + return ResourceManager.GetString("cmd.minimap.zoom_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimap command is only available in TUI mode.. + /// + internal static string cmd_minimap_tui_only { + get { + return ResourceManager.GetString("cmd.minimap.tui_only", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hostile. + /// + internal static string tui_minimap_legend_hostile { + get { + return ResourceManager.GetString("tui.minimap.legend.hostile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passive. + /// + internal static string tui_minimap_legend_passive { + get { + return ResourceManager.GetString("tui.minimap.legend.passive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Neutral. + /// + internal static string tui_minimap_legend_neutral { + get { + return ResourceManager.GetString("tui.minimap.legend.neutral", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Player. + /// + internal static string tui_minimap_legend_player { + get { + return ResourceManager.GetString("tui.minimap.legend.player", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}. + /// + internal static string cmd_minimap_names_status { + get { + return ResourceManager.GetString("cmd.minimap.names_status", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity name labels enabled.. + /// + internal static string cmd_minimap_names_all_on { + get { + return ResourceManager.GetString("cmd.minimap.names_all_on", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity name labels disabled.. + /// + internal static string cmd_minimap_names_all_off { + get { + return ResourceManager.GetString("cmd.minimap.names_all_off", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} name display: {1}. + /// + internal static string cmd_minimap_names_cat { + get { + return ResourceManager.GetString("cmd.minimap.names_cat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} name display set to {1}.. + /// + internal static string cmd_minimap_names_cat_set { + get { + return ResourceManager.GetString("cmd.minimap.names_cat_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current minimap position: {0}. + /// + internal static string cmd_minimap_position_current { + get { + return ResourceManager.GetString("cmd.minimap.position_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap position set to: {0}. + /// + internal static string cmd_minimap_position_set { + get { + return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index c3ecd406..ad782375 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2419,4 +2419,55 @@ see item details. Crafting + + Toggle the TUI minimap overlay, or adjust its zoom level. + + + Minimap enabled. + + + Minimap disabled. + + + Minimap zoom set to {0}:1 (blocks per pixel). + + + Current minimap zoom: {0}:1 blocks/px (range 1-{1}). + + + The minimap command is only available in TUI mode. + + + Hostile + + + Passive + + + Neutral + + + Player + + + Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3} + + + All entity name labels enabled. + + + All entity name labels disabled. + + + {0} name display: {1} + + + {0} name display set to {1}. + + + Current minimap position: {0} + + + Minimap position set to: {0} + diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index e77514da..299fe990 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1108,6 +1108,9 @@ namespace MinecraftClient [TomlPrecedingComment("$Console.CommandSuggestion$")] public CommandSuggestionConfig CommandSuggestion = new(); + [TomlPrecedingComment("$Console.Minimap$")] + public MinimapConfig Minimap = new(); + public void OnSettingUpdate() { var backend = ConsoleIO.Backend; @@ -1246,6 +1249,50 @@ namespace MinecraftClient public enum ConsoleModeType { classic, tui }; public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit }; + + [TomlDoNotInlineObject] + public class MinimapConfig + { + [TomlInlineComment("$Console.Minimap.Enabled$")] + public bool Enabled = false; + + [TomlInlineComment("$Console.Minimap.Zoom$")] + public int Zoom = Tui.MinimapControl.DefaultZoom; + + [TomlInlineComment("$Console.Minimap.Width$")] + public int Width = Tui.MinimapControl.DefaultWidth; + + [TomlInlineComment("$Console.Minimap.Height$")] + public int Height = Tui.MinimapControl.DefaultHeight; + + [TomlInlineComment("$Console.Minimap.Position$")] + public Tui.MinimapPosition Position = Tui.MinimapPosition.top_right; + + [TomlInlineComment("$Console.Minimap.ShowPlayerNames$")] + public bool ShowPlayerNames = false; + + [TomlInlineComment("$Console.Minimap.ShowHostileNames$")] + public bool ShowHostileNames = false; + + [TomlInlineComment("$Console.Minimap.ShowNeutralNames$")] + public bool ShowNeutralNames = false; + + [TomlInlineComment("$Console.Minimap.ShowPassiveNames$")] + public bool ShowPassiveNames = false; + + [TomlInlineComment("$Console.Minimap.RefreshInterval$")] + public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs; + + public void OnSettingUpdate() + { + Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom); + Width = Math.Clamp(Width, 10, 120); + Height = Math.Clamp(Height, 4, 80); + if (Height % 2 != 0) Height++; + RefreshInterval = Math.Clamp(RefreshInterval, + Tui.MinimapControl.MinRefreshMs, Tui.MinimapControl.MaxRefreshMs); + } + } } } diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index c2d51220..1cde95e4 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -41,6 +41,10 @@ namespace MinecraftClient.Tui private long _lastLogClickTicks; private const int DoubleClickMsec = 500; + private readonly Border _minimapBorder; + private readonly MinimapControl _minimapControl; + private volatile bool _minimapVisible; + private readonly Border _suggestionBorder; private readonly StackPanel _suggestionPanel; private CommandSuggestion[] _suggestions = Array.Empty(); @@ -150,6 +154,29 @@ namespace MinecraftClient.Tui Margin = new Thickness(0, 0, 0, 1), }; + var mmCfg = Settings.Config.Console.Minimap; + mmCfg.OnSettingUpdate(); + _minimapControl = new MinimapControl(mmCfg.Width, mmCfg.Height); + _minimapControl.BlocksPerPixel = mmCfg.Zoom; + _minimapControl.RefreshIntervalMs = mmCfg.RefreshInterval; + _minimapControl.NameConfig.Players = mmCfg.ShowPlayerNames; + _minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames; + _minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames; + _minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames; + + var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position); + _minimapBorder = new Border + { + Background = new SolidColorBrush(Color.FromArgb(220, 15, 15, 15)), + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Child = _minimapControl, + IsVisible = false, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Margin = margin, + }; + _mainContent = new DockPanel { Background = Brushes.Black, @@ -164,11 +191,18 @@ namespace MinecraftClient.Tui _rootPanel = new Panel { Background = Brushes.Black, - Children = { _mainContent, _notificationBorder, _suggestionBorder } + Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder } }; Content = _rootPanel; + if (mmCfg.Enabled) + { + _minimapVisible = true; + _minimapBorder.IsVisible = true; + _minimapControl.Start(); + } + StartStatusBarTimer(); } @@ -986,6 +1020,95 @@ namespace MinecraftClient.Tui #endregion + #region Minimap + + public void ShowMinimap() + { + if (_minimapVisible) return; + _minimapVisible = true; + _minimapBorder.IsVisible = true; + _minimapControl.Start(); + Settings.Config.Console.Minimap.Enabled = true; + } + + public void HideMinimap() + { + if (!_minimapVisible) return; + _minimapVisible = false; + _minimapControl.Stop(); + _minimapBorder.IsVisible = false; + Settings.Config.Console.Minimap.Enabled = false; + } + + public void ToggleMinimap() + { + if (_minimapVisible) + HideMinimap(); + else + ShowMinimap(); + } + + public bool IsMinimapVisible => _minimapVisible; + + public void SetMinimapZoom(int level) + { + _minimapControl.BlocksPerPixel = level; + Settings.Config.Console.Minimap.Zoom = level; + } + + public int GetMinimapZoom() => _minimapControl.BlocksPerPixel; + + public NameDisplayConfig GetMinimapNameConfig() => _minimapControl.NameConfig; + + public void SyncMinimapNameConfig() + { + var nc = _minimapControl.NameConfig; + var cfg = Settings.Config.Console.Minimap; + cfg.ShowPlayerNames = nc.Players; + cfg.ShowHostileNames = nc.Hostile; + cfg.ShowNeutralNames = nc.Neutral; + cfg.ShowPassiveNames = nc.Passive; + } + + public void ResizeMinimap(int width, int height) + { + _minimapControl.Resize(width, height); + Settings.Config.Console.Minimap.Width = width; + Settings.Config.Console.Minimap.Height = height; + } + + public void SetMinimapPosition(MinimapPosition pos) + { + var (hAlign, vAlign, margin) = GetMinimapAlignment(pos); + _minimapBorder.HorizontalAlignment = hAlign; + _minimapBorder.VerticalAlignment = vAlign; + _minimapBorder.Margin = margin; + Settings.Config.Console.Minimap.Position = pos; + } + + public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position; + + private static (HorizontalAlignment h, VerticalAlignment v, Thickness margin) GetMinimapAlignment(MinimapPosition pos) => pos switch + { + MinimapPosition.top_left => (HorizontalAlignment.Left, VerticalAlignment.Top, new Thickness(1, 1, 0, 0)), + MinimapPosition.top_right => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)), + MinimapPosition.center => (HorizontalAlignment.Center, VerticalAlignment.Center, new Thickness(0)), + MinimapPosition.bottom_left => (HorizontalAlignment.Left, VerticalAlignment.Bottom, new Thickness(1, 0, 0, 2)), + MinimapPosition.bottom_right => (HorizontalAlignment.Right, VerticalAlignment.Bottom, new Thickness(0, 0, 1, 2)), + _ => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)), + }; + + public void ApplyMinimapConfig() + { + var cfg = Settings.Config.Console.Minimap; + if (cfg.Enabled && !_minimapVisible) + ShowMinimap(); + else if (!cfg.Enabled && _minimapVisible) + HideMinimap(); + } + + #endregion + #region Overlay public void ShowOverlay(Control content, Action? onClose = null) diff --git a/MinecraftClient/Tui/MinimapBlockColors.json b/MinecraftClient/Tui/MinimapBlockColors.json new file mode 100644 index 00000000..af7d726c --- /dev/null +++ b/MinecraftClient/Tui/MinimapBlockColors.json @@ -0,0 +1,3552 @@ +{ + "version": "26.1-rc-2", + "colors": { + "AcaciaDoor": [ + 216, + 127, + 51 + ], + "AcaciaFence": [ + 216, + 127, + 51 + ], + "AcaciaFenceGate": [ + 216, + 127, + 51 + ], + "AcaciaHangingSign": [ + 216, + 127, + 51 + ], + "AcaciaPlanks": [ + 216, + 127, + 51 + ], + "AcaciaPressurePlate": [ + 216, + 127, + 51 + ], + "AcaciaSapling": [ + 0, + 124, + 0 + ], + "AcaciaShelf": [ + 216, + 127, + 51 + ], + "AcaciaSign": [ + 216, + 127, + 51 + ], + "AcaciaSlab": [ + 216, + 127, + 51 + ], + "AcaciaTrapdoor": [ + 216, + 127, + 51 + ], + "AcaciaWallHangingSign": [ + 216, + 127, + 51 + ], + "AcaciaWallSign": [ + 216, + 127, + 51 + ], + "AcaciaWood": [ + 76, + 76, + 76 + ], + "Allium": [ + 0, + 124, + 0 + ], + "AmethystBlock": [ + 127, + 63, + 178 + ], + "AmethystCluster": [ + 127, + 63, + 178 + ], + "AncientDebris": [ + 25, + 25, + 25 + ], + "Andesite": [ + 112, + 112, + 112 + ], + "Anvil": [ + 167, + 167, + 167 + ], + "AttachedMelonStem": [ + 0, + 124, + 0 + ], + "AttachedPumpkinStem": [ + 0, + 124, + 0 + ], + "Azalea": [ + 0, + 124, + 0 + ], + "AzureBluet": [ + 0, + 124, + 0 + ], + "Bamboo": [ + 0, + 124, + 0 + ], + "BambooDoor": [ + 229, + 229, + 51 + ], + "BambooFence": [ + 229, + 229, + 51 + ], + "BambooFenceGate": [ + 229, + 229, + 51 + ], + "BambooHangingSign": [ + 229, + 229, + 51 + ], + "BambooMosaic": [ + 229, + 229, + 51 + ], + "BambooMosaicSlab": [ + 229, + 229, + 51 + ], + "BambooPlanks": [ + 229, + 229, + 51 + ], + "BambooPressurePlate": [ + 229, + 229, + 51 + ], + "BambooSapling": [ + 143, + 119, + 72 + ], + "BambooShelf": [ + 229, + 229, + 51 + ], + "BambooSign": [ + 229, + 229, + 51 + ], + "BambooSlab": [ + 229, + 229, + 51 + ], + "BambooTrapdoor": [ + 229, + 229, + 51 + ], + "BambooWallHangingSign": [ + 229, + 229, + 51 + ], + "BambooWallSign": [ + 229, + 229, + 51 + ], + "Barrel": [ + 143, + 119, + 72 + ], + "Barrier": [ + 0, + 0, + 0 + ], + "Basalt": [ + 25, + 25, + 25 + ], + "Beacon": [ + 92, + 219, + 213 + ], + "Bedrock": [ + 112, + 112, + 112 + ], + "BeeNest": [ + 229, + 229, + 51 + ], + "Beehive": [ + 143, + 119, + 72 + ], + "Beetroots": [ + 0, + 124, + 0 + ], + "Bell": [ + 250, + 238, + 77 + ], + "BigDripleaf": [ + 0, + 124, + 0 + ], + "BigDripleafStem": [ + 0, + 124, + 0 + ], + "BirchDoor": [ + 247, + 233, + 163 + ], + "BirchFence": [ + 247, + 233, + 163 + ], + "BirchFenceGate": [ + 247, + 233, + 163 + ], + "BirchHangingSign": [ + 247, + 233, + 163 + ], + "BirchPlanks": [ + 247, + 233, + 163 + ], + "BirchPressurePlate": [ + 247, + 233, + 163 + ], + "BirchSapling": [ + 0, + 124, + 0 + ], + "BirchShelf": [ + 247, + 233, + 163 + ], + "BirchSign": [ + 247, + 233, + 163 + ], + "BirchSlab": [ + 247, + 233, + 163 + ], + "BirchTrapdoor": [ + 247, + 233, + 163 + ], + "BirchWallHangingSign": [ + 247, + 233, + 163 + ], + "BirchWallSign": [ + 247, + 233, + 163 + ], + "BirchWood": [ + 247, + 233, + 163 + ], + "BlackBanner": [ + 143, + 119, + 72 + ], + "BlackCarpet": [ + 25, + 25, + 25 + ], + "BlackConcrete": [ + 25, + 25, + 25 + ], + "BlackConcretePowder": [ + 25, + 25, + 25 + ], + "BlackGlazedTerracotta": [ + 25, + 25, + 25 + ], + "BlackTerracotta": [ + 37, + 22, + 16 + ], + "BlackWallBanner": [ + 143, + 119, + 72 + ], + "BlackWool": [ + 25, + 25, + 25 + ], + "Blackstone": [ + 25, + 25, + 25 + ], + "BlastFurnace": [ + 112, + 112, + 112 + ], + "BlueBanner": [ + 143, + 119, + 72 + ], + "BlueCarpet": [ + 51, + 76, + 178 + ], + "BlueConcrete": [ + 51, + 76, + 178 + ], + "BlueConcretePowder": [ + 51, + 76, + 178 + ], + "BlueGlazedTerracotta": [ + 51, + 76, + 178 + ], + "BlueIce": [ + 160, + 160, + 255 + ], + "BlueOrchid": [ + 0, + 124, + 0 + ], + "BlueTerracotta": [ + 76, + 62, + 92 + ], + "BlueWallBanner": [ + 143, + 119, + 72 + ], + "BlueWool": [ + 51, + 76, + 178 + ], + "BoneBlock": [ + 247, + 233, + 163 + ], + "Bookshelf": [ + 143, + 119, + 72 + ], + "BrainCoral": [ + 242, + 127, + 165 + ], + "BrainCoralBlock": [ + 242, + 127, + 165 + ], + "BrainCoralFan": [ + 242, + 127, + 165 + ], + "BrainCoralWallFan": [ + 242, + 127, + 165 + ], + "BrewingStand": [ + 167, + 167, + 167 + ], + "BrickSlab": [ + 153, + 51, + 51 + ], + "Bricks": [ + 153, + 51, + 51 + ], + "BrownBanner": [ + 143, + 119, + 72 + ], + "BrownCarpet": [ + 102, + 76, + 51 + ], + "BrownConcrete": [ + 102, + 76, + 51 + ], + "BrownConcretePowder": [ + 102, + 76, + 51 + ], + "BrownGlazedTerracotta": [ + 102, + 76, + 51 + ], + "BrownMushroom": [ + 102, + 76, + 51 + ], + "BrownMushroomBlock": [ + 151, + 109, + 77 + ], + "BrownTerracotta": [ + 76, + 50, + 35 + ], + "BrownWallBanner": [ + 143, + 119, + 72 + ], + "BrownWool": [ + 102, + 76, + 51 + ], + "BubbleColumn": [ + 64, + 64, + 255 + ], + "BubbleCoral": [ + 127, + 63, + 178 + ], + "BubbleCoralBlock": [ + 127, + 63, + 178 + ], + "BubbleCoralFan": [ + 127, + 63, + 178 + ], + "BubbleCoralWallFan": [ + 127, + 63, + 178 + ], + "BuddingAmethyst": [ + 127, + 63, + 178 + ], + "Bush": [ + 0, + 124, + 0 + ], + "Cactus": [ + 0, + 124, + 0 + ], + "CactusFlower": [ + 242, + 127, + 165 + ], + "Calcite": [ + 209, + 177, + 161 + ], + "Campfire": [ + 129, + 86, + 49 + ], + "Carrots": [ + 0, + 124, + 0 + ], + "CartographyTable": [ + 143, + 119, + 72 + ], + "CarvedPumpkin": [ + 216, + 127, + 51 + ], + "Cauldron": [ + 112, + 112, + 112 + ], + "CaveVines": [ + 0, + 124, + 0 + ], + "CaveVinesPlant": [ + 0, + 124, + 0 + ], + "ChainCommandBlock": [ + 102, + 127, + 51 + ], + "CherryDoor": [ + 209, + 177, + 161 + ], + "CherryFence": [ + 209, + 177, + 161 + ], + "CherryFenceGate": [ + 209, + 177, + 161 + ], + "CherryHangingSign": [ + 160, + 77, + 78 + ], + "CherryLeaves": [ + 242, + 127, + 165 + ], + "CherryPlanks": [ + 209, + 177, + 161 + ], + "CherryPressurePlate": [ + 209, + 177, + 161 + ], + "CherrySapling": [ + 242, + 127, + 165 + ], + "CherryShelf": [ + 209, + 177, + 161 + ], + "CherrySign": [ + 209, + 177, + 161 + ], + "CherrySlab": [ + 209, + 177, + 161 + ], + "CherryTrapdoor": [ + 209, + 177, + 161 + ], + "CherryWallHangingSign": [ + 160, + 77, + 78 + ], + "CherryWood": [ + 57, + 41, + 35 + ], + "Chest": [ + 143, + 119, + 72 + ], + "ChippedAnvil": [ + 167, + 167, + 167 + ], + "ChiseledBookshelf": [ + 143, + 119, + 72 + ], + "ChiseledNetherBricks": [ + 112, + 2, + 0 + ], + "ChiseledQuartzBlock": [ + 255, + 252, + 245 + ], + "ChiseledRedSandstone": [ + 216, + 127, + 51 + ], + "ChiseledResinBricks": [ + 159, + 82, + 36 + ], + "ChiseledSandstone": [ + 247, + 233, + 163 + ], + "ChiseledStoneBricks": [ + 112, + 112, + 112 + ], + "ChorusFlower": [ + 127, + 63, + 178 + ], + "ChorusPlant": [ + 127, + 63, + 178 + ], + "Clay": [ + 164, + 168, + 184 + ], + "ClosedEyeblossom": [ + 167, + 167, + 167 + ], + "CoalBlock": [ + 25, + 25, + 25 + ], + "CoalOre": [ + 112, + 112, + 112 + ], + "CoarseDirt": [ + 151, + 109, + 77 + ], + "Cobblestone": [ + 112, + 112, + 112 + ], + "CobblestoneSlab": [ + 112, + 112, + 112 + ], + "Cobweb": [ + 199, + 199, + 199 + ], + "Cocoa": [ + 0, + 124, + 0 + ], + "CommandBlock": [ + 102, + 76, + 51 + ], + "Composter": [ + 143, + 119, + 72 + ], + "Conduit": [ + 92, + 219, + 213 + ], + "CopperBlock": [ + 216, + 127, + 51 + ], + "CopperBulb": [ + 216, + 127, + 51 + ], + "CopperChest": [ + 216, + 127, + 51 + ], + "CopperDoor": [ + 216, + 127, + 51 + ], + "CopperGolemStatue": [ + 216, + 127, + 51 + ], + "CopperGrate": [ + 216, + 127, + 51 + ], + "CopperTrapdoor": [ + 216, + 127, + 51 + ], + "Cornflower": [ + 0, + 124, + 0 + ], + "CrackedNetherBricks": [ + 112, + 2, + 0 + ], + "CrackedStoneBricks": [ + 112, + 112, + 112 + ], + "Crafter": [ + 112, + 112, + 112 + ], + "CraftingTable": [ + 143, + 119, + 72 + ], + "CreakingHeart": [ + 216, + 127, + 51 + ], + "CrimsonDoor": [ + 148, + 63, + 97 + ], + "CrimsonFence": [ + 148, + 63, + 97 + ], + "CrimsonFenceGate": [ + 148, + 63, + 97 + ], + "CrimsonFungus": [ + 112, + 2, + 0 + ], + "CrimsonHangingSign": [ + 148, + 63, + 97 + ], + "CrimsonHyphae": [ + 92, + 25, + 29 + ], + "CrimsonNylium": [ + 189, + 48, + 49 + ], + "CrimsonPlanks": [ + 148, + 63, + 97 + ], + "CrimsonPressurePlate": [ + 148, + 63, + 97 + ], + "CrimsonRoots": [ + 112, + 2, + 0 + ], + "CrimsonShelf": [ + 148, + 63, + 97 + ], + "CrimsonSign": [ + 148, + 63, + 97 + ], + "CrimsonSlab": [ + 148, + 63, + 97 + ], + "CrimsonTrapdoor": [ + 148, + 63, + 97 + ], + "CrimsonWallHangingSign": [ + 148, + 63, + 97 + ], + "CrimsonWallSign": [ + 148, + 63, + 97 + ], + "CryingObsidian": [ + 25, + 25, + 25 + ], + "CutRedSandstone": [ + 216, + 127, + 51 + ], + "CutRedSandstoneSlab": [ + 216, + 127, + 51 + ], + "CutSandstone": [ + 247, + 233, + 163 + ], + "CutSandstoneSlab": [ + 247, + 233, + 163 + ], + "CyanBanner": [ + 143, + 119, + 72 + ], + "CyanCarpet": [ + 76, + 127, + 153 + ], + "CyanConcrete": [ + 76, + 127, + 153 + ], + "CyanConcretePowder": [ + 76, + 127, + 153 + ], + "CyanGlazedTerracotta": [ + 76, + 127, + 153 + ], + "CyanTerracotta": [ + 87, + 92, + 92 + ], + "CyanWallBanner": [ + 143, + 119, + 72 + ], + "CyanWool": [ + 76, + 127, + 153 + ], + "DamagedAnvil": [ + 167, + 167, + 167 + ], + "Dandelion": [ + 0, + 124, + 0 + ], + "DarkOakDoor": [ + 102, + 76, + 51 + ], + "DarkOakFence": [ + 102, + 76, + 51 + ], + "DarkOakFenceGate": [ + 102, + 76, + 51 + ], + "DarkOakPlanks": [ + 102, + 76, + 51 + ], + "DarkOakPressurePlate": [ + 102, + 76, + 51 + ], + "DarkOakSapling": [ + 0, + 124, + 0 + ], + "DarkOakSlab": [ + 102, + 76, + 51 + ], + "DarkOakTrapdoor": [ + 102, + 76, + 51 + ], + "DarkOakWood": [ + 102, + 76, + 51 + ], + "DarkPrismarine": [ + 92, + 219, + 213 + ], + "DarkPrismarineSlab": [ + 92, + 219, + 213 + ], + "DaylightDetector": [ + 143, + 119, + 72 + ], + "DeadBrainCoral": [ + 76, + 76, + 76 + ], + "DeadBrainCoralBlock": [ + 76, + 76, + 76 + ], + "DeadBrainCoralFan": [ + 76, + 76, + 76 + ], + "DeadBrainCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadBubbleCoral": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralBlock": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralFan": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadBush": [ + 143, + 119, + 72 + ], + "DeadFireCoral": [ + 76, + 76, + 76 + ], + "DeadFireCoralBlock": [ + 76, + 76, + 76 + ], + "DeadFireCoralFan": [ + 76, + 76, + 76 + ], + "DeadFireCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadHornCoral": [ + 76, + 76, + 76 + ], + "DeadHornCoralBlock": [ + 76, + 76, + 76 + ], + "DeadHornCoralFan": [ + 76, + 76, + 76 + ], + "DeadHornCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadTubeCoral": [ + 76, + 76, + 76 + ], + "DeadTubeCoralBlock": [ + 76, + 76, + 76 + ], + "DeadTubeCoralFan": [ + 76, + 76, + 76 + ], + "DeadTubeCoralWallFan": [ + 76, + 76, + 76 + ], + "DecoratedPot": [ + 142, + 60, + 46 + ], + "Deepslate": [ + 100, + 100, + 100 + ], + "DeepslateCoalOre": [ + 100, + 100, + 100 + ], + "DeepslateCopperOre": [ + 100, + 100, + 100 + ], + "DeepslateDiamondOre": [ + 100, + 100, + 100 + ], + "DeepslateEmeraldOre": [ + 100, + 100, + 100 + ], + "DeepslateGoldOre": [ + 100, + 100, + 100 + ], + "DeepslateIronOre": [ + 100, + 100, + 100 + ], + "DeepslateLapisOre": [ + 100, + 100, + 100 + ], + "DeepslateRedstoneOre": [ + 100, + 100, + 100 + ], + "DiamondBlock": [ + 92, + 219, + 213 + ], + "DiamondOre": [ + 112, + 112, + 112 + ], + "Diorite": [ + 255, + 252, + 245 + ], + "Dirt": [ + 151, + 109, + 77 + ], + "DirtPath": [ + 151, + 109, + 77 + ], + "Dispenser": [ + 112, + 112, + 112 + ], + "DragonEgg": [ + 25, + 25, + 25 + ], + "DriedGhast": [ + 76, + 76, + 76 + ], + "DriedKelpBlock": [ + 102, + 127, + 51 + ], + "DripstoneBlock": [ + 76, + 50, + 35 + ], + "Dropper": [ + 112, + 112, + 112 + ], + "EmeraldBlock": [ + 0, + 217, + 58 + ], + "EmeraldOre": [ + 112, + 112, + 112 + ], + "EnchantingTable": [ + 153, + 51, + 51 + ], + "EndGateway": [ + 25, + 25, + 25 + ], + "EndPortal": [ + 25, + 25, + 25 + ], + "EndPortalFrame": [ + 102, + 127, + 51 + ], + "EndStone": [ + 247, + 233, + 163 + ], + "EndStoneBricks": [ + 247, + 233, + 163 + ], + "EnderChest": [ + 112, + 112, + 112 + ], + "ExposedCopper": [ + 135, + 107, + 98 + ], + "ExposedCopperBulb": [ + 135, + 107, + 98 + ], + "ExposedCopperChest": [ + 135, + 107, + 98 + ], + "ExposedCopperDoor": [ + 135, + 107, + 98 + ], + "ExposedCopperGolemStatue": [ + 135, + 107, + 98 + ], + "ExposedCopperGrate": [ + 135, + 107, + 98 + ], + "ExposedCopperTrapdoor": [ + 135, + 107, + 98 + ], + "ExposedLightningRod": [ + 135, + 107, + 98 + ], + "Farmland": [ + 151, + 109, + 77 + ], + "Fern": [ + 0, + 124, + 0 + ], + "Fire": [ + 255, + 0, + 0 + ], + "FireCoral": [ + 153, + 51, + 51 + ], + "FireCoralBlock": [ + 153, + 51, + 51 + ], + "FireCoralFan": [ + 153, + 51, + 51 + ], + "FireCoralWallFan": [ + 153, + 51, + 51 + ], + "FireflyBush": [ + 0, + 124, + 0 + ], + "FletchingTable": [ + 143, + 119, + 72 + ], + "FloweringAzalea": [ + 0, + 124, + 0 + ], + "Frogspawn": [ + 64, + 64, + 255 + ], + "FrostedIce": [ + 160, + 160, + 255 + ], + "Furnace": [ + 112, + 112, + 112 + ], + "GlowLichen": [ + 127, + 167, + 150 + ], + "Glowstone": [ + 247, + 233, + 163 + ], + "GoldBlock": [ + 250, + 238, + 77 + ], + "GoldOre": [ + 112, + 112, + 112 + ], + "GoldenDandelion": [ + 0, + 124, + 0 + ], + "Granite": [ + 151, + 109, + 77 + ], + "GrassBlock": [ + 127, + 178, + 56 + ], + "Gravel": [ + 112, + 112, + 112 + ], + "GrayBanner": [ + 143, + 119, + 72 + ], + "GrayCarpet": [ + 76, + 76, + 76 + ], + "GrayConcrete": [ + 76, + 76, + 76 + ], + "GrayConcretePowder": [ + 76, + 76, + 76 + ], + "GrayGlazedTerracotta": [ + 76, + 76, + 76 + ], + "GrayTerracotta": [ + 57, + 41, + 35 + ], + "GrayWallBanner": [ + 143, + 119, + 72 + ], + "GrayWool": [ + 76, + 76, + 76 + ], + "GreenBanner": [ + 143, + 119, + 72 + ], + "GreenCarpet": [ + 102, + 127, + 51 + ], + "GreenConcrete": [ + 102, + 127, + 51 + ], + "GreenConcretePowder": [ + 102, + 127, + 51 + ], + "GreenGlazedTerracotta": [ + 102, + 127, + 51 + ], + "GreenTerracotta": [ + 76, + 82, + 42 + ], + "GreenWallBanner": [ + 143, + 119, + 72 + ], + "GreenWool": [ + 102, + 127, + 51 + ], + "Grindstone": [ + 167, + 167, + 167 + ], + "HangingRoots": [ + 151, + 109, + 77 + ], + "HayBlock": [ + 229, + 229, + 51 + ], + "HeavyCore": [ + 167, + 167, + 167 + ], + "HeavyWeightedPressurePlate": [ + 167, + 167, + 167 + ], + "HoneyBlock": [ + 216, + 127, + 51 + ], + "HoneycombBlock": [ + 216, + 127, + 51 + ], + "Hopper": [ + 112, + 112, + 112 + ], + "HornCoral": [ + 229, + 229, + 51 + ], + "HornCoralBlock": [ + 229, + 229, + 51 + ], + "HornCoralFan": [ + 229, + 229, + 51 + ], + "HornCoralWallFan": [ + 229, + 229, + 51 + ], + "Ice": [ + 160, + 160, + 255 + ], + "InfestedChiseledStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedCobblestone": [ + 164, + 168, + 184 + ], + "InfestedCrackedStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedDeepslate": [ + 100, + 100, + 100 + ], + "InfestedMossyStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedStone": [ + 164, + 168, + 184 + ], + "InfestedStoneBricks": [ + 164, + 168, + 184 + ], + "IronBlock": [ + 167, + 167, + 167 + ], + "IronDoor": [ + 167, + 167, + 167 + ], + "IronOre": [ + 112, + 112, + 112 + ], + "IronTrapdoor": [ + 167, + 167, + 167 + ], + "JackOLantern": [ + 216, + 127, + 51 + ], + "Jigsaw": [ + 153, + 153, + 153 + ], + "Jukebox": [ + 151, + 109, + 77 + ], + "JungleDoor": [ + 151, + 109, + 77 + ], + "JungleFence": [ + 151, + 109, + 77 + ], + "JungleFenceGate": [ + 151, + 109, + 77 + ], + "JunglePlanks": [ + 151, + 109, + 77 + ], + "JunglePressurePlate": [ + 151, + 109, + 77 + ], + "JungleSapling": [ + 0, + 124, + 0 + ], + "JungleSlab": [ + 151, + 109, + 77 + ], + "JungleTrapdoor": [ + 151, + 109, + 77 + ], + "JungleWood": [ + 151, + 109, + 77 + ], + "Kelp": [ + 64, + 64, + 255 + ], + "KelpPlant": [ + 64, + 64, + 255 + ], + "Lantern": [ + 167, + 167, + 167 + ], + "LapisBlock": [ + 74, + 128, + 255 + ], + "LapisOre": [ + 112, + 112, + 112 + ], + "LargeFern": [ + 0, + 124, + 0 + ], + "Lava": [ + 255, + 0, + 0 + ], + "LeafLitter": [ + 102, + 76, + 51 + ], + "Lectern": [ + 143, + 119, + 72 + ], + "Light": [ + 0, + 0, + 0 + ], + "LightBlueBanner": [ + 143, + 119, + 72 + ], + "LightBlueCarpet": [ + 102, + 153, + 216 + ], + "LightBlueConcrete": [ + 102, + 153, + 216 + ], + "LightBlueConcretePowder": [ + 102, + 153, + 216 + ], + "LightBlueGlazedTerracotta": [ + 102, + 153, + 216 + ], + "LightBlueTerracotta": [ + 112, + 108, + 138 + ], + "LightBlueWallBanner": [ + 143, + 119, + 72 + ], + "LightBlueWool": [ + 102, + 153, + 216 + ], + "LightGrayBanner": [ + 143, + 119, + 72 + ], + "LightGrayCarpet": [ + 153, + 153, + 153 + ], + "LightGrayConcrete": [ + 153, + 153, + 153 + ], + "LightGrayConcretePowder": [ + 153, + 153, + 153 + ], + "LightGrayGlazedTerracotta": [ + 153, + 153, + 153 + ], + "LightGrayTerracotta": [ + 135, + 107, + 98 + ], + "LightGrayWallBanner": [ + 143, + 119, + 72 + ], + "LightGrayWool": [ + 153, + 153, + 153 + ], + "LightWeightedPressurePlate": [ + 250, + 238, + 77 + ], + "LightningRod": [ + 216, + 127, + 51 + ], + "Lilac": [ + 0, + 124, + 0 + ], + "LilyOfTheValley": [ + 0, + 124, + 0 + ], + "LilyPad": [ + 0, + 124, + 0 + ], + "LimeBanner": [ + 143, + 119, + 72 + ], + "LimeCarpet": [ + 127, + 204, + 25 + ], + "LimeConcrete": [ + 127, + 204, + 25 + ], + "LimeConcretePowder": [ + 127, + 204, + 25 + ], + "LimeGlazedTerracotta": [ + 127, + 204, + 25 + ], + "LimeTerracotta": [ + 103, + 117, + 53 + ], + "LimeWallBanner": [ + 143, + 119, + 72 + ], + "LimeWool": [ + 127, + 204, + 25 + ], + "Lodestone": [ + 167, + 167, + 167 + ], + "Loom": [ + 143, + 119, + 72 + ], + "MagentaBanner": [ + 143, + 119, + 72 + ], + "MagentaCarpet": [ + 178, + 76, + 216 + ], + "MagentaConcrete": [ + 178, + 76, + 216 + ], + "MagentaConcretePowder": [ + 178, + 76, + 216 + ], + "MagentaGlazedTerracotta": [ + 178, + 76, + 216 + ], + "MagentaTerracotta": [ + 149, + 87, + 108 + ], + "MagentaWallBanner": [ + 143, + 119, + 72 + ], + "MagentaWool": [ + 178, + 76, + 216 + ], + "MagmaBlock": [ + 112, + 2, + 0 + ], + "MangroveDoor": [ + 153, + 51, + 51 + ], + "MangroveFence": [ + 153, + 51, + 51 + ], + "MangroveFenceGate": [ + 153, + 51, + 51 + ], + "MangrovePlanks": [ + 153, + 51, + 51 + ], + "MangrovePressurePlate": [ + 153, + 51, + 51 + ], + "MangrovePropagule": [ + 0, + 124, + 0 + ], + "MangroveRoots": [ + 129, + 86, + 49 + ], + "MangroveSlab": [ + 153, + 51, + 51 + ], + "MangroveTrapdoor": [ + 153, + 51, + 51 + ], + "MangroveWood": [ + 153, + 51, + 51 + ], + "Melon": [ + 127, + 204, + 25 + ], + "MelonStem": [ + 0, + 124, + 0 + ], + "MossBlock": [ + 102, + 127, + 51 + ], + "MossCarpet": [ + 102, + 127, + 51 + ], + "MossyCobblestone": [ + 112, + 112, + 112 + ], + "MossyStoneBricks": [ + 112, + 112, + 112 + ], + "MovingPiston": [ + 112, + 112, + 112 + ], + "Mud": [ + 87, + 92, + 92 + ], + "MudBrickSlab": [ + 135, + 107, + 98 + ], + "MudBricks": [ + 135, + 107, + 98 + ], + "MuddyMangroveRoots": [ + 129, + 86, + 49 + ], + "MushroomStem": [ + 199, + 199, + 199 + ], + "Mycelium": [ + 127, + 63, + 178 + ], + "NetherBrickFence": [ + 112, + 2, + 0 + ], + "NetherBrickSlab": [ + 112, + 2, + 0 + ], + "NetherBricks": [ + 112, + 2, + 0 + ], + "NetherGoldOre": [ + 112, + 2, + 0 + ], + "NetherQuartzOre": [ + 112, + 2, + 0 + ], + "NetherSprouts": [ + 76, + 127, + 153 + ], + "NetherWart": [ + 153, + 51, + 51 + ], + "NetherWartBlock": [ + 153, + 51, + 51 + ], + "NetheriteBlock": [ + 25, + 25, + 25 + ], + "Netherrack": [ + 112, + 2, + 0 + ], + "NoteBlock": [ + 143, + 119, + 72 + ], + "OakDoor": [ + 143, + 119, + 72 + ], + "OakFence": [ + 143, + 119, + 72 + ], + "OakFenceGate": [ + 143, + 119, + 72 + ], + "OakPlanks": [ + 143, + 119, + 72 + ], + "OakPressurePlate": [ + 143, + 119, + 72 + ], + "OakSapling": [ + 0, + 124, + 0 + ], + "OakShelf": [ + 143, + 119, + 72 + ], + "OakSign": [ + 143, + 119, + 72 + ], + "OakSlab": [ + 143, + 119, + 72 + ], + "OakTrapdoor": [ + 143, + 119, + 72 + ], + "OakWallSign": [ + 143, + 119, + 72 + ], + "OakWood": [ + 143, + 119, + 72 + ], + "Observer": [ + 112, + 112, + 112 + ], + "Obsidian": [ + 25, + 25, + 25 + ], + "OchreFroglight": [ + 247, + 233, + 163 + ], + "OpenEyeblossom": [ + 216, + 127, + 51 + ], + "OrangeBanner": [ + 143, + 119, + 72 + ], + "OrangeCarpet": [ + 216, + 127, + 51 + ], + "OrangeConcrete": [ + 216, + 127, + 51 + ], + "OrangeConcretePowder": [ + 216, + 127, + 51 + ], + "OrangeGlazedTerracotta": [ + 216, + 127, + 51 + ], + "OrangeTerracotta": [ + 159, + 82, + 36 + ], + "OrangeTulip": [ + 0, + 124, + 0 + ], + "OrangeWallBanner": [ + 143, + 119, + 72 + ], + "OrangeWool": [ + 216, + 127, + 51 + ], + "OxeyeDaisy": [ + 0, + 124, + 0 + ], + "OxidizedCopper": [ + 22, + 126, + 134 + ], + "OxidizedCopperBulb": [ + 22, + 126, + 134 + ], + "OxidizedCopperChest": [ + 22, + 126, + 134 + ], + "OxidizedCopperDoor": [ + 22, + 126, + 134 + ], + "OxidizedCopperGolemStatue": [ + 22, + 126, + 134 + ], + "OxidizedCopperGrate": [ + 22, + 126, + 134 + ], + "OxidizedCopperTrapdoor": [ + 22, + 126, + 134 + ], + "OxidizedLightningRod": [ + 22, + 126, + 134 + ], + "PackedIce": [ + 160, + 160, + 255 + ], + "PaleHangingMoss": [ + 153, + 153, + 153 + ], + "PaleMossBlock": [ + 153, + 153, + 153 + ], + "PaleMossCarpet": [ + 153, + 153, + 153 + ], + "PaleOakDoor": [ + 255, + 252, + 245 + ], + "PaleOakFence": [ + 255, + 252, + 245 + ], + "PaleOakFenceGate": [ + 255, + 252, + 245 + ], + "PaleOakHangingSign": [ + 255, + 252, + 245 + ], + "PaleOakLeaves": [ + 167, + 167, + 167 + ], + "PaleOakPlanks": [ + 255, + 252, + 245 + ], + "PaleOakPressurePlate": [ + 255, + 252, + 245 + ], + "PaleOakSapling": [ + 167, + 167, + 167 + ], + "PaleOakShelf": [ + 255, + 252, + 245 + ], + "PaleOakSign": [ + 255, + 252, + 245 + ], + "PaleOakSlab": [ + 255, + 252, + 245 + ], + "PaleOakTrapdoor": [ + 255, + 252, + 245 + ], + "PaleOakWallHangingSign": [ + 255, + 252, + 245 + ], + "PaleOakWallSign": [ + 255, + 252, + 245 + ], + "PaleOakWood": [ + 112, + 112, + 112 + ], + "PearlescentFroglight": [ + 242, + 127, + 165 + ], + "Peony": [ + 0, + 124, + 0 + ], + "PetrifiedOakSlab": [ + 143, + 119, + 72 + ], + "PinkBanner": [ + 143, + 119, + 72 + ], + "PinkCarpet": [ + 242, + 127, + 165 + ], + "PinkConcrete": [ + 242, + 127, + 165 + ], + "PinkConcretePowder": [ + 242, + 127, + 165 + ], + "PinkGlazedTerracotta": [ + 242, + 127, + 165 + ], + "PinkPetals": [ + 0, + 124, + 0 + ], + "PinkTerracotta": [ + 160, + 77, + 78 + ], + "PinkTulip": [ + 0, + 124, + 0 + ], + "PinkWallBanner": [ + 143, + 119, + 72 + ], + "PinkWool": [ + 242, + 127, + 165 + ], + "PistonHead": [ + 112, + 112, + 112 + ], + "PitcherCrop": [ + 0, + 124, + 0 + ], + "PitcherPlant": [ + 0, + 124, + 0 + ], + "Podzol": [ + 129, + 86, + 49 + ], + "PointedDripstone": [ + 76, + 50, + 35 + ], + "PolishedAndesite": [ + 112, + 112, + 112 + ], + "PolishedBasalt": [ + 25, + 25, + 25 + ], + "PolishedBlackstonePressurePlate": [ + 25, + 25, + 25 + ], + "PolishedDiorite": [ + 255, + 252, + 245 + ], + "PolishedGranite": [ + 151, + 109, + 77 + ], + "Poppy": [ + 0, + 124, + 0 + ], + "Potatoes": [ + 0, + 124, + 0 + ], + "PowderSnow": [ + 255, + 255, + 255 + ], + "Prismarine": [ + 76, + 127, + 153 + ], + "PrismarineBrickSlab": [ + 92, + 219, + 213 + ], + "PrismarineBricks": [ + 92, + 219, + 213 + ], + "PrismarineSlab": [ + 76, + 127, + 153 + ], + "Pumpkin": [ + 216, + 127, + 51 + ], + "PumpkinStem": [ + 0, + 124, + 0 + ], + "PurpleBanner": [ + 143, + 119, + 72 + ], + "PurpleCarpet": [ + 127, + 63, + 178 + ], + "PurpleConcrete": [ + 127, + 63, + 178 + ], + "PurpleConcretePowder": [ + 127, + 63, + 178 + ], + "PurpleGlazedTerracotta": [ + 127, + 63, + 178 + ], + "PurpleTerracotta": [ + 122, + 73, + 88 + ], + "PurpleWallBanner": [ + 143, + 119, + 72 + ], + "PurpleWool": [ + 127, + 63, + 178 + ], + "PurpurBlock": [ + 178, + 76, + 216 + ], + "PurpurPillar": [ + 178, + 76, + 216 + ], + "PurpurSlab": [ + 178, + 76, + 216 + ], + "QuartzBlock": [ + 255, + 252, + 245 + ], + "QuartzPillar": [ + 255, + 252, + 245 + ], + "QuartzSlab": [ + 255, + 252, + 245 + ], + "RawCopperBlock": [ + 216, + 127, + 51 + ], + "RawGoldBlock": [ + 250, + 238, + 77 + ], + "RawIronBlock": [ + 216, + 175, + 147 + ], + "RedBanner": [ + 143, + 119, + 72 + ], + "RedCarpet": [ + 153, + 51, + 51 + ], + "RedConcrete": [ + 153, + 51, + 51 + ], + "RedConcretePowder": [ + 153, + 51, + 51 + ], + "RedGlazedTerracotta": [ + 153, + 51, + 51 + ], + "RedMushroom": [ + 153, + 51, + 51 + ], + "RedMushroomBlock": [ + 153, + 51, + 51 + ], + "RedNetherBricks": [ + 112, + 2, + 0 + ], + "RedSand": [ + 216, + 127, + 51 + ], + "RedSandstone": [ + 216, + 127, + 51 + ], + "RedSandstoneSlab": [ + 216, + 127, + 51 + ], + "RedTerracotta": [ + 142, + 60, + 46 + ], + "RedTulip": [ + 0, + 124, + 0 + ], + "RedWallBanner": [ + 143, + 119, + 72 + ], + "RedWool": [ + 153, + 51, + 51 + ], + "RedstoneBlock": [ + 255, + 0, + 0 + ], + "RedstoneLamp": [ + 159, + 82, + 36 + ], + "RedstoneOre": [ + 112, + 112, + 112 + ], + "ReinforcedDeepslate": [ + 100, + 100, + 100 + ], + "RepeatingCommandBlock": [ + 127, + 63, + 178 + ], + "ResinBlock": [ + 159, + 82, + 36 + ], + "ResinBrickSlab": [ + 159, + 82, + 36 + ], + "ResinBrickWall": [ + 159, + 82, + 36 + ], + "ResinBricks": [ + 159, + 82, + 36 + ], + "ResinClump": [ + 159, + 82, + 36 + ], + "RespawnAnchor": [ + 25, + 25, + 25 + ], + "RootedDirt": [ + 151, + 109, + 77 + ], + "RoseBush": [ + 0, + 124, + 0 + ], + "Sand": [ + 247, + 233, + 163 + ], + "Sandstone": [ + 247, + 233, + 163 + ], + "SandstoneSlab": [ + 247, + 233, + 163 + ], + "Scaffolding": [ + 247, + 233, + 163 + ], + "Sculk": [ + 25, + 25, + 25 + ], + "SculkCatalyst": [ + 25, + 25, + 25 + ], + "SculkSensor": [ + 76, + 127, + 153 + ], + "SculkShrieker": [ + 25, + 25, + 25 + ], + "SculkVein": [ + 25, + 25, + 25 + ], + "SeaLantern": [ + 255, + 252, + 245 + ], + "SeaPickle": [ + 102, + 127, + 51 + ], + "Seagrass": [ + 64, + 64, + 255 + ], + "ShortDryGrass": [ + 229, + 229, + 51 + ], + "ShortGrass": [ + 0, + 124, + 0 + ], + "Shroomlight": [ + 153, + 51, + 51 + ], + "SlimeBlock": [ + 127, + 178, + 56 + ], + "SmallDripleaf": [ + 0, + 124, + 0 + ], + "SmithingTable": [ + 143, + 119, + 72 + ], + "Smoker": [ + 112, + 112, + 112 + ], + "SmoothQuartz": [ + 255, + 252, + 245 + ], + "SmoothRedSandstone": [ + 216, + 127, + 51 + ], + "SmoothSandstone": [ + 247, + 233, + 163 + ], + "SmoothStone": [ + 112, + 112, + 112 + ], + "SmoothStoneSlab": [ + 112, + 112, + 112 + ], + "SnifferEgg": [ + 153, + 51, + 51 + ], + "Snow": [ + 255, + 255, + 255 + ], + "SnowBlock": [ + 255, + 255, + 255 + ], + "SoulCampfire": [ + 129, + 86, + 49 + ], + "SoulFire": [ + 102, + 153, + 216 + ], + "SoulLantern": [ + 167, + 167, + 167 + ], + "SoulSand": [ + 102, + 76, + 51 + ], + "SoulSoil": [ + 102, + 76, + 51 + ], + "Spawner": [ + 112, + 112, + 112 + ], + "Sponge": [ + 229, + 229, + 51 + ], + "SporeBlossom": [ + 0, + 124, + 0 + ], + "SpruceDoor": [ + 129, + 86, + 49 + ], + "SpruceFence": [ + 129, + 86, + 49 + ], + "SpruceFenceGate": [ + 129, + 86, + 49 + ], + "SprucePlanks": [ + 129, + 86, + 49 + ], + "SprucePressurePlate": [ + 129, + 86, + 49 + ], + "SpruceSapling": [ + 0, + 124, + 0 + ], + "SpruceSlab": [ + 129, + 86, + 49 + ], + "SpruceTrapdoor": [ + 129, + 86, + 49 + ], + "SpruceWallHangingSign": [ + 143, + 119, + 72 + ], + "SpruceWood": [ + 129, + 86, + 49 + ], + "Stone": [ + 112, + 112, + 112 + ], + "StoneBrickSlab": [ + 112, + 112, + 112 + ], + "StoneBricks": [ + 112, + 112, + 112 + ], + "StonePressurePlate": [ + 112, + 112, + 112 + ], + "StoneSlab": [ + 112, + 112, + 112 + ], + "Stonecutter": [ + 112, + 112, + 112 + ], + "StrippedAcaciaWood": [ + 216, + 127, + 51 + ], + "StrippedBirchWood": [ + 247, + 233, + 163 + ], + "StrippedCherryWood": [ + 160, + 77, + 78 + ], + "StrippedCrimsonHyphae": [ + 92, + 25, + 29 + ], + "StrippedDarkOakWood": [ + 102, + 76, + 51 + ], + "StrippedJungleWood": [ + 151, + 109, + 77 + ], + "StrippedOakWood": [ + 143, + 119, + 72 + ], + "StrippedPaleOakWood": [ + 255, + 252, + 245 + ], + "StrippedSpruceWood": [ + 129, + 86, + 49 + ], + "StrippedWarpedHyphae": [ + 86, + 44, + 62 + ], + "StructureBlock": [ + 153, + 153, + 153 + ], + "SugarCane": [ + 0, + 124, + 0 + ], + "Sunflower": [ + 0, + 124, + 0 + ], + "SuspiciousGravel": [ + 112, + 112, + 112 + ], + "SuspiciousSand": [ + 247, + 233, + 163 + ], + "SweetBerryBush": [ + 0, + 124, + 0 + ], + "TallDryGrass": [ + 229, + 229, + 51 + ], + "TallGrass": [ + 0, + 124, + 0 + ], + "TallSeagrass": [ + 64, + 64, + 255 + ], + "Target": [ + 255, + 252, + 245 + ], + "Terracotta": [ + 216, + 127, + 51 + ], + "TestBlock": [ + 153, + 153, + 153 + ], + "TintedGlass": [ + 76, + 76, + 76 + ], + "Tnt": [ + 255, + 0, + 0 + ], + "Torchflower": [ + 0, + 124, + 0 + ], + "TorchflowerCrop": [ + 0, + 124, + 0 + ], + "TrappedChest": [ + 143, + 119, + 72 + ], + "TrialSpawner": [ + 112, + 112, + 112 + ], + "TubeCoral": [ + 51, + 76, + 178 + ], + "TubeCoralBlock": [ + 51, + 76, + 178 + ], + "TubeCoralFan": [ + 51, + 76, + 178 + ], + "TubeCoralWallFan": [ + 51, + 76, + 178 + ], + "Tuff": [ + 57, + 41, + 35 + ], + "TurtleEgg": [ + 247, + 233, + 163 + ], + "TwistingVines": [ + 76, + 127, + 153 + ], + "TwistingVinesPlant": [ + 76, + 127, + 153 + ], + "Vault": [ + 112, + 112, + 112 + ], + "VerdantFroglight": [ + 127, + 167, + 150 + ], + "Vine": [ + 0, + 124, + 0 + ], + "WarpedDoor": [ + 58, + 142, + 140 + ], + "WarpedFence": [ + 58, + 142, + 140 + ], + "WarpedFenceGate": [ + 58, + 142, + 140 + ], + "WarpedFungus": [ + 76, + 127, + 153 + ], + "WarpedHangingSign": [ + 58, + 142, + 140 + ], + "WarpedHyphae": [ + 86, + 44, + 62 + ], + "WarpedNylium": [ + 22, + 126, + 134 + ], + "WarpedPlanks": [ + 58, + 142, + 140 + ], + "WarpedPressurePlate": [ + 58, + 142, + 140 + ], + "WarpedRoots": [ + 76, + 127, + 153 + ], + "WarpedShelf": [ + 58, + 142, + 140 + ], + "WarpedSign": [ + 58, + 142, + 140 + ], + "WarpedSlab": [ + 58, + 142, + 140 + ], + "WarpedTrapdoor": [ + 58, + 142, + 140 + ], + "WarpedWallHangingSign": [ + 58, + 142, + 140 + ], + "WarpedWallSign": [ + 58, + 142, + 140 + ], + "WarpedWartBlock": [ + 20, + 180, + 133 + ], + "Water": [ + 64, + 64, + 255 + ], + "WeatheredCopper": [ + 58, + 142, + 140 + ], + "WeatheredCopperBulb": [ + 58, + 142, + 140 + ], + "WeatheredCopperChest": [ + 58, + 142, + 140 + ], + "WeatheredCopperDoor": [ + 58, + 142, + 140 + ], + "WeatheredCopperGolemStatue": [ + 58, + 142, + 140 + ], + "WeatheredCopperGrate": [ + 58, + 142, + 140 + ], + "WeatheredCopperTrapdoor": [ + 58, + 142, + 140 + ], + "WeatheredLightningRod": [ + 58, + 142, + 140 + ], + "WeepingVines": [ + 112, + 2, + 0 + ], + "WeepingVinesPlant": [ + 112, + 2, + 0 + ], + "WetSponge": [ + 229, + 229, + 51 + ], + "WhiteBanner": [ + 143, + 119, + 72 + ], + "WhiteCarpet": [ + 255, + 255, + 255 + ], + "WhiteConcrete": [ + 255, + 255, + 255 + ], + "WhiteConcretePowder": [ + 255, + 255, + 255 + ], + "WhiteGlazedTerracotta": [ + 255, + 255, + 255 + ], + "WhiteTerracotta": [ + 209, + 177, + 161 + ], + "WhiteTulip": [ + 0, + 124, + 0 + ], + "WhiteWallBanner": [ + 143, + 119, + 72 + ], + "WhiteWool": [ + 255, + 255, + 255 + ], + "Wildflowers": [ + 0, + 124, + 0 + ], + "WitherRose": [ + 0, + 124, + 0 + ], + "YellowBanner": [ + 143, + 119, + 72 + ], + "YellowCarpet": [ + 229, + 229, + 51 + ], + "YellowConcrete": [ + 229, + 229, + 51 + ], + "YellowConcretePowder": [ + 229, + 229, + 51 + ], + "YellowGlazedTerracotta": [ + 229, + 229, + 51 + ], + "YellowTerracotta": [ + 186, + 133, + 36 + ], + "YellowWallBanner": [ + 143, + 119, + 72 + ], + "YellowWool": [ + 229, + 229, + 51 + ] + }, + "transparent": [ + "Air", + "Barrier", + "BlackStainedGlass", + "BlackStainedGlassPane", + "BlueStainedGlass", + "BlueStainedGlassPane", + "BrownStainedGlass", + "BrownStainedGlassPane", + "CaveAir", + "CyanStainedGlass", + "CyanStainedGlassPane", + "Glass", + "GlassPane", + "GrayStainedGlass", + "GrayStainedGlassPane", + "GreenStainedGlass", + "GreenStainedGlassPane", + "Light", + "LightBlueStainedGlass", + "LightBlueStainedGlassPane", + "LightGrayStainedGlass", + "LightGrayStainedGlassPane", + "LimeStainedGlass", + "LimeStainedGlassPane", + "MagentaStainedGlass", + "MagentaStainedGlassPane", + "OrangeStainedGlass", + "OrangeStainedGlassPane", + "PinkStainedGlass", + "PinkStainedGlassPane", + "PurpleStainedGlass", + "PurpleStainedGlassPane", + "RedStainedGlass", + "RedStainedGlassPane", + "StructureVoid", + "TintedGlass", + "VoidAir", + "WhiteStainedGlass", + "WhiteStainedGlassPane", + "YellowStainedGlass", + "YellowStainedGlassPane" + ], + "water": [ + "Water" + ], + "ice": [ + "Ice", + "PackedIce", + "BlueIce", + "FrostedIce" + ] +} \ No newline at end of file diff --git a/MinecraftClient/Tui/MinimapColorMap.cs b/MinecraftClient/Tui/MinimapColorMap.cs new file mode 100644 index 00000000..ae1bf21c --- /dev/null +++ b/MinecraftClient/Tui/MinimapColorMap.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using Avalonia.Media; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + /// + /// Maps block Materials to minimap colors using data extracted from Minecraft's + /// official MapColor table. Colors are loaded from the embedded MinimapBlockColors.json + /// resource generated by tools/gen_block_color_map.py. + /// + public static class MinimapColorMap + { + public static readonly Color WaterColor = Color.FromRgb(64, 64, 255); + public static readonly Color IceColor = Color.FromRgb(160, 160, 255); + public static readonly Color LavaColor = Color.FromRgb(255, 100, 0); + public static readonly Color DefaultColor = Color.FromRgb(60, 60, 60); + public static readonly Color VoidColor = Color.FromRgb(0, 0, 0); + + private static readonly FrozenDictionary ColorTable; + private static readonly FrozenSet FullyTransparentMats; + private static readonly FrozenSet WaterMats; + private static readonly FrozenSet IceMats; + + static MinimapColorMap() + { + var colors = new Dictionary(); + var transparent = new HashSet(); + var water = new HashSet(); + var ice = new HashSet(); + + try + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("MinimapBlockColors.json"); + if (stream is not null) + { + using var doc = JsonDocument.Parse(stream); + var root = doc.RootElement; + + if (root.TryGetProperty("colors", out var colorsEl)) + { + foreach (var prop in colorsEl.EnumerateObject()) + { + if (!Enum.TryParse(prop.Name, out var mat)) + continue; + var arr = prop.Value; + if (arr.GetArrayLength() < 3) continue; + byte r = (byte)arr[0].GetInt32(); + byte g = (byte)arr[1].GetInt32(); + byte b = (byte)arr[2].GetInt32(); + colors[mat] = Color.FromRgb(r, g, b); + } + } + + if (root.TryGetProperty("transparent", out var transEl)) + { + foreach (var item in transEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + transparent.Add(mat); + } + } + + if (root.TryGetProperty("water", out var waterEl)) + { + foreach (var item in waterEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + water.Add(mat); + } + } + + if (root.TryGetProperty("ice", out var iceEl)) + { + foreach (var item in iceEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + ice.Add(mat); + } + } + } + } + catch (Exception ex) + { + ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Failed to load color data: {ex.Message}"); + } + + if (transparent.Count == 0) + { + transparent.Add(Material.Air); + transparent.Add(Material.CaveAir); + transparent.Add(Material.VoidAir); + } + if (water.Count == 0) + water.Add(Material.Water); + if (ice.Count == 0) + { + ice.Add(Material.Ice); + ice.Add(Material.PackedIce); + ice.Add(Material.BlueIce); + ice.Add(Material.FrostedIce); + } + + ColorTable = colors.ToFrozenDictionary(); + FullyTransparentMats = transparent.ToFrozenSet(); + WaterMats = water.ToFrozenSet(); + IceMats = ice.ToFrozenSet(); + } + + public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m); + + public static bool IsWater(Material m) => WaterMats.Contains(m); + + public static bool IsIce(Material m) => IceMats.Contains(m); + + public static Color GetBaseColor(Material m) + { + if (m == Material.Lava) + return LavaColor; + return ColorTable.GetValueOrDefault(m, DefaultColor); + } + + /// + /// Apply Minecraft-style height shading. The shade multiplier depends on + /// the height difference between the current block and the block to its north. + /// Vanilla maps use four brightness levels: LOW (180/255), NORMAL (220/255), + /// HIGH (255/255), and LOWEST (135/255). We use NORMAL as baseline and shift + /// up/down based on delta. + /// + public static Color ApplyHeightShade(Color baseColor, int heightDelta) + { + int multiplier = heightDelta switch + { + > 0 => 255, // higher than neighbor: brightest + 0 => 220, // same height: normal + _ => 180, // lower than neighbor: darker + }; + byte r = (byte)(baseColor.R * multiplier / 255); + byte g = (byte)(baseColor.G * multiplier / 255); + byte b = (byte)(baseColor.B * multiplier / 255); + return Color.FromRgb(r, g, b); + } + + public static Color BlendWaterColor(Color bottomColor, int waterDepth) + { + double alpha = Math.Min(0.85, 0.35 + waterDepth * 0.08); + return Blend(WaterColor, bottomColor, alpha); + } + + public static Color BlendIceColor(Color bottomColor) + { + return Blend(IceColor, bottomColor, 0.35); + } + + private static Color Blend(Color top, Color bottom, double topAlpha) + { + byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha)); + byte g = (byte)(top.G * topAlpha + bottom.G * (1.0 - topAlpha)); + byte b = (byte)(top.B * topAlpha + bottom.B * (1.0 - topAlpha)); + return Color.FromRgb(r, g, b); + } + } +} diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs new file mode 100644 index 00000000..44db58ba --- /dev/null +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + /// + /// TUI minimap control rendered as a grid of TextBlocks using half-block characters. + /// Zoom is expressed as blocks-per-pixel (1 = 1:1, 16 = 16 blocks per pixel). + /// Entity names are drawn directly on the map below their icon. + /// + public class MinimapControl : UserControl + { + public const int MinZoom = 1; + public const int MaxZoom = 16; + public const int DefaultZoom = 2; + public const int DefaultWidth = 40; + public const int DefaultHeight = 40; + public const int DefaultRefreshMs = 1000; + public const int MinRefreshMs = 100; + public const int MaxRefreshMs = 5000; + + private int _mapWidth; + private int _mapHeight; + private int _cellRows; + + private int _blocksPerPixel = DefaultZoom; + private volatile bool _sampling; + private CancellationTokenSource? _cts; + + private readonly NameDisplayConfig _nameConfig = new(); + + private TextBlock[,] _cells; + private readonly StackPanel _infoRow; + private readonly StackPanel _legendPanel; + private readonly Grid _mapGrid; + private readonly DispatcherTimer _timer; + + public int BlocksPerPixel + { + get => _blocksPerPixel; + set => _blocksPerPixel = Math.Clamp(value, MinZoom, MaxZoom); + } + + public NameDisplayConfig NameConfig => _nameConfig; + + public int MapPixelWidth => _mapWidth; + public int MapPixelHeight => _mapHeight; + + public int RefreshIntervalMs + { + get => (int)_timer.Interval.TotalMilliseconds; + set => _timer.Interval = TimeSpan.FromMilliseconds(Math.Clamp(value, MinRefreshMs, MaxRefreshMs)); + } + + public MinimapControl() : this(DefaultWidth, DefaultHeight) { } + + public MinimapControl(int width, int height) + { + _mapWidth = Math.Max(10, width); + _mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1); + _cellRows = _mapHeight / 2; + + _mapGrid = new Grid(); + _cells = BuildGrid(_mapGrid, _cellRows, _mapWidth); + + _infoRow = new StackPanel { Orientation = Orientation.Horizontal }; + _legendPanel = new StackPanel { Orientation = Orientation.Horizontal }; + + var root = new StackPanel + { + Orientation = Orientation.Vertical, + Children = { _mapGrid, _infoRow, _legendPanel }, + }; + + Content = root; + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(DefaultRefreshMs), + }; + _timer.Tick += (_, _) => RequestSample(); + } + + public void Resize(int width, int height) + { + _mapWidth = Math.Max(10, width); + _mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1); + _cellRows = _mapHeight / 2; + + _mapGrid.Children.Clear(); + _mapGrid.RowDefinitions.Clear(); + _mapGrid.ColumnDefinitions.Clear(); + _cells = BuildGrid(_mapGrid, _cellRows, _mapWidth); + } + + private static TextBlock[,] BuildGrid(Grid grid, int rows, int cols) + { + var cells = new TextBlock[rows, cols]; + for (int r = 0; r < rows; r++) + grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + for (int c = 0; c < cols; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + var tb = new TextBlock + { + Text = "\u2580", + Foreground = Brushes.Black, + Background = Brushes.Black, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }; + Grid.SetRow(tb, r); + Grid.SetColumn(tb, c); + grid.Children.Add(tb); + cells[r, c] = tb; + } + } + return cells; + } + + public void Start() + { + _cts = new CancellationTokenSource(); + _timer.Start(); + RequestSample(); + } + + public void Stop() + { + _timer.Stop(); + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } + + private void RequestSample() + { + if (_sampling) return; + if (McClient.Instance is not McClient client) return; + if (!client.GetTerrainEnabled()) return; + + _sampling = true; + var ct = _cts?.Token ?? CancellationToken.None; + int bpp = _blocksPerPixel; + int w = _mapWidth; + int h = _mapHeight; + + bool showPlayers = _nameConfig.Players; + bool showHostile = _nameConfig.Hostile; + bool showNeutral = _nameConfig.Neutral; + bool showPassive = _nameConfig.Passive; + + Task.Run(() => + { + try + { + var result = SampleTerrain(client, bpp, w, h, + showPlayers, showHostile, showNeutral, showPassive, ct); + if (ct.IsCancellationRequested) return; + + Dispatcher.UIThread.Post(() => + { + ApplyPixelBuffer(result, w, h); + UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w); + }); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Sample error: {ex.Message}"); + } + finally + { + _sampling = false; + } + }, ct); + } + + internal sealed class EntityLabel + { + public string Name = ""; + public Color LabelColor; + public int PixelX; + public int PixelY; + } + + private sealed class SampleResult + { + public Color[,] Pixels = null!; + public (char Ch, Color Fg, Color Bg)?[,] CharOverlay = null!; + public HashSet VisibleCategories = []; + public int[,] Heights = null!; + } + + private static bool ShouldShowNameLocal(MobCategory cat, + bool showPlayers, bool showHostile, bool showNeutral, bool showPassive) + { + return cat switch + { + MobCategory.Player => showPlayers, + MobCategory.Hostile => showHostile, + MobCategory.Neutral => showNeutral, + MobCategory.Passive => showPassive, + _ => false, + }; + } + + private static SampleResult SampleTerrain(McClient client, int bpp, int mapW, int mapH, + bool showPlayers, bool showHostile, bool showNeutral, bool showPassive, + CancellationToken ct) + { + var result = new SampleResult + { + Pixels = new Color[mapW, mapH], + CharOverlay = new (char, Color, Color)?[mapW, mapH / 2], + Heights = new int[mapW, mapH], + }; + var world = client.GetWorld(); + var playerLoc = client.GetCurrentLocation(); + + int playerBlockX = (int)Math.Floor(playerLoc.X); + int playerBlockZ = (int)Math.Floor(playerLoc.Z); + int playerBlockY = (int)Math.Floor(playerLoc.Y); + + var dim = World.GetDimension(); + int minY = dim.minY; + int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1); + + var entities = client.GetEntityHandlingEnabled() + ? client.GetEntities() + : null; + + var entityPixels = new Dictionary<(int, int), (Color Color, int Priority)>(); + int centerX = mapW / 2; + int centerY = mapH / 2; + + var nameLabels = new List(); + var uuidNameMap = client.GetOnlinePlayersWithUUID(); + + if (entities is not null) + { + int playerEntityId = client.GetPlayerEntityID(); + foreach (var kvp in entities) + { + if (ct.IsCancellationRequested) return result; + var entity = kvp.Value; + var cat = MinimapEntityClassifier.Classify(entity.Type); + if (cat == MobCategory.NonLiving) continue; + if (kvp.Key == playerEntityId) continue; + + if (!MinimapEntityClassifier.ShouldDisplay(cat, playerLoc.Y, entity.Location.Y)) + continue; + + double relX = (entity.Location.X - playerLoc.X) / bpp; + double relZ = (entity.Location.Z - playerLoc.Z) / bpp; + int px = (int)Math.Floor(relX) + centerX; + int py = (int)Math.Floor(relZ) + centerY; + + if (px < 0 || px >= mapW || py < 0 || py >= mapH) continue; + + var baseColor = MinimapEntityClassifier.GetBaseColor(cat); + Color color; + if (cat == MobCategory.Player) + color = baseColor; + else + color = MinimapEntityClassifier.ApplyDepthFade(baseColor, playerLoc.Y, entity.Location.Y); + int priority = MinimapEntityClassifier.GetPriority(cat); + + var key = (px, py); + if (!entityPixels.TryGetValue(key, out var existing) || priority > existing.Priority) + entityPixels[key] = (color, priority); + + result.VisibleCategories.Add(cat); + + if (ShouldShowNameLocal(cat, showPlayers, showHostile, showNeutral, showPassive)) + { + string name = ResolveEntityName(client, entity, cat, uuidNameMap); + nameLabels.Add(new EntityLabel + { + Name = name, + LabelColor = color, + PixelX = px, + PixelY = py, + }); + } + } + } + + entityPixels[(centerX, centerY)] = (MinimapEntityClassifier.PlayerColor, 5); + result.VisibleCategories.Add(MobCategory.Player); + + ChunkColumn? cachedColumn = null; + int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue; + + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (ct.IsCancellationRequested) return result; + + int baseX = playerBlockX + (px - centerX) * bpp; + int baseZ = playerBlockZ + (py - centerY) * bpp; + + if (bpp == 1) + { + var (color, surfY) = SampleColumn(world, baseX, baseZ, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + } + else + { + var (color, surfY) = SampleAreaDominant(world, baseX, baseZ, bpp, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + } + } + } + + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (entityPixels.ContainsKey((px, py))) continue; + + int northHeight = py > 0 ? result.Heights[px, py - 1] : result.Heights[px, py]; + int delta = result.Heights[px, py] - northHeight; + result.Pixels[px, py] = MinimapColorMap.ApplyHeightShade(result.Pixels[px, py], delta); + } + } + + foreach (var (key, info) in entityPixels) + { + var (px, py) = key; + if (px >= 0 && px < mapW && py >= 0 && py < mapH) + result.Pixels[px, py] = info.Color; + } + + BakeNameLabels(result, nameLabels, mapW, mapH); + + return result; + } + + private static string ResolveEntityName(McClient client, Entity entity, + MobCategory cat, Dictionary? uuidNameMap) + { + if (cat == MobCategory.Player) + { + if (!string.IsNullOrWhiteSpace(entity.Name)) + return entity.Name; + + if (entity.UUID != System.Guid.Empty) + { + var playerInfo = client.GetPlayerInfo(entity.UUID); + if (!string.IsNullOrWhiteSpace(playerInfo?.Name)) + return playerInfo.Name; + + if (uuidNameMap is not null && + uuidNameMap.TryGetValue(entity.UUID.ToString(), out string? mapped) && + !string.IsNullOrWhiteSpace(mapped)) + return mapped; + } + + return "Player"; + } + + if (!string.IsNullOrWhiteSpace(entity.Name)) + return entity.Name; + + return entity.Type.ToString(); + } + + private static void BakeNameLabels(SampleResult result, List labels, + int mapW, int mapH) + { + if (labels.Count == 0) return; + int cellRows = mapH / 2; + + var occupied = new HashSet<(int col, int row)>(); + + labels.Sort((a, b) => + { + int pa = MinimapEntityClassifier.GetPriority( + a.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player : + a.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile : + a.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive); + int pb = MinimapEntityClassifier.GetPriority( + b.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player : + b.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile : + b.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive); + return pb.CompareTo(pa); + }); + + foreach (var lbl in labels) + { + int cellRow = (lbl.PixelY / 2) + 1; + if (cellRow >= cellRows) cellRow = lbl.PixelY / 2 - 1; + if (cellRow < 0 || cellRow >= cellRows) continue; + + int startCol = lbl.PixelX - lbl.Name.Length / 2; + startCol = Math.Clamp(startCol, 0, mapW - 1); + + bool fits = true; + int endCol = Math.Min(startCol + lbl.Name.Length, mapW); + for (int c = startCol; c < endCol; c++) + { + if (occupied.Contains((c, cellRow))) + { + fits = false; + break; + } + } + if (!fits) continue; + + for (int i = 0; i < lbl.Name.Length && startCol + i < mapW; i++) + { + int col = startCol + i; + occupied.Add((col, cellRow)); + + var bgTop = result.Pixels[col, cellRow * 2]; + var bgBot = (cellRow * 2 + 1 < mapH) + ? result.Pixels[col, cellRow * 2 + 1] + : bgTop; + + var avgBg = Color.FromRgb( + (byte)((bgTop.R + bgBot.R) / 2), + (byte)((bgTop.G + bgBot.G) / 2), + (byte)((bgTop.B + bgBot.B) / 2)); + + result.CharOverlay[col, cellRow] = (lbl.Name[i], lbl.LabelColor, avgBg); + } + } + } + + private static (Color color, int surfaceY) SampleColumn(World world, int x, int z, + int scanTop, int minY, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + int chunkX = x >> 4; + int chunkZ = z >> 4; + if (chunkX != cachedChunkX || chunkZ != cachedChunkZ) + { + cachedColumn = world[chunkX, chunkZ]; + cachedChunkX = chunkX; + cachedChunkZ = chunkZ; + } + + if (cachedColumn is null) + return (MinimapColorMap.VoidColor, minY); + + int waterDepth = 0; + bool inIce = false; + int surfaceY = minY; + + for (int y = scanTop; y >= minY; y--) + { + var loc = new Mapping.Location(x, y, z); + var chunk = cachedColumn.GetChunk(loc); + if (chunk is null) continue; + + var block = chunk.GetBlock(loc); + var mat = block.Type; + + if (MinimapColorMap.IsFullyTransparent(mat)) + continue; + + if (MinimapColorMap.IsWater(mat)) + { + if (waterDepth == 0) surfaceY = y; + waterDepth++; + continue; + } + + if (MinimapColorMap.IsIce(mat) && !inIce) + { + if (waterDepth == 0) surfaceY = y; + inIce = true; + continue; + } + + if (waterDepth == 0 && !inIce) surfaceY = y; + + var baseColor = MinimapColorMap.GetBaseColor(mat); + + if (waterDepth > 0) + baseColor = MinimapColorMap.BlendWaterColor(baseColor, waterDepth); + if (inIce) + baseColor = MinimapColorMap.BlendIceColor(baseColor); + + return (baseColor, surfaceY); + } + + if (waterDepth > 0) + return (MinimapColorMap.WaterColor, surfaceY); + + return (MinimapColorMap.VoidColor, minY); + } + + private static (Color color, int surfaceY) SampleAreaDominant(World world, int baseX, int baseZ, + int size, int scanTop, int minY, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + var colorCounts = new Dictionary(); + + int step = Math.Max(1, size / 3); + for (int dx = 0; dx < size; dx += step) + { + for (int dz = 0; dz < size; dz += step) + { + var (c, surfY) = SampleColumn(world, baseX + dx, baseZ + dz, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + + if (colorCounts.TryGetValue(c, out var existing)) + colorCounts[c] = (existing.Count + 1, existing.SumY + surfY); + else + colorCounts[c] = (1, surfY); + } + } + + Color best = MinimapColorMap.VoidColor; + int bestCount = 0; + int avgY = minY; + foreach (var kvp in colorCounts) + { + if (kvp.Value.Count > bestCount) + { + bestCount = kvp.Value.Count; + best = kvp.Key; + avgY = kvp.Value.SumY / kvp.Value.Count; + } + } + return (best, avgY); + } + + private void ApplyPixelBuffer(SampleResult result, int w, int h) + { + int rows = h / 2; + for (int row = 0; row < rows && row < _cellRows; row++) + { + for (int col = 0; col < w && col < _mapWidth; col++) + { + var overlay = result.CharOverlay[col, row]; + if (overlay is not null) + { + var (ch, fg, bg) = overlay.Value; + _cells[row, col].Text = ch.ToString(); + _cells[row, col].Foreground = new SolidColorBrush(fg); + _cells[row, col].Background = new SolidColorBrush(bg); + } + else + { + var topColor = result.Pixels[col, row * 2]; + var bottomColor = result.Pixels[col, row * 2 + 1]; + + _cells[row, col].Text = "\u2580"; + _cells[row, col].Foreground = new SolidColorBrush(topColor); + _cells[row, col].Background = new SolidColorBrush(bottomColor); + } + } + } + } + + private void UpdateInfoBarAndLegend(McClient client, int bpp, + HashSet categories, int mapW) + { + var loc = client.GetCurrentLocation(); + float yaw = client.GetYaw(); + string arrow = GetDirectionArrow(yaw); + + int x = (int)Math.Floor(loc.X); + int y = (int)Math.Floor(loc.Y); + int z = (int)Math.Floor(loc.Z); + + string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1"; + + var legendParts = new List(); + var legendColors = new List(); + + var sorted = categories + .Where(c => c != MobCategory.NonLiving) + .OrderByDescending(MinimapEntityClassifier.GetPriority); + + int catCount = 0; + foreach (var cat in sorted) + { + if (catCount >= 4) break; + legendParts.Add(MinimapEntityClassifier.GetCategoryLabel(cat)); + legendColors.Add(MinimapEntityClassifier.GetBaseColor(cat)); + catCount++; + } + + int legendLen = 0; + for (int i = 0; i < legendParts.Count; i++) + legendLen += 1 + legendParts[i].Length + (i > 0 ? 1 : 0); + + bool fitsOnOneLine = legendParts.Count > 0 + && coordPart.Length + 2 + legendLen <= mapW; + + _infoRow.Children.Clear(); + _infoRow.Children.Add(new TextBlock + { + Text = coordPart, + Foreground = Brushes.Gray, + Padding = new Thickness(0), + }); + + if (fitsOnOneLine) + { + AppendLegendItems(_infoRow, legendParts, legendColors, leftMargin: 2); + _legendPanel.Children.Clear(); + _legendPanel.IsVisible = false; + } + else + { + _legendPanel.IsVisible = legendParts.Count > 0; + _legendPanel.Children.Clear(); + AppendLegendItems(_legendPanel, legendParts, legendColors, leftMargin: 0); + } + } + + private static void AppendLegendItems(StackPanel panel, + List parts, List colors, int leftMargin) + { + for (int i = 0; i < parts.Count; i++) + { + int ml = i == 0 ? leftMargin : 1; + panel.Children.Add(new TextBlock + { + Text = "\u25cf", + Foreground = new SolidColorBrush(colors[i]), + Padding = new Thickness(0), + Margin = ml > 0 ? new Thickness(ml, 0, 0, 0) : new Thickness(0), + }); + panel.Children.Add(new TextBlock + { + Text = parts[i], + Foreground = Brushes.Gray, + Padding = new Thickness(0), + Margin = new Thickness(0), + }); + } + } + + private static string GetDirectionArrow(float yaw) + { + double normalized = ((yaw % 360) + 360) % 360; + int index = (int)Math.Round(normalized / 45.0) % 8; + return index switch + { + 0 => "\u2193", // S + 1 => "\u2199", // SW + 2 => "\u2190", // W + 3 => "\u2196", // NW + 4 => "\u2191", // N + 5 => "\u2197", // NE + 6 => "\u2192", // E + 7 => "\u2198", // SE + _ => "\u2193", + }; + } + } +} diff --git a/MinecraftClient/Tui/MinimapEntityCategories.json b/MinecraftClient/Tui/MinimapEntityCategories.json new file mode 100644 index 00000000..c80b7c0b --- /dev/null +++ b/MinecraftClient/Tui/MinimapEntityCategories.json @@ -0,0 +1,167 @@ +{ + "version": "26.1-rc-2", + "hostile": [ + "Blaze", + "Bogged", + "Breeze", + "CamelHusk", + "Creaking", + "Creeper", + "Drowned", + "ElderGuardian", + "EnderDragon", + "Endermite", + "Evoker", + "Ghast", + "Giant", + "Guardian", + "Hoglin", + "Husk", + "Illusioner", + "MagmaCube", + "Parched", + "Phantom", + "Piglin", + "PiglinBrute", + "Pillager", + "Ravager", + "Shulker", + "Silverfish", + "Skeleton", + "Slime", + "Stray", + "Vex", + "Vindicator", + "Warden", + "Witch", + "Wither", + "WitherSkeleton", + "Zoglin", + "Zombie", + "ZombieNautilus", + "ZombieVillager" + ], + "passive": [ + "Allay", + "Armadillo", + "Axolotl", + "Bat", + "Camel", + "Cat", + "Chicken", + "Cod", + "Cow", + "Donkey", + "Fox", + "Frog", + "GlowSquid", + "HappyGhast", + "Horse", + "Mooshroom", + "Mule", + "Nautilus", + "Ocelot", + "Parrot", + "Pig", + "Pufferfish", + "Rabbit", + "Salmon", + "Sheep", + "SkeletonHorse", + "Sniffer", + "Squid", + "Strider", + "Tadpole", + "TropicalFish", + "Turtle", + "Villager", + "WanderingTrader", + "ZombieHorse" + ], + "neutral": [ + "Bee", + "CaveSpider", + "CopperGolem", + "Dolphin", + "Enderman", + "Goat", + "IronGolem", + "Llama", + "Panda", + "PolarBear", + "SnowGolem", + "Spider", + "TraderLlama", + "Wolf", + "ZombifiedPiglin" + ], + "non_living": [ + "AcaciaBoat", + "AcaciaChestBoat", + "AreaEffectCloud", + "ArmorStand", + "Arrow", + "BambooChestRaft", + "BambooRaft", + "BirchBoat", + "BirchChestBoat", + "BlockDisplay", + "BreezeWindCharge", + "CherryBoat", + "CherryChestBoat", + "ChestMinecart", + "CommandBlockMinecart", + "DarkOakBoat", + "DarkOakChestBoat", + "DragonFireball", + "Egg", + "EndCrystal", + "EnderPearl", + "EvokerFangs", + "ExperienceBottle", + "ExperienceOrb", + "EyeOfEnder", + "FallingBlock", + "Fireball", + "FireworkRocket", + "FishingBobber", + "FurnaceMinecart", + "GlowItemFrame", + "HopperMinecart", + "Interaction", + "Item", + "ItemDisplay", + "ItemFrame", + "JungleBoat", + "JungleChestBoat", + "LeashKnot", + "LightningBolt", + "LingeringPotion", + "LlamaSpit", + "MangroveBoat", + "MangroveChestBoat", + "Mannequin", + "Marker", + "Minecart", + "OakBoat", + "OakChestBoat", + "OminousItemSpawner", + "Painting", + "PaleOakBoat", + "PaleOakChestBoat", + "ShulkerBullet", + "SmallFireball", + "Snowball", + "SpawnerMinecart", + "SpectralArrow", + "SplashPotion", + "SpruceBoat", + "SpruceChestBoat", + "TextDisplay", + "Tnt", + "TntMinecart", + "Trident", + "WindCharge", + "WitherSkull" + ] +} \ No newline at end of file diff --git a/MinecraftClient/Tui/MinimapEntityClassifier.cs b/MinecraftClient/Tui/MinimapEntityClassifier.cs new file mode 100644 index 00000000..2daf1ea6 --- /dev/null +++ b/MinecraftClient/Tui/MinimapEntityClassifier.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using Avalonia.Media; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + public enum MobCategory + { + Hostile, + Passive, + Neutral, + Player, + NonLiving, + } + + public enum MinimapPosition + { + top_left, + top_right, + center, + bottom_left, + bottom_right, + } + + public sealed class NameDisplayConfig + { + public volatile bool Players = false; + public volatile bool Hostile = false; + public volatile bool Neutral = false; + public volatile bool Passive = false; + + public bool AnyEnabled => Players || Hostile || Neutral || Passive; + + public void SetAll(bool value) + { + Players = value; + Hostile = value; + Neutral = value; + Passive = value; + } + + public bool ShouldShowName(MobCategory category) => category switch + { + MobCategory.Player => Players, + MobCategory.Hostile => Hostile, + MobCategory.Neutral => Neutral, + MobCategory.Passive => Passive, + _ => false, + }; + } + + /// + /// Classifies entities into minimap categories using data extracted from + /// Minecraft's MobCategory assignments. Categories are loaded from the + /// embedded MinimapEntityCategories.json resource generated by + /// tools/gen_entity_category_map.py. + /// + public static class MinimapEntityClassifier + { + public static readonly Color HostileColor = Color.FromRgb(255, 68, 68); + public static readonly Color PassiveColor = Color.FromRgb(68, 255, 68); + public static readonly Color NeutralColor = Color.FromRgb(255, 170, 0); + public static readonly Color PlayerColor = Color.FromRgb(255, 255, 255); + public static readonly Color FadedGray = Color.FromRgb(100, 100, 100); + + private static readonly FrozenDictionary CategoryTable; + + static MinimapEntityClassifier() + { + var table = new Dictionary(); + + try + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("MinimapEntityCategories.json"); + if (stream is not null) + { + using var doc = JsonDocument.Parse(stream); + var root = doc.RootElement; + + LoadCategory(root, "hostile", MobCategory.Hostile, table); + LoadCategory(root, "passive", MobCategory.Passive, table); + LoadCategory(root, "neutral", MobCategory.Neutral, table); + LoadCategory(root, "non_living", MobCategory.NonLiving, table); + } + } + catch (Exception ex) + { + ConsoleIO.WriteLogLine($"[Minimap] Failed to load entity categories: {ex.Message}"); + } + + CategoryTable = table.ToFrozenDictionary(); + } + + private static void LoadCategory(JsonElement root, string key, + MobCategory category, Dictionary table) + { + if (!root.TryGetProperty(key, out var arr)) + return; + + foreach (var el in arr.EnumerateArray()) + { + var name = el.GetString(); + if (name is not null && Enum.TryParse(name, out var et)) + table.TryAdd(et, category); + } + } + + public static MobCategory Classify(EntityType type) + { + if (type == EntityType.Player) + return MobCategory.Player; + return CategoryTable.GetValueOrDefault(type, MobCategory.NonLiving); + } + + public static Color GetBaseColor(MobCategory category) => category switch + { + MobCategory.Hostile => HostileColor, + MobCategory.Passive => PassiveColor, + MobCategory.Neutral => NeutralColor, + MobCategory.Player => PlayerColor, + _ => FadedGray, + }; + + public static Color ApplyDepthFade(Color baseColor, double playerY, double entityY) + { + double depth = playerY - entityY; + + if (depth <= 5.0) + return baseColor; + + if (depth >= 15.0) + return FadedGray; + + double t = (depth - 5.0) / 10.0; + return Lerp(baseColor, FadedGray, t); + } + + public static bool ShouldDisplay(MobCategory category, double playerY, double entityY) + { + if (category == MobCategory.Player) + return true; + if (entityY >= playerY) + return true; + return playerY - entityY <= 15.0; + } + + public static int GetPriority(MobCategory category) => category switch + { + MobCategory.Hostile => 4, + MobCategory.Player => 3, + MobCategory.Neutral => 2, + MobCategory.Passive => 1, + _ => 0, + }; + + public static string GetCategoryLabel(MobCategory category) => category switch + { + MobCategory.Hostile => Translations.tui_minimap_legend_hostile, + MobCategory.Passive => Translations.tui_minimap_legend_passive, + MobCategory.Neutral => Translations.tui_minimap_legend_neutral, + MobCategory.Player => Translations.tui_minimap_legend_player, + _ => "?", + }; + + private static Color Lerp(Color a, Color b, double t) + { + byte r = (byte)(a.R + (b.R - a.R) * t); + byte g = (byte)(a.G + (b.G - a.G) * t); + byte bl = (byte)(a.B + (b.B - a.B) * t); + return Color.FromRgb(r, g, bl); + } + } +} diff --git a/tools/README.md b/tools/README.md index 4dceae9c..7d33d2cf 100644 --- a/tools/README.md +++ b/tools/README.md @@ -135,6 +135,44 @@ Data source: `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/mast Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted. +## gen_block_color_map.py -- Generate minimap block color JSON + +Extracts block-to-MapColor RGB mappings from decompiled Minecraft source for the TUI minimap. + +```bash +python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +# -> MinecraftClient/Tui/MinimapBlockColors.json +``` + +Parses three files from the decompiled source: +- `MapColor.java` -- extracts the 64 base MapColor constants and their RGB values +- `DyeColor.java` -- maps dye colors to MapColor constants +- `Blocks.java` -- determines each block's assigned MapColor via `.mapColor()` calls + +Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). Contains color entries, plus lists of transparent, water, and ice materials. + +Validates each block name against MCC's `Material.cs` enum. Blocks without a matching enum value are skipped. + +## gen_entity_category_map.py -- Generate minimap entity category JSON + +Extracts entity-to-MobCategory mappings from decompiled Minecraft source for the TUI minimap. + +```bash +python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +# -> MinecraftClient/Tui/MinimapEntityCategories.json +``` + +Parses `EntityType.java` to read each entity's `MobCategory` assignment from the `EntityType.Builder.of(Factory, MobCategory.XXX)` call. Maps Minecraft categories to MCC minimap categories: +- `MONSTER` -> hostile +- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive +- `MISC` -> non_living + +The script maintains manual override lists for: +- **Neutral mobs** (e.g. Enderman, Spider, Wolf, Bee) -- Minecraft has no "neutral" category; these are MONSTER or CREATURE in code but only attack when provoked +- **Passive overrides** (e.g. Villager, WanderingTrader) -- classified as MISC in Minecraft for spawning reasons but should appear as passive on the minimap + +Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). Validates each entity name against MCC's `EntityType.cs` enum. + ## Recommended workflow 1. Generate server reports (Step 0) @@ -145,6 +183,9 @@ Uses `curl` with resume (`-C -`) for reliable download over slow connections. Fa - Entities: `gen_entity_palette.py` - Metadata: `gen_entity_metadata_palette.py` 4. Update block collision shapes: `gen_block_shapes.py` -5. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs` -6. Update version routing (see SKILL.md) -7. Build and test +5. Update minimap data (if blocks or entities changed): + - Block colors: `gen_block_color_map.py` + - Entity categories: `gen_entity_category_map.py` +6. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs` +7. Update version routing (see SKILL.md) +8. Build and test diff --git a/tools/gen_block_color_map.py b/tools/gen_block_color_map.py new file mode 100644 index 00000000..69cbb502 --- /dev/null +++ b/tools/gen_block_color_map.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +""" +Generate MinimapBlockColors.json from decompiled Minecraft source. + +Parses MapColor.java for the 62 base map colors (ID -> RGB), then parses +Blocks.java to extract each block's mapColor assignment, and outputs a +JSON mapping from MCC Material enum names (PascalCase) to RGB triples. + +Usage: + python3 tools/gen_block_color_map.py + +Example: + python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +""" + +import json +import re +import sys +from pathlib import Path + +OUTPUT_PATH = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Tui" / "MinimapBlockColors.json") +MATERIAL_CS = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Mapping" / "Material.cs") + + +def mc_name_to_csharp(mc_name: str) -> str: + name = mc_name.removeprefix("minecraft:") + return "".join(word.capitalize() for word in name.split("_")) + + +def parse_map_colors(map_color_java: Path) -> dict[str, tuple[int, int, int]]: + """Parse MapColor.java: extract name -> (R, G, B) for each constant.""" + text = map_color_java.read_text() + colors: dict[str, tuple[int, int, int]] = {} + + pattern = re.compile( + r'public static final MapColor\s+(\w+)\s*=\s*new\s+MapColor\(\s*(\d+)\s*,\s*(\d+)\s*\)') + for m in pattern.finditer(text): + name = m.group(1) + color_int = int(m.group(3)) + r = (color_int >> 16) & 0xFF + g = (color_int >> 8) & 0xFF + b = color_int & 0xFF + colors[name] = (r, g, b) + + return colors + + +def parse_dye_to_map_color(dye_color_java: Path) -> dict[str, str]: + """Parse DyeColor.java: extract DyeColor name -> MapColor name.""" + text = dye_color_java.read_text() + mapping: dict[str, str] = {} + + pattern = re.compile( + r'(\w+)\(\d+,\s*"[^"]+",\s*\d+,\s*MapColor\.(\w+)') + for m in pattern.finditer(text): + mapping[m.group(1)] = m.group(2) + + return mapping + + +def extract_block_declarations(text: str) -> list[tuple[str, str, str]]: + """Extract (field_name, block_id, full_register_body) for each block declaration. + + Returns list of (FIELD_NAME, "block_name", "register(...) content"). + """ + results = [] + + # Find all "public static final Block FIELD = register(...)" declarations. + # These span multiple lines and end with ");". + # Strategy: find start pattern, then track parens to find matching end. + field_pattern = re.compile( + r'public\s+static\s+final\s+Block\s+(\w+)\s*=\s*register\s*\(') + + pos = 0 + while pos < len(text): + m = field_pattern.search(text, pos) + if not m: + break + + field_name = m.group(1) + paren_start = m.end() - 1 # position of opening '(' + + # Find matching closing ')' then ';' + depth = 1 + i = paren_start + 1 + while i < len(text) and depth > 0: + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + i += 1 + + register_body = text[paren_start:i] + + # Extract block name string from register call + name_match = re.search(r'(?:BlockIds\.(\w+)|"(\w+)")', register_body) + if name_match: + raw_id = name_match.group(1) or name_match.group(2) + block_id = raw_id.lower() if raw_id.isupper() else raw_id + else: + block_id = field_name.lower() + + results.append((field_name, block_id, register_body)) + pos = i + + return results + + +def parse_blocks(blocks_java: Path, map_colors: dict[str, tuple[int, int, int]], + dye_to_map: dict[str, str]) -> dict[str, tuple[int, int, int]]: + """Parse Blocks.java: extract block_name -> (R, G, B).""" + text = blocks_java.read_text() + + declarations = extract_block_declarations(text) + print(f" Found {len(declarations)} block register() declarations") + + # First pass: assign MapColor name to each block + field_to_block_id: dict[str, str] = {} + block_color_name: dict[str, str] = {} + + map_color_direct = re.compile(r'\.mapColor\(MapColor\.(\w+)\)') + map_color_dye = re.compile(r'\.mapColor\(DyeColor\.(\w+)\)') + map_color_ref = re.compile(r'\.mapColor\((\w+)\.defaultMapColor\(\)') + map_color_waterlogged = re.compile(r'\.mapColor\(waterloggedMapColor\(MapColor\.(\w+)\)') + + for field_name, block_id, body in declarations: + field_to_block_id[field_name] = block_id + + mc = map_color_direct.search(body) + if mc: + block_color_name[block_id] = mc.group(1) + continue + + mc = map_color_dye.search(body) + if mc: + dye_name = mc.group(1) + if dye_name in dye_to_map: + block_color_name[block_id] = dye_to_map[dye_name] + continue + + mc = map_color_waterlogged.search(body) + if mc: + block_color_name[block_id] = mc.group(1) + continue + + mc = map_color_ref.search(body) + if mc: + ref_field = mc.group(1) + ref_block = field_to_block_id.get(ref_field) + if ref_block and ref_block in block_color_name: + block_color_name[block_id] = block_color_name[ref_block] + + # Second pass: resolve remaining BLOCK.defaultMapColor() references + for field_name, block_id, body in declarations: + if block_id in block_color_name: + continue + mc = map_color_ref.search(body) + if mc: + ref_field = mc.group(1) + ref_block = field_to_block_id.get(ref_field) + if ref_block and ref_block in block_color_name: + block_color_name[block_id] = block_color_name[ref_block] + + result: dict[str, tuple[int, int, int]] = {} + for block_id, color_name in block_color_name.items(): + if color_name in map_colors: + cs_name = mc_name_to_csharp(block_id) + result[cs_name] = map_colors[color_name] + + return result + + +def load_known_materials() -> set[str]: + known = set() + if MATERIAL_CS.exists(): + with open(MATERIAL_CS) as f: + for line in f: + m = re.match(r'\s+(\w+),?\s*$', line) + if m: + known.add(m.group(1)) + return known + + +TRANSPARENT_BLOCKS = [ + "Air", "CaveAir", "VoidAir", + "Glass", "GlassPane", + "WhiteStainedGlass", "OrangeStainedGlass", "MagentaStainedGlass", + "LightBlueStainedGlass", "YellowStainedGlass", "LimeStainedGlass", + "PinkStainedGlass", "GrayStainedGlass", "LightGrayStainedGlass", + "CyanStainedGlass", "PurpleStainedGlass", "BlueStainedGlass", + "BrownStainedGlass", "GreenStainedGlass", "RedStainedGlass", + "BlackStainedGlass", + "WhiteStainedGlassPane", "OrangeStainedGlassPane", "MagentaStainedGlassPane", + "LightBlueStainedGlassPane", "YellowStainedGlassPane", "LimeStainedGlassPane", + "PinkStainedGlassPane", "GrayStainedGlassPane", "LightGrayStainedGlassPane", + "CyanStainedGlassPane", "PurpleStainedGlassPane", "BlueStainedGlassPane", + "BrownStainedGlassPane", "GreenStainedGlassPane", "RedStainedGlassPane", + "BlackStainedGlassPane", + "TintedGlass", "Barrier", "Light", "StructureVoid", +] + +WATER_BLOCKS = ["Water"] +ICE_BLOCKS = ["Ice", "PackedIce", "BlueIce", "FrostedIce"] + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + root = Path(sys.argv[1]) + if not root.is_dir(): + print(f"Error: {root} is not a directory") + sys.exit(1) + + map_color_java = root / "net/minecraft/world/level/material/MapColor.java" + dye_color_java = root / "net/minecraft/world/item/DyeColor.java" + blocks_java = root / "net/minecraft/world/level/block/Blocks.java" + + for f in [map_color_java, dye_color_java, blocks_java]: + if not f.exists(): + print(f"Error: {f} not found") + sys.exit(1) + + print("Parsing MapColor.java...") + map_colors = parse_map_colors(map_color_java) + print(f" Found {len(map_colors)} map colors") + + print("Parsing DyeColor.java...") + dye_to_map = parse_dye_to_map_color(dye_color_java) + print(f" Found {len(dye_to_map)} dye->map color mappings") + + print("Parsing Blocks.java...") + block_colors = parse_blocks(blocks_java, map_colors, dye_to_map) + print(f" Extracted colors for {len(block_colors)} blocks") + + known_materials = load_known_materials() + if known_materials: + matched = {k: v for k, v in block_colors.items() if k in known_materials} + unmatched = [k for k in block_colors if k not in known_materials] + if unmatched: + print(f"\n {len(unmatched)} blocks not in Material.cs (will be skipped):") + for name in sorted(unmatched)[:20]: + print(f" {name}") + if len(unmatched) > 20: + print(f" ... and {len(unmatched) - 20} more") + block_colors = matched + print(f" {len(block_colors)} blocks matched to Material.cs entries") + + output = { + "version": root.name.replace("-decompiled", "").replace("-client", ""), + "colors": {k: list(v) for k, v in sorted(block_colors.items())}, + "transparent": sorted(TRANSPARENT_BLOCKS), + "water": WATER_BLOCKS, + "ice": ICE_BLOCKS, + } + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, 'w') as f: + json.dump(output, f, indent=2) + print(f"\nGenerated {OUTPUT_PATH}") + print(f" {len(block_colors)} color entries") + + +if __name__ == "__main__": + main() diff --git a/tools/gen_entity_category_map.py b/tools/gen_entity_category_map.py new file mode 100644 index 00000000..e258d186 --- /dev/null +++ b/tools/gen_entity_category_map.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Generate MinimapEntityCategories.json from decompiled Minecraft source. + +Parses EntityType.java to extract each entity's MobCategory assignment, +then maps them to MCC minimap categories (hostile/passive/neutral/non_living). + +Minecraft's MobCategory values: + MONSTER -> hostile (with neutral overrides for conditionally hostile mobs) + CREATURE -> passive (with neutral overrides for conditionally hostile mobs) + AMBIENT -> passive + AXOLOTLS -> passive + WATER_CREATURE -> passive + WATER_AMBIENT -> passive + UNDERGROUND_WATER_CREATURE -> passive + MISC -> non_living + +Some mobs classified as MONSTER or CREATURE are actually "neutral" -- they +only attack when provoked. These are listed in NEUTRAL_OVERRIDES below and +should be updated when new conditionally-hostile mobs are added. + +Usage: + python3 tools/gen_entity_category_map.py + +Example: + python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +""" + +import json +import re +import sys +from pathlib import Path + +OUTPUT_PATH = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Tui" / "MinimapEntityCategories.json") +ENTITY_TYPE_CS = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Mapping" / "EntityType.cs") + + +def mc_name_to_csharp(mc_name: str) -> str: + name = mc_name.removeprefix("minecraft:") + return "".join(word.capitalize() for word in name.split("_")) + + +# Mobs that Minecraft classifies as MONSTER or CREATURE but behave as +# "neutral" -- they only attack when provoked. This list is maintained +# manually because there is no machine-readable flag in the game data. +NEUTRAL_OVERRIDES = { + "bee", "dolphin", "goat", "iron_golem", "llama", "panda", + "polar_bear", "snow_golem", "trader_llama", "wolf", + "zombified_piglin", "enderman", "spider", "cave_spider", + "copper_golem", +} + +# Entities whose MobCategory in the game code doesn't match how they +# should appear on the minimap. For example, Villager and WanderingTrader +# are MISC in MC code (for spawning reasons) but should be passive on the map. +# ZombieHorse is MONSTER but is a rideable passive mob in practice. +PASSIVE_OVERRIDES = { + "villager", "wandering_trader", "zombie_horse", +} + +# Player has its own category in MCC -- extracted from MISC to "player". +PLAYER_OVERRIDES = {"player"} + +MC_TO_MCC = { + "MONSTER": "hostile", + "CREATURE": "passive", + "AMBIENT": "passive", + "AXOLOTLS": "passive", + "WATER_CREATURE": "passive", + "WATER_AMBIENT": "passive", + "UNDERGROUND_WATER_CREATURE": "passive", + "MISC": "non_living", +} + + +def extract_entity_categories(entity_type_java: Path) -> list[tuple[str, str, str]]: + """Extract (entity_id, field_name, MobCategory) from EntityType.java. + + Returns list of (entity_id, FIELD_NAME, MobCategory_name). + """ + text = entity_type_java.read_text() + results = [] + + field_pat = re.compile( + r'public\s+static\s+final\s+EntityType<[^>]+>\s+(\w+)\s*=\s*register\s*\(') + + pos = 0 + while pos < len(text): + m = field_pat.search(text, pos) + if not m: + break + + field_name = m.group(1) + paren_start = m.end() - 1 + depth = 1 + i = paren_start + 1 + while i < len(text) and depth > 0: + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + i += 1 + + body = text[paren_start:i] + + name_match = re.search(r'"(\w+)"', body) + entity_id = name_match.group(1) if name_match else field_name.lower() + + cat_match = re.search(r'MobCategory\.(\w+)', body) + mob_cat = cat_match.group(1) if cat_match else "MISC" + + results.append((entity_id, field_name, mob_cat)) + pos = i + + return results + + +def load_known_entity_types() -> set[str]: + known = set() + if ENTITY_TYPE_CS.exists(): + with open(ENTITY_TYPE_CS) as f: + for line in f: + m = re.match(r'\s+(\w+),?\s*$', line) + if m: + known.add(m.group(1)) + return known + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + root = Path(sys.argv[1]) + entity_type_java = root / "net/minecraft/world/entity/EntityType.java" + + if not entity_type_java.exists(): + print(f"Error: {entity_type_java} not found") + sys.exit(1) + + print("Parsing EntityType.java...") + entities = extract_entity_categories(entity_type_java) + print(f" Found {len(entities)} entity type declarations") + + known_types = load_known_entity_types() + + hostile = [] + passive = [] + neutral = [] + non_living = [] + + for entity_id, field_name, mob_cat in entities: + cs_name = mc_name_to_csharp(entity_id) + + if known_types and cs_name not in known_types: + continue + + if entity_id in PLAYER_OVERRIDES: + continue + elif entity_id in NEUTRAL_OVERRIDES: + neutral.append(cs_name) + elif entity_id in PASSIVE_OVERRIDES: + passive.append(cs_name) + elif mob_cat in MC_TO_MCC: + cat = MC_TO_MCC[mob_cat] + if cat == "hostile": + hostile.append(cs_name) + elif cat == "passive": + passive.append(cs_name) + elif cat == "non_living": + non_living.append(cs_name) + else: + non_living.append(cs_name) + else: + non_living.append(cs_name) + + output = { + "version": root.name.replace("-decompiled", "").replace("-client", ""), + "hostile": sorted(hostile), + "passive": sorted(passive), + "neutral": sorted(neutral), + "non_living": sorted(non_living), + } + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, 'w') as f: + json.dump(output, f, indent=2) + + print(f"\nGenerated {OUTPUT_PATH}") + print(f" hostile: {len(hostile)}") + print(f" passive: {len(passive)}") + print(f" neutral: {len(neutral)}") + print(f" non_living: {len(non_living)}") + print(f" total: {len(hostile) + len(passive) + len(neutral) + len(non_living)}") + + +if __name__ == "__main__": + main() From a1516e96806a99242b17231c9f9de14ba4159e98 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 18:48:00 +0800 Subject: [PATCH 25/76] Add message aggregation and relay options for DiscordBridge - Introduced message aggregation functionality with a configurable interval to reduce Discord API rate limits. - Added options to relay all messages from Minecraft, including system messages, to Discord. - Updated configuration comments to reflect new settings and their purposes. --- MinecraftClient/ChatBots/DiscordBridge.cs | 71 +++++++++++++++++-- .../ConfigComments/ConfigComments.resx | 8 ++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index fa13f84e..3938ab6a 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -1,8 +1,11 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Brigadier.NET.Builder; using DSharpPlus; @@ -34,6 +37,9 @@ namespace MinecraftClient.ChatBots private DiscordChannel? discordChannel; private BridgeDirection bridgeDirection = BridgeDirection.Both; + private readonly ConcurrentQueue aggregationBuffer = new(); + private Timer? aggregationTimer; + public static Configs Config = new(); [TomlDoNotInlineObject] @@ -62,6 +68,12 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$ChatBot.DiscordBridge.AllowOtherBotMessages$")] public bool Allow_Other_Bot_Messages = false; + [TomlInlineComment("$ChatBot.DiscordBridge.RelayAllMessages$")] + public bool Relay_All_Messages = false; + + [TomlInlineComment("$ChatBot.DiscordBridge.MessageAggregationInterval$")] + public double Message_Aggregation_Interval = 3.0; + [TomlPrecedingComment("$ChatBot.DiscordBridge.Formats$")] public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; public string PublicMessageFormat = "{username}: {message}"; @@ -70,6 +82,8 @@ namespace MinecraftClient.ChatBots public void OnSettingUpdate() { Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + if (Message_Aggregation_Interval < 0) + Message_Aggregation_Interval = 0; } } @@ -100,6 +114,12 @@ namespace MinecraftClient.ChatBots .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) ); + if (Config.Message_Aggregation_Interval > 0) + { + var intervalMs = (int)(Config.Message_Aggregation_Interval * 1000); + aggregationTimer = new Timer(_ => FlushAggregationBuffer(), null, intervalMs, intervalMs); + } + Task.Run(async () => await MainAsync()); } @@ -107,6 +127,7 @@ namespace MinecraftClient.ChatBots { McClient.dispatcher.Unregister(CommandName); McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); + StopAggregation(); Disconnect(); } @@ -147,6 +168,40 @@ namespace MinecraftClient.ChatBots return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.bot_DiscordBridge_direction, bridgeName)); } + private void FlushAggregationBuffer() + { + if (aggregationBuffer.IsEmpty || !CanSendMessages()) + return; + + var sb = new StringBuilder(); + while (aggregationBuffer.TryDequeue(out var line)) + { + if (sb.Length + line.Length + 1 > 1900) + { + SendMessage(sb.ToString()); + sb.Clear(); + } + + if (sb.Length > 0) + sb.AppendLine(); + sb.Append(line); + } + + if (sb.Length > 0) + SendMessage(sb.ToString()); + } + + private void StopAggregation() + { + if (aggregationTimer is not null) + { + aggregationTimer.Dispose(); + aggregationTimer = null; + } + + FlushAggregationBuffer(); + } + ~DiscordBridge() { Disconnect(); @@ -188,7 +243,6 @@ namespace MinecraftClient.ChatBots text = GetVerbatim(text).Trim(); - // Stop the crash when an empty text is recived somehow if (string.IsNullOrEmpty(text)) return; @@ -205,7 +259,10 @@ namespace MinecraftClient.ChatBots message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); teleportRequest = true; } - else message = text; + else if (Config.Relay_All_Messages) + message = text; + else + return; if (teleportRequest) { @@ -223,7 +280,13 @@ namespace MinecraftClient.ChatBots SendMessage(messageBuilder); return; } - else SendMessage(GetDiscordText(message)); + + string discordText = GetDiscordText(message); + + if (Config.Message_Aggregation_Interval > 0) + aggregationBuffer.Enqueue(discordText); + else + SendMessage(discordText); } /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index d4b82e42..2b09765e 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -393,6 +393,12 @@ For Discord message formatting, check the following: https://mccteam.github.io/r When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat. The bridge always ignores its own messages to prevent loops. + + When enabled, all text received from the Minecraft server (including system messages, join/leave notifications, etc.) will be relayed to Discord, not just player chat and private messages. + + + Interval in seconds to aggregate messages before sending them to Discord. When set to 0, messages are sent immediately one by one. When set to a value like 1.0, messages received within that interval are batched into a single Discord message. Useful for reducing Discord API rate limits. + Automatically farms crops for you (plants, breaks and bonemeals them). Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. @@ -964,7 +970,7 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be Show passive mob names on the minimap. - Minimap refresh interval in milliseconds (200-5000, default 1000). + Minimap refresh interval in milliseconds (100-5000). Yggdrasil authlib multi-user selection. From 0566f2518b6d0b6837f254973b8ef8ef1bab94ac Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 19:16:33 +0800 Subject: [PATCH 26/76] Add tooltip functionality to MinimapControl - Introduced a tooltip system for displaying entity information on the minimap. - Enhanced the SampleResult class to include entity mapping and block type summaries. - Updated the rendering logic to incorporate tooltips and improve user interaction with the minimap. --- MinecraftClient/Tui/MinimapControl.cs | 354 ++++++++++++++++++++++++-- 1 file changed, 339 insertions(+), 15 deletions(-) diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs index 44db58ba..1e93c390 100644 --- a/MinecraftClient/Tui/MinimapControl.cs +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; @@ -44,6 +45,13 @@ namespace MinecraftClient.Tui private readonly Grid _mapGrid; private readonly DispatcherTimer _timer; + private readonly Canvas _tooltipCanvas; + private readonly Border _tooltipBorder; + private readonly StackPanel _tooltipContent; + private SampleResult? _lastResult; + private int _hoverCol = -1; + private int _hoverRow = -1; + public int BlocksPerPixel { get => _blocksPerPixel; @@ -75,14 +83,40 @@ namespace MinecraftClient.Tui _infoRow = new StackPanel { Orientation = Orientation.Horizontal }; _legendPanel = new StackPanel { Orientation = Orientation.Horizontal }; + _tooltipContent = new StackPanel { Orientation = Orientation.Vertical }; + _tooltipBorder = new Border + { + Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), + BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), + BorderThickness = new Thickness(1), + Padding = new Thickness(1), + Child = _tooltipContent, + IsVisible = false, + }; + + _tooltipCanvas = new Canvas + { + IsHitTestVisible = false, + Children = { _tooltipBorder }, + }; + + var mapLayer = new Panel + { + ClipToBounds = true, + Children = { _mapGrid, _tooltipCanvas }, + }; + var root = new StackPanel { Orientation = Orientation.Vertical, - Children = { _mapGrid, _infoRow, _legendPanel }, + Children = { mapLayer, _infoRow, _legendPanel }, }; Content = root; + _mapGrid.PointerMoved += OnMapPointerMoved; + _mapGrid.PointerExited += OnMapPointerExited; + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(DefaultRefreshMs), @@ -198,12 +232,29 @@ namespace MinecraftClient.Tui public int PixelY; } + internal sealed class PixelEntityInfo + { + public string Name = ""; + public MobCategory Category; + public float Health; + public float MaxHealth; + public int Priority; + } + private sealed class SampleResult { public Color[,] Pixels = null!; public (char Ch, Color Fg, Color Bg)?[,] CharOverlay = null!; public HashSet VisibleCategories = []; public int[,] Heights = null!; + public Material[,]? BlockTypes; + public List<(Material Mat, int Count)>?[,]? BlockSummary; + public List?[,]? EntityMap; + public int PlayerBlockX; + public int PlayerBlockZ; + public int CenterX; + public int CenterY; + public int Bpp; } private static bool ShouldShowNameLocal(MobCategory cat, @@ -228,6 +279,10 @@ namespace MinecraftClient.Tui Pixels = new Color[mapW, mapH], CharOverlay = new (char, Color, Color)?[mapW, mapH / 2], Heights = new int[mapW, mapH], + EntityMap = new List?[mapW, mapH], + BlockTypes = bpp == 1 ? new Material[mapW, mapH] : null, + BlockSummary = bpp > 1 ? new List<(Material, int)>?[mapW, mapH] : null, + Bpp = bpp, }; var world = client.GetWorld(); var playerLoc = client.GetCurrentLocation(); @@ -236,6 +291,11 @@ namespace MinecraftClient.Tui int playerBlockZ = (int)Math.Floor(playerLoc.Z); int playerBlockY = (int)Math.Floor(playerLoc.Y); + result.PlayerBlockX = playerBlockX; + result.PlayerBlockZ = playerBlockZ; + result.CenterX = mapW / 2; + result.CenterY = mapH / 2; + var dim = World.GetDimension(); int minY = dim.minY; int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1); @@ -286,6 +346,17 @@ namespace MinecraftClient.Tui result.VisibleCategories.Add(cat); + string eName = ResolveEntityName(client, entity, cat, uuidNameMap); + var pixelList = result.EntityMap![px, py] ??= []; + pixelList.Add(new PixelEntityInfo + { + Name = eName, + Category = cat, + Health = entity.Health, + MaxHealth = -1, + Priority = priority, + }); + if (ShouldShowNameLocal(cat, showPlayers, showHostile, showNeutral, showPassive)) { string name = ResolveEntityName(client, entity, cat, uuidNameMap); @@ -303,6 +374,16 @@ namespace MinecraftClient.Tui entityPixels[(centerX, centerY)] = (MinimapEntityClassifier.PlayerColor, 5); result.VisibleCategories.Add(MobCategory.Player); + var selfList = result.EntityMap![centerX, centerY] ??= []; + selfList.Add(new PixelEntityInfo + { + Name = client.GetUsername(), + Category = MobCategory.Player, + Health = client.GetHealth(), + MaxHealth = 20f, + Priority = 5, + }); + ChunkColumn? cachedColumn = null; int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue; @@ -317,17 +398,21 @@ namespace MinecraftClient.Tui if (bpp == 1) { - var (color, surfY) = SampleColumn(world, baseX, baseZ, scanTop, minY, + var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY, ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); result.Pixels[px, py] = color; result.Heights[px, py] = surfY; + result.BlockTypes![px, py] = surfMat; } else { - var (color, surfY) = SampleAreaDominant(world, baseX, baseZ, bpp, scanTop, minY, + var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp, + scanTop, minY, result.BlockSummary is not null, ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); result.Pixels[px, py] = color; result.Heights[px, py] = surfY; + if (result.BlockSummary is not null) + result.BlockSummary[px, py] = matSum; } } } @@ -447,7 +532,7 @@ namespace MinecraftClient.Tui } } - private static (Color color, int surfaceY) SampleColumn(World world, int x, int z, + private static (Color color, int surfaceY, Material surfaceMat) SampleColumn(World world, int x, int z, int scanTop, int minY, ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) { @@ -461,11 +546,12 @@ namespace MinecraftClient.Tui } if (cachedColumn is null) - return (MinimapColorMap.VoidColor, minY); + return (MinimapColorMap.VoidColor, minY, Material.Air); int waterDepth = 0; bool inIce = false; int surfaceY = minY; + Material topMat = Material.Air; for (int y = scanTop; y >= minY; y--) { @@ -481,19 +567,19 @@ namespace MinecraftClient.Tui if (MinimapColorMap.IsWater(mat)) { - if (waterDepth == 0) surfaceY = y; + if (waterDepth == 0) { surfaceY = y; topMat = mat; } waterDepth++; continue; } if (MinimapColorMap.IsIce(mat) && !inIce) { - if (waterDepth == 0) surfaceY = y; + if (waterDepth == 0) { surfaceY = y; topMat = mat; } inIce = true; continue; } - if (waterDepth == 0 && !inIce) surfaceY = y; + if (waterDepth == 0 && !inIce) { surfaceY = y; topMat = mat; } var baseColor = MinimapColorMap.GetBaseColor(mat); @@ -502,33 +588,43 @@ namespace MinecraftClient.Tui if (inIce) baseColor = MinimapColorMap.BlendIceColor(baseColor); - return (baseColor, surfaceY); + return (baseColor, surfaceY, topMat); } if (waterDepth > 0) - return (MinimapColorMap.WaterColor, surfaceY); + return (MinimapColorMap.WaterColor, surfaceY, topMat); - return (MinimapColorMap.VoidColor, minY); + return (MinimapColorMap.VoidColor, minY, Material.Air); } - private static (Color color, int surfaceY) SampleAreaDominant(World world, int baseX, int baseZ, - int size, int scanTop, int minY, + private static (Color color, int surfaceY, List<(Material Mat, int Count)>? matSummary) + SampleAreaDominant(World world, int baseX, int baseZ, + int size, int scanTop, int minY, bool collectMats, ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) { var colorCounts = new Dictionary(); + Dictionary? matCounts = collectMats ? [] : null; int step = Math.Max(1, size / 3); for (int dx = 0; dx < size; dx += step) { for (int dz = 0; dz < size; dz += step) { - var (c, surfY) = SampleColumn(world, baseX + dx, baseZ + dz, scanTop, minY, + var (c, surfY, surfMat) = SampleColumn(world, baseX + dx, baseZ + dz, scanTop, minY, ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); if (colorCounts.TryGetValue(c, out var existing)) colorCounts[c] = (existing.Count + 1, existing.SumY + surfY); else colorCounts[c] = (1, surfY); + + if (matCounts is not null) + { + if (matCounts.TryGetValue(surfMat, out int mc)) + matCounts[surfMat] = mc + 1; + else + matCounts[surfMat] = 1; + } } } @@ -544,7 +640,17 @@ namespace MinecraftClient.Tui avgY = kvp.Value.SumY / kvp.Value.Count; } } - return (best, avgY); + + List<(Material, int)>? summary = null; + if (matCounts is not null && matCounts.Count > 0) + { + summary = matCounts + .OrderByDescending(kv => kv.Value) + .Select(kv => (kv.Key, kv.Value)) + .ToList(); + } + + return (best, avgY, summary); } private void ApplyPixelBuffer(SampleResult result, int w, int h) @@ -573,6 +679,224 @@ namespace MinecraftClient.Tui } } } + + _lastResult = result; + + if (_hoverCol >= 0 && _hoverRow >= 0) + UpdateTooltip(_hoverCol, _hoverRow); + } + + private void OnMapPointerMoved(object? sender, PointerEventArgs e) + { + var pos = e.GetPosition(_mapGrid); + int col = (int)pos.X; + int row = (int)pos.Y; + + if (col < 0 || col >= _mapWidth || row < 0 || row >= _cellRows) + { + HideTooltip(); + return; + } + + _hoverCol = col; + _hoverRow = row; + UpdateTooltip(col, row); + } + + private void OnMapPointerExited(object? sender, PointerEventArgs e) + { + HideTooltip(); + } + + private void HideTooltip() + { + _hoverCol = -1; + _hoverRow = -1; + _tooltipBorder.IsVisible = false; + } + + private void UpdateTooltip(int col, int row) + { + var result = _lastResult; + if (result is null) { _tooltipBorder.IsVisible = false; return; } + + int bpp = result.Bpp; + int centerX = result.CenterX; + int centerY = result.CenterY; + + int topPixelY = row * 2; + int botPixelY = row * 2 + 1; + + int baseX = result.PlayerBlockX + (col - centerX) * bpp; + int baseZ_top = result.PlayerBlockZ + (topPixelY - centerY) * bpp; + int baseZ_bot = result.PlayerBlockZ + (botPixelY - centerY) * bpp; + + _tooltipContent.Children.Clear(); + + if (bpp == 1) + { + int surfY_top = (topPixelY < result.Heights.GetLength(1)) ? result.Heights[col, topPixelY] : 0; + int surfY_bot = (botPixelY < result.Heights.GetLength(1)) ? result.Heights[col, botPixelY] : 0; + + string coordLine = baseZ_top == baseZ_bot + ? $"{baseX}, {surfY_top}, {baseZ_top}" + : $"{baseX}, {surfY_top}, {baseZ_top} / {baseX}, {surfY_bot}, {baseZ_bot}"; + _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + + if (result.BlockTypes is not null) + { + var mat_top = result.BlockTypes[col, topPixelY]; + var mat_bot = (botPixelY < result.BlockTypes.GetLength(1)) + ? result.BlockTypes[col, botPixelY] : mat_top; + string blockLine = mat_top == mat_bot + ? FormatMaterialName(mat_top) + : $"{FormatMaterialName(mat_top)} / {FormatMaterialName(mat_bot)}"; + _tooltipContent.Children.Add(MakeTooltipText(blockLine, Brushes.LightGray)); + } + } + else + { + int endX = baseX + bpp - 1; + int endZ_bot = baseZ_bot + bpp - 1; + string coordLine = $"X {baseX}~{endX} Z {baseZ_top}~{endZ_bot}"; + _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + + AppendBlockSummary(result, col, topPixelY, botPixelY); + } + + AppendEntityInfo(result, col, topPixelY, botPixelY); + + if (_tooltipContent.Children.Count == 0) + { + _tooltipBorder.IsVisible = false; + return; + } + + int maxTipW = Math.Max(10, _mapWidth / 2 - 2); + _tooltipBorder.MaxWidth = maxTipW; + _tooltipBorder.MaxHeight = _cellRows; + + bool showRight = col < _mapWidth / 2; + int tipX = showRight ? col + 2 : Math.Max(0, col - maxTipW - 1); + int tipY = Math.Clamp(row, 0, _cellRows - 1); + + Canvas.SetLeft(_tooltipBorder, tipX); + Canvas.SetTop(_tooltipBorder, tipY); + _tooltipBorder.IsVisible = true; + } + + private void AppendBlockSummary(SampleResult result, int col, int topPy, int botPy) + { + if (result.BlockSummary is null) return; + + var merged = new Dictionary(); + MergeBlockCounts(result.BlockSummary, col, topPy, merged); + if (botPy < result.BlockSummary.GetLength(1)) + MergeBlockCounts(result.BlockSummary, col, botPy, merged); + + if (merged.Count == 0) return; + + var sorted = merged.OrderByDescending(kv => kv.Value).Take(4); + int totalSamples = 0; + foreach (var kv in merged) totalSamples += kv.Value; + + var parts = new List(); + foreach (var kv in sorted) + { + if (kv.Key == Material.Air && merged.Count > 1) continue; + parts.Add(kv.Value > 1 + ? $"{FormatMaterialName(kv.Key)} x{kv.Value}" + : FormatMaterialName(kv.Key)); + } + + if (parts.Count == 0) return; + + string line = string.Join(", ", parts); + _tooltipContent.Children.Add(MakeTooltipText(line, Brushes.LightGray)); + } + + private static void MergeBlockCounts(List<(Material Mat, int Count)>?[,] summary, + int px, int py, Dictionary target) + { + var list = summary[px, py]; + if (list is null) return; + foreach (var (mat, count) in list) + { + if (target.TryGetValue(mat, out int c)) + target[mat] = c + count; + else + target[mat] = count; + } + } + + private void AppendEntityInfo(SampleResult result, int col, int topPy, int botPy) + { + var entityMap = result.EntityMap; + if (entityMap is null) return; + + var combined = new List(); + AddEntitiesFromPixel(entityMap, col, topPy, combined); + if (botPy < entityMap.GetLength(1)) + AddEntitiesFromPixel(entityMap, col, botPy, combined); + + if (combined.Count == 0) return; + + combined.Sort((a, b) => b.Priority.CompareTo(a.Priority)); + int shown = 0; + var seen = new HashSet(); + foreach (var ent in combined) + { + if (shown >= 4) break; + string key = $"{ent.Name}_{ent.Health:F0}"; + if (!seen.Add(key)) continue; + + var catColor = MinimapEntityClassifier.GetBaseColor(ent.Category); + string hpStr; + if (ent.Health > 0) + { + hpStr = ent.MaxHealth > 0 + ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" + : $" HP:{ent.Health:F0}"; + } + else + hpStr = ""; + + _tooltipContent.Children.Add(MakeTooltipText( + $"{ent.Name}{hpStr}", + new SolidColorBrush(catColor))); + shown++; + } + } + + private static void AddEntitiesFromPixel(List?[,] map, + int px, int py, List target) + { + if (px >= 0 && px < map.GetLength(0) && py >= 0 && py < map.GetLength(1)) + { + var list = map[px, py]; + if (list is not null) + target.AddRange(list); + } + } + + private static TextBlock MakeTooltipText(string text, IBrush foreground) + { + return new TextBlock + { + Text = text, + Foreground = foreground, + TextWrapping = TextWrapping.Wrap, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }; + } + + private static string FormatMaterialName(Material mat) + { + if (mat == Material.Air) return "Air"; + string raw = mat.ToString(); + return raw.Replace('_', ' '); } private void UpdateInfoBarAndLegend(McClient client, int bpp, From d427a6e16080a63160ab0a756625a4cacc6dcaec Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 19:53:31 +0800 Subject: [PATCH 27/76] Add tooltip service and integrate with MinimapControl - Introduced TuiTooltipService for managing tooltips across TUI components. - Updated MinimapControl to utilize the new tooltip service for enhanced entity information display. - Refactored tooltip rendering logic to improve visibility and interaction based on mouse position. --- MinecraftClient/Tui/MainTuiView.cs | 9 ++ MinecraftClient/Tui/MinimapControl.cs | 144 +++++++++++------------ MinecraftClient/Tui/TuiTooltipService.cs | 114 ++++++++++++++++++ 3 files changed, 194 insertions(+), 73 deletions(-) create mode 100644 MinecraftClient/Tui/TuiTooltipService.cs diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 1cde95e4..34ff06db 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -45,6 +45,8 @@ namespace MinecraftClient.Tui private readonly MinimapControl _minimapControl; private volatile bool _minimapVisible; + private TuiTooltipService? _tooltipService; + private readonly Border _suggestionBorder; private readonly StackPanel _suggestionPanel; private CommandSuggestion[] _suggestions = Array.Empty(); @@ -57,6 +59,8 @@ namespace MinecraftClient.Tui private int MaxVisibleSuggestions => Math.Max(1, Settings.Config.Console.CommandSuggestion.Max_Displayed_Suggestions); + public TuiTooltipService? TooltipService => _tooltipService; + public MainTuiView() { Background = Brushes.Black; @@ -194,6 +198,10 @@ namespace MinecraftClient.Tui Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder } }; + _tooltipService = new TuiTooltipService(_rootPanel); + _minimapControl.TooltipService = _tooltipService; + _minimapControl.Position = mmCfg.Position; + Content = _rootPanel; if (mmCfg.Enabled) @@ -1083,6 +1091,7 @@ namespace MinecraftClient.Tui _minimapBorder.HorizontalAlignment = hAlign; _minimapBorder.VerticalAlignment = vAlign; _minimapBorder.Margin = margin; + _minimapControl.Position = pos; Settings.Config.Console.Minimap.Position = pos; } diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs index 1e93c390..33f25465 100644 --- a/MinecraftClient/Tui/MinimapControl.cs +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -45,12 +45,11 @@ namespace MinecraftClient.Tui private readonly Grid _mapGrid; private readonly DispatcherTimer _timer; - private readonly Canvas _tooltipCanvas; - private readonly Border _tooltipBorder; - private readonly StackPanel _tooltipContent; private SampleResult? _lastResult; private int _hoverCol = -1; private int _hoverRow = -1; + private double _hoverGlobalX; + private double _hoverGlobalY; public int BlocksPerPixel { @@ -60,6 +59,10 @@ namespace MinecraftClient.Tui public NameDisplayConfig NameConfig => _nameConfig; + public TuiTooltipService? TooltipService { get; set; } + + public MinimapPosition Position { get; set; } = MinimapPosition.top_right; + public int MapPixelWidth => _mapWidth; public int MapPixelHeight => _mapHeight; @@ -83,33 +86,10 @@ namespace MinecraftClient.Tui _infoRow = new StackPanel { Orientation = Orientation.Horizontal }; _legendPanel = new StackPanel { Orientation = Orientation.Horizontal }; - _tooltipContent = new StackPanel { Orientation = Orientation.Vertical }; - _tooltipBorder = new Border - { - Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), - BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), - BorderThickness = new Thickness(1), - Padding = new Thickness(1), - Child = _tooltipContent, - IsVisible = false, - }; - - _tooltipCanvas = new Canvas - { - IsHitTestVisible = false, - Children = { _tooltipBorder }, - }; - - var mapLayer = new Panel - { - ClipToBounds = true, - Children = { _mapGrid, _tooltipCanvas }, - }; - var root = new StackPanel { Orientation = Orientation.Vertical, - Children = { mapLayer, _infoRow, _legendPanel }, + Children = { _mapGrid, _infoRow, _legendPanel }, }; Content = root; @@ -236,6 +216,7 @@ namespace MinecraftClient.Tui { public string Name = ""; public MobCategory Category; + public double X, Y, Z; public float Health; public float MaxHealth; public int Priority; @@ -352,6 +333,9 @@ namespace MinecraftClient.Tui { Name = eName, Category = cat, + X = entity.Location.X, + Y = entity.Location.Y, + Z = entity.Location.Z, Health = entity.Health, MaxHealth = -1, Priority = priority, @@ -379,6 +363,9 @@ namespace MinecraftClient.Tui { Name = client.GetUsername(), Category = MobCategory.Player, + X = playerLoc.X, + Y = playerLoc.Y, + Z = playerLoc.Z, Health = client.GetHealth(), MaxHealth = 20f, Priority = 5, @@ -700,6 +687,19 @@ namespace MinecraftClient.Tui _hoverCol = col; _hoverRow = row; + + if (this.VisualRoot is Visual root + && _mapGrid.TranslatePoint(pos, root) is { } gp) + { + _hoverGlobalX = gp.X; + _hoverGlobalY = gp.Y; + } + else + { + _hoverGlobalX = pos.X; + _hoverGlobalY = pos.Y; + } + UpdateTooltip(col, row); } @@ -712,13 +712,14 @@ namespace MinecraftClient.Tui { _hoverCol = -1; _hoverRow = -1; - _tooltipBorder.IsVisible = false; + TooltipService?.Hide(); } private void UpdateTooltip(int col, int row) { + var svc = TooltipService; var result = _lastResult; - if (result is null) { _tooltipBorder.IsVisible = false; return; } + if (svc is null || result is null) { svc?.Hide(); return; } int bpp = result.Bpp; int centerX = result.CenterX; @@ -731,7 +732,7 @@ namespace MinecraftClient.Tui int baseZ_top = result.PlayerBlockZ + (topPixelY - centerY) * bpp; int baseZ_bot = result.PlayerBlockZ + (botPixelY - centerY) * bpp; - _tooltipContent.Children.Clear(); + var lines = new List(); if (bpp == 1) { @@ -741,7 +742,7 @@ namespace MinecraftClient.Tui string coordLine = baseZ_top == baseZ_bot ? $"{baseX}, {surfY_top}, {baseZ_top}" : $"{baseX}, {surfY_top}, {baseZ_top} / {baseX}, {surfY_bot}, {baseZ_bot}"; - _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); if (result.BlockTypes is not null) { @@ -751,7 +752,7 @@ namespace MinecraftClient.Tui string blockLine = mat_top == mat_bot ? FormatMaterialName(mat_top) : $"{FormatMaterialName(mat_top)} / {FormatMaterialName(mat_bot)}"; - _tooltipContent.Children.Add(MakeTooltipText(blockLine, Brushes.LightGray)); + lines.Add(new TuiTooltipLine { Text = blockLine, Foreground = Brushes.LightGray }); } } else @@ -759,33 +760,40 @@ namespace MinecraftClient.Tui int endX = baseX + bpp - 1; int endZ_bot = baseZ_bot + bpp - 1; string coordLine = $"X {baseX}~{endX} Z {baseZ_top}~{endZ_bot}"; - _tooltipContent.Children.Add(MakeTooltipText(coordLine, Brushes.White)); + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); - AppendBlockSummary(result, col, topPixelY, botPixelY); + AppendBlockSummaryLines(result, col, topPixelY, botPixelY, lines); } - AppendEntityInfo(result, col, topPixelY, botPixelY); + AppendEntityInfoLines(result, col, topPixelY, botPixelY, lines); - if (_tooltipContent.Children.Count == 0) + if (lines.Count == 0) { - _tooltipBorder.IsVisible = false; + svc.Hide(); return; } - int maxTipW = Math.Max(10, _mapWidth / 2 - 2); - _tooltipBorder.MaxWidth = maxTipW; - _tooltipBorder.MaxHeight = _cellRows; + bool preferRight = Position switch + { + MinimapPosition.top_left or MinimapPosition.bottom_left => true, + MinimapPosition.top_right or MinimapPosition.bottom_right => false, + _ => true, + }; - bool showRight = col < _mapWidth / 2; - int tipX = showRight ? col + 2 : Math.Max(0, col - maxTipW - 1); - int tipY = Math.Clamp(row, 0, _cellRows - 1); + double mx = _hoverGlobalX; + double my = _hoverGlobalY; - Canvas.SetLeft(_tooltipBorder, tipX); - Canvas.SetTop(_tooltipBorder, tipY); - _tooltipBorder.IsVisible = true; + if (Position == MinimapPosition.center + && this.VisualRoot is Visual root) + { + preferRight = mx < root.Bounds.Width / 2; + } + + svc.Show(mx, my, lines, preferRight); } - private void AppendBlockSummary(SampleResult result, int col, int topPy, int botPy) + private void AppendBlockSummaryLines(SampleResult result, int col, int topPy, int botPy, + List lines) { if (result.BlockSummary is null) return; @@ -797,8 +805,6 @@ namespace MinecraftClient.Tui if (merged.Count == 0) return; var sorted = merged.OrderByDescending(kv => kv.Value).Take(4); - int totalSamples = 0; - foreach (var kv in merged) totalSamples += kv.Value; var parts = new List(); foreach (var kv in sorted) @@ -811,8 +817,11 @@ namespace MinecraftClient.Tui if (parts.Count == 0) return; - string line = string.Join(", ", parts); - _tooltipContent.Children.Add(MakeTooltipText(line, Brushes.LightGray)); + lines.Add(new TuiTooltipLine + { + Text = string.Join(", ", parts), + Foreground = Brushes.LightGray, + }); } private static void MergeBlockCounts(List<(Material Mat, int Count)>?[,] summary, @@ -829,7 +838,8 @@ namespace MinecraftClient.Tui } } - private void AppendEntityInfo(SampleResult result, int col, int topPy, int botPy) + private static void AppendEntityInfoLines(SampleResult result, int col, int topPy, int botPy, + List lines) { var entityMap = result.EntityMap; if (entityMap is null) return; @@ -851,19 +861,20 @@ namespace MinecraftClient.Tui if (!seen.Add(key)) continue; var catColor = MinimapEntityClassifier.GetBaseColor(ent.Category); - string hpStr; + string coordStr = $"({ent.X:F1}, {ent.Y:F1}, {ent.Z:F1})"; + string hpStr = ""; if (ent.Health > 0) { hpStr = ent.MaxHealth > 0 - ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" - : $" HP:{ent.Health:F0}"; + ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" + : $" HP:{ent.Health:F0}"; } - else - hpStr = ""; - _tooltipContent.Children.Add(MakeTooltipText( - $"{ent.Name}{hpStr}", - new SolidColorBrush(catColor))); + lines.Add(new TuiTooltipLine + { + Text = $"{ent.Name} {coordStr}{hpStr}", + Foreground = new SolidColorBrush(catColor), + }); shown++; } } @@ -879,19 +890,6 @@ namespace MinecraftClient.Tui } } - private static TextBlock MakeTooltipText(string text, IBrush foreground) - { - return new TextBlock - { - Text = text, - Foreground = foreground, - TextWrapping = TextWrapping.Wrap, - Padding = new Thickness(0), - Margin = new Thickness(0), - FontSize = 1, - }; - } - private static string FormatMaterialName(Material mat) { if (mat == Material.Air) return "Air"; diff --git a/MinecraftClient/Tui/TuiTooltipService.cs b/MinecraftClient/Tui/TuiTooltipService.cs new file mode 100644 index 00000000..0ae51607 --- /dev/null +++ b/MinecraftClient/Tui/TuiTooltipService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + public sealed class TuiTooltipLine + { + public string Text { get; init; } = ""; + public IBrush Foreground { get; init; } = Brushes.White; + } + + /// + /// Global tooltip that floats above all TUI content. + /// Owned by MainTuiView, used by minimap / chat / other components. + /// + public sealed class TuiTooltipService + { + private readonly Panel _rootPanel; + private readonly Canvas _canvas; + private readonly Border _border; + private readonly StackPanel _content; + + internal TuiTooltipService(Panel rootPanel) + { + _content = new StackPanel { Orientation = Avalonia.Layout.Orientation.Vertical }; + _border = new Border + { + Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), + BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), + BorderThickness = new Thickness(1), + Padding = new Thickness(1), + Child = _content, + IsVisible = false, + }; + + _canvas = new Canvas + { + IsHitTestVisible = false, + Children = { _border }, + }; + + _rootPanel = rootPanel; + rootPanel.Children.Add(_canvas); + } + + /// Global X of the mouse cursor. + /// Global Y of the mouse cursor. + /// + /// If true, try placing tooltip to the right of mouseX; + /// if false, try placing to the left. + /// The service auto-flips when the tooltip would overflow the screen. + /// + public void Show(double mouseX, double mouseY, IReadOnlyList lines, + bool preferRight = true) + { + _content.Children.Clear(); + + if (lines.Count == 0) + { + _border.IsVisible = false; + return; + } + + int maxChars = 0; + foreach (var line in lines) + { + _content.Children.Add(new TextBlock + { + Text = line.Text, + Foreground = line.Foreground, + TextWrapping = TextWrapping.Wrap, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }); + if (line.Text.Length > maxChars) + maxChars = line.Text.Length; + } + + double tipW = maxChars + 4; + double screenW = _rootPanel.Bounds.Width; + + const double gap = 1; + double gx; + if (preferRight) + { + gx = mouseX + gap; + if (gx + tipW > screenW) + gx = mouseX - tipW - gap; + } + else + { + gx = mouseX - tipW - gap; + if (gx < 0) + gx = mouseX + gap; + } + + Canvas.SetLeft(_border, Math.Max(0, gx)); + Canvas.SetTop(_border, Math.Max(0, mouseY)); + _border.IsVisible = true; + } + + public void Hide() + { + _border.IsVisible = false; + _content.Children.Clear(); + } + + public bool IsVisible => _border.IsVisible; + } +} From d05e3148f1240f238933b3a78a3a403d468c5212 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 22:38:39 +0800 Subject: [PATCH 28/76] Refactor explosion packet handling for protocol version updates - Updated explosion packet processing to accommodate changes in Minecraft protocol versions, specifically for versions 1.21.2 and 1.20.4. - Removed obsolete fields such as explosion strength and block records for newer versions, and added support for optional knockback and particle data. - Enhanced backward compatibility for earlier versions by maintaining existing logic for explosion data retrieval. --- .../Protocol/Handlers/Protocol18.cs | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index b6cdcd05..aa004b5e 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2822,43 +2822,71 @@ namespace MinecraftClient.Protocol.Handlers break; case PacketTypesIn.Explosion: Location explosionLocation; - if (protocolVersion >= MC_1_19_3_Version) + float explosionStrength; + int explosionBlockCount; + + if (protocolVersion >= MC_1_21_2_Version) + { + // 1.21.2+: removed strength, block records, and player motion floats; + // added optional knockback (doubles) and single particle explosionLocation = new(dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); - else - explosionLocation = new(dataTypes.ReadNextFloat(packetData), - dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + explosionStrength = 0; + explosionBlockCount = 0; - var explosionStrength = dataTypes.ReadNextFloat(packetData); - var explosionBlockCount = protocolVersion >= MC_1_17_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextInt(packetData); // Record count + if (dataTypes.ReadNextBool(packetData)) // Has player knockback + { + dataTypes.ReadNextDouble(packetData); // Knockback X + dataTypes.ReadNextDouble(packetData); // Knockback Y + dataTypes.ReadNextDouble(packetData); // Knockback Z + } - // Records - for (var i = 0; i < explosionBlockCount; i++) - dataTypes.ReadNextByteArray(packetData, 3); + dataTypes.ReadParticleData(packetData, itemPalette); // Explosion particle - dataTypes.ReadNextFloat(packetData); // Player Motion X - dataTypes.ReadNextFloat(packetData); // Player Motion Y - dataTypes.ReadNextFloat(packetData); // Player Motion Z - - if (protocolVersion >= MC_1_20_4_Version) - { - dataTypes.ReadNextVarInt(packetData); // Block Interaction (enum ordinal) - dataTypes.ReadParticleData(packetData, itemPalette); // Small Explosion Particles - dataTypes.ReadParticleData(packetData, itemPalette); // Large Explosion Particles - - // Explosion Sound: Holder via ByteBufCodecs.holder() - // VarInt id: 0 = inline (read DIRECT_STREAM_CODEC), >0 = registry ref (id-1) var soundHolderId = dataTypes.ReadNextVarInt(packetData); if (soundHolderId == 0) { dataTypes.ReadNextString(packetData); // Sound ResourceLocation - var hasFixedRange = dataTypes.ReadNextBool(packetData); - if (hasFixedRange) + if (dataTypes.ReadNextBool(packetData)) dataTypes.ReadNextFloat(packetData); // Fixed range } } + else + { + if (protocolVersion >= MC_1_19_3_Version) + explosionLocation = new(dataTypes.ReadNextDouble(packetData), + dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); + else + explosionLocation = new(dataTypes.ReadNextFloat(packetData), + dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + + explosionStrength = dataTypes.ReadNextFloat(packetData); + explosionBlockCount = protocolVersion >= MC_1_17_Version + ? dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextInt(packetData); + + for (var i = 0; i < explosionBlockCount; i++) + dataTypes.ReadNextByteArray(packetData, 3); + + dataTypes.ReadNextFloat(packetData); // Player Motion X + dataTypes.ReadNextFloat(packetData); // Player Motion Y + dataTypes.ReadNextFloat(packetData); // Player Motion Z + + if (protocolVersion >= MC_1_20_4_Version) + { + dataTypes.ReadNextVarInt(packetData); // Block Interaction + dataTypes.ReadParticleData(packetData, itemPalette); // Small Explosion Particles + dataTypes.ReadParticleData(packetData, itemPalette); // Large Explosion Particles + + var soundHolderId = dataTypes.ReadNextVarInt(packetData); + if (soundHolderId == 0) + { + dataTypes.ReadNextString(packetData); // Sound ResourceLocation + if (dataTypes.ReadNextBool(packetData)) + dataTypes.ReadNextFloat(packetData); // Fixed range + } + } + } handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); break; From 2f03f66f5ccd7447573fe3a6fbeaec724396d23d Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 29 Mar 2026 22:38:47 +0800 Subject: [PATCH 29/76] Enhance useblock command to support hand selection - Updated the 'useblock' command to allow specifying the hand (mainhand or offhand) for block placement. - Modified command usage description to reflect the new optional parameter. - Adjusted the command execution logic to handle the selected hand during block placement. --- MinecraftClient/Commands/Useblock.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/MinecraftClient/Commands/Useblock.cs b/MinecraftClient/Commands/Useblock.cs index 2df6a92f..7e482ba4 100644 --- a/MinecraftClient/Commands/Useblock.cs +++ b/MinecraftClient/Commands/Useblock.cs @@ -1,6 +1,7 @@ using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; +using MinecraftClient.Inventory; using MinecraftClient.Mapping; using static MinecraftClient.CommandHandler.CmdResult; @@ -9,7 +10,7 @@ namespace MinecraftClient.Commands class Useblock : Command { public override string CmdName { get { return "useblock"; } } - public override string CmdUsage { get { return "useblock "; } } + public override string CmdUsage { get { return "useblock [mainhand|offhand]"; } } public override string CmdDesc { get { return Translations.cmd_useblock_desc; } } public override void RegisterCommand(CommandDispatcher dispatcher) @@ -22,7 +23,11 @@ namespace MinecraftClient.Commands dispatcher.Register(l => l.Literal(CmdName) .Then(l => l.Argument("Location", MccArguments.Location()) - .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location")))) + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand)) + .Then(l => l.Literal("mainhand") + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand))) + .Then(l => l.Literal("offhand") + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.OffHand)))) .Then(l => l.Literal("_help") .Executes(r => GetUsage(r.Source, string.Empty)) .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) @@ -39,7 +44,7 @@ namespace MinecraftClient.Commands }); } - private int UseBlockAtLocation(CmdResult r, Location block) + private int UseBlockAtLocation(CmdResult r, Location block, Hand hand) { McClient handler = CmdResult.currentHandler!; if (!handler.GetTerrainEnabled()) @@ -48,7 +53,7 @@ namespace MinecraftClient.Commands Location current = handler.GetCurrentLocation(); block = block.ToAbsolute(current).ToFloor(); Location blockCenter = block.ToCenter(); - bool res = handler.PlaceBlock(block, Direction.Down, lookAtBlock: true); + bool res = handler.PlaceBlock(block, Direction.Down, hand, lookAtBlock: true); return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res); } } From 871305bd722048e5d232e11733fb878b11b35263 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 30 Mar 2026 01:35:38 +0800 Subject: [PATCH 30/76] Add server status display and protocol version upgrade handling - Introduced a new `ServerStatusInfo` class to encapsulate server status data including MOTD, player counts, and version information. - Implemented `ServerStatusDisplay` to format and display server status information in both classic and TUI modes. - Added protocol version upgrade logic in `ProtocolHandler` to determine the highest supported protocol version for multi-version servers. - Updated translations to support new server status labels and messages. - Created `ServerStatusPanelBuilder` for TUI to visually represent server status with player information and connection details. --- AGENTS.md | 1 + .../Protocol/Handlers/Protocol18.cs | 588 ++++++++++-------- MinecraftClient/Protocol/ProtocolHandler.cs | 56 ++ .../Protocol/ServerStatusDisplay.cs | 118 ++++ MinecraftClient/Protocol/ServerStatusInfo.cs | 30 + .../Translations/Translations.Designer.cs | 60 ++ .../Resources/Translations/Translations.resx | 34 +- MinecraftClient/Tui/MainTuiView.cs | 13 + MinecraftClient/Tui/McColorParser.cs | 27 +- .../Tui/ServerStatusPanelBuilder.cs | 296 +++++++++ 10 files changed, 954 insertions(+), 269 deletions(-) create mode 100644 MinecraftClient/Protocol/ServerStatusDisplay.cs create mode 100644 MinecraftClient/Protocol/ServerStatusInfo.cs create mode 100644 MinecraftClient/Tui/ServerStatusPanelBuilder.cs diff --git a/AGENTS.md b/AGENTS.md index 40f216f7..fec9d8f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - Minecraft Console Client (MCC) is a cross-platform text/TUI client for Minecraft Java Edition. - Primary scope: connect to servers, send chat and commands, receive text, automate gameplay/admin tasks, and extend behavior through built-in bots or runtime C# scripts. - Secondary scope: protocol/version adaptation tooling, docs site, legacy GUI wrapper, and debug tooling. +- Decompiled server source for both the old and new MC versions in `$MCC_REPO/MinecraftOfficial/-decompiled/` ## Build / Run - Init submodules first: `git submodule update --init --recursive` diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index aa004b5e..5329cdfb 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -449,7 +449,7 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + // Ignore other packets at this stage default: return true; @@ -467,7 +467,7 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + case ConfigurationPacketTypesIn.Disconnect: handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, dataTypes.ReadNextChat(packetData)); @@ -509,7 +509,7 @@ namespace MinecraftClient.Protocol.Handlers var dimensionIdMap = isDimension ? new Dictionary() : null; var attributeIdMap = isAttribute ? new Dictionary() : null; var enchantmentIdMap = isEnchantment ? new Dictionary() : null; - + for (var i = 0; i < entryCount; i++) { var entryId = dataTypes.ReadNextString(packetData); @@ -537,7 +537,7 @@ namespace MinecraftClient.Protocol.Handlers else if (isEnchantment) enchantmentIdMap!.Add(i, entryId); } - + if (isChat) ChatParser.ReadChatType(availableChats!); else if (isDimension) @@ -553,7 +553,7 @@ namespace MinecraftClient.Protocol.Handlers } break; - + case ConfigurationPacketTypesIn.RemoveResourcePack: if (dataTypes.ReadNextBool(packetData)) // Has UUID dataTypes.ReadNextUUID(packetData); // UUID @@ -562,24 +562,24 @@ namespace MinecraftClient.Protocol.Handlers case ConfigurationPacketTypesIn.ResourcePack: HandleResourcePackPacket(packetData); break; - + case ConfigurationPacketTypesIn.StoreCookie: var name = dataTypes.ReadNextString(packetData); var data = dataTypes.ReadNextByteArray(packetData); McClient.Instance?.SetCookie(name, data); break; - + case ConfigurationPacketTypesIn.Transfer: var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - + McClient.Instance?.Transfer(host, port); break; - + case ConfigurationPacketTypesIn.KnownDataPacks: var knownPacksCount = dataTypes.ReadNextVarInt(packetData); List<(string, string, string)> knownDataPacks = new(); - + for (var i = 0; i < knownPacksCount; i++) { var nameSpace = dataTypes.ReadNextString(packetData); @@ -645,7 +645,7 @@ namespace MinecraftClient.Protocol.Handlers currentState == CurrentState.Login, innerException.GetType()), innerException); - + SentrySdk.AddBreadcrumb(new Breadcrumb("S -> C Packet", "network", new Dictionary() { { "Packet ID", packetId.ToString() }, @@ -786,26 +786,26 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_16_Version: - { - switch (protocolVersion) { - case >= MC_1_19_Version: - dimensionTypeName = - dataTypes.ReadNextString(packetData); // Dimension Type: Identifier - break; - case >= MC_1_16_2_Version: - dimensionType = - dataTypes.ReadNextNbt( - packetData); // Dimension Type: NBT Tag Compound - break; - default: - dataTypes.ReadNextString(packetData); - break; - } + switch (protocolVersion) + { + case >= MC_1_19_Version: + dimensionTypeName = + dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + break; + case >= MC_1_16_2_Version: + dimensionType = + dataTypes.ReadNextNbt( + packetData); // Dimension Type: NBT Tag Compound + break; + default: + dataTypes.ReadNextString(packetData); + break; + } - currentDimension = 0; - break; - } + currentDimension = 0; + break; + } case >= MC_1_9_1_Version: currentDimension = dataTypes.ReadNextInt(packetData); break; @@ -820,27 +820,27 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below break; case >= MC_1_16_Version: - { - var dimensionName = - dataTypes.ReadNextString( - packetData); // Dimension Name (World Name) - 1.16 and above - - if (handler.GetTerrainEnabled()) { - switch (protocolVersion) - { - case >= MC_1_16_2_Version and <= MC_1_18_2_Version: - World.StoreOneDimension(dimensionName, dimensionType!); - World.SetDimension(dimensionName); - break; - default: - World.SetDimension(dimensionTypeName!); - break; - } - } + var dimensionName = + dataTypes.ReadNextString( + packetData); // Dimension Name (World Name) - 1.16 and above - break; - } + if (handler.GetTerrainEnabled()) + { + switch (protocolVersion) + { + case >= MC_1_16_2_Version and <= MC_1_18_2_Version: + World.StoreOneDimension(dimensionName, dimensionType!); + World.SetDimension(dimensionName); + break; + default: + World.SetDimension(dimensionTypeName!); + break; + } + } + + break; + } } } @@ -1354,7 +1354,7 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.Respawn: string? dimensionTypeNameRespawn = null; Dictionary? dimensionTypeRespawn = null; - + if (protocolVersion >= MC_1_16_Version) { switch (protocolVersion) @@ -1386,27 +1386,27 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_16_Version: - { - var dimensionName = - dataTypes.ReadNextString( - packetData); // Dimension Name (World Name) - 1.16 and above - - if (handler.GetTerrainEnabled()) { - switch (protocolVersion) - { - case >= MC_1_16_2_Version and <= MC_1_18_2_Version: - World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); - World.SetDimension(dimensionName); - break; - default: - World.SetDimension(dimensionTypeNameRespawn!); - break; - } - } + var dimensionName = + dataTypes.ReadNextString( + packetData); // Dimension Name (World Name) - 1.16 and above - break; - } + if (handler.GetTerrainEnabled()) + { + switch (protocolVersion) + { + case >= MC_1_16_2_Version and <= MC_1_18_2_Version: + World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); + World.SetDimension(dimensionName); + break; + default: + World.SetDimension(dimensionTypeNameRespawn!); + break; + } + } + + break; + } case < MC_1_14_Version: dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below break; @@ -1453,77 +1453,77 @@ namespace MinecraftClient.Protocol.Handlers handler.OnRespawn(); break; case PacketTypesIn.PlayerPositionAndLook: - { - int teleportId; - Location location; - float yaw, pitch; - int locMask; + { + int teleportId; + Location location; + float yaw, pitch; + int locMask; - if (protocolVersion >= MC_1_21_2_Version) - { - teleportId = dataTypes.ReadNextVarInt(packetData); - location = new Location( - dataTypes.ReadNextDouble(packetData), // X - dataTypes.ReadNextDouble(packetData), // Y - dataTypes.ReadNextDouble(packetData) // Z - ); - dataTypes.ReadNextDouble(packetData); // Delta X - dataTypes.ReadNextDouble(packetData); // Delta Y - dataTypes.ReadNextDouble(packetData); // Delta Z - yaw = dataTypes.ReadNextFloat(packetData); - pitch = dataTypes.ReadNextFloat(packetData); - locMask = dataTypes.ReadNextInt(packetData); // Int flags (was Byte before 1.21.2) - } - else - { - location = new Location( - dataTypes.ReadNextDouble(packetData), // X - dataTypes.ReadNextDouble(packetData), // Y - dataTypes.ReadNextDouble(packetData) // Z - ); - yaw = dataTypes.ReadNextFloat(packetData); - pitch = dataTypes.ReadNextFloat(packetData); - locMask = dataTypes.ReadNextByte(packetData); - teleportId = protocolVersion >= MC_1_9_Version - ? dataTypes.ReadNextVarInt(packetData) : -1; - } - - if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) - { - if (protocolVersion >= MC_1_8_Version) + if (protocolVersion >= MC_1_21_2_Version) { - var currentLocation = handler.GetCurrentLocation(); - location.X = (locMask & 1 << 0) != 0 ? currentLocation.X + location.X : location.X; - location.Y = (locMask & 1 << 1) != 0 ? currentLocation.Y + location.Y : location.Y; - location.Z = (locMask & 1 << 2) != 0 ? currentLocation.Z + location.Z : location.Z; + teleportId = dataTypes.ReadNextVarInt(packetData); + location = new Location( + dataTypes.ReadNextDouble(packetData), // X + dataTypes.ReadNextDouble(packetData), // Y + dataTypes.ReadNextDouble(packetData) // Z + ); + dataTypes.ReadNextDouble(packetData); // Delta X + dataTypes.ReadNextDouble(packetData); // Delta Y + dataTypes.ReadNextDouble(packetData); // Delta Z + yaw = dataTypes.ReadNextFloat(packetData); + pitch = dataTypes.ReadNextFloat(packetData); + locMask = dataTypes.ReadNextInt(packetData); // Int flags (was Byte before 1.21.2) } - } - - if (teleportId >= 0) - { - LastYaw = yaw; - LastPitch = pitch; - handler.UpdateLocation(location, yaw, pitch); - SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportId)); - - if (Config.Main.Advanced.TemporaryFixBadpacket) + else { - SendLocationUpdate(location, true, false, yaw, pitch, true); + location = new Location( + dataTypes.ReadNextDouble(packetData), // X + dataTypes.ReadNextDouble(packetData), // Y + dataTypes.ReadNextDouble(packetData) // Z + ); + yaw = dataTypes.ReadNextFloat(packetData); + pitch = dataTypes.ReadNextFloat(packetData); + locMask = dataTypes.ReadNextByte(packetData); + teleportId = protocolVersion >= MC_1_9_Version + ? dataTypes.ReadNextVarInt(packetData) : -1; + } - if (teleportId == 1) + if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) + { + if (protocolVersion >= MC_1_8_Version) + { + var currentLocation = handler.GetCurrentLocation(); + location.X = (locMask & 1 << 0) != 0 ? currentLocation.X + location.X : location.X; + location.Y = (locMask & 1 << 1) != 0 ? currentLocation.Y + location.Y : location.Y; + location.Z = (locMask & 1 << 2) != 0 ? currentLocation.Z + location.Z : location.Z; + } + } + + if (teleportId >= 0) + { + LastYaw = yaw; + LastPitch = pitch; + handler.UpdateLocation(location, yaw, pitch); + SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportId)); + + if (Config.Main.Advanced.TemporaryFixBadpacket) + { SendLocationUpdate(location, true, false, yaw, pitch, true); - } - } - else - { - handler.UpdateLocation(location, yaw, pitch); - LastYaw = yaw; - LastPitch = pitch; - } - if (protocolVersion is >= MC_1_17_Version and < MC_1_19_4_Version) - dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 - } + if (teleportId == 1) + SendLocationUpdate(location, true, false, yaw, pitch, true); + } + } + else + { + handler.UpdateLocation(location, yaw, pitch); + LastYaw = yaw; + LastPitch = pitch; + } + + if (protocolVersion is >= MC_1_17_Version and < MC_1_19_4_Version) + dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 + } break; case PacketTypesIn.ChunkData: if (handler.GetTerrainEnabled()) @@ -1679,26 +1679,26 @@ namespace MinecraftClient.Protocol.Handlers { // 1.8 - 1.13 case < MC_1_13_2_Version: - { - var directionAndType = dataTypes.ReadNextByte(packetData); - byte direction, type; - - // 1.12.2+ - if (protocolVersion >= MC_1_12_2_Version) { - direction = (byte)(directionAndType & 0xF); - type = (byte)(directionAndType >> 4 & 0xF); - } - else // 1.8 - 1.12 - { - direction = (byte)(directionAndType >> 4 & 0xF); - type = (byte)(directionAndType & 0xF); - } + var directionAndType = dataTypes.ReadNextByte(packetData); + byte direction, type; - mapIcon.Type = (MapIconType)type; - mapIcon.Direction = direction; - break; - } + // 1.12.2+ + if (protocolVersion >= MC_1_12_2_Version) + { + direction = (byte)(directionAndType & 0xF); + type = (byte)(directionAndType >> 4 & 0xF); + } + else // 1.8 - 1.12 + { + direction = (byte)(directionAndType >> 4 & 0xF); + type = (byte)(directionAndType & 0xF); + } + + mapIcon.Type = (MapIconType)type; + mapIcon.Direction = direction; + break; + } // 1.13.2+ case >= MC_1_13_2_Version: mapIcon.Type = (MapIconType)dataTypes.ReadNextVarInt(packetData); @@ -2290,7 +2290,7 @@ namespace MinecraftClient.Protocol.Handlers handler.OnPluginChannelMessage(channel, packetData.ToArray()); return pForge.HandlePluginMessage(channel, packetData, ref currentDimension); case PacketTypesIn.Disconnect: - handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, + handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, dataTypes.ReadNextChat(packetData)); return false; case PacketTypesIn.SetCompression: @@ -2427,17 +2427,17 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entity = dataTypes.ReadNextEntity(packetData, entityPalette, false); - + if (protocolVersion >= MC_1_20_2_Version) { if (entity.Type == EntityType.Player) handler.OnSpawnPlayer(entity.ID, entity.UUID, entity.Location, (byte)entity.Yaw, (byte)entity.Pitch); else handler.OnSpawnEntity(entity); - + break; } - + handler.OnSpawnEntity(entity); } @@ -2648,7 +2648,7 @@ namespace MinecraftClient.Protocol.Handlers var numberOfProperties = protocolVersion >= MC_1_17_Version ? dataTypes.ReadNextVarInt(packetData) : dataTypes.ReadNextInt(packetData); - + Dictionary keys = new(); for (var i = 0; i < numberOfProperties; i++) { @@ -3008,17 +3008,17 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + case PacketTypesIn.StoreCookie: var cookieName2 = dataTypes.ReadNextString(packetData); var cookieData2 = dataTypes.ReadNextByteArray(packetData); McClient.Instance?.SetCookie(cookieName2, cookieData2); break; - + case PacketTypesIn.Transfer: var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - + McClient.Instance?.Transfer(host, port); break; @@ -3286,17 +3286,17 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_19_2_Version and < MC_1_20_2_Version: - { - if (uuid == Guid.Empty) - fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID - else { - fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID - fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID - } + if (uuid == Guid.Empty) + fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID + else + { + fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID + fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID + } - break; - } + break; + } case >= MC_1_20_2_Version: uuid = handler.GetUserUuid(); @@ -3324,42 +3324,42 @@ namespace MinecraftClient.Protocol.Handlers // Encryption request case 0x01: - { - isOnlineMode = true; - var serverId = dataTypes.ReadNextString(packetData); - var serverPublicKey = dataTypes.ReadNextByteArray(packetData); - var token = dataTypes.ReadNextByteArray(packetData); + { + isOnlineMode = true; + var serverId = dataTypes.ReadNextString(packetData); + var serverPublicKey = dataTypes.ReadNextByteArray(packetData); + var token = dataTypes.ReadNextByteArray(packetData); - var shouldAuthetnicate = false; + var shouldAuthetnicate = false; - if (protocolVersion >= MC_1_20_6_Version) - shouldAuthetnicate = dataTypes.ReadNextBool(packetData); - - return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), - Config.Main.General.AccountType, token, serverId, - serverPublicKey, playerKeyPair, session, shouldAuthetnicate); - } + if (protocolVersion >= MC_1_20_6_Version) + shouldAuthetnicate = dataTypes.ReadNextBool(packetData); + + return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), + Config.Main.General.AccountType, token, serverId, + serverPublicKey, playerKeyPair, session, shouldAuthetnicate); + } // Login successful case 0x02: - { - log.Info($"§8{Translations.mcc_server_offline}"); - currentState = protocolVersion < MC_1_20_2_Version - ? CurrentState.Play - : CurrentState.Configuration; - - if (protocolVersion >= MC_1_20_2_Version) - SendPacket(0x03, new List()); - - if (!pForge.CompleteForgeHandshake()) { - log.Error($"§8{Translations.error_forge}"); - return false; - } + log.Info($"§8{Translations.mcc_server_offline}"); + currentState = protocolVersion < MC_1_20_2_Version + ? CurrentState.Play + : CurrentState.Configuration; - StartUpdating(); - return true; //No need to check session or start encryption - } + if (protocolVersion >= MC_1_20_2_Version) + SendPacket(0x03, new List()); + + if (!pForge.CompleteForgeHandshake()) + { + log.Error($"§8{Translations.error_forge}"); + return false; + } + + StartUpdating(); + return true; //No need to check session or start encryption + } default: HandlePacket(packetId, packetData); break; @@ -3392,7 +3392,7 @@ namespace MinecraftClient.Protocol.Handlers if (session.SessionPreCheckTask.Result) // PreCheck Success needCheckSession = false; } - + // 1.20.6++ if (shouldAuthetnicate) needCheckSession = true; @@ -3465,51 +3465,51 @@ namespace MinecraftClient.Protocol.Handlers handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); return false; - + //Login successful case 0x02: - { - var uuidReceived = protocolVersion >= MC_1_16_Version - ? dataTypes.ReadNextUUID(packetData) - : Guid.Parse(dataTypes.ReadNextString(packetData)); - var userName = dataTypes.ReadNextString(packetData); - Tuple[]? playerProperty = null; - if (protocolVersion >= MC_1_19_Version) { - var count = dataTypes.ReadNextVarInt(packetData); // Number Of Properties - playerProperty = new Tuple[count]; - for (var i = 0; i < count; ++i) + var uuidReceived = protocolVersion >= MC_1_16_Version + ? dataTypes.ReadNextUUID(packetData) + : Guid.Parse(dataTypes.ReadNextString(packetData)); + var userName = dataTypes.ReadNextString(packetData); + Tuple[]? playerProperty = null; + if (protocolVersion >= MC_1_19_Version) { - var name = dataTypes.ReadNextString(packetData); - var value = dataTypes.ReadNextString(packetData); - var isSigned = dataTypes.ReadNextBool(packetData); - var signature = isSigned ? dataTypes.ReadNextString(packetData) : string.Empty; - playerProperty[i] = new Tuple(name, value, signature); + var count = dataTypes.ReadNextVarInt(packetData); // Number Of Properties + playerProperty = new Tuple[count]; + for (var i = 0; i < count; ++i) + { + var name = dataTypes.ReadNextString(packetData); + var value = dataTypes.ReadNextString(packetData); + var isSigned = dataTypes.ReadNextBool(packetData); + var signature = isSigned ? dataTypes.ReadNextString(packetData) : string.Empty; + playerProperty[i] = new Tuple(name, value, signature); + } } + + // Strict Error Handling (removed in 1.21.2) + if (protocolVersion >= MC_1_20_6_Version && protocolVersion < MC_1_21_2_Version) + dataTypes.ReadNextBool(packetData); + + currentState = protocolVersion < MC_1_20_2_Version + ? CurrentState.Play + : CurrentState.Configuration; + + if (protocolVersion >= MC_1_20_2_Version) + SendPacket(0x03, new List()); + + handler.OnLoginSuccess(uuidReceived, userName, playerProperty); + + if (!pForge.CompleteForgeHandshake()) + { + log.Error($"§8{Translations.error_forge_encrypt}"); + return false; + } + + StartUpdating(); + return true; } - - // Strict Error Handling (removed in 1.21.2) - if (protocolVersion >= MC_1_20_6_Version && protocolVersion < MC_1_21_2_Version) - dataTypes.ReadNextBool(packetData); - - currentState = protocolVersion < MC_1_20_2_Version - ? CurrentState.Play - : CurrentState.Configuration; - - if (protocolVersion >= MC_1_20_2_Version) - SendPacket(0x03, new List()); - - handler.OnLoginSuccess(uuidReceived, userName, playerProperty); - - if (!pForge.CompleteForgeHandshake()) - { - log.Error($"§8{Translations.error_forge_encrypt}"); - return false; - } - - StartUpdating(); - return true; - } default: HandlePacket(packetId, packetData); break; @@ -3548,15 +3548,15 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.GetString(BehindCursor.Replace(' ', (char)0x00))); break; case >= MC_1_8_Version: - { - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, dataTypes.GetString(BehindCursor)); + { + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, dataTypes.GetString(BehindCursor)); - if (protocolVersion >= MC_1_9_Version) - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, assumeCommand); + if (protocolVersion >= MC_1_9_Version) + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, assumeCommand); - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, hasPosition); - break; - } + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, hasPosition); + break; + } default: tabCompletePacket = dataTypes.ConcatBytes(dataTypes.GetString(BehindCursor)); break; @@ -3620,7 +3620,8 @@ namespace MinecraftClient.Protocol.Handlers if (dataTypes.ReadNextVarInt(packetData) != 0x00) return false; - var result = dataTypes.ReadNextString(packetData); // Get the Json data + // Get the Json data + var result = dataTypes.ReadNextString(packetData); if (Config.Logging.DebugMessages) { @@ -3651,7 +3652,44 @@ namespace MinecraftClient.Protocol.Handlers // Check for forge on the server. Protocol18Forge.ServerInfoCheckForge(jsonObj, ref forgeInfo); - // Complete the normal status exchange so the probe connection closes cleanly server-side. + int onlinePlayers = 0, maxPlayers = 0; + List samplePlayers = []; + + if (jsonObj["players"] is System.Text.Json.Nodes.JsonObject playersObj) + { + if (playersObj["online"] is { } onlineNode) + onlinePlayers = int.Parse(onlineNode.GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture); + if (playersObj["max"] is { } maxNode) + maxPlayers = int.Parse(maxNode.GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture); + if (playersObj["sample"] is System.Text.Json.Nodes.JsonArray sampleArray) + { + foreach (var entry in sampleArray) + { + if (entry is not System.Text.Json.Nodes.JsonObject playerObj) continue; + samplePlayers.Add(new ServerStatusInfo.SamplePlayer + { + Name = playerObj["name"]?.GetStringValue() ?? "", + Id = playerObj["id"]?.GetStringValue() ?? "" + }); + } + } + } + + string motdRaw = ""; + if (jsonObj["description"] is { } descNode) + motdRaw = descNode.ToJsonString(); + + string? faviconBase64 = null; + if (jsonObj["favicon"] is { } faviconNode) + { + var faviconStr = faviconNode.GetStringValue(); + const string prefix = "data:image/png;base64,"; + faviconBase64 = faviconStr.StartsWith(prefix, StringComparison.Ordinal) + ? faviconStr[prefix.Length..] + : faviconStr; + } + + long pingMs = -1; try { long pingPayload = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -3663,7 +3701,10 @@ namespace MinecraftClient.Protocol.Handlers { packetData = new Queue(socketWrapper.ReadDataRAW(packetLength)); if (dataTypes.ReadNextVarInt(packetData) == 0x01) - dataTypes.ReadNextLong(packetData); + { + long pongPayload = dataTypes.ReadNextLong(packetData); + pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload; + } } } catch @@ -3671,9 +3712,28 @@ namespace MinecraftClient.Protocol.Handlers // Some servers may close the probe connection immediately after the status response. } + var statusInfo = new ServerStatusInfo + { + Host = host, + Port = port, + VersionName = version, + ProtocolVersion = protocolVersion, + OnlinePlayers = onlinePlayers, + MaxPlayers = maxPlayers, + SamplePlayers = samplePlayers, + MotdRaw = motdRaw, + FaviconBase64 = faviconBase64, + PingMs = pingMs + }; + + ProtocolHandler.TryUpgradeProtocolVersion(version, ref protocolVersion); + statusInfo.ResolvedProtocol = protocolVersion; + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version, protocolVersion + (forgeInfo is not null ? Translations.mcc_with_forge : ""))); + ServerStatusDisplay.Show(statusInfo); + return true; } finally @@ -3792,7 +3852,7 @@ namespace MinecraftClient.Protocol.Handlers SendMessageAcknowledgment(ConsumeAcknowledgment()); } } - + /// /// Send a chat command to the server, with or without signing based on the online mode and version. /// @@ -3917,7 +3977,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + /// /// Send a chat message to the server /// @@ -4541,7 +4601,7 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(dataTypes.GetFloat(LastYaw)); packet.AddRange(dataTypes.GetFloat(LastPitch)); } - + SendPacket(PacketTypesOut.UseItem, packet); return true; } @@ -4604,12 +4664,12 @@ namespace MinecraftClient.Protocol.Handlers if (playerInventory?.Items is null) return false; - int[] slotWindowIds = [36, 37, 38, 39, 40, 41, 42, 43, 44]; + int[] slotWindowIds = [36, 37, 38, 39, 40, 41, 42, 43, 44]; var currentSlot = ((McClient)handler).GetCurrentSlot(); - + playerInventory.Items.TryGetValue(slotWindowIds[currentSlot], out var item); packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); - + packet.Add(0); // cursorX packet.Add(0); // cursorY packet.Add(0); // cursorZ @@ -4626,12 +4686,12 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); break; } - + packet.AddRange(dataTypes.GetFloat(cursorX)); // cursorX packet.AddRange(dataTypes.GetFloat(cursorY)); // cursorY packet.AddRange(dataTypes.GetFloat(cursorZ)); // cursorZ - - if(protocolVersion >= MC_1_14_Version) + + if (protocolVersion >= MC_1_14_Version) packet.Add(0); // insideBlock = false if (protocolVersion >= MC_1_21_2_Version) @@ -4639,7 +4699,7 @@ namespace MinecraftClient.Protocol.Handlers if (protocolVersion >= MC_1_19_Version) packet.AddRange(DataTypes.GetVarInt(sequenceId)); - + SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); return true; } @@ -5235,7 +5295,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + public bool SendCookieResponse(string name, byte[]? data) { try @@ -5244,7 +5304,7 @@ namespace MinecraftClient.Protocol.Handlers var hasPayload = data is not null; packet.AddRange(dataTypes.GetString(name)); // Identifier packet.AddRange(dataTypes.GetBool(hasPayload)); // Has payload - + if (hasPayload) packet.AddRange(dataTypes.GetArray(data!)); // Payload Data Array Size + Data Array @@ -5262,7 +5322,7 @@ namespace MinecraftClient.Protocol.Handlers SendPacket(PacketTypesOut.CookieResponse, packet); break; } - + McClient.Instance?.DeleteCookie(name); return true; } @@ -5293,17 +5353,17 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(dataTypes.GetString(dataPack.Item3)); } - switch(currentState) + switch (currentState) { - case CurrentState.Configuration: + case CurrentState.Configuration: SendPacket(ConfigurationPacketTypesOut.KnownDataPacks, packet); break; - + case CurrentState.Play: SendPacket(PacketTypesOut.KnownDataPacks, packet); break; } - + return true; } catch (SocketException) @@ -5319,7 +5379,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + private byte[] GenerateSalt() { var salt = new byte[8]; diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index c000385e..a8568f38 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Text; +using System.Text.RegularExpressions; using DnsClient; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Handlers.Forge; @@ -388,6 +389,61 @@ namespace MinecraftClient.Protocol } } + private static readonly Regex VersionTokenRegex = new(@"\d+\.\d+(?:\.\d+)?", RegexOptions.Compiled); + + private static readonly int[] SupportedProtocols18 = + [ + 4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404, + 477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756, + 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, + 772, 773, 774, 775 + ]; + + /// + /// For multi-version servers (e.g. "Requires MC 1.8 / 1.21"), try to find the + /// highest protocol version that both the server and MCC support. + /// Returns true if the protocol was upgraded, with the new value in + /// . + /// + public static bool TryUpgradeProtocolVersion(string versionName, ref int protocolVersion) + { + if (string.IsNullOrEmpty(versionName)) + return false; + + var matches = VersionTokenRegex.Matches(versionName); + if (matches.Count < 2) + return false; + + int bestProtocol = protocolVersion; + string bestVersion = ""; + + foreach (Match m in matches) + { + int proto = MCVer2ProtocolVersion(m.Value); + if (proto <= 0) + continue; + if (Array.IndexOf(SupportedProtocols18, proto) < 0) + continue; + if (proto > bestProtocol) + { + bestProtocol = proto; + bestVersion = m.Value; + } + } + + if (bestProtocol > protocolVersion && bestVersion.Length > 0) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format( + Translations.mcc_server_info_version_upgrade, + ProtocolVersion2MCVer(protocolVersion), protocolVersion, + "§a" + bestVersion + "§8", bestProtocol)); + protocolVersion = bestProtocol; + return true; + } + + return false; + } + /// /// Convert a network protocol version number to human-readable Minecraft version number /// diff --git a/MinecraftClient/Protocol/ServerStatusDisplay.cs b/MinecraftClient/Protocol/ServerStatusDisplay.cs new file mode 100644 index 00000000..21e386ab --- /dev/null +++ b/MinecraftClient/Protocol/ServerStatusDisplay.cs @@ -0,0 +1,118 @@ +using System; +using System.Text; +using MinecraftClient.Protocol.Message; + +namespace MinecraftClient.Protocol +{ + internal static class ServerStatusDisplay + { + private const int MaxSamplePlayers = 10; + + internal static void Show(ServerStatusInfo info) + { + if (ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBackend) + ShowTui(info, tuiBackend); + else + ShowClassic(info); + } + + private static void ShowClassic(ServerStatusInfo info) + { + var sb = new StringBuilder(); + + sb.AppendLine(); + sb.Append("§8§m"); + sb.Append(new string('-', 50)); + sb.AppendLine("§r"); + + if (!string.IsNullOrEmpty(info.MotdRaw)) + { + try + { + sb.AppendLine(ChatParser.ParseText(info.MotdRaw)); + } + catch + { + sb.AppendLine(info.MotdRaw); + } + } + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_server); + sb.Append(" §b"); + sb.Append(info.Host); + sb.Append("§7:§b"); + sb.AppendLine(info.Port.ToString()); + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_version); + sb.Append(" §b"); + sb.Append(info.VersionName); + sb.Append(" §7("); + sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7")); + sb.AppendLine(")"); + + if (info.ResolvedProtocol != 0 && info.ResolvedProtocol != info.ProtocolVersion) + { + string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_connecting_as); + sb.Append(" §a"); + sb.Append(resolvedMcVer); + sb.Append(" §7("); + sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§a" + info.ResolvedProtocol + "§7")); + sb.AppendLine(")"); + } + + if (info.PingMs >= 0) + { + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_ping); + sb.Append(" §a"); + sb.AppendLine(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs)); + } + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_players); + sb.Append(" §a"); + sb.Append(info.OnlinePlayers); + sb.Append("§7/§c"); + sb.AppendLine(info.MaxPlayers.ToString()); + + if (info.SamplePlayers.Count > 0) + { + sb.Append("§f"); + sb.AppendLine(Translations.mcc_server_info_label_online); + + int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers); + for (int i = 0; i < shown; i++) + sb.AppendLine($" §a{info.SamplePlayers[i].Name}"); + + if (info.SamplePlayers.Count > shown) + sb.AppendLine($" §7{string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}"); + } + + sb.Append("§8§m"); + sb.Append(new string('-', 50)); + sb.Append("§r"); + + ConsoleIO.WriteLineFormatted(sb.ToString(), acceptnewlines: true); + } + + private static void ShowTui(ServerStatusInfo info, Tui.TuiConsoleBackend backend) + { + var view = backend.GetView(); + if (view is null) + { + ShowClassic(info); + return; + } + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + var panel = Tui.ServerStatusPanelBuilder.Build(info); + view.AppendControlToLog(panel); + }); + } + } +} diff --git a/MinecraftClient/Protocol/ServerStatusInfo.cs b/MinecraftClient/Protocol/ServerStatusInfo.cs new file mode 100644 index 00000000..b864e64d --- /dev/null +++ b/MinecraftClient/Protocol/ServerStatusInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace MinecraftClient.Protocol +{ + /// + /// Holds the structured result of a Minecraft server status (SLP) ping, + /// including MOTD, player counts, sample player list, version, and favicon. + /// + public sealed class ServerStatusInfo + { + public string Host { get; init; } = string.Empty; + public int Port { get; init; } + public string VersionName { get; init; } = string.Empty; + public int ProtocolVersion { get; init; } + public int ResolvedProtocol { get; set; } + public int OnlinePlayers { get; init; } + public int MaxPlayers { get; init; } + public List SamplePlayers { get; init; } = []; + public string MotdRaw { get; init; } = string.Empty; + public string? FaviconBase64 { get; init; } + public long PingMs { get; init; } + + public sealed class SamplePlayer + { + public string Name { get; init; } = string.Empty; + public string Id { get; init; } = string.Empty; + } + } +} diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 071bee44..3fc6e722 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -2269,6 +2269,66 @@ namespace MinecraftClient { } } + internal static string mcc_server_info_label_server { + get { + return ResourceManager.GetString("mcc.server_info.label_server", resourceCulture); + } + } + + internal static string mcc_server_info_label_version { + get { + return ResourceManager.GetString("mcc.server_info.label_version", resourceCulture); + } + } + + internal static string mcc_server_info_label_protocol { + get { + return ResourceManager.GetString("mcc.server_info.label_protocol", resourceCulture); + } + } + + internal static string mcc_server_info_label_players { + get { + return ResourceManager.GetString("mcc.server_info.label_players", resourceCulture); + } + } + + internal static string mcc_server_info_label_ping { + get { + return ResourceManager.GetString("mcc.server_info.label_ping", resourceCulture); + } + } + + internal static string mcc_server_info_label_ping_ms { + get { + return ResourceManager.GetString("mcc.server_info.label_ping_ms", resourceCulture); + } + } + + internal static string mcc_server_info_label_connecting_as { + get { + return ResourceManager.GetString("mcc.server_info.label_connecting_as", resourceCulture); + } + } + + internal static string mcc_server_info_label_online { + get { + return ResourceManager.GetString("mcc.server_info.label_online", resourceCulture); + } + } + + internal static string mcc_server_info_sample_more { + get { + return ResourceManager.GetString("mcc.server_info.sample_more", resourceCulture); + } + } + + internal static string mcc_server_info_version_upgrade { + get { + return ResourceManager.GetString("mcc.server_info.version_upgrade", resourceCulture); + } + } + /// /// Looks up a localized string similar to Converting session cache from disk: {0}. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index ad782375..c3acc0fc 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -830,6 +830,36 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file TestBot + + Server: + + + Version: + + + Protocol: {0} + + + Players: + + + Ping: + + + {0} ms + + + Connecting as: + + + Online: + + + ... +{0} + + + Server reported protocol {0} ({1}), upgraded to {2} ({3}) for best compatibility + Converting session cache from disk: {0} @@ -2015,10 +2045,10 @@ MCC is running with default settings. Server is in offline mode. - Server version : {0} (protocol v{1}) + Server version: {0} (protocol v{1}) - Server version : + Server version: Checking Session... diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 34ff06db..1106f763 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -1171,5 +1171,18 @@ namespace MinecraftClient.Tui _commandInput.Focus(); }, DispatcherPriority.Loaded); } + + #region Custom Control Append + + public void AppendControlToLog(Control control) + { + _logLines.Add(string.Empty); + _logControls.Add(control); + TrimLog(); + if (_autoScroll) + ScheduleScrollToEnd(); + } + + #endregion } } diff --git a/MinecraftClient/Tui/McColorParser.cs b/MinecraftClient/Tui/McColorParser.cs index c46b0adf..30f8f280 100644 --- a/MinecraftClient/Tui/McColorParser.cs +++ b/MinecraftClient/Tui/McColorParser.cs @@ -52,6 +52,8 @@ namespace MinecraftClient.Tui IBrush currentColor = Brushes.White; bool bold = false; bool italic = false; + bool underline = false; + bool strikethrough = false; int start = 0; for (int i = 0; i < text.Length; i++) @@ -59,7 +61,7 @@ namespace MinecraftClient.Tui if (text[i] == '§' && i + 1 < text.Length) { if (i > start) - AddRun(tb, text[start..i], currentColor, bold, italic); + AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough); char code = char.ToLower(text[i + 1]); @@ -68,6 +70,8 @@ namespace MinecraftClient.Tui currentColor = brush; bold = false; italic = false; + underline = false; + strikethrough = false; } else { @@ -75,10 +79,14 @@ namespace MinecraftClient.Tui { case 'l': bold = true; break; case 'o': italic = true; break; + case 'n': underline = true; break; + case 'm': strikethrough = true; break; case 'r': currentColor = Brushes.White; bold = false; italic = false; + underline = false; + strikethrough = false; break; } } @@ -89,7 +97,7 @@ namespace MinecraftClient.Tui } if (start < text.Length) - AddRun(tb, text[start..], currentColor, bold, italic); + AddRun(tb, text[start..], currentColor, bold, italic, underline, strikethrough); if (tb.Inlines?.Count == 0) { @@ -100,16 +108,29 @@ namespace MinecraftClient.Tui return tb; } - private static void AddRun(TextBlock tb, string text, IBrush color, bool bold, bool italic) + private static void AddRun(TextBlock tb, string text, IBrush color, + bool bold, bool italic, bool underline, bool strikethrough) { if (text.Length == 0) return; tb.Inlines ??= new InlineCollection(); + + TextDecorationCollection? decorations = null; + if (underline || strikethrough) + { + decorations = []; + if (underline) + decorations.Add(new TextDecoration { Location = TextDecorationLocation.Underline }); + if (strikethrough) + decorations.Add(new TextDecoration { Location = TextDecorationLocation.Strikethrough }); + } + tb.Inlines.Add(new Run(text) { Foreground = color, FontWeight = bold ? FontWeight.Bold : FontWeight.Normal, FontStyle = italic ? FontStyle.Italic : FontStyle.Normal, + TextDecorations = decorations, }); } } diff --git a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs new file mode 100644 index 00000000..3b6e8f9f --- /dev/null +++ b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs @@ -0,0 +1,296 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Layout; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class ServerStatusPanelBuilder + { + private const int MaxSamplePlayers = 10; + private const int FaviconDisplaySize = 16; + + internal static Border Build(Protocol.ServerStatusInfo info) + { + var contentPanel = new DockPanel { Background = Brushes.Black }; + + if (info.FaviconBase64 is not null) + { + var iconGrid = BuildFaviconGrid(info.FaviconBase64, FaviconDisplaySize); + DockPanel.SetDock(iconGrid, Dock.Left); + contentPanel.Children.Add(iconGrid); + } + + var infoPanel = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(1, 0, 0, 0), + }; + + AddMotd(infoPanel, info); + AddAddress(infoPanel, info); + AddVersion(infoPanel, info); + AddConnectingAs(infoPanel, info); + AddPing(infoPanel, info); + AddPlayers(infoPanel, info); + AddSamplePlayers(infoPanel, info); + + contentPanel.Children.Add(infoPanel); + + return new Border + { + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)), + Padding = new Thickness(1, 0), + Child = contentPanel, + Margin = new Thickness(0, 1), + }; + } + + private static void AddMotd(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (string.IsNullOrEmpty(info.MotdRaw)) + return; + + try + { + string motdFormatted = Protocol.Message.ChatParser.ParseText(info.MotdRaw); + foreach (string line in motdFormatted.Split('\n')) + panel.Children.Add(McColorParser.CreateColoredTextBlock(line, TextWrapping.NoWrap)); + } + catch + { + panel.Children.Add(new TextBlock + { + Text = info.MotdRaw, + Foreground = Brushes.White, + TextWrapping = TextWrapping.NoWrap, + }); + } + } + + private static void AddAddress(StackPanel panel, Protocol.ServerStatusInfo info) + { + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_server)); + row.Inlines.Add(Value(info.Host, McColors.Aqua)); + row.Inlines.Add(new Run($":{info.Port}") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddVersion(StackPanel panel, Protocol.ServerStatusInfo info) + { + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_version)); + row.Inlines.Add(Value(info.VersionName, McColors.Aqua)); + row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ProtocolVersion)) + { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(")") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddConnectingAs(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.ResolvedProtocol == 0 || info.ResolvedProtocol == info.ProtocolVersion) + return; + + string resolvedMcVer = Protocol.ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_connecting_as)); + row.Inlines.Add(Value(resolvedMcVer, McColors.Green)); + row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ResolvedProtocol)) + { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(")") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddPing(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.PingMs < 0) + return; + + var pingColor = info.PingMs < 100 + ? McColors.Green + : info.PingMs < 300 + ? McColors.Yellow + : McColors.Red; + + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_ping)); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs)) + { Foreground = pingColor }); + panel.Children.Add(row); + } + + private static void AddPlayers(StackPanel panel, Protocol.ServerStatusInfo info) + { + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_players)); + row.Inlines.Add(Value($"{info.OnlinePlayers}", McColors.Green)); + row.Inlines.Add(new Run("/") { Foreground = McColors.Gray }); + row.Inlines.Add(Value($"{info.MaxPlayers}", McColors.Red)); + panel.Children.Add(row); + } + + private static void AddSamplePlayers(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.SamplePlayers.Count == 0) + return; + + panel.Children.Add(new TextBlock + { + Text = Translations.mcc_server_info_label_online, + Foreground = McColors.Gray, + Margin = new Thickness(0, 1, 0, 0), + }); + + int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers); + for (int i = 0; i < shown; i++) + { + panel.Children.Add(new TextBlock + { + Text = $" {info.SamplePlayers[i].Name}", + Foreground = McColors.Green, + }); + } + + if (info.SamplePlayers.Count > shown) + { + panel.Children.Add(new TextBlock + { + Text = $" {string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}", + Foreground = McColors.Gray, + }); + } + } + + private static Run Label(string text) => + new(text + " ") { Foreground = McColors.Gray }; + + private static Run Value(string text, IBrush color) => + new(text) { Foreground = color }; + + #region Favicon Rendering + + private static Grid BuildFaviconGrid(string base64Png, int displaySize) + { + byte[] pngBytes; + try + { + pngBytes = Convert.FromBase64String(base64Png); + } + catch + { + return new Grid(); + } + + int srcWidth, srcHeight; + byte[] rgba; + try + { + (srcWidth, srcHeight, rgba) = DecodePngToRgba(pngBytes); + } + catch + { + return new Grid(); + } + + int cellCols = displaySize; + int cellRows = displaySize / 2; + + var grid = new Grid(); + for (int c = 0; c < cellCols; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); + for (int r = 0; r < cellRows; r++) + grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); + + for (int row = 0; row < cellRows; row++) + { + for (int col = 0; col < cellCols; col++) + { + int topPixelY = row * 2; + int bottomPixelY = row * 2 + 1; + + var topColor = SamplePixel(rgba, srcWidth, srcHeight, col, topPixelY, cellCols, displaySize); + var bottomColor = SamplePixel(rgba, srcWidth, srcHeight, col, bottomPixelY, cellCols, displaySize); + + var cell = new TextBlock + { + Text = "\u2580", + Foreground = new SolidColorBrush(topColor), + Background = new SolidColorBrush(bottomColor), + Padding = new Thickness(0), + Margin = new Thickness(0), + }; + + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + grid.Children.Add(cell); + } + } + + return grid; + } + + private static Color SamplePixel(byte[] rgba, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH) + { + int srcX = dstX * srcW / dstW; + int srcY = dstY * srcH / dstH; + srcX = Math.Clamp(srcX, 0, srcW - 1); + srcY = Math.Clamp(srcY, 0, srcH - 1); + + int idx = (srcY * srcW + srcX) * 4; + if (idx + 3 >= rgba.Length) + return Color.FromRgb(0, 0, 0); + + byte r = rgba[idx]; + byte g = rgba[idx + 1]; + byte b = rgba[idx + 2]; + byte a = rgba[idx + 3]; + + return a < 128 ? Color.FromRgb(0, 0, 0) : Color.FromRgb(r, g, b); + } + + private static (int Width, int Height, byte[] Rgba) DecodePngToRgba(byte[] png) + { + using var image = new ImageMagick.MagickImage(png); + int w = (int)image.Width; + int h = (int)image.Height; + + using var pixels = image.GetPixelsUnsafe(); + var rgba = new byte[w * h * 4]; + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var pixel = pixels.GetPixel(x, y)!; + int idx = (y * w + x) * 4; + var color = pixel.ToColor()!; + rgba[idx] = (byte)(color.R >> 8); + rgba[idx + 1] = (byte)(color.G >> 8); + rgba[idx + 2] = (byte)(color.B >> 8); + rgba[idx + 3] = (byte)(color.A >> 8); + } + } + + return (w, h, rgba); + } + + #endregion + + private static class McColors + { + public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170)); + public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255)); + public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85)); + public static readonly IBrush Red = new SolidColorBrush(Color.FromRgb(255, 85, 85)); + public static readonly IBrush Yellow = new SolidColorBrush(Color.FromRgb(255, 255, 85)); + } + } +} From 3a28634592a4a0408cf0013f8718f74d6ad673c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:48:49 +0000 Subject: [PATCH 31/76] Initial plan From d5308ba8c650b8f68f5e21d07198e411c1642d6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:00:44 +0000 Subject: [PATCH 32/76] feat: add recipe book command support Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/00c8527f-5755-43c1-8916-8d571d28860b Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Commands/RecipeBook.cs | 92 +++++++++++++++ MinecraftClient/McClient.cs | 110 ++++++++++++++++++ .../Protocol/Handlers/Protocol16.cs | 5 + .../Protocol/Handlers/Protocol18.cs | 94 +++++++++++++++ MinecraftClient/Protocol/IMinecraftCom.cs | 9 ++ .../Protocol/IMinecraftComHandler.cs | 13 +++ .../Translations/Translations.Designer.cs | 63 ++++++++++ .../Resources/Translations/Translations.resx | 21 ++++ 8 files changed, 407 insertions(+) create mode 100644 MinecraftClient/Commands/RecipeBook.cs diff --git a/MinecraftClient/Commands/RecipeBook.cs b/MinecraftClient/Commands/RecipeBook.cs new file mode 100644 index 00000000..a66b2004 --- /dev/null +++ b/MinecraftClient/Commands/RecipeBook.cs @@ -0,0 +1,92 @@ +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class RecipeBook : Command + { + public override string CmdName => "recipebook"; + public override string CmdUsage => "recipebook [recipe id]"; + public override string CmdDesc => Translations.cmd_recipebook_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + .Then(l => l.Literal("list") + .Executes(r => GetUsage(r.Source, "list"))) + .Then(l => l.Literal("craft") + .Executes(r => GetUsage(r.Source, "craft"))) + .Then(l => l.Literal("craftall") + .Executes(r => GetUsage(r.Source, "craftall"))) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Then(l => l.Literal("list") + .Executes(r => ListRecipes(r.Source))) + .Then(l => l.Literal("craft") + .Then(l => l.Argument("RecipeId", Arguments.String()) + .Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: false)))) + .Then(l => l.Literal("craftall") + .Then(l => l.Argument("RecipeId", Arguments.String()) + .Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: true)))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + "list" => GetCmdDescTranslated(), + "craft" => GetCmdDescTranslated(), + "craftall" => GetCmdDescTranslated(), + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private int ListRecipes(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetInventoryEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + + string[] recipeIds = handler.GetUnlockedRecipes(); + if (recipeIds.Length == 0) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_recipes); + + StringBuilder response = new(); + response.AppendLine(Translations.cmd_recipebook_list); + foreach (string recipeId in recipeIds) + response.AppendLine("- " + recipeId); + + handler.Log.Info(response.ToString().TrimEnd()); + return r.SetAndReturn(CmdResult.Status.Done); + } + + private int CraftRecipe(CmdResult r, string recipeId, bool makeAll) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetInventoryEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + + if (handler.GetProtocolVersion() < Protocol.Handlers.Protocol18Handler.MC_1_13_Version) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_unsupported); + + if (handler.GetActiveRecipeBookInventory() is null) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory); + + return handler.SendPlaceRecipe(recipeId, makeAll) + ? r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_recipebook_craft_sent, recipeId, makeAll)) + : r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, recipeId)); + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 938a85bf..3e12d920 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -44,10 +44,12 @@ namespace MinecraftClient private readonly Queue threadTasks = new(); private readonly Lock threadTasksLock = new(); + private readonly Lock recipeBookLock = new(); private readonly List bots = new(); private static readonly List botsOnHold = new(); private static readonly Dictionary inventories = new(); + private readonly HashSet unlockedRecipes = new(StringComparer.Ordinal); private readonly Dictionary> registeredBotPluginChannels = new(); private readonly List registeredServerPluginChannels = new(); @@ -1237,6 +1239,7 @@ namespace MinecraftClient inventoryHandlingEnabled = false; inventoryHandlingRequested = false; inventories.Clear(); + ClearUnlockedRecipes(); } return true; } @@ -1338,6 +1341,18 @@ namespace MinecraftClient return lastEnchantment; } + /// + /// Get all unlocked recipe book recipe identifiers. + /// + /// Unlocked recipe identifiers sorted alphabetically + public string[] GetUnlockedRecipes() + { + lock (recipeBookLock) + { + return [.. unlockedRecipes.OrderBy(static recipeId => recipeId, StringComparer.Ordinal)]; + } + } + /// /// Get all Entities /// @@ -1384,6 +1399,22 @@ namespace MinecraftClient return GetInventory(0)!; } + /// + /// Get the currently active inventory if it supports recipe book crafting. + /// + /// Active recipe book inventory, or null if the active inventory does not support recipe book crafting + public Container? GetActiveRecipeBookInventory() + { + if (InvokeRequired) + return InvokeOnMainThread(() => GetActiveRecipeBookInventory()); + + if (inventories.Count == 0) + return null; + + Container activeInventory = inventories.Values.Last(); + return SupportsRecipeBook(activeInventory.Type) ? activeInventory : null; + } + /// /// Get a set of online player names /// @@ -2476,6 +2507,7 @@ namespace MinecraftClient inventories.Clear(); inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); + ClearUnlockedRecipes(); return true; } @@ -2677,6 +2709,27 @@ namespace MinecraftClient return handler.SendRenameItem(itemName); } + + /// + /// Send a recipe book craft request for the currently active crafting inventory. + /// + /// Recipe identifier to craft + /// True to craft as many items as possible + /// True if the packet was sent + public bool SendPlaceRecipe(string recipeId, bool makeAll) + { + if (InvokeRequired) + return InvokeOnMainThread(() => SendPlaceRecipe(recipeId, makeAll)); + + if (protocolversion < Protocol18Handler.MC_1_13_Version) + return false; + + Container? activeInventory = GetActiveRecipeBookInventory(); + if (activeInventory is null) + return false; + + return handler.SendPlaceRecipe(activeInventory.ID, NormalizeRecipeId(recipeId), makeAll); + } #endregion #region Event handlers: An event occurs on the Server @@ -4054,6 +4107,33 @@ namespace MinecraftClient Log.Debug("CanSendMessage = " + canSendMessage); } + public void OnRecipeBookAdd(string[] recipeIds, bool replace) + { + lock (recipeBookLock) + { + if (replace) + unlockedRecipes.Clear(); + + foreach (string recipeId in recipeIds) + { + if (!string.IsNullOrWhiteSpace(recipeId)) + unlockedRecipes.Add(recipeId); + } + } + } + + public void OnRecipeBookRemove(string[] recipeIds) + { + lock (recipeBookLock) + { + foreach (string recipeId in recipeIds) + { + if (!string.IsNullOrWhiteSpace(recipeId)) + unlockedRecipes.Remove(recipeId); + } + } + } + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom @@ -4067,6 +4147,36 @@ namespace MinecraftClient return handler.ClickContainerButton(windowId, buttonId); } + private static bool SupportsRecipeBook(ContainerType containerType) + { + return containerType switch + { + ContainerType.PlayerInventory or + ContainerType.Crafting or + ContainerType.Furnace or + ContainerType.BlastFurnace or + ContainerType.Smoker or + ContainerType.Stonecutter => true, + _ => false, + }; + } + + private void ClearUnlockedRecipes() + { + lock (recipeBookLock) + { + unlockedRecipes.Clear(); + } + } + + private static string NormalizeRecipeId(string recipeId) + { + string trimmedRecipeId = recipeId.Trim(); + return trimmedRecipeId.Contains(':', StringComparison.Ordinal) + ? trimmedRecipeId + : "minecraft:" + trimmedRecipeId; + } + #endregion } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 15d20b71..6777200d 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -811,6 +811,11 @@ namespace MinecraftClient.Protocol.Handlers return false; //Currently not implemented } + public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll) + { + return false; //MC 1.8-1.12.1 recipe book not supported + } + public bool SendCloseWindow(int windowId) { return false; //Currently not implemented diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 5329cdfb..317090a9 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3116,8 +3116,17 @@ namespace MinecraftClient.Protocol.Handlers } break; + case PacketTypesIn.UnlockRecipes: + if (protocolVersion >= MC_1_13_Version) + HandleUnlockRecipes(packetData); + break; + case PacketTypesIn.RecipeBookAdd: + HandleRecipeBookAdd(packetData); + break; case PacketTypesIn.RecipeBookRemove: + handler.OnRecipeBookRemove(ReadRecipeBookRecipeIds(packetData)); + break; case PacketTypesIn.RecipeBookSettings: break; @@ -3128,6 +3137,63 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + private void HandleUnlockRecipes(Queue packetData) + { + int action = dataTypes.ReadNextVarInt(packetData); + SkipRecipeBookSettings(packetData); + + string[] recipeIds = ReadRecipeBookRecipeIds(packetData); + + switch (action) + { + case 0: + handler.OnRecipeBookAdd(recipeIds, replace: true); + _ = ReadRecipeBookRecipeIds(packetData); + break; + case 1: + case 3: + handler.OnRecipeBookAdd(recipeIds, replace: false); + break; + case 2: + handler.OnRecipeBookRemove(recipeIds); + break; + } + } + + private void HandleRecipeBookAdd(Queue packetData) + { + int entryCount = dataTypes.ReadNextVarInt(packetData); + string[] recipeIds = new string[entryCount]; + + for (int i = 0; i < entryCount; i++) + { + recipeIds[i] = dataTypes.ReadNextString(packetData); + _ = dataTypes.ReadNextBool(packetData); // notification + _ = dataTypes.ReadNextBool(packetData); // highlight + } + + bool replace = dataTypes.ReadNextBool(packetData); + handler.OnRecipeBookAdd(recipeIds, replace); + } + + private string[] ReadRecipeBookRecipeIds(Queue packetData) + { + int recipeCount = dataTypes.ReadNextVarInt(packetData); + string[] recipeIds = new string[recipeCount]; + + for (int i = 0; i < recipeCount; i++) + recipeIds[i] = dataTypes.ReadNextString(packetData); + + return recipeIds; + } + + private void SkipRecipeBookSettings(Queue packetData) + { + int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4; + for (int i = 0; i < boolCount; i++) + _ = dataTypes.ReadNextBool(packetData); + } + /// /// Start the updating thread. Should be called after login success. /// @@ -5018,6 +5084,34 @@ namespace MinecraftClient.Protocol.Handlers } } + public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll) + { + try + { + List packet = new(); + if (protocolVersion < MC_1_13_Version) + return false; + + packet.AddRange(DataTypes.GetVarInt(windowId)); + packet.AddRange(dataTypes.GetString(recipeId)); + packet.AddRange(dataTypes.GetBool(makeAll)); + SendPacket(PacketTypesOut.CraftRecipeRequest, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + public bool SendAnimation(int animation, int playerId) { try diff --git a/MinecraftClient/Protocol/IMinecraftCom.cs b/MinecraftClient/Protocol/IMinecraftCom.cs index 6c7dd596..96b261b1 100644 --- a/MinecraftClient/Protocol/IMinecraftCom.cs +++ b/MinecraftClient/Protocol/IMinecraftCom.cs @@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol bool ClickContainerButton(int windowId, int buttonId); + /// + /// Send a place recipe packet to the server for the active recipe book container. + /// + /// Id of the window being clicked + /// Recipe identifier to craft + /// True to craft as many items as possible + /// True if packet was successfully sent + bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll); + /// /// Plays animation /// diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 94fe0590..d6bd5eb7 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -517,6 +517,19 @@ namespace MinecraftClient.Protocol public void SetCanSendMessage(bool canSendMessage); + /// + /// Called when recipe book recipes are added or replaced. + /// + /// Recipe identifiers to add + /// True to replace the currently tracked recipe book entries + public void OnRecipeBookAdd(string[] recipeIds, bool replace); + + /// + /// Called when recipe book recipes are removed. + /// + /// Recipe identifiers to remove + public void OnRecipeBookRemove(string[] recipeIds); + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 3fc6e722..b6bea198 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -4359,6 +4359,69 @@ namespace MinecraftClient { return ResourceManager.GetString("cmd.nameitem.successful", resourceCulture); } } + + /// + /// Looks up a localized string similar to Failed to send recipe book craft request for {0}.. + /// + internal static string cmd_recipebook_craft_failed { + get { + return ResourceManager.GetString("cmd.recipebook.craft.failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requested recipe {0} (craft all: {1}).. + /// + internal static string cmd_recipebook_craft_sent { + get { + return ResourceManager.GetString("cmd.recipebook.craft.sent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory.. + /// + internal static string cmd_recipebook_desc { + get { + return ResourceManager.GetString("cmd.recipebook.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlocked recipe book recipes. + /// + internal static string cmd_recipebook_list { + get { + return ResourceManager.GetString("cmd.recipebook.list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.. + /// + internal static string cmd_recipebook_no_active_inventory { + get { + return ResourceManager.GetString("cmd.recipebook.no.active.inventory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No unlocked recipe book recipes are currently tracked.. + /// + internal static string cmd_recipebook_no_recipes { + get { + return ResourceManager.GetString("cmd.recipebook.no.recipes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer.. + /// + internal static string cmd_recipebook_unsupported { + get { + return ResourceManager.GetString("cmd.recipebook.unsupported", resourceCulture); + } + } /// /// Looks up a localized string similar to restart and reconnect to the server.. diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index c3acc0fc..42a9d8db 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2208,6 +2208,27 @@ Logging in... Set an item name when an Anvil inventory is active and the item is in the first slot. + + Failed to send recipe book craft request for {0}. + + + Requested recipe {0} (craft all: {1}). + + + List unlocked recipe book recipes and craft them through the active recipe book inventory. + + + Unlocked recipe book recipes + + + You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory. + + + No unlocked recipe book recipes are currently tracked. + + + Recipe book crafting is only supported on Minecraft 1.13 and newer. + Bot movement lock is held by bot {0}, so the Anti AFK bot might not move! From 893be203e5b528774eae39d890d269ff59837618 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:07:09 +0000 Subject: [PATCH 33/76] chore: polish recipe book support Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/00c8527f-5755-43c1-8916-8d571d28860b Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Commands/RecipeBook.cs | 18 ++++++++++++-- MinecraftClient/McClient.cs | 11 +++++++-- .../Protocol/Handlers/Protocol18.cs | 24 +++++++++++++++---- .../Translations/Translations.Designer.cs | 20 +++++++++++++++- .../Resources/Translations/Translations.resx | 8 ++++++- 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/MinecraftClient/Commands/RecipeBook.cs b/MinecraftClient/Commands/RecipeBook.cs index a66b2004..b51d211b 100644 --- a/MinecraftClient/Commands/RecipeBook.cs +++ b/MinecraftClient/Commands/RecipeBook.cs @@ -78,15 +78,29 @@ namespace MinecraftClient.Commands if (!handler.GetInventoryEnabled()) return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + if (string.IsNullOrWhiteSpace(recipeId)) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_recipe_id_empty); + if (handler.GetProtocolVersion() < Protocol.Handlers.Protocol18Handler.MC_1_13_Version) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_unsupported); if (handler.GetActiveRecipeBookInventory() is null) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory); + string normalizedRecipeId = NormalizeRecipeId(recipeId); + string successMessage = string.Format(makeAll ? Translations.cmd_recipebook_craftall_sent : Translations.cmd_recipebook_craft_sent, normalizedRecipeId); + return handler.SendPlaceRecipe(recipeId, makeAll) - ? r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_recipebook_craft_sent, recipeId, makeAll)) - : r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, recipeId)); + ? r.SetAndReturn(CmdResult.Status.Done, successMessage) + : r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, normalizedRecipeId)); + } + + private static string NormalizeRecipeId(string recipeId) + { + string trimmedRecipeId = recipeId.Trim(); + return trimmedRecipeId.Contains(':') + ? trimmedRecipeId + : "minecraft:" + trimmedRecipeId; } } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 3e12d920..751b79e6 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1411,7 +1411,7 @@ namespace MinecraftClient if (inventories.Count == 0) return null; - Container activeInventory = inventories.Values.Last(); + Container activeInventory = inventories.MaxBy(static pair => pair.Key).Value; return SupportsRecipeBook(activeInventory.Type) ? activeInventory : null; } @@ -2728,7 +2728,11 @@ namespace MinecraftClient if (activeInventory is null) return false; - return handler.SendPlaceRecipe(activeInventory.ID, NormalizeRecipeId(recipeId), makeAll); + string normalizedRecipeId = NormalizeRecipeId(recipeId); + if (normalizedRecipeId.Length == 0) + return false; + + return handler.SendPlaceRecipe(activeInventory.ID, normalizedRecipeId, makeAll); } #endregion @@ -4172,6 +4176,9 @@ namespace MinecraftClient private static string NormalizeRecipeId(string recipeId) { string trimmedRecipeId = recipeId.Trim(); + if (trimmedRecipeId.Length == 0) + return string.Empty; + return trimmedRecipeId.Contains(':', StringComparison.Ordinal) ? trimmedRecipeId : "minecraft:" + trimmedRecipeId; diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 317090a9..c5636cd1 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3122,10 +3122,12 @@ namespace MinecraftClient.Protocol.Handlers break; case PacketTypesIn.RecipeBookAdd: - HandleRecipeBookAdd(packetData); + if (protocolVersion >= MC_1_21_2_Version) + HandleRecipeBookAdd(packetData); break; case PacketTypesIn.RecipeBookRemove: - handler.OnRecipeBookRemove(ReadRecipeBookRecipeIds(packetData)); + if (protocolVersion >= MC_1_21_2_Version) + handler.OnRecipeBookRemove(ReadRecipeBookRecipeIds(packetData)); break; case PacketTypesIn.RecipeBookSettings: break; @@ -3140,7 +3142,8 @@ namespace MinecraftClient.Protocol.Handlers private void HandleUnlockRecipes(Queue packetData) { int action = dataTypes.ReadNextVarInt(packetData); - SkipRecipeBookSettings(packetData); + if (!SkipRecipeBookSettings(packetData)) + return; string[] recipeIds = ReadRecipeBookRecipeIds(packetData); @@ -3148,10 +3151,15 @@ namespace MinecraftClient.Protocol.Handlers { case 0: handler.OnRecipeBookAdd(recipeIds, replace: true); + // INIT packets also include a second "to be displayed" recipe list. + // MCC only needs the unlocked recipe identifiers for listing/crafting. _ = ReadRecipeBookRecipeIds(packetData); break; case 1: + handler.OnRecipeBookAdd(recipeIds, replace: false); + break; case 3: + // Action 3 is the silent-add variant, so MCC tracks it like a regular add. handler.OnRecipeBookAdd(recipeIds, replace: false); break; case 2: @@ -3165,6 +3173,9 @@ namespace MinecraftClient.Protocol.Handlers int entryCount = dataTypes.ReadNextVarInt(packetData); string[] recipeIds = new string[entryCount]; + // RecipeBookAdd contains one entry per recipe: + // recipe id, notification flag, then highlight flag. + // MCC only tracks the unlocked recipe identifiers for now. for (int i = 0; i < entryCount; i++) { recipeIds[i] = dataTypes.ReadNextString(packetData); @@ -3187,11 +3198,16 @@ namespace MinecraftClient.Protocol.Handlers return recipeIds; } - private void SkipRecipeBookSettings(Queue packetData) + private bool SkipRecipeBookSettings(Queue packetData) { int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4; + if (packetData.Count < boolCount) + return false; + for (int i = 0; i < boolCount; i++) _ = dataTypes.ReadNextBool(packetData); + + return true; } /// diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index b6bea198..022e9cad 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -4370,7 +4370,7 @@ namespace MinecraftClient { } /// - /// Looks up a localized string similar to Requested recipe {0} (craft all: {1}).. + /// Looks up a localized string similar to Requested recipe {0}.. /// internal static string cmd_recipebook_craft_sent { get { @@ -4378,6 +4378,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Requested recipe {0} with craft-all.. + /// + internal static string cmd_recipebook_craftall_sent { + get { + return ResourceManager.GetString("cmd.recipebook.craftall.sent", resourceCulture); + } + } + /// /// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory.. /// @@ -4414,6 +4423,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to The recipe identifier cannot be empty.. + /// + internal static string cmd_recipebook_recipe_id_empty { + get { + return ResourceManager.GetString("cmd.recipebook.recipe.id.empty", resourceCulture); + } + } + /// /// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer.. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 42a9d8db..c3ebe466 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2212,7 +2212,10 @@ Logging in... Failed to send recipe book craft request for {0}. - Requested recipe {0} (craft all: {1}). + Requested recipe {0}. + + + Requested recipe {0} with craft-all. List unlocked recipe book recipes and craft them through the active recipe book inventory. @@ -2226,6 +2229,9 @@ Logging in... No unlocked recipe book recipes are currently tracked. + + The recipe identifier cannot be empty. + Recipe book crafting is only supported on Minecraft 1.13 and newer. From dfc11648399fb079ac8f6e8fb8725be46a5895e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:12:01 +0000 Subject: [PATCH 34/76] chore: finalize recipe book support polish Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/00c8527f-5755-43c1-8916-8d571d28860b Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Commands/RecipeBook.cs | 10 +--------- MinecraftClient/McClient.cs | 2 +- MinecraftClient/Protocol/Handlers/Protocol18.cs | 2 ++ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/MinecraftClient/Commands/RecipeBook.cs b/MinecraftClient/Commands/RecipeBook.cs index b51d211b..24a56a86 100644 --- a/MinecraftClient/Commands/RecipeBook.cs +++ b/MinecraftClient/Commands/RecipeBook.cs @@ -87,20 +87,12 @@ namespace MinecraftClient.Commands if (handler.GetActiveRecipeBookInventory() is null) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory); - string normalizedRecipeId = NormalizeRecipeId(recipeId); + string normalizedRecipeId = McClient.NormalizeRecipeId(recipeId); string successMessage = string.Format(makeAll ? Translations.cmd_recipebook_craftall_sent : Translations.cmd_recipebook_craft_sent, normalizedRecipeId); return handler.SendPlaceRecipe(recipeId, makeAll) ? r.SetAndReturn(CmdResult.Status.Done, successMessage) : r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, normalizedRecipeId)); } - - private static string NormalizeRecipeId(string recipeId) - { - string trimmedRecipeId = recipeId.Trim(); - return trimmedRecipeId.Contains(':') - ? trimmedRecipeId - : "minecraft:" + trimmedRecipeId; - } } } diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 751b79e6..906eff74 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -4173,7 +4173,7 @@ namespace MinecraftClient } } - private static string NormalizeRecipeId(string recipeId) + internal static string NormalizeRecipeId(string recipeId) { string trimmedRecipeId = recipeId.Trim(); if (trimmedRecipeId.Length == 0) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index c5636cd1..e6518c7b 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3200,6 +3200,8 @@ namespace MinecraftClient.Protocol.Handlers private bool SkipRecipeBookSettings(Queue packetData) { + // MC 1.13 uses 4 booleans for the crafting/smelting recipe book states. + // MC 1.14+ expands this to 8 booleans by adding blast furnace and smoker states. int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4; if (packetData.Count < boolCount) return false; From b25579a105bd86c112027b4c2536500e7688ee5a Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 30 Mar 2026 02:24:50 +0800 Subject: [PATCH 35/76] Fix CI script injection via commit message special characters Pass commit message through env vars instead of direct ${{ }} expansion in shell scripts to prevent backticks and other special characters from being interpreted as shell commands. Made-with: Cursor --- .github/workflows/build-and-release.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 359bd388..8649102c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -21,13 +21,14 @@ jobs: - name: Check skip CI id: check-skip run: | - MSG="${{ github.event.head_commit.message }}" - LOWER=$(echo "$MSG" | tr '[:upper:]' '[:lower:]') + LOWER=$(echo "$COMMIT_MSG" | tr '[:upper:]' '[:lower:]') if echo "$LOWER" | grep -qE 'skip.?ci|ci.?skip'; then echo "skip=true" >> $GITHUB_OUTPUT else echo "skip=false" >> $GITHUB_OUTPUT - fi + fi + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} fetch-translations: strategy: @@ -221,12 +222,13 @@ jobs: - name: Truncate commit message for release name id: release-name run: | - RAW="${{ github.event.head_commit.message }}" - # Take only the first line (subject), then truncate to safe length - SUBJECT=$(echo "$RAW" | head -n 1) - MAX=220 # leave room for tag prefix + ": " + SUBJECT=$(echo "$COMMIT_MSG" | head -n 1) + MAX=220 TRUNCATED="${SUBJECT:0:$MAX}" - echo "name=${{ needs.create-tag.outputs.build-tag }}: $TRUNCATED" >> $GITHUB_OUTPUT + echo "name=${BUILD_TAG}: $TRUNCATED" >> $GITHUB_OUTPUT + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + BUILD_TAG: ${{ needs.create-tag.outputs.build-tag }} - name: Create Release uses: ncipollo/release-action@v1.14.0 From b05c8cfe0d3eea2af2b440d784eab57279512168 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:45:14 +0000 Subject: [PATCH 36/76] fix: support 1.21.11 recipe book display ids Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/4cdf26f2-112b-4502-88f7-8f589c424f69 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Commands/RecipeBook.cs | 10 +- MinecraftClient/McClient.cs | 31 ++- .../Protocol/Handlers/Protocol18.cs | 190 ++++++++++++++++-- .../Protocol/IMinecraftComHandler.cs | 4 +- MinecraftClient/RecipeBookRecipeEntry.cs | 4 + 5 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 MinecraftClient/RecipeBookRecipeEntry.cs diff --git a/MinecraftClient/Commands/RecipeBook.cs b/MinecraftClient/Commands/RecipeBook.cs index 24a56a86..4cf0d873 100644 --- a/MinecraftClient/Commands/RecipeBook.cs +++ b/MinecraftClient/Commands/RecipeBook.cs @@ -59,14 +59,14 @@ namespace MinecraftClient.Commands if (!handler.GetInventoryEnabled()) return r.SetAndReturn(CmdResult.Status.FailNeedInventory); - string[] recipeIds = handler.GetUnlockedRecipes(); - if (recipeIds.Length == 0) + RecipeBookRecipeEntry[] recipes = handler.GetUnlockedRecipes(); + if (recipes.Length == 0) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_recipes); StringBuilder response = new(); response.AppendLine(Translations.cmd_recipebook_list); - foreach (string recipeId in recipeIds) - response.AppendLine("- " + recipeId); + foreach (RecipeBookRecipeEntry recipe in recipes) + response.AppendLine("- " + recipe.DisplayText); handler.Log.Info(response.ToString().TrimEnd()); return r.SetAndReturn(CmdResult.Status.Done); @@ -87,7 +87,7 @@ namespace MinecraftClient.Commands if (handler.GetActiveRecipeBookInventory() is null) return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory); - string normalizedRecipeId = McClient.NormalizeRecipeId(recipeId); + string normalizedRecipeId = McClient.NormalizeRecipeArgument(recipeId, handler.GetProtocolVersion()); string successMessage = string.Format(makeAll ? Translations.cmd_recipebook_craftall_sent : Translations.cmd_recipebook_craft_sent, normalizedRecipeId); return handler.SendPlaceRecipe(recipeId, makeAll) diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 906eff74..24069342 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -49,7 +49,7 @@ namespace MinecraftClient private readonly List bots = new(); private static readonly List botsOnHold = new(); private static readonly Dictionary inventories = new(); - private readonly HashSet unlockedRecipes = new(StringComparer.Ordinal); + private readonly Dictionary unlockedRecipes = new(StringComparer.Ordinal); private readonly Dictionary> registeredBotPluginChannels = new(); private readonly List registeredServerPluginChannels = new(); @@ -1345,11 +1345,11 @@ namespace MinecraftClient /// Get all unlocked recipe book recipe identifiers. /// /// Unlocked recipe identifiers sorted alphabetically - public string[] GetUnlockedRecipes() + public RecipeBookRecipeEntry[] GetUnlockedRecipes() { lock (recipeBookLock) { - return [.. unlockedRecipes.OrderBy(static recipeId => recipeId, StringComparer.Ordinal)]; + return unlockedRecipes.Values.OrderBy(static recipe => recipe.CommandId, StringComparer.Ordinal).ToArray(); } } @@ -2728,7 +2728,7 @@ namespace MinecraftClient if (activeInventory is null) return false; - string normalizedRecipeId = NormalizeRecipeId(recipeId); + string normalizedRecipeId = NormalizeRecipeArgument(recipeId, protocolversion); if (normalizedRecipeId.Length == 0) return false; @@ -4111,17 +4111,18 @@ namespace MinecraftClient Log.Debug("CanSendMessage = " + canSendMessage); } - public void OnRecipeBookAdd(string[] recipeIds, bool replace) + public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace) { lock (recipeBookLock) { if (replace) unlockedRecipes.Clear(); - foreach (string recipeId in recipeIds) + foreach (RecipeBookRecipeEntry recipe in recipes) { - if (!string.IsNullOrWhiteSpace(recipeId)) - unlockedRecipes.Add(recipeId); + // Guard against malformed server packets that send empty display IDs. + if (!string.IsNullOrWhiteSpace(recipe.CommandId)) + unlockedRecipes[recipe.CommandId] = recipe; } } } @@ -4173,7 +4174,19 @@ namespace MinecraftClient } } - internal static string NormalizeRecipeId(string recipeId) + /// + /// Normalize a recipe argument for the target protocol version. + /// Legacy recipe-book packets use identifiers and default to the minecraft namespace. + /// 1.21.2+ recipe-book packets use numeric recipe display ids and should be left trimmed-only. + /// + internal static string NormalizeRecipeArgument(string recipeId, int protocolVersion) + { + return protocolVersion >= Protocol18Handler.MC_1_21_2_Version + ? recipeId.Trim() + : NormalizeRecipeId(recipeId); + } + + private static string NormalizeRecipeId(string recipeId) { string trimmedRecipeId = recipeId.Trim(); if (trimmedRecipeId.Length == 0) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index e6518c7b..bc9537cd 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3127,7 +3127,7 @@ namespace MinecraftClient.Protocol.Handlers break; case PacketTypesIn.RecipeBookRemove: if (protocolVersion >= MC_1_21_2_Version) - handler.OnRecipeBookRemove(ReadRecipeBookRecipeIds(packetData)); + handler.OnRecipeBookRemove(ReadRecipeBookDisplayIds(packetData)); break; case PacketTypesIn.RecipeBookSettings: break; @@ -3146,21 +3146,20 @@ namespace MinecraftClient.Protocol.Handlers return; string[] recipeIds = ReadRecipeBookRecipeIds(packetData); + RecipeBookRecipeEntry[] recipeEntries = recipeIds.Select(static recipeId => new RecipeBookRecipeEntry(recipeId, recipeId)).ToArray(); switch (action) { case 0: - handler.OnRecipeBookAdd(recipeIds, replace: true); + handler.OnRecipeBookAdd(recipeEntries, replace: true); // INIT packets also include a second "to be displayed" recipe list. // MCC only needs the unlocked recipe identifiers for listing/crafting. _ = ReadRecipeBookRecipeIds(packetData); break; case 1: - handler.OnRecipeBookAdd(recipeIds, replace: false); - break; case 3: // Action 3 is the silent-add variant, so MCC tracks it like a regular add. - handler.OnRecipeBookAdd(recipeIds, replace: false); + handler.OnRecipeBookAdd(recipeEntries, replace: false); break; case 2: handler.OnRecipeBookRemove(recipeIds); @@ -3171,20 +3170,18 @@ namespace MinecraftClient.Protocol.Handlers private void HandleRecipeBookAdd(Queue packetData) { int entryCount = dataTypes.ReadNextVarInt(packetData); - string[] recipeIds = new string[entryCount]; + RecipeBookRecipeEntry[] recipeEntries = new RecipeBookRecipeEntry[entryCount]; - // RecipeBookAdd contains one entry per recipe: - // recipe id, notification flag, then highlight flag. - // MCC only tracks the unlocked recipe identifiers for now. + // 1.21.2+ RecipeBookAdd contains one display entry per recipe: + // RecipeDisplayEntry (display id, recipe display, group, category, optional requirements), then flags. for (int i = 0; i < entryCount; i++) { - recipeIds[i] = dataTypes.ReadNextString(packetData); - _ = dataTypes.ReadNextBool(packetData); // notification - _ = dataTypes.ReadNextBool(packetData); // highlight + recipeEntries[i] = ReadRecipeBookDisplayEntry(packetData); + _ = dataTypes.ReadNextByte(packetData); // flags } bool replace = dataTypes.ReadNextBool(packetData); - handler.OnRecipeBookAdd(recipeIds, replace); + handler.OnRecipeBookAdd(recipeEntries, replace); } private string[] ReadRecipeBookRecipeIds(Queue packetData) @@ -3198,6 +3195,168 @@ namespace MinecraftClient.Protocol.Handlers return recipeIds; } + private string[] ReadRecipeBookDisplayIds(Queue packetData) + { + int recipeCount = dataTypes.ReadNextVarInt(packetData); + string[] recipeIds = new string[recipeCount]; + + for (int i = 0; i < recipeCount; i++) + recipeIds[i] = dataTypes.ReadNextVarInt(packetData).ToString(CultureInfo.InvariantCulture); + + return recipeIds; + } + + private RecipeBookRecipeEntry ReadRecipeBookDisplayEntry(Queue packetData) + { + int displayId = dataTypes.ReadNextVarInt(packetData); + string resultLabel = ReadRecipeDisplayResultLabel(packetData); + + _ = dataTypes.ReadNextVarInt(packetData); // Optional group, encoded as varint+1 or 0 + _ = dataTypes.ReadNextVarInt(packetData); // Recipe book category registry id + SkipOptionalCraftingRequirements(packetData); + + string commandId = displayId.ToString(CultureInfo.InvariantCulture); + string displayText = $"{commandId}: {resultLabel}"; + return new RecipeBookRecipeEntry(commandId, displayText); + } + + private string ReadRecipeDisplayResultLabel(Queue packetData) + { + int displayType = dataTypes.ReadNextVarInt(packetData); + return displayType switch + { + 0 => ReadShapelessRecipeDisplayResultLabel(packetData), + 1 => ReadShapedRecipeDisplayResultLabel(packetData), + 2 => ReadFurnaceRecipeDisplayResultLabel(packetData), + 3 => ReadStonecutterRecipeDisplayResultLabel(packetData), + 4 => ReadSmithingRecipeDisplayResultLabel(packetData), + _ => $"recipe_display_{displayType}", + }; + } + + private string ReadShapelessRecipeDisplayResultLabel(Queue packetData) + { + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + _ = ReadSlotDisplayLabel(packetData); + + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadShapedRecipeDisplayResultLabel(Queue packetData) + { + _ = dataTypes.ReadNextVarInt(packetData); // width + _ = dataTypes.ReadNextVarInt(packetData); // height + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + _ = ReadSlotDisplayLabel(packetData); + + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadFurnaceRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // ingredient + _ = ReadSlotDisplayLabel(packetData); // fuel + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + _ = dataTypes.ReadNextVarInt(packetData); // duration + _ = dataTypes.ReadNextFloat(packetData); // experience + return result; + } + + private string ReadStonecutterRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // input + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadSmithingRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // template + _ = ReadSlotDisplayLabel(packetData); // base + _ = ReadSlotDisplayLabel(packetData); // addition + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadSlotDisplayLabel(Queue packetData) + { + int slotDisplayType = dataTypes.ReadNextVarInt(packetData); + return slotDisplayType switch + { + 0 => "Empty", + 1 => "Any Fuel", + 2 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))), + 3 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty", + 4 => "#" + dataTypes.ReadNextString(packetData), + 5 => ReadSmithingTrimSlotDisplayLabel(packetData), + 6 => ReadWithRemainderSlotDisplayLabel(packetData), + 7 => ReadCompositeSlotDisplayLabel(packetData), + _ => $"slot_display_{slotDisplayType}", + }; + } + + private string ReadSmithingTrimSlotDisplayLabel(Queue packetData) + { + string baseLabel = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // material + _ = dataTypes.ReadNextVarInt(packetData); // trim pattern registry id + return baseLabel; + } + + private string ReadWithRemainderSlotDisplayLabel(Queue packetData) + { + string inputLabel = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // remainder + return inputLabel; + } + + private string ReadCompositeSlotDisplayLabel(Queue packetData) + { + int optionCount = dataTypes.ReadNextVarInt(packetData); + string label = "Composite"; + + for (int i = 0; i < optionCount; i++) + { + string optionLabel = ReadSlotDisplayLabel(packetData); + if (label == "Composite" && optionLabel is not "Empty" and not "Composite") + label = optionLabel; + } + + return label; + } + + private void SkipOptionalCraftingRequirements(Queue packetData) + { + if (!dataTypes.ReadNextBool(packetData)) + return; + + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + SkipItemHolderSet(packetData); + } + + private void SkipItemHolderSet(Queue packetData) + { + int entryCount = dataTypes.ReadNextVarInt(packetData) - 1; + if (entryCount == -1) + { + _ = dataTypes.ReadNextString(packetData); + return; + } + + for (int i = 0; i < entryCount; i++) + _ = dataTypes.ReadNextVarInt(packetData); + } + private bool SkipRecipeBookSettings(Queue packetData) { // MC 1.13 uses 4 booleans for the crafting/smelting recipe book states. @@ -5111,7 +5270,10 @@ namespace MinecraftClient.Protocol.Handlers return false; packet.AddRange(DataTypes.GetVarInt(windowId)); - packet.AddRange(dataTypes.GetString(recipeId)); + if (protocolVersion >= MC_1_21_2_Version) + packet.AddRange(DataTypes.GetVarInt(int.Parse(recipeId, CultureInfo.InvariantCulture))); + else + packet.AddRange(dataTypes.GetString(recipeId)); packet.AddRange(dataTypes.GetBool(makeAll)); SendPacket(PacketTypesOut.CraftRecipeRequest, packet); return true; diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index d6bd5eb7..81a4a056 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -520,9 +520,9 @@ namespace MinecraftClient.Protocol /// /// Called when recipe book recipes are added or replaced. /// - /// Recipe identifiers to add + /// Recipe entries to add /// True to replace the currently tracked recipe book entries - public void OnRecipeBookAdd(string[] recipeIds, bool replace); + public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace); /// /// Called when recipe book recipes are removed. diff --git a/MinecraftClient/RecipeBookRecipeEntry.cs b/MinecraftClient/RecipeBookRecipeEntry.cs new file mode 100644 index 00000000..a5648ba7 --- /dev/null +++ b/MinecraftClient/RecipeBookRecipeEntry.cs @@ -0,0 +1,4 @@ +namespace MinecraftClient +{ + public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText); +} From 90ea05b17dc640fdc1ac8fa70ffa702cf728ad6f Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 30 Mar 2026 02:45:56 +0800 Subject: [PATCH 37/76] Enhance server status display and player information handling - Updated `ServerStatusDisplay` to use `ChatBot.GetVerbatim` for version name formatting. - Modified `ServerStatusPanelBuilder` to improve player name display with color parsing. - Changed translation for online player label to "Online Players:" for clarity. - Refactored protocol version checks to streamline logic in server status handling. --- .../Protocol/ServerStatusDisplay.cs | 5 +++-- .../Resources/Translations/Translations.resx | 2 +- .../Tui/ServerStatusPanelBuilder.cs | 21 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/MinecraftClient/Protocol/ServerStatusDisplay.cs b/MinecraftClient/Protocol/ServerStatusDisplay.cs index 21e386ab..291745e5 100644 --- a/MinecraftClient/Protocol/ServerStatusDisplay.cs +++ b/MinecraftClient/Protocol/ServerStatusDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Text; using MinecraftClient.Protocol.Message; +using MinecraftClient.Scripting; namespace MinecraftClient.Protocol { @@ -47,12 +48,12 @@ namespace MinecraftClient.Protocol sb.Append("§f"); sb.Append(Translations.mcc_server_info_label_version); sb.Append(" §b"); - sb.Append(info.VersionName); + sb.Append(ChatBot.GetVerbatim(info.VersionName)); sb.Append(" §7("); sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7")); sb.AppendLine(")"); - if (info.ResolvedProtocol != 0 && info.ResolvedProtocol != info.ProtocolVersion) + if (info.ResolvedProtocol != 0) { string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); sb.Append("§f"); diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index c3acc0fc..fa9a3378 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -852,7 +852,7 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file Connecting as: - Online: + Online Players: ... +{0} diff --git a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs index 3b6e8f9f..dd859242 100644 --- a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs +++ b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs @@ -19,6 +19,7 @@ namespace MinecraftClient.Tui if (info.FaviconBase64 is not null) { var iconGrid = BuildFaviconGrid(info.FaviconBase64, FaviconDisplaySize); + iconGrid.VerticalAlignment = VerticalAlignment.Center; DockPanel.SetDock(iconGrid, Dock.Left); contentPanel.Children.Add(iconGrid); } @@ -83,9 +84,10 @@ namespace MinecraftClient.Tui private static void AddVersion(StackPanel panel, Protocol.ServerStatusInfo info) { + string versionClean = Scripting.ChatBot.GetVerbatim(info.VersionName); var row = new TextBlock(); row.Inlines!.Add(Label(Translations.mcc_server_info_label_version)); - row.Inlines.Add(Value(info.VersionName, McColors.Aqua)); + row.Inlines.Add(Value(versionClean, McColors.Aqua)); row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray }); row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ProtocolVersion)) { Foreground = McColors.Gray }); @@ -95,7 +97,7 @@ namespace MinecraftClient.Tui private static void AddConnectingAs(StackPanel panel, Protocol.ServerStatusInfo info) { - if (info.ResolvedProtocol == 0 || info.ResolvedProtocol == info.ProtocolVersion) + if (info.ResolvedProtocol == 0) return; string resolvedMcVer = Protocol.ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); @@ -146,17 +148,20 @@ namespace MinecraftClient.Tui { Text = Translations.mcc_server_info_label_online, Foreground = McColors.Gray, - Margin = new Thickness(0, 1, 0, 0), }); int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers); for (int i = 0; i < shown; i++) { - panel.Children.Add(new TextBlock - { - Text = $" {info.SamplePlayers[i].Name}", - Foreground = McColors.Green, - }); + string name = info.SamplePlayers[i].Name; + if (name.Contains('\u00a7')) + panel.Children.Add(McColorParser.CreateColoredTextBlock($" {name}", TextWrapping.NoWrap)); + else + panel.Children.Add(new TextBlock + { + Text = $" {name}", + Foreground = McColors.Green, + }); } if (info.SamplePlayers.Count > shown) From d97861888dd831d57232018797dd0dcd646eff2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:52:32 +0000 Subject: [PATCH 38/76] docs: document recipebook command Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/1287ecfd-6d64-45ee-9aaf-96cbce9c3713 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/guide/usage.md | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/guide/usage.md b/docs/guide/usage.md index 833d5dd3..0a439aff 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -650,6 +650,79 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+recipebook + +- **Description:** + + List unlocked recipe book entries and ask the server to place one of them into the active crafting inventory. + +

Note

+ + **You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this command to work.** + +
+ +

Note

+ + **`craft` and `craftall` need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.** + +
+ +

Warning

+ + **Recipe book crafting is supported on Minecraft `1.13+`.** + +
+ + `list` shows the recipe book entries MCC is currently tracking. + + On newer versions, the list can contain numeric display ids instead of plain recipe names. If you see something like `838: Oak Planks`, use `838` with `craft` or `craftall`. + + `craft` and `craftall` send a recipe-book request to the server. They do not automatically take the result item for you. After the recipe appears in the active inventory, take the output slot the same way you would handle any other inventory action. + +- **Usage:** + + ``` + /recipebook list + ``` + + ``` + /recipebook craft + ``` + + ``` + /recipebook craftall + ``` + +- **Examples:** + + Show the currently tracked recipe book entries: + + ``` + /recipebook list + ``` + + Request one recipe placement: + + ``` + /recipebook craft minecraft:oak_planks + ``` + + On newer versions, use the numeric id shown by `/recipebook list`: + + ``` + /recipebook craftall 838 + ``` + + If the recipe is placed in the player crafting grid, take the result from slot `0`: + + ``` + /inventory player click 0 + ``` + +
+
connect From 301ea6b9db3e3d614331b3c92d2a7aef988d2bd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:25:17 +0000 Subject: [PATCH 39/76] Initial plan From d08cf803b8f603cd57388024d3bf6e7a164da37c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:30:05 +0000 Subject: [PATCH 40/76] feat: add install.sh and install.ps1 download scripts with docs update Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/bfeecfa8-81d9-45da-b1a7-b2dd6804b34f Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- .../.installation | 1 + docs/.vuepress/public/install.ps1 | 48 +++++++++ docs/.vuepress/public/install.sh | 100 ++++++++++++++++++ docs/guide/installation.md | 37 +++++++ 4 files changed, 186 insertions(+) create mode 100644 Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation create mode 100644 docs/.vuepress/public/install.ps1 create mode 100644 docs/.vuepress/public/install.sh diff --git a/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation b/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation new file mode 100644 index 00000000..17b48a06 --- /dev/null +++ b/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation @@ -0,0 +1 @@ +2c16f5a2-7133-410c-80fb-d99a09cc7988 \ No newline at end of file diff --git a/docs/.vuepress/public/install.ps1 b/docs/.vuepress/public/install.ps1 new file mode 100644 index 00000000..4d998b01 --- /dev/null +++ b/docs/.vuepress/public/install.ps1 @@ -0,0 +1,48 @@ +# Minecraft Console Client - Installer for Windows +# Downloads the latest MinecraftClient binary for your Windows architecture. +# Usage (PowerShell): iwr -useb https://mccteam.github.io/install.ps1 | iex + +$ErrorActionPreference = 'Stop' + +$REPO = "MCCTeam/Minecraft-Console-Client" +$OUTPUT = "MinecraftClient.exe" + +# --- Detect CPU architecture --- +$arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture +$archId = switch ($arch) { + 'X64' { 'x64' } + 'X86' { 'x86' } + 'Arm64' { 'arm64' } + default { + Write-Error "Unsupported CPU architecture: $arch" + exit 1 + } +} + +$suffix = "win-$archId" + +# --- Fetch latest release metadata from GitHub API --- +$apiUrl = "https://api.github.com/repos/$REPO/releases/latest" +Write-Host "Fetching latest release information..." +$release = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + +# --- Locate the correct asset --- +$asset = $release.assets | Where-Object { $_.name -like "*-$suffix.exe" } | Select-Object -First 1 + +if (-not $asset) { + Write-Error "Could not find a release asset for '$suffix'." + exit 1 +} + +$downloadUrl = $asset.browser_download_url +$tag = $release.tag_name + +Write-Host "Downloading MinecraftClient $tag ($suffix)..." + +# Suppress the progress bar to avoid cluttering the terminal and speed up the download +$ProgressPreference = 'SilentlyContinue' +Invoke-WebRequest -Uri $downloadUrl -OutFile $OUTPUT -UseBasicParsing + +Write-Host "" +Write-Host "Downloaded: .\$OUTPUT" +Write-Host "Run with: .\$OUTPUT --help" diff --git a/docs/.vuepress/public/install.sh b/docs/.vuepress/public/install.sh new file mode 100644 index 00000000..f1cf57d0 --- /dev/null +++ b/docs/.vuepress/public/install.sh @@ -0,0 +1,100 @@ +#!/bin/sh +# Minecraft Console Client - Installer +# Downloads the latest MinecraftClient binary for your Linux or macOS platform. +# Usage: curl -fsSL https://mccteam.github.io/install.sh | sh +# or: wget -qO- https://mccteam.github.io/install.sh | sh + +set -e + +REPO="MCCTeam/Minecraft-Console-Client" +OUTPUT="MinecraftClient" + +# --- Detect OS --- +OS=$(uname -s) +case "$OS" in + Linux) PLATFORM="linux" ;; + Darwin) PLATFORM="osx" ;; + *) + echo "Error: Unsupported OS '$OS'. This script supports Linux and macOS." >&2 + exit 1 + ;; +esac + +# --- Detect CPU architecture --- +ARCH=$(uname -m) +case "$ARCH" in + x86_64|amd64) ARCH_ID="x64" ;; + aarch64|arm64) ARCH_ID="arm64" ;; + armv7l|armv8l|armhf) ARCH_ID="arm" ;; + arm*) ARCH_ID="arm" ;; + *) + echo "Error: Unsupported CPU architecture '$ARCH'." >&2 + exit 1 + ;; +esac + +# macOS does not have an arm (32-bit) build +if [ "$PLATFORM" = "osx" ] && [ "$ARCH_ID" = "arm" ]; then + echo "Error: 32-bit ARM is not supported on macOS." >&2 + exit 1 +fi + +SUFFIX="${PLATFORM}-${ARCH_ID}" + +# --- Download helpers: prefer curl, fall back to wget --- +_download_stdout() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$1" + elif command -v wget >/dev/null 2>&1; then + wget -qO- "$1" + else + echo "Error: Neither 'curl' nor 'wget' is available. Please install one and retry." >&2 + exit 1 + fi +} + +_download_file() { + if command -v curl >/dev/null 2>&1; then + curl -fL --progress-bar -o "$2" "$1" + elif command -v wget >/dev/null 2>&1; then + wget -O "$2" "$1" + else + echo "Error: Neither 'curl' nor 'wget' is available. Please install one and retry." >&2 + exit 1 + fi +} + +# --- Fetch latest release metadata from GitHub API --- +API_URL="https://api.github.com/repos/${REPO}/releases/latest" +echo "Fetching latest release information..." +RELEASE_JSON=$(_download_stdout "$API_URL") + +# --- Parse asset download URL (no external tools required) --- +# The JSON key "browser_download_url" appears once per asset. +# We match the key followed by the URL, anchoring on the platform-arch suffix +# and the closing quote so that e.g. "linux-arm" does not match "linux-arm64". +# The ' *: *' pattern handles optional spaces around the colon (GitHub API adds spaces). +ASSET_URL=$(printf '%s' "$RELEASE_JSON" \ + | grep -o '"browser_download_url" *: *"[^"]*-'"${SUFFIX}"'"' \ + | grep -o 'https://[^"]*' \ + | head -1) + +if [ -z "$ASSET_URL" ]; then + echo "Error: Could not find a release asset for platform '${SUFFIX}'." >&2 + exit 1 +fi + +# --- Extract tag name for display --- +TAG=$(printf '%s' "$RELEASE_JSON" \ + | grep -o '"tag_name" *: *"[^"]*"' \ + | head -1 \ + | grep -o '"[^"]*"$' \ + | tr -d '"') + +echo "Downloading MinecraftClient ${TAG} (${SUFFIX})..." +_download_file "$ASSET_URL" "$OUTPUT" +chmod +x "$OUTPUT" + +echo "" +echo "Downloaded: ./${OUTPUT}" +echo "Run with: ./${OUTPUT} --help" diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 612cf37c..1ba686ed 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -4,6 +4,7 @@ title: Installation # Installation +- [Quick Install (one-liner)](#quick-install) - [YouTube Tutorials](#youtube-tutorials) - [Download a compiled binary](#download-a-compiled-binary) - [Building from the source code](#building-from-the-source-code) @@ -11,6 +12,42 @@ title: Installation - [Run on Android](#run-on-android) - [Run MCC 24/7 on a VPS](#run-on-a-vps) +## Quick Install + +The quickest way to get MCC is to run the installer script for your platform. It auto-detects your OS and CPU architecture, fetches the latest release from GitHub, and saves the binary to your current directory. + +### Linux / macOS + +Open a terminal in the folder where you want MCC and run: + +```bash +curl -fsSL https://mccteam.github.io/install.sh | sh +``` + +If you prefer `wget`: + +```bash +wget -qO- https://mccteam.github.io/install.sh | sh +``` + +The script downloads `MinecraftClient` and marks it executable. Supported architectures: `x64`, `arm64`, `arm` (Linux only). + +### Windows + +Open **PowerShell** in the folder where you want MCC and run: + +```powershell +iwr -useb https://mccteam.github.io/install.ps1 | iex +``` + +The script downloads `MinecraftClient.exe`. Supported architectures: `x64`, `x86`, `arm64`. + +::: tip +You can also download the scripts directly and inspect them before running: +- Linux/macOS: [install.sh](https://mccteam.github.io/install.sh) +- Windows: [install.ps1](https://mccteam.github.io/install.ps1) +::: + ## YouTube Tutorials If you're not the kind of person that likes textual tutorials, our community has made video tutorials available on YouTube. From 8c4acb0ad556bae9285010ef259688067f7f3988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:30:24 +0000 Subject: [PATCH 41/76] chore: remove Sentry cache dir and add to .gitignore Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/bfeecfa8-81d9-45da-b1a7-b2dd6804b34f Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- .gitignore | 1 + Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation diff --git a/.gitignore b/.gitignore index fe783fa4..91c3a8b6 100644 --- a/.gitignore +++ b/.gitignore @@ -436,3 +436,4 @@ FodyWeavers.xsd # SpecStory files /.specstory/ /.vscode/settings.json +/Sentry/ diff --git a/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation b/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation deleted file mode 100644 index 17b48a06..00000000 --- a/Sentry/E84A17A5B8C1640BCE4BE9BA61BAF7487EDC56C0/.installation +++ /dev/null @@ -1 +0,0 @@ -2c16f5a2-7133-410c-80fb-d99a09cc7988 \ No newline at end of file From 53afc252ea629fa410c25042e07d06c9ea63369a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:31:26 +0000 Subject: [PATCH 42/76] fix: use precise regex in install.ps1 asset matching Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/bfeecfa8-81d9-45da-b1a7-b2dd6804b34f Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/.vuepress/public/install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.vuepress/public/install.ps1 b/docs/.vuepress/public/install.ps1 index 4d998b01..7255738d 100644 --- a/docs/.vuepress/public/install.ps1 +++ b/docs/.vuepress/public/install.ps1 @@ -27,7 +27,7 @@ Write-Host "Fetching latest release information..." $release = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing # --- Locate the correct asset --- -$asset = $release.assets | Where-Object { $_.name -like "*-$suffix.exe" } | Select-Object -First 1 +$asset = $release.assets | Where-Object { $_.name -match "^MinecraftClient-.*-$([regex]::Escape($suffix))\.exe$" } | Select-Object -First 1 if (-not $asset) { Write-Error "Could not find a release asset for '$suffix'." From 46fdd687c763322f55a6ec5bf51e98d3841a48eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:39:58 +0000 Subject: [PATCH 43/76] feat: add ASCII progress bars to both install scripts Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/00d3b725-2246-40c5-93e4-d9813d95c333 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/.vuepress/public/install.ps1 | 38 ++++++++++++++++++++++++++++--- docs/.vuepress/public/install.sh | 6 +++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/.vuepress/public/install.ps1 b/docs/.vuepress/public/install.ps1 index 7255738d..8a14a86c 100644 --- a/docs/.vuepress/public/install.ps1 +++ b/docs/.vuepress/public/install.ps1 @@ -39,9 +39,41 @@ $tag = $release.tag_name Write-Host "Downloading MinecraftClient $tag ($suffix)..." -# Suppress the progress bar to avoid cluttering the terminal and speed up the download -$ProgressPreference = 'SilentlyContinue' -Invoke-WebRequest -Uri $downloadUrl -OutFile $OUTPUT -UseBasicParsing +# Download with a built-in ASCII progress bar (no external tools required). +# HttpWebRequest streams the body on the main thread so we can update the +# progress bar inline without any Runspace or thread-safety concerns. +$outPath = Join-Path (Get-Location).Path $OUTPUT +$request = [System.Net.HttpWebRequest]::Create($downloadUrl) +$response = $request.GetResponse() +$totalBytes = $response.ContentLength + +$responseStream = $response.GetResponseStream() +$fileStream = [System.IO.File]::Create($outPath) +$buffer = New-Object byte[] 32768 +$totalRead = 0 + +try { + while ($true) { + $read = $responseStream.Read($buffer, 0, $buffer.Length) + if ($read -le 0) { break } + $fileStream.Write($buffer, 0, $read) + $totalRead += $read + if ($totalBytes -gt 0) { + $pct = [int]($totalRead * 100 / $totalBytes) + $filled = '=' * [int]($pct / 2) + $bar = $filled.PadRight(50) + $recv = [math]::Round($totalRead / 1MB, 1) + $total = [math]::Round($totalBytes / 1MB, 1) + Write-Host -NoNewline ("`r[{0}] {1,3}% {2,6:N1} / {3,6:N1} MB" -f $bar, $pct, $recv, $total) + } + } +} finally { + $fileStream.Close() + $responseStream.Close() + $response.Close() +} + +Write-Host "" # end the progress line Write-Host "" Write-Host "Downloaded: .\$OUTPUT" diff --git a/docs/.vuepress/public/install.sh b/docs/.vuepress/public/install.sh index f1cf57d0..d3e74d3d 100644 --- a/docs/.vuepress/public/install.sh +++ b/docs/.vuepress/public/install.sh @@ -57,6 +57,12 @@ _download_file() { if command -v curl >/dev/null 2>&1; then curl -fL --progress-bar -o "$2" "$1" elif command -v wget >/dev/null 2>&1; then + # --show-progress forces the progress bar even when stdout is not a TTY. + # Fall back silently to default output if the flag is not supported + # (older wget versions, e.g. BusyBox wget). + if wget --show-progress -O "$2" "$1" 2>/dev/null; then + return 0 + fi wget -O "$2" "$1" else echo "Error: Neither 'curl' nor 'wget' is available. Please install one and retry." >&2 From f10556a162ccbd8098f8725b0fa66e11a1bb03c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:45:40 +0000 Subject: [PATCH 44/76] fix: use Console::Write for progress bar to avoid duplicate bars when piped via iex Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/2c598307-7028-46d4-ac37-045f5ce125ed Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/.vuepress/public/install.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/.vuepress/public/install.ps1 b/docs/.vuepress/public/install.ps1 index 8a14a86c..31f2e324 100644 --- a/docs/.vuepress/public/install.ps1 +++ b/docs/.vuepress/public/install.ps1 @@ -64,7 +64,12 @@ try { $bar = $filled.PadRight(50) $recv = [math]::Round($totalRead / 1MB, 1) $total = [math]::Round($totalBytes / 1MB, 1) - Write-Host -NoNewline ("`r[{0}] {1,3}% {2,6:N1} / {3,6:N1} MB" -f $bar, $pct, $recv, $total) + # Use [Console]::Write with an explicit \r so the cursor returns to + # column 0 and overwrites the previous bar. Write-Host -NoNewline + # does not reliably reposition the cursor when the script is run + # via iex (pipe mode), producing multiple bars on one line. + $line = "`r[{0}] {1,3}% {2,6:N1} / {3,6:N1} MB" -f $bar, $pct, $recv, $total + [Console]::Write($line) } } } finally { @@ -73,7 +78,7 @@ try { $response.Close() } -Write-Host "" # end the progress line +[Console]::WriteLine() # end the progress line Write-Host "" Write-Host "Downloaded: .\$OUTPUT" From 031dbb16d461da7d75ee68ce90036f824ea942c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:03:11 +0000 Subject: [PATCH 45/76] Initial plan From 534e337f107a4cb99d052a3e1ad6453d72efac74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:05:16 +0000 Subject: [PATCH 46/76] Add Quick Install section to README with one-liner install commands Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/0a622d0a-bb4e-4cba-96f6-df2afa3bf4b7 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb690dd9..76228060 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,25 @@ ## Download -Get development builds from the [Releases section](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest) +Get the latest release from the [Releases section](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest). + +## Quick Install ⚡ + +Open a terminal in the folder where you want MCC and run: + +Linux / macOS: + +```bash +curl -fsSL https://mccteam.github.io/install.sh | sh +``` + +Windows (PowerShell): + +```powershell +iwr -useb https://mccteam.github.io/install.ps1 | iex +``` + +The script detects your architecture and downloads the right binary. For more options (including `wget` and manual downloads), see the [installation guide](https://mccteam.github.io/guide/installation.html). ## How to use 📚 From 6dc42d9bd1f077a4f9523722ed5437864964dbaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:10:15 +0000 Subject: [PATCH 47/76] Initial plan From 65ef3dde6b07d97aab65226e70665585a76f6c97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:22:31 +0000 Subject: [PATCH 48/76] Implement unified achievements feature: data model, protocol handling, state management, ChatBot API, and /achievement command Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/9db483a8-4a5f-47b1-a6f4-30b6e39075bd Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Achievement.cs | 36 ++++ .../Commands/AchievementCommand.cs | 106 ++++++++++ MinecraftClient/McClient.cs | 100 +++++++++ .../Protocol/Handlers/Protocol18.cs | 191 ++++++++++++++++++ .../Protocol/IMinecraftComHandler.cs | 14 ++ .../Translations/Translations.Designer.cs | 99 +++++++++ .../Resources/Translations/Translations.resx | 33 +++ MinecraftClient/Scripting/ChatBot.cs | 35 ++++ 8 files changed, 614 insertions(+) create mode 100644 MinecraftClient/Achievement.cs create mode 100644 MinecraftClient/Commands/AchievementCommand.cs diff --git a/MinecraftClient/Achievement.cs b/MinecraftClient/Achievement.cs new file mode 100644 index 00000000..760e4054 --- /dev/null +++ b/MinecraftClient/Achievement.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace MinecraftClient +{ + /// + /// The type of an achievement or advancement. + /// + public enum AchievementType + { + Task, + Challenge, + Goal, + Legacy + } + + /// + /// Represents a Minecraft achievement (pre-1.12) or advancement (1.12+). + /// + /// Resource identifier, e.g. "minecraft:story/root" or "achievement.openInventory" + /// Display title (null for legacy achievements without display info) + /// Display description (null for legacy achievements without display info) + /// The frame type / achievement category + /// Whether this advancement is hidden in the UI + /// Whether all requirements have been met + /// OR-groups of criterion names; all groups must be satisfied + /// Per-criterion completion status + public record Achievement( + string Id, + string? Title, + string? Description, + AchievementType Type, + bool IsHidden, + bool IsCompleted, + IReadOnlyList> Requirements, + IReadOnlyDictionary CriteriaProgress); +} diff --git a/MinecraftClient/Commands/AchievementCommand.cs b/MinecraftClient/Commands/AchievementCommand.cs new file mode 100644 index 00000000..ee99c4d7 --- /dev/null +++ b/MinecraftClient/Commands/AchievementCommand.cs @@ -0,0 +1,106 @@ +using System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class AchievementCommand : Command + { + public override string CmdName => "achievement"; + public override string CmdUsage => "achievement "; + public override string CmdDesc => Translations.cmd_achievement_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + .Then(l => l.Literal("list") + .Executes(r => GetUsage(r.Source, "list"))) + .Then(l => l.Literal("locked") + .Executes(r => GetUsage(r.Source, "locked"))) + .Then(l => l.Literal("unlocked") + .Executes(r => GetUsage(r.Source, "unlocked"))) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ListAchievements(r.Source, null)) + .Then(l => l.Literal("list") + .Executes(r => ListAchievements(r.Source, null))) + .Then(l => l.Literal("locked") + .Executes(r => ListAchievements(r.Source, false))) + .Then(l => l.Literal("unlocked") + .Executes(r => ListAchievements(r.Source, true))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format + "list" => GetCmdDescTranslated(), + "locked" => GetCmdDescTranslated(), + "unlocked" => GetCmdDescTranslated(), + _ => GetCmdDescTranslated(), +#pragma warning restore format + }); + } + + /// null = all, true = unlocked only, false = locked only + private static int ListAchievements(CmdResult r, bool? completed) + { + McClient handler = CmdResult.currentHandler!; + + Achievement[] items = completed switch + { + true => handler.GetUnlockedAchievements(), + false => handler.GetLockedAchievements(), + null => handler.GetAchievements() + }; + + if (items.Length == 0) + { + string msg = completed switch + { + true => Translations.cmd_achievement_none_unlocked, + false => Translations.cmd_achievement_none_locked, + _ => Translations.cmd_achievement_none + }; + return r.SetAndReturn(CmdResult.Status.Done, msg); + } + + string header = completed switch + { + true => Translations.cmd_achievement_header_unlocked, + false => Translations.cmd_achievement_header_locked, + _ => Translations.cmd_achievement_header + }; + + StringBuilder sb = new(); + sb.AppendLine(header); + + foreach (Achievement a in items.OrderBy(static a => a.Id)) + { + string status = a.IsCompleted + ? Translations.cmd_achievement_done + : Translations.cmd_achievement_todo; + + string display = a.Title is not null + ? string.Format(Translations.cmd_achievement_entry_titled, status, a.Title, a.Id, a.Type) + : string.Format(Translations.cmd_achievement_entry, status, a.Id, a.Type); + + sb.AppendLine(display); + } + + handler.Log.Info(sb.ToString().TrimEnd()); + return r.SetAndReturn(CmdResult.Status.Done); + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 24069342..3eabfbd8 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -45,11 +45,14 @@ namespace MinecraftClient private readonly Queue threadTasks = new(); private readonly Lock threadTasksLock = new(); private readonly Lock recipeBookLock = new(); + private readonly Lock achievementsLock = new(); private readonly List bots = new(); private static readonly List botsOnHold = new(); private static readonly Dictionary inventories = new(); private readonly Dictionary unlockedRecipes = new(StringComparer.Ordinal); + private readonly Dictionary achievements = new(StringComparer.Ordinal); + private string? activeAdvancementTab; private readonly Dictionary> registeredBotPluginChannels = new(); private readonly List registeredServerPluginChannels = new(); @@ -1353,6 +1356,42 @@ namespace MinecraftClient } } + /// + /// Get all achievements/advancements known to the client. + /// + /// Snapshot of all achievements + public Achievement[] GetAchievements() + { + lock (achievementsLock) + { + return [.. achievements.Values]; + } + } + + /// + /// Get only completed achievements/advancements. + /// + /// Snapshot of completed achievements + public Achievement[] GetUnlockedAchievements() + { + lock (achievementsLock) + { + return achievements.Values.Where(static a => a.IsCompleted).ToArray(); + } + } + + /// + /// Get only incomplete achievements/advancements. + /// + /// Snapshot of locked achievements + public Achievement[] GetLockedAchievements() + { + lock (achievementsLock) + { + return achievements.Values.Where(static a => !a.IsCompleted).ToArray(); + } + } + /// /// Get all Entities /// @@ -4139,6 +4178,67 @@ namespace MinecraftClient } } + public void OnAchievementsUpdate(IReadOnlyList added, IReadOnlyList removedIds, bool reset) + { + lock (achievementsLock) + { + if (reset) + achievements.Clear(); + + // Remove entries + foreach (string id in removedIds) + achievements.Remove(id); + + // Add/update entries. For progress-only updates (no definition), + // merge with existing definition if available. + foreach (Achievement entry in added) + { + if (entry.Title is null && achievements.TryGetValue(entry.Id, out Achievement? existing)) + { + // Progress-only update - merge with existing definition + bool isCompleted = ComputeAchievementCompleted(existing.Requirements, entry.CriteriaProgress); + achievements[entry.Id] = existing with { IsCompleted = isCompleted, CriteriaProgress = entry.CriteriaProgress }; + } + else + { + achievements[entry.Id] = entry; + } + } + } + + DispatchBotEvent(bot => bot.OnAchievementUpdate(added, removedIds, reset)); + } + + public void OnSelectAdvancementTab(string? tabId) + { + activeAdvancementTab = tabId; + } + + /// + /// Compute whether an achievement is completed based on AND-of-ORs requirements. + /// + private static bool ComputeAchievementCompleted(IReadOnlyList> requirements, IReadOnlyDictionary criteria) + { + if (requirements.Count == 0) + return true; + + foreach (IReadOnlyList group in requirements) + { + bool groupSatisfied = false; + foreach (string criterion in group) + { + if (criteria.TryGetValue(criterion, out bool done) && done) + { + groupSatisfied = true; + break; + } + } + if (!groupSatisfied) + return false; + } + return true; + } + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index bc9537cd..2d934561 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3132,6 +3132,14 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.RecipeBookSettings: break; + case PacketTypesIn.Advancements: + HandleAdvancements(packetData); + break; + + case PacketTypesIn.SelectAdvancementTab: + HandleSelectAdvancementTab(packetData); + break; + default: return false; //Ignored packet } @@ -3139,6 +3147,189 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Handle the Advancements packet (1.12+). + /// Also handles the Statistics packet for pre-1.12 legacy achievements. + /// + private void HandleAdvancements(Queue packetData) + { + bool reset = dataTypes.ReadNextBool(packetData); + + // --- Added advancements --- + int addedCount = dataTypes.ReadNextVarInt(packetData); + var added = new List(addedCount); + var addedDefinitions = new Dictionary> requirements)>(addedCount); + + for (int i = 0; i < addedCount; i++) + { + string id = dataTypes.ReadNextString(packetData); + + // Parent + bool hasParent = dataTypes.ReadNextBool(packetData); + if (hasParent) + dataTypes.ReadNextString(packetData); // parentId - read and discard + + // Display + string? title = null; + string? description = null; + var type = AchievementType.Task; + bool isHidden = false; + + bool hasDisplay = dataTypes.ReadNextBool(packetData); + if (hasDisplay) + { + title = dataTypes.ReadNextChat(packetData); + description = dataTypes.ReadNextChat(packetData); + dataTypes.ReadNextItemSlot(packetData, itemPalette); // icon - read and discard + + int frameType = dataTypes.ReadNextVarInt(packetData); + type = frameType switch + { + 1 => AchievementType.Challenge, + 2 => AchievementType.Goal, + _ => AchievementType.Task + }; + + int flags = dataTypes.ReadNextInt(packetData); + isHidden = (flags & 0x04) != 0; + if ((flags & 0x01) != 0) + dataTypes.ReadNextString(packetData); // background texture - read and discard + + dataTypes.ReadNextFloat(packetData); // x + dataTypes.ReadNextFloat(packetData); // y + } + + // Criteria and requirements differ by version + var requirements = new List>(); + + if (protocolVersion < MC_1_20_6_Version) + { + // Builder-based: criteria names list, then requirements + int criteriaCount = dataTypes.ReadNextVarInt(packetData); + for (int c = 0; c < criteriaCount; c++) + dataTypes.ReadNextString(packetData); // criterion name only, no trigger data + + int reqGroupCount = dataTypes.ReadNextVarInt(packetData); + for (int g = 0; g < reqGroupCount; g++) + { + int groupSize = dataTypes.ReadNextVarInt(packetData); + var group = new List(groupSize); + for (int s = 0; s < groupSize; s++) + group.Add(dataTypes.ReadNextString(packetData)); + requirements.Add(group); + } + } + else + { + // AdvancementHolder-based (1.20.6+): requirements only, then sendsTelemetryEvent + int reqGroupCount = dataTypes.ReadNextVarInt(packetData); + for (int g = 0; g < reqGroupCount; g++) + { + int groupSize = dataTypes.ReadNextVarInt(packetData); + var group = new List(groupSize); + for (int s = 0; s < groupSize; s++) + group.Add(dataTypes.ReadNextString(packetData)); + requirements.Add(group); + } + + dataTypes.ReadNextBool(packetData); // sendsTelemetryEvent + } + + addedDefinitions[id] = (title, description, type, isHidden, requirements); + } + + // --- Removed advancement IDs --- + int removedCount = dataTypes.ReadNextVarInt(packetData); + var removedIds = new List(removedCount); + for (int i = 0; i < removedCount; i++) + removedIds.Add(dataTypes.ReadNextString(packetData)); + + // --- Progress updates --- + int progressCount = dataTypes.ReadNextVarInt(packetData); + var progressMap = new Dictionary>(progressCount); + + for (int i = 0; i < progressCount; i++) + { + string id = dataTypes.ReadNextString(packetData); + int criteriaEntries = dataTypes.ReadNextVarInt(packetData); + var criteria = new Dictionary(criteriaEntries); + + for (int c = 0; c < criteriaEntries; c++) + { + string criterionName = dataTypes.ReadNextString(packetData); + bool isDone = dataTypes.ReadNextBool(packetData); + if (isDone) + dataTypes.ReadNextLong(packetData); // epochMs - read and discard + criteria[criterionName] = isDone; + } + + progressMap[id] = criteria; + } + + // showAdvancements boolean added in 1.21.11+ + if (protocolVersion >= MC_1_21_11_Version) + dataTypes.ReadNextBool(packetData); // showAdvancements - read and discard + + // Build Achievement records from definitions + progress + foreach (var (id, def) in addedDefinitions) + { + progressMap.TryGetValue(id, out var criteria); + criteria ??= new Dictionary(); + + bool isCompleted = ComputeAdvancementCompleted(def.requirements, criteria); + + var readOnlyReqs = def.requirements.ConvertAll>(static g => g.AsReadOnly()); + added.Add(new Achievement(id, def.title, def.description, def.type, def.isHidden, isCompleted, readOnlyReqs.AsReadOnly(), criteria)); + } + + // Also build Achievement records for progress-only updates (no definition change) + var progressOnly = new List(); + foreach (var (id, criteria) in progressMap) + { + if (!addedDefinitions.ContainsKey(id)) + progressOnly.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria)); + } + + handler.OnAchievementsUpdate([.. added, .. progressOnly], removedIds, reset); + } + + /// + /// Compute whether an advancement is completed based on AND-of-ORs requirements. + /// + private static bool ComputeAdvancementCompleted(List> requirements, Dictionary criteria) + { + // Zero requirements = automatically done + if (requirements.Count == 0) + return true; + + // Each OR-group must have at least one satisfied criterion + foreach (var group in requirements) + { + bool groupSatisfied = false; + foreach (string criterion in group) + { + if (criteria.TryGetValue(criterion, out bool done) && done) + { + groupSatisfied = true; + break; + } + } + if (!groupSatisfied) + return false; + } + return true; + } + + /// + /// Handle the SelectAdvancementTab packet. + /// + private void HandleSelectAdvancementTab(Queue packetData) + { + bool hasTab = dataTypes.ReadNextBool(packetData); + string? tabId = hasTab ? dataTypes.ReadNextString(packetData) : null; + handler.OnSelectAdvancementTab(tabId); + } + private void HandleUnlockRecipes(Queue packetData) { int action = dataTypes.ReadNextVarInt(packetData); diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 81a4a056..9bfa44e8 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -530,6 +530,20 @@ namespace MinecraftClient.Protocol /// Recipe identifiers to remove public void OnRecipeBookRemove(string[] recipeIds); + /// + /// Called when achievement/advancement data is received from the server. + /// + /// Achievements that were added or updated + /// IDs of achievements that were removed + /// True if all existing state should be cleared before applying + public void OnAchievementsUpdate(IReadOnlyList added, IReadOnlyList removedIds, bool reset); + + /// + /// Called when the server selects an advancement tab. + /// + /// The tab identifier, or null if no tab is selected + public void OnSelectAdvancementTab(string? tabId); + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 022e9cad..b77b0f39 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -7174,5 +7174,104 @@ namespace MinecraftClient { return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture); } } + + /// + /// Looks up a localized string similar to list achievements/advancements from the server.. + /// + internal static string cmd_achievement_desc { + get { + return ResourceManager.GetString("cmd.achievement.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No achievements/advancements received yet.. + /// + internal static string cmd_achievement_none { + get { + return ResourceManager.GetString("cmd.achievement.none", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No completed achievements/advancements.. + /// + internal static string cmd_achievement_none_unlocked { + get { + return ResourceManager.GetString("cmd.achievement.none_unlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No incomplete achievements/advancements.. + /// + internal static string cmd_achievement_none_locked { + get { + return ResourceManager.GetString("cmd.achievement.none_locked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Achievements/Advancements:. + /// + internal static string cmd_achievement_header { + get { + return ResourceManager.GetString("cmd.achievement.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Completed achievements/advancements:. + /// + internal static string cmd_achievement_header_unlocked { + get { + return ResourceManager.GetString("cmd.achievement.header_unlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Incomplete achievements/advancements:. + /// + internal static string cmd_achievement_header_locked { + get { + return ResourceManager.GetString("cmd.achievement.header_locked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [DONE]. + /// + internal static string cmd_achievement_done { + get { + return ResourceManager.GetString("cmd.achievement.done", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [TODO]. + /// + internal static string cmd_achievement_todo { + get { + return ResourceManager.GetString("cmd.achievement.todo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} ({2}) [{3}]. + /// + internal static string cmd_achievement_entry_titled { + get { + return ResourceManager.GetString("cmd.achievement.entry_titled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} [{2}]. + /// + internal static string cmd_achievement_entry { + get { + return ResourceManager.GetString("cmd.achievement.entry", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 7c883622..48a07470 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2527,4 +2527,37 @@ see item details. Minimap position set to: {0} + + list achievements/advancements from the server. + + + No achievements/advancements received yet. + + + No completed achievements/advancements. + + + No incomplete achievements/advancements. + + + Achievements/Advancements: + + + Completed achievements/advancements: + + + Incomplete achievements/advancements: + + + [DONE] + + + [TODO] + + + {0} {1} ({2}) [{3}] + + + {0} {1} [{2}] + diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index f62e1377..422fc1a4 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -514,6 +514,14 @@ namespace MinecraftClient.Scripting /// The block public virtual void OnBlockChange(Location location, Block block) { } + /// + /// Called when achievement/advancement data is updated. + /// + /// Achievements that were added or updated + /// IDs of achievements that were removed + /// Whether the achievement state was fully reset before this update + public virtual void OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset) { } + /* =================================================================== */ /* ToolBox - Methods below might be useful while creating your bot. */ /* You should not need to interact with other classes of the program. */ @@ -1120,6 +1128,33 @@ namespace MinecraftClient.Scripting return Handler.GetEntities(); } + /// + /// Get all achievements/advancements. + /// + /// Snapshot of all achievements + protected Achievement[] GetAchievements() + { + return Handler.GetAchievements(); + } + + /// + /// Get only completed achievements/advancements. + /// + /// Snapshot of unlocked achievements + protected Achievement[] GetUnlockedAchievements() + { + return Handler.GetUnlockedAchievements(); + } + + /// + /// Get only incomplete achievements/advancements. + /// + /// Snapshot of locked achievements + protected Achievement[] GetLockedAchievements() + { + return Handler.GetLockedAchievements(); + } + /// /// Get all players Latency /// From 7b3e5ee49215ddc76b5addf7db599e43267f8852 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:27:12 +0000 Subject: [PATCH 49/76] Address code review: eliminate unnecessary allocation in progress-only updates Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/9db483a8-4a5f-47b1-a6f4-30b6e39075bd Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Protocol/Handlers/Protocol18.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 2d934561..bb603f9b 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3283,14 +3283,13 @@ namespace MinecraftClient.Protocol.Handlers } // Also build Achievement records for progress-only updates (no definition change) - var progressOnly = new List(); foreach (var (id, criteria) in progressMap) { if (!addedDefinitions.ContainsKey(id)) - progressOnly.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria)); + added.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria)); } - handler.OnAchievementsUpdate([.. added, .. progressOnly], removedIds, reset); + handler.OnAchievementsUpdate(added, removedIds, reset); } /// From 5705df43bdb8f91daa29256eeb0821ee11fc5e6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:07:17 +0000 Subject: [PATCH 50/76] Fix Advancements packet parsing: sendsTelemetryEvent added in 1.20, deduplicate requirements reading Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/5da0ec37-35e2-4aae-b165-66ddd82df985 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- .../Protocol/Handlers/Protocol18.cs | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index bb603f9b..73c47a59 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3204,37 +3204,27 @@ namespace MinecraftClient.Protocol.Handlers if (protocolVersion < MC_1_20_6_Version) { - // Builder-based: criteria names list, then requirements + // Builder-based (pre-1.20.6): criteria names list, then requirements int criteriaCount = dataTypes.ReadNextVarInt(packetData); for (int c = 0; c < criteriaCount; c++) dataTypes.ReadNextString(packetData); // criterion name only, no trigger data - - int reqGroupCount = dataTypes.ReadNextVarInt(packetData); - for (int g = 0; g < reqGroupCount; g++) - { - int groupSize = dataTypes.ReadNextVarInt(packetData); - var group = new List(groupSize); - for (int s = 0; s < groupSize; s++) - group.Add(dataTypes.ReadNextString(packetData)); - requirements.Add(group); - } } - else + + // Requirements (all versions) + int reqGroupCount = dataTypes.ReadNextVarInt(packetData); + for (int g = 0; g < reqGroupCount; g++) { - // AdvancementHolder-based (1.20.6+): requirements only, then sendsTelemetryEvent - int reqGroupCount = dataTypes.ReadNextVarInt(packetData); - for (int g = 0; g < reqGroupCount; g++) - { - int groupSize = dataTypes.ReadNextVarInt(packetData); - var group = new List(groupSize); - for (int s = 0; s < groupSize; s++) - group.Add(dataTypes.ReadNextString(packetData)); - requirements.Add(group); - } - - dataTypes.ReadNextBool(packetData); // sendsTelemetryEvent + int groupSize = dataTypes.ReadNextVarInt(packetData); + var group = new List(groupSize); + for (int s = 0; s < groupSize; s++) + group.Add(dataTypes.ReadNextString(packetData)); + requirements.Add(group); } + // sendsTelemetryEvent (added in 1.20, present in all versions since) + if (protocolVersion >= MC_1_20_Version) + dataTypes.ReadNextBool(packetData); + addedDefinitions[id] = (title, description, type, isHidden, requirements); } From 0f3289dfdffa8156ae4a1508d52041dd2f2d3984 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:16:45 +0000 Subject: [PATCH 51/76] Fix criteria version boundary: criteria list removed from wire format in MC 1.20.2, not 1.20.6 Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/5da0ec37-35e2-4aae-b165-66ddd82df985 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Protocol/Handlers/Protocol18.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 73c47a59..ac6b3b4f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3202,9 +3202,9 @@ namespace MinecraftClient.Protocol.Handlers // Criteria and requirements differ by version var requirements = new List>(); - if (protocolVersion < MC_1_20_6_Version) + if (protocolVersion < MC_1_20_2_Version) { - // Builder-based (pre-1.20.6): criteria names list, then requirements + // Builder-based (pre-1.20.2): criteria names list, then requirements int criteriaCount = dataTypes.ReadNextVarInt(packetData); for (int c = 0; c < criteriaCount; c++) dataTypes.ReadNextString(packetData); // criterion name only, no trigger data From c95fe131e31ad63142619bab0122352131d6c46d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:40:08 +0000 Subject: [PATCH 52/76] Initial plan From c55d32bb7009156723f36f9d77901ac54612c8f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:03:56 +0000 Subject: [PATCH 53/76] Diagnose root cause of 26.1 RecipeBookAdd crash Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/47b0c937-1491-4216-8ee0-1aca866e99ab Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- server.pid | 1 + 1 file changed, 1 insertion(+) create mode 100644 server.pid diff --git a/server.pid b/server.pid new file mode 100644 index 00000000..7b6ceee2 --- /dev/null +++ b/server.pid @@ -0,0 +1 @@ +5586 From 962c8b1ab24776773a28b10eb5eadeeea8fae580 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:09:33 +0000 Subject: [PATCH 54/76] Fix 26.1 RecipeBookAdd crash: update SlotDisplay registry IDs for 26.1 MC 26.1 changed the minecraft:slot_display registry, inserting 3 new types (with_any_potion, only_with_component, dyed) and shifting all existing IDs. This caused MCC to misparse recipe display data, leading to a Queue empty crash in SkipItemHolderSet. Add version-gated ReadSlotDisplayLabel with correct 26.1 type mapping and reader methods for the 3 new slot display types. Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/47b0c937-1491-4216-8ee0-1aca866e99ab Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- .gitignore | 1 + .../Protocol/Handlers/Protocol18.cs | 51 +++++++++++++++++++ server.pid | 1 - 3 files changed, 52 insertions(+), 1 deletion(-) delete mode 100644 server.pid diff --git a/.gitignore b/.gitignore index 91c3a8b6..d0f86370 100644 --- a/.gitignore +++ b/.gitignore @@ -437,3 +437,4 @@ FodyWeavers.xsd /.specstory/ /.vscode/settings.json /Sentry/ +server.pid diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index ac6b3b4f..7032bcbd 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3470,6 +3470,29 @@ namespace MinecraftClient.Protocol.Handlers private string ReadSlotDisplayLabel(Queue packetData) { int slotDisplayType = dataTypes.ReadNextVarInt(packetData); + + // 26.1 changed the slot display registry order, inserting 3 new types: + // Pre-26.1: 0=empty, 1=any_fuel, 2=item, 3=item_stack, 4=tag, 5=smithing_trim, 6=with_remainder, 7=composite + // 26.1+: 0=empty, 1=any_fuel, 2=with_any_potion, 3=only_with_component, 4=item, 5=item_stack, 6=tag, 7=dyed, 8=smithing_trim, 9=with_remainder, 10=composite + if (protocolVersion >= MC_26_1_Version) + { + return slotDisplayType switch + { + 0 => "Empty", + 1 => "Any Fuel", + 2 => ReadWithAnyPotionSlotDisplayLabel(packetData), + 3 => ReadOnlyWithComponentSlotDisplayLabel(packetData), + 4 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))), + 5 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty", + 6 => "#" + dataTypes.ReadNextString(packetData), + 7 => ReadDyedSlotDisplayLabel(packetData), + 8 => ReadSmithingTrimSlotDisplayLabel(packetData), + 9 => ReadWithRemainderSlotDisplayLabel(packetData), + 10 => ReadCompositeSlotDisplayLabel(packetData), + _ => $"slot_display_{slotDisplayType}", + }; + } + return slotDisplayType switch { 0 => "Empty", @@ -3484,6 +3507,34 @@ namespace MinecraftClient.Protocol.Handlers }; } + /// + /// Reads a with_any_potion slot display (26.1+): contains a nested SlotDisplay. + /// + private string ReadWithAnyPotionSlotDisplayLabel(Queue packetData) + { + return ReadSlotDisplayLabel(packetData); + } + + /// + /// Reads an only_with_component slot display (26.1+): contains a nested SlotDisplay and a DataComponentType VarInt ID. + /// + private string ReadOnlyWithComponentSlotDisplayLabel(Queue packetData) + { + string sourceLabel = ReadSlotDisplayLabel(packetData); + _ = dataTypes.ReadNextVarInt(packetData); // DataComponentType registry id + return sourceLabel; + } + + /// + /// Reads a dyed slot display (26.1+): contains two nested SlotDisplays (dye + target). + /// + private string ReadDyedSlotDisplayLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // dye + string targetLabel = ReadSlotDisplayLabel(packetData); // target + return targetLabel; + } + private string ReadSmithingTrimSlotDisplayLabel(Queue packetData) { string baseLabel = ReadSlotDisplayLabel(packetData); diff --git a/server.pid b/server.pid deleted file mode 100644 index 7b6ceee2..00000000 --- a/server.pid +++ /dev/null @@ -1 +0,0 @@ -5586 From 22455905a7952a706024f852ac21c25ca01ca1b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:24:00 +0000 Subject: [PATCH 55/76] Add automatic mining-speed and dig-duration handling - Add BlockHardness.cs with hardness data for all 1053 blocks from MC 1.21.11 - Add MiningCalculator.cs with version-aware dig duration computation - Tool speed from ToolComponent (1.20.6+) or legacy hardcoded tables - Efficiency enchantment (legacy: level^2+1, 1.21.11+: mining_efficiency attribute) - Haste/Conduit Power/Mining Fatigue effects - BLOCK_BREAK_SPEED attribute (1.20.6+) - MINING_EFFICIENCY and SUBMERGED_MINING_SPEED attributes (1.21.11+) - Underwater penalty with Aqua Affinity support (legacy) or attribute (modern) - Airborne penalty - Correct tool for drops check (30 vs 100 divisor) - Modify McClient.DigBlock to auto-compute duration for survival/adventure mode - Cache player attributes from OnEntityProperties in McClient - Expose duration parameter in ChatBot.cs scripting wrapper - Add /downloads/ to .gitignore Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/579df446-a335-4174-9a8b-4d66173c82b1 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- .gitignore | 1 + MinecraftClient/Mapping/BlockHardness.cs | 1326 +++++++++++++++++++ MinecraftClient/Mapping/MiningCalculator.cs | 493 +++++++ MinecraftClient/McClient.cs | 59 + MinecraftClient/Scripting/ChatBot.cs | 5 +- 5 files changed, 1882 insertions(+), 2 deletions(-) create mode 100644 MinecraftClient/Mapping/BlockHardness.cs create mode 100644 MinecraftClient/Mapping/MiningCalculator.cs diff --git a/.gitignore b/.gitignore index 91c3a8b6..cf6e90b9 100644 --- a/.gitignore +++ b/.gitignore @@ -437,3 +437,4 @@ FodyWeavers.xsd /.specstory/ /.vscode/settings.json /Sentry/ +/downloads/ diff --git a/MinecraftClient/Mapping/BlockHardness.cs b/MinecraftClient/Mapping/BlockHardness.cs new file mode 100644 index 00000000..311507b0 --- /dev/null +++ b/MinecraftClient/Mapping/BlockHardness.cs @@ -0,0 +1,1326 @@ +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace MinecraftClient.Mapping +{ + /// + /// Provides block hardness values and tool requirement data for mining calculations. + /// Data extracted from Minecraft 1.21.11 decompiled source (Blocks.java). + /// + public static class BlockHardness + { + /// + /// Default hardness for blocks not in the table (assumes stone-like). + /// + public const float DefaultHardness = 1.5f; + + /// + /// Get the hardness value for a block material. + /// Returns -1 for unbreakable blocks, 0 for instant-break blocks. + /// + public static float GetHardness(Material material) + { + if (HardnessTable.TryGetValue(material, out float hardness)) + return hardness; + return DefaultHardness; + } + + /// + /// Check whether a block requires the correct tool to get drops + /// (and uses the 100 divisor instead of 30 when mined without the correct tool). + /// + public static bool RequiresCorrectTool(Material material) + { + return RequiresCorrectToolSet.Contains(material); + } + + private static readonly FrozenDictionary HardnessTable = new Dictionary + { + // Hardness -1.0: 15 blocks + { Material.Barrier, -1.0f }, + { Material.Bedrock, -1.0f }, + { Material.ChainCommandBlock, -1.0f }, + { Material.CommandBlock, -1.0f }, + { Material.EndGateway, -1.0f }, + { Material.EndPortal, -1.0f }, + { Material.EndPortalFrame, -1.0f }, + { Material.Jigsaw, -1.0f }, + { Material.Light, -1.0f }, + { Material.MovingPiston, -1.0f }, + { Material.NetherPortal, -1.0f }, + { Material.RepeatingCommandBlock, -1.0f }, + { Material.StructureBlock, -1.0f }, + { Material.TestBlock, -1.0f }, + { Material.TestInstanceBlock, -1.0f }, + // Hardness 0.0: 443 blocks + { Material.AcaciaButton, 0.0f }, + { Material.AcaciaLeaves, 0.0f }, + { Material.AcaciaLog, 0.0f }, + { Material.AcaciaSapling, 0.0f }, + { Material.Air, 0.0f }, + { Material.Allium, 0.0f }, + { Material.AndesiteSlab, 0.0f }, + { Material.AndesiteWall, 0.0f }, + { Material.Azalea, 0.0f }, + { Material.AzaleaLeaves, 0.0f }, + { Material.AzureBluet, 0.0f }, + { Material.Bamboo, 0.0f }, + { Material.BambooBlock, 0.0f }, + { Material.BambooButton, 0.0f }, + { Material.BambooSapling, 0.0f }, + { Material.Beetroots, 0.0f }, + { Material.BirchButton, 0.0f }, + { Material.BirchLeaves, 0.0f }, + { Material.BirchLog, 0.0f }, + { Material.BirchSapling, 0.0f }, + { Material.BlackCandle, 0.0f }, + { Material.BlackCandleCake, 0.0f }, + { Material.BlackShulkerBox, 0.0f }, + { Material.BlackstoneWall, 0.0f }, + { Material.BlueCandle, 0.0f }, + { Material.BlueCandleCake, 0.0f }, + { Material.BlueOrchid, 0.0f }, + { Material.BlueShulkerBox, 0.0f }, + { Material.BrainCoral, 0.0f }, + { Material.BrainCoralFan, 0.0f }, + { Material.BrainCoralWallFan, 0.0f }, + { Material.BrickWall, 0.0f }, + { Material.BrownCandle, 0.0f }, + { Material.BrownCandleCake, 0.0f }, + { Material.BrownMushroom, 0.0f }, + { Material.BrownShulkerBox, 0.0f }, + { Material.BubbleColumn, 0.0f }, + { Material.BubbleCoral, 0.0f }, + { Material.BubbleCoralFan, 0.0f }, + { Material.BubbleCoralWallFan, 0.0f }, + { Material.Bush, 0.0f }, + { Material.CactusFlower, 0.0f }, + { Material.CalibratedSculkSensor, 0.0f }, + { Material.Candle, 0.0f }, + { Material.CandleCake, 0.0f }, + { Material.Carrots, 0.0f }, + { Material.CaveAir, 0.0f }, + { Material.CaveVines, 0.0f }, + { Material.CaveVinesPlant, 0.0f }, + { Material.CherryButton, 0.0f }, + { Material.CherryLog, 0.0f }, + { Material.CherrySapling, 0.0f }, + { Material.ChiseledCopper, 0.0f }, + { Material.ChiseledDeepslate, 0.0f }, + { Material.ChiseledTuff, 0.0f }, + { Material.ChiseledTuffBricks, 0.0f }, + { Material.ClosedEyeblossom, 0.0f }, + { Material.CobbledDeepslateSlab, 0.0f }, + { Material.CobbledDeepslateWall, 0.0f }, + { Material.CobblestoneWall, 0.0f }, + { Material.Comparator, 0.0f }, + { Material.CopperOre, 0.0f }, + { Material.CopperTorch, 0.0f }, + { Material.CopperWallTorch, 0.0f }, + { Material.Cornflower, 0.0f }, + { Material.CrackedDeepslateBricks, 0.0f }, + { Material.CrackedDeepslateTiles, 0.0f }, + { Material.CrackedPolishedBlackstoneBricks, 0.0f }, + { Material.CrimsonButton, 0.0f }, + { Material.CrimsonFungus, 0.0f }, + { Material.CrimsonRoots, 0.0f }, + { Material.CrimsonStem, 0.0f }, + { Material.CutCopper, 0.0f }, + { Material.CutCopperSlab, 0.0f }, + { Material.CutCopperStairs, 0.0f }, + { Material.CyanCandle, 0.0f }, + { Material.CyanCandleCake, 0.0f }, + { Material.CyanShulkerBox, 0.0f }, + { Material.Dandelion, 0.0f }, + { Material.DarkOakButton, 0.0f }, + { Material.DarkOakLeaves, 0.0f }, + { Material.DarkOakLog, 0.0f }, + { Material.DarkOakSapling, 0.0f }, + { Material.DeadBrainCoral, 0.0f }, + { Material.DeadBrainCoralFan, 0.0f }, + { Material.DeadBrainCoralWallFan, 0.0f }, + { Material.DeadBubbleCoral, 0.0f }, + { Material.DeadBubbleCoralFan, 0.0f }, + { Material.DeadBubbleCoralWallFan, 0.0f }, + { Material.DeadBush, 0.0f }, + { Material.DeadFireCoral, 0.0f }, + { Material.DeadFireCoralFan, 0.0f }, + { Material.DeadFireCoralWallFan, 0.0f }, + { Material.DeadHornCoral, 0.0f }, + { Material.DeadHornCoralFan, 0.0f }, + { Material.DeadHornCoralWallFan, 0.0f }, + { Material.DeadTubeCoral, 0.0f }, + { Material.DeadTubeCoralFan, 0.0f }, + { Material.DeadTubeCoralWallFan, 0.0f }, + { Material.DecoratedPot, 0.0f }, + { Material.DeepslateBrickSlab, 0.0f }, + { Material.DeepslateBrickWall, 0.0f }, + { Material.DeepslateBricks, 0.0f }, + { Material.DeepslateTileSlab, 0.0f }, + { Material.DeepslateTileWall, 0.0f }, + { Material.DeepslateTiles, 0.0f }, + { Material.DioriteSlab, 0.0f }, + { Material.DioriteWall, 0.0f }, + { Material.DriedGhast, 0.0f }, + { Material.EndRod, 0.0f }, + { Material.EndStoneBrickSlab, 0.0f }, + { Material.EndStoneBrickWall, 0.0f }, + { Material.ExposedChiseledCopper, 0.0f }, + { Material.ExposedCopper, 0.0f }, + { Material.ExposedCopperBulb, 0.0f }, + { Material.ExposedCopperChest, 0.0f }, + { Material.ExposedCopperDoor, 0.0f }, + { Material.ExposedCopperGolemStatue, 0.0f }, + { Material.ExposedCopperGrate, 0.0f }, + { Material.ExposedCopperTrapdoor, 0.0f }, + { Material.ExposedCutCopper, 0.0f }, + { Material.ExposedCutCopperSlab, 0.0f }, + { Material.ExposedCutCopperStairs, 0.0f }, + { Material.ExposedLightningRod, 0.0f }, + { Material.Fern, 0.0f }, + { Material.Fire, 0.0f }, + { Material.FireCoral, 0.0f }, + { Material.FireCoralFan, 0.0f }, + { Material.FireCoralWallFan, 0.0f }, + { Material.FireflyBush, 0.0f }, + { Material.FlowerPot, 0.0f }, + { Material.FloweringAzalea, 0.0f }, + { Material.FloweringAzaleaLeaves, 0.0f }, + { Material.Frogspawn, 0.0f }, + { Material.GildedBlackstone, 0.0f }, + { Material.GlassPane, 0.0f }, + { Material.GraniteSlab, 0.0f }, + { Material.GraniteWall, 0.0f }, + { Material.GrayCandle, 0.0f }, + { Material.GrayCandleCake, 0.0f }, + { Material.GrayShulkerBox, 0.0f }, + { Material.GreenCandle, 0.0f }, + { Material.GreenCandleCake, 0.0f }, + { Material.GreenShulkerBox, 0.0f }, + { Material.HangingRoots, 0.0f }, + { Material.HoneyBlock, 0.0f }, + { Material.HornCoral, 0.0f }, + { Material.HornCoralFan, 0.0f }, + { Material.HornCoralWallFan, 0.0f }, + { Material.InfestedChiseledStoneBricks, 0.0f }, + { Material.InfestedCobblestone, 0.0f }, + { Material.InfestedCrackedStoneBricks, 0.0f }, + { Material.InfestedDeepslate, 0.0f }, + { Material.InfestedMossyStoneBricks, 0.0f }, + { Material.InfestedStone, 0.0f }, + { Material.InfestedStoneBricks, 0.0f }, + { Material.JungleButton, 0.0f }, + { Material.JungleLeaves, 0.0f }, + { Material.JungleLog, 0.0f }, + { Material.JungleSapling, 0.0f }, + { Material.Kelp, 0.0f }, + { Material.KelpPlant, 0.0f }, + { Material.LargeAmethystBud, 0.0f }, + { Material.LargeFern, 0.0f }, + { Material.LavaCauldron, 0.0f }, + { Material.LeafLitter, 0.0f }, + { Material.LightBlueCandle, 0.0f }, + { Material.LightBlueCandleCake, 0.0f }, + { Material.LightBlueShulkerBox, 0.0f }, + { Material.LightGrayCandle, 0.0f }, + { Material.LightGrayCandleCake, 0.0f }, + { Material.LightGrayShulkerBox, 0.0f }, + { Material.Lilac, 0.0f }, + { Material.LilyOfTheValley, 0.0f }, + { Material.LilyPad, 0.0f }, + { Material.LimeCandle, 0.0f }, + { Material.LimeCandleCake, 0.0f }, + { Material.LimeShulkerBox, 0.0f }, + { Material.MagentaCandle, 0.0f }, + { Material.MagentaCandleCake, 0.0f }, + { Material.MagentaShulkerBox, 0.0f }, + { Material.MangroveButton, 0.0f }, + { Material.MangroveLeaves, 0.0f }, + { Material.MangroveLog, 0.0f }, + { Material.MangrovePropagule, 0.0f }, + { Material.MediumAmethystBud, 0.0f }, + { Material.MossyCobblestoneSlab, 0.0f }, + { Material.MossyCobblestoneWall, 0.0f }, + { Material.MossyStoneBrickSlab, 0.0f }, + { Material.MossyStoneBrickWall, 0.0f }, + { Material.Mud, 0.0f }, + { Material.MudBrickWall, 0.0f }, + { Material.NetherBrickWall, 0.0f }, + { Material.NetherSprouts, 0.0f }, + { Material.NetherWart, 0.0f }, + { Material.OakButton, 0.0f }, + { Material.OakLeaves, 0.0f }, + { Material.OakLog, 0.0f }, + { Material.OakSapling, 0.0f }, + { Material.OpenEyeblossom, 0.0f }, + { Material.OrangeCandle, 0.0f }, + { Material.OrangeCandleCake, 0.0f }, + { Material.OrangeShulkerBox, 0.0f }, + { Material.OrangeTulip, 0.0f }, + { Material.OxeyeDaisy, 0.0f }, + { Material.OxidizedChiseledCopper, 0.0f }, + { Material.OxidizedCopper, 0.0f }, + { Material.OxidizedCopperBulb, 0.0f }, + { Material.OxidizedCopperChest, 0.0f }, + { Material.OxidizedCopperDoor, 0.0f }, + { Material.OxidizedCopperGolemStatue, 0.0f }, + { Material.OxidizedCopperGrate, 0.0f }, + { Material.OxidizedCopperTrapdoor, 0.0f }, + { Material.OxidizedCutCopper, 0.0f }, + { Material.OxidizedCutCopperSlab, 0.0f }, + { Material.OxidizedCutCopperStairs, 0.0f }, + { Material.OxidizedLightningRod, 0.0f }, + { Material.PaleHangingMoss, 0.0f }, + { Material.PaleOakButton, 0.0f }, + { Material.PaleOakLog, 0.0f }, + { Material.PaleOakSapling, 0.0f }, + { Material.Peony, 0.0f }, + { Material.PinkCandle, 0.0f }, + { Material.PinkCandleCake, 0.0f }, + { Material.PinkPetals, 0.0f }, + { Material.PinkShulkerBox, 0.0f }, + { Material.PinkTulip, 0.0f }, + { Material.Piston, 0.0f }, + { Material.PitcherCrop, 0.0f }, + { Material.PitcherPlant, 0.0f }, + { Material.PolishedAndesiteSlab, 0.0f }, + { Material.PolishedBlackstoneBrickWall, 0.0f }, + { Material.PolishedBlackstoneButton, 0.0f }, + { Material.PolishedBlackstoneSlab, 0.0f }, + { Material.PolishedBlackstoneWall, 0.0f }, + { Material.PolishedDeepslate, 0.0f }, + { Material.PolishedDeepslateSlab, 0.0f }, + { Material.PolishedDeepslateWall, 0.0f }, + { Material.PolishedDioriteSlab, 0.0f }, + { Material.PolishedGraniteSlab, 0.0f }, + { Material.PolishedTuff, 0.0f }, + { Material.PolishedTuffSlab, 0.0f }, + { Material.PolishedTuffStairs, 0.0f }, + { Material.PolishedTuffWall, 0.0f }, + { Material.Poppy, 0.0f }, + { Material.Potatoes, 0.0f }, + { Material.PottedAcaciaSapling, 0.0f }, + { Material.PottedAllium, 0.0f }, + { Material.PottedAzaleaBush, 0.0f }, + { Material.PottedAzureBluet, 0.0f }, + { Material.PottedBamboo, 0.0f }, + { Material.PottedBirchSapling, 0.0f }, + { Material.PottedBlueOrchid, 0.0f }, + { Material.PottedBrownMushroom, 0.0f }, + { Material.PottedCactus, 0.0f }, + { Material.PottedCherrySapling, 0.0f }, + { Material.PottedClosedEyeblossom, 0.0f }, + { Material.PottedCornflower, 0.0f }, + { Material.PottedCrimsonFungus, 0.0f }, + { Material.PottedCrimsonRoots, 0.0f }, + { Material.PottedDandelion, 0.0f }, + { Material.PottedDarkOakSapling, 0.0f }, + { Material.PottedDeadBush, 0.0f }, + { Material.PottedFern, 0.0f }, + { Material.PottedFloweringAzaleaBush, 0.0f }, + { Material.PottedJungleSapling, 0.0f }, + { Material.PottedLilyOfTheValley, 0.0f }, + { Material.PottedMangrovePropagule, 0.0f }, + { Material.PottedOakSapling, 0.0f }, + { Material.PottedOpenEyeblossom, 0.0f }, + { Material.PottedOrangeTulip, 0.0f }, + { Material.PottedOxeyeDaisy, 0.0f }, + { Material.PottedPaleOakSapling, 0.0f }, + { Material.PottedPinkTulip, 0.0f }, + { Material.PottedPoppy, 0.0f }, + { Material.PottedRedMushroom, 0.0f }, + { Material.PottedRedTulip, 0.0f }, + { Material.PottedSpruceSapling, 0.0f }, + { Material.PottedTorchflower, 0.0f }, + { Material.PottedWarpedFungus, 0.0f }, + { Material.PottedWarpedRoots, 0.0f }, + { Material.PottedWhiteTulip, 0.0f }, + { Material.PottedWitherRose, 0.0f }, + { Material.PowderSnowCauldron, 0.0f }, + { Material.PrismarineWall, 0.0f }, + { Material.PurpleCandle, 0.0f }, + { Material.PurpleCandleCake, 0.0f }, + { Material.PurpleShulkerBox, 0.0f }, + { Material.QuartzBricks, 0.0f }, + { Material.RedCandle, 0.0f }, + { Material.RedCandleCake, 0.0f }, + { Material.RedMushroom, 0.0f }, + { Material.RedNetherBrickSlab, 0.0f }, + { Material.RedNetherBrickWall, 0.0f }, + { Material.RedSandstoneWall, 0.0f }, + { Material.RedShulkerBox, 0.0f }, + { Material.RedTulip, 0.0f }, + { Material.RedstoneTorch, 0.0f }, + { Material.RedstoneWallTorch, 0.0f }, + { Material.RedstoneWire, 0.0f }, + { Material.Repeater, 0.0f }, + { Material.ResinBlock, 0.0f }, + { Material.ResinClump, 0.0f }, + { Material.RoseBush, 0.0f }, + { Material.SandstoneWall, 0.0f }, + { Material.Scaffolding, 0.0f }, + { Material.SeaPickle, 0.0f }, + { Material.Seagrass, 0.0f }, + { Material.ShortDryGrass, 0.0f }, + { Material.ShortGrass, 0.0f }, + { Material.ShulkerBox, 0.0f }, + { Material.SlimeBlock, 0.0f }, + { Material.SmallAmethystBud, 0.0f }, + { Material.SmallDripleaf, 0.0f }, + { Material.SmoothBasalt, 0.0f }, + { Material.SmoothQuartzSlab, 0.0f }, + { Material.SmoothRedSandstoneSlab, 0.0f }, + { Material.SmoothSandstoneSlab, 0.0f }, + { Material.SoulFire, 0.0f }, + { Material.SoulTorch, 0.0f }, + { Material.SoulWallTorch, 0.0f }, + { Material.SporeBlossom, 0.0f }, + { Material.SpruceButton, 0.0f }, + { Material.SpruceLeaves, 0.0f }, + { Material.SpruceLog, 0.0f }, + { Material.SpruceSapling, 0.0f }, + { Material.StickyPiston, 0.0f }, + { Material.StoneBrickWall, 0.0f }, + { Material.StoneButton, 0.0f }, + { Material.StrippedAcaciaLog, 0.0f }, + { Material.StrippedBambooBlock, 0.0f }, + { Material.StrippedBirchLog, 0.0f }, + { Material.StrippedCherryLog, 0.0f }, + { Material.StrippedCrimsonStem, 0.0f }, + { Material.StrippedDarkOakLog, 0.0f }, + { Material.StrippedJungleLog, 0.0f }, + { Material.StrippedMangroveLog, 0.0f }, + { Material.StrippedMangroveWood, 0.0f }, + { Material.StrippedOakLog, 0.0f }, + { Material.StrippedPaleOakLog, 0.0f }, + { Material.StrippedSpruceLog, 0.0f }, + { Material.StrippedWarpedStem, 0.0f }, + { Material.StructureVoid, 0.0f }, + { Material.SugarCane, 0.0f }, + { Material.Sunflower, 0.0f }, + { Material.SweetBerryBush, 0.0f }, + { Material.TallDryGrass, 0.0f }, + { Material.TallGrass, 0.0f }, + { Material.TallSeagrass, 0.0f }, + { Material.TintedGlass, 0.0f }, + { Material.Tnt, 0.0f }, + { Material.Torch, 0.0f }, + { Material.Torchflower, 0.0f }, + { Material.TorchflowerCrop, 0.0f }, + { Material.Tripwire, 0.0f }, + { Material.TripwireHook, 0.0f }, + { Material.TubeCoral, 0.0f }, + { Material.TubeCoralFan, 0.0f }, + { Material.TubeCoralWallFan, 0.0f }, + { Material.TuffBrickSlab, 0.0f }, + { Material.TuffBrickStairs, 0.0f }, + { Material.TuffBrickWall, 0.0f }, + { Material.TuffBricks, 0.0f }, + { Material.TuffSlab, 0.0f }, + { Material.TuffStairs, 0.0f }, + { Material.TuffWall, 0.0f }, + { Material.TwistingVines, 0.0f }, + { Material.TwistingVinesPlant, 0.0f }, + { Material.VoidAir, 0.0f }, + { Material.WallTorch, 0.0f }, + { Material.WarpedButton, 0.0f }, + { Material.WarpedFungus, 0.0f }, + { Material.WarpedRoots, 0.0f }, + { Material.WarpedStem, 0.0f }, + { Material.WaterCauldron, 0.0f }, + { Material.WaxedChiseledCopper, 0.0f }, + { Material.WaxedCopperBlock, 0.0f }, + { Material.WaxedCopperBulb, 0.0f }, + { Material.WaxedCopperChest, 0.0f }, + { Material.WaxedCopperDoor, 0.0f }, + { Material.WaxedCopperGolemStatue, 0.0f }, + { Material.WaxedCopperGrate, 0.0f }, + { Material.WaxedCopperTrapdoor, 0.0f }, + { Material.WaxedCutCopper, 0.0f }, + { Material.WaxedCutCopperSlab, 0.0f }, + { Material.WaxedExposedChiseledCopper, 0.0f }, + { Material.WaxedExposedCopper, 0.0f }, + { Material.WaxedExposedCopperBulb, 0.0f }, + { Material.WaxedExposedCopperChest, 0.0f }, + { Material.WaxedExposedCopperDoor, 0.0f }, + { Material.WaxedExposedCopperGolemStatue, 0.0f }, + { Material.WaxedExposedCopperGrate, 0.0f }, + { Material.WaxedExposedCopperTrapdoor, 0.0f }, + { Material.WaxedExposedCutCopper, 0.0f }, + { Material.WaxedExposedCutCopperSlab, 0.0f }, + { Material.WaxedExposedLightningRod, 0.0f }, + { Material.WaxedLightningRod, 0.0f }, + { Material.WaxedOxidizedChiseledCopper, 0.0f }, + { Material.WaxedOxidizedCopper, 0.0f }, + { Material.WaxedOxidizedCopperBulb, 0.0f }, + { Material.WaxedOxidizedCopperChest, 0.0f }, + { Material.WaxedOxidizedCopperDoor, 0.0f }, + { Material.WaxedOxidizedCopperGolemStatue, 0.0f }, + { Material.WaxedOxidizedCopperGrate, 0.0f }, + { Material.WaxedOxidizedCopperTrapdoor, 0.0f }, + { Material.WaxedOxidizedCutCopper, 0.0f }, + { Material.WaxedOxidizedCutCopperSlab, 0.0f }, + { Material.WaxedOxidizedLightningRod, 0.0f }, + { Material.WaxedWeatheredChiseledCopper, 0.0f }, + { Material.WaxedWeatheredCopper, 0.0f }, + { Material.WaxedWeatheredCopperBulb, 0.0f }, + { Material.WaxedWeatheredCopperChest, 0.0f }, + { Material.WaxedWeatheredCopperDoor, 0.0f }, + { Material.WaxedWeatheredCopperGolemStatue, 0.0f }, + { Material.WaxedWeatheredCopperGrate, 0.0f }, + { Material.WaxedWeatheredCopperTrapdoor, 0.0f }, + { Material.WaxedWeatheredCutCopper, 0.0f }, + { Material.WaxedWeatheredCutCopperSlab, 0.0f }, + { Material.WaxedWeatheredLightningRod, 0.0f }, + { Material.WeatheredChiseledCopper, 0.0f }, + { Material.WeatheredCopper, 0.0f }, + { Material.WeatheredCopperBulb, 0.0f }, + { Material.WeatheredCopperChest, 0.0f }, + { Material.WeatheredCopperDoor, 0.0f }, + { Material.WeatheredCopperGolemStatue, 0.0f }, + { Material.WeatheredCopperGrate, 0.0f }, + { Material.WeatheredCopperTrapdoor, 0.0f }, + { Material.WeatheredCutCopper, 0.0f }, + { Material.WeatheredCutCopperSlab, 0.0f }, + { Material.WeatheredCutCopperStairs, 0.0f }, + { Material.WeatheredLightningRod, 0.0f }, + { Material.WeepingVines, 0.0f }, + { Material.WeepingVinesPlant, 0.0f }, + { Material.Wheat, 0.0f }, + { Material.WhiteCandle, 0.0f }, + { Material.WhiteCandleCake, 0.0f }, + { Material.WhiteShulkerBox, 0.0f }, + { Material.WhiteTulip, 0.0f }, + { Material.Wildflowers, 0.0f }, + { Material.WitherRose, 0.0f }, + { Material.YellowCandle, 0.0f }, + { Material.YellowCandleCake, 0.0f }, + { Material.YellowShulkerBox, 0.0f }, + // Hardness 0.1: 23 blocks + { Material.BigDripleaf, 0.1f }, + { Material.BigDripleafStem, 0.1f }, + { Material.BlackCarpet, 0.1f }, + { Material.BlueCarpet, 0.1f }, + { Material.BrownCarpet, 0.1f }, + { Material.CyanCarpet, 0.1f }, + { Material.GrayCarpet, 0.1f }, + { Material.GreenCarpet, 0.1f }, + { Material.LightBlueCarpet, 0.1f }, + { Material.LightGrayCarpet, 0.1f }, + { Material.LimeCarpet, 0.1f }, + { Material.MagentaCarpet, 0.1f }, + { Material.MossBlock, 0.1f }, + { Material.MossCarpet, 0.1f }, + { Material.OrangeCarpet, 0.1f }, + { Material.PaleMossBlock, 0.1f }, + { Material.PaleMossCarpet, 0.1f }, + { Material.PinkCarpet, 0.1f }, + { Material.PurpleCarpet, 0.1f }, + { Material.RedCarpet, 0.1f }, + { Material.Snow, 0.1f }, + { Material.WhiteCarpet, 0.1f }, + { Material.YellowCarpet, 0.1f }, + // Hardness 0.2: 12 blocks + { Material.BrownMushroomBlock, 0.2f }, + { Material.CherryLeaves, 0.2f }, + { Material.Cocoa, 0.2f }, + { Material.DaylightDetector, 0.2f }, + { Material.GlowLichen, 0.2f }, + { Material.MushroomStem, 0.2f }, + { Material.PaleOakLeaves, 0.2f }, + { Material.RedMushroomBlock, 0.2f }, + { Material.Sculk, 0.2f }, + { Material.SculkVein, 0.2f }, + { Material.SnowBlock, 0.2f }, + { Material.Vine, 0.2f }, + { Material.PowderSnow, 0.25f }, + { Material.SuspiciousGravel, 0.25f }, + { Material.SuspiciousSand, 0.25f }, + // Hardness 0.3: 24 blocks + { Material.BeeNest, 0.3f }, + { Material.BlackStainedGlassPane, 0.3f }, + { Material.BlueStainedGlassPane, 0.3f }, + { Material.BrownStainedGlassPane, 0.3f }, + { Material.CyanStainedGlassPane, 0.3f }, + { Material.Glass, 0.3f }, + { Material.Glowstone, 0.3f }, + { Material.GrayStainedGlassPane, 0.3f }, + { Material.GreenStainedGlassPane, 0.3f }, + { Material.LightBlueStainedGlassPane, 0.3f }, + { Material.LightGrayStainedGlassPane, 0.3f }, + { Material.LimeStainedGlassPane, 0.3f }, + { Material.MagentaStainedGlassPane, 0.3f }, + { Material.OchreFroglight, 0.3f }, + { Material.OrangeStainedGlassPane, 0.3f }, + { Material.PearlescentFroglight, 0.3f }, + { Material.PinkStainedGlassPane, 0.3f }, + { Material.PurpleStainedGlassPane, 0.3f }, + { Material.RedStainedGlassPane, 0.3f }, + { Material.RedstoneLamp, 0.3f }, + { Material.SeaLantern, 0.3f }, + { Material.VerdantFroglight, 0.3f }, + { Material.WhiteStainedGlassPane, 0.3f }, + { Material.YellowStainedGlassPane, 0.3f }, + { Material.Cactus, 0.4f }, + { Material.ChorusFlower, 0.4f }, + { Material.ChorusPlant, 0.4f }, + { Material.CrimsonNylium, 0.4f }, + { Material.Ladder, 0.4f }, + { Material.Netherrack, 0.4f }, + { Material.WarpedNylium, 0.4f }, + // Hardness 0.5: 52 blocks + { Material.AcaciaPressurePlate, 0.5f }, + { Material.BambooPressurePlate, 0.5f }, + { Material.BirchPressurePlate, 0.5f }, + { Material.BlackConcretePowder, 0.5f }, + { Material.BlueConcretePowder, 0.5f }, + { Material.BrewingStand, 0.5f }, + { Material.BrownConcretePowder, 0.5f }, + { Material.Cake, 0.5f }, + { Material.CherryPressurePlate, 0.5f }, + { Material.CoarseDirt, 0.5f }, + { Material.CrimsonPressurePlate, 0.5f }, + { Material.CyanConcretePowder, 0.5f }, + { Material.DarkOakPressurePlate, 0.5f }, + { Material.Dirt, 0.5f }, + { Material.DriedKelpBlock, 0.5f }, + { Material.FrostedIce, 0.5f }, + { Material.GrayConcretePowder, 0.5f }, + { Material.GreenConcretePowder, 0.5f }, + { Material.HayBlock, 0.5f }, + { Material.HeavyWeightedPressurePlate, 0.5f }, + { Material.Ice, 0.5f }, + { Material.JunglePressurePlate, 0.5f }, + { Material.Lever, 0.5f }, + { Material.LightBlueConcretePowder, 0.5f }, + { Material.LightGrayConcretePowder, 0.5f }, + { Material.LightWeightedPressurePlate, 0.5f }, + { Material.LimeConcretePowder, 0.5f }, + { Material.MagentaConcretePowder, 0.5f }, + { Material.MagmaBlock, 0.5f }, + { Material.MangrovePressurePlate, 0.5f }, + { Material.OakPressurePlate, 0.5f }, + { Material.OrangeConcretePowder, 0.5f }, + { Material.PackedIce, 0.5f }, + { Material.PaleOakPressurePlate, 0.5f }, + { Material.PinkConcretePowder, 0.5f }, + { Material.Podzol, 0.5f }, + { Material.PolishedBlackstonePressurePlate, 0.5f }, + { Material.PurpleConcretePowder, 0.5f }, + { Material.RedConcretePowder, 0.5f }, + { Material.RedSand, 0.5f }, + { Material.RootedDirt, 0.5f }, + { Material.Sand, 0.5f }, + { Material.SnifferEgg, 0.5f }, + { Material.SoulSand, 0.5f }, + { Material.SoulSoil, 0.5f }, + { Material.SprucePressurePlate, 0.5f }, + { Material.StonePressurePlate, 0.5f }, + { Material.Target, 0.5f }, + { Material.TurtleEgg, 0.5f }, + { Material.WarpedPressurePlate, 0.5f }, + { Material.WhiteConcretePowder, 0.5f }, + { Material.YellowConcretePowder, 0.5f }, + // Hardness 0.6: 10 blocks + { Material.Beehive, 0.6f }, + { Material.Clay, 0.6f }, + { Material.Composter, 0.6f }, + { Material.Farmland, 0.6f }, + { Material.GrassBlock, 0.6f }, + { Material.Gravel, 0.6f }, + { Material.HoneycombBlock, 0.6f }, + { Material.Mycelium, 0.6f }, + { Material.Sponge, 0.6f }, + { Material.WetSponge, 0.6f }, + { Material.DirtPath, 0.65f }, + { Material.ActivatorRail, 0.7f }, + { Material.DetectorRail, 0.7f }, + { Material.MangroveRoots, 0.7f }, + { Material.MuddyMangroveRoots, 0.7f }, + { Material.PoweredRail, 0.7f }, + { Material.Rail, 0.7f }, + { Material.Calcite, 0.75f }, + // Hardness 0.8: 26 blocks + { Material.BlackWool, 0.8f }, + { Material.BlueWool, 0.8f }, + { Material.BrownWool, 0.8f }, + { Material.ChiseledQuartzBlock, 0.8f }, + { Material.ChiseledRedSandstone, 0.8f }, + { Material.ChiseledSandstone, 0.8f }, + { Material.CutRedSandstone, 0.8f }, + { Material.CutSandstone, 0.8f }, + { Material.CyanWool, 0.8f }, + { Material.GrayWool, 0.8f }, + { Material.GreenWool, 0.8f }, + { Material.LightBlueWool, 0.8f }, + { Material.LightGrayWool, 0.8f }, + { Material.LimeWool, 0.8f }, + { Material.MagentaWool, 0.8f }, + { Material.NoteBlock, 0.8f }, + { Material.OrangeWool, 0.8f }, + { Material.PinkWool, 0.8f }, + { Material.PurpleWool, 0.8f }, + { Material.QuartzBlock, 0.8f }, + { Material.QuartzPillar, 0.8f }, + { Material.RedSandstone, 0.8f }, + { Material.RedWool, 0.8f }, + { Material.Sandstone, 0.8f }, + { Material.WhiteWool, 0.8f }, + { Material.YellowWool, 0.8f }, + // Hardness 1.0: 100 blocks + { Material.AcaciaHangingSign, 1.0f }, + { Material.AcaciaSign, 1.0f }, + { Material.AcaciaWallHangingSign, 1.0f }, + { Material.AcaciaWallSign, 1.0f }, + { Material.BambooHangingSign, 1.0f }, + { Material.BambooSign, 1.0f }, + { Material.BambooWallHangingSign, 1.0f }, + { Material.BambooWallSign, 1.0f }, + { Material.BirchHangingSign, 1.0f }, + { Material.BirchSign, 1.0f }, + { Material.BirchWallHangingSign, 1.0f }, + { Material.BirchWallSign, 1.0f }, + { Material.BlackBanner, 1.0f }, + { Material.BlackWallBanner, 1.0f }, + { Material.BlueBanner, 1.0f }, + { Material.BlueWallBanner, 1.0f }, + { Material.BrownBanner, 1.0f }, + { Material.BrownWallBanner, 1.0f }, + { Material.CarvedPumpkin, 1.0f }, + { Material.CherryHangingSign, 1.0f }, + { Material.CherrySign, 1.0f }, + { Material.CherryWallHangingSign, 1.0f }, + { Material.CherryWallSign, 1.0f }, + { Material.CreeperHead, 1.0f }, + { Material.CreeperWallHead, 1.0f }, + { Material.CrimsonHangingSign, 1.0f }, + { Material.CrimsonSign, 1.0f }, + { Material.CrimsonWallHangingSign, 1.0f }, + { Material.CrimsonWallSign, 1.0f }, + { Material.CyanBanner, 1.0f }, + { Material.CyanWallBanner, 1.0f }, + { Material.DarkOakHangingSign, 1.0f }, + { Material.DarkOakSign, 1.0f }, + { Material.DarkOakWallHangingSign, 1.0f }, + { Material.DarkOakWallSign, 1.0f }, + { Material.DragonHead, 1.0f }, + { Material.DragonWallHead, 1.0f }, + { Material.GrayBanner, 1.0f }, + { Material.GrayWallBanner, 1.0f }, + { Material.GreenBanner, 1.0f }, + { Material.GreenWallBanner, 1.0f }, + { Material.JackOLantern, 1.0f }, + { Material.JungleHangingSign, 1.0f }, + { Material.JungleSign, 1.0f }, + { Material.JungleWallHangingSign, 1.0f }, + { Material.JungleWallSign, 1.0f }, + { Material.LightBlueBanner, 1.0f }, + { Material.LightBlueWallBanner, 1.0f }, + { Material.LightGrayBanner, 1.0f }, + { Material.LightGrayWallBanner, 1.0f }, + { Material.LimeBanner, 1.0f }, + { Material.LimeWallBanner, 1.0f }, + { Material.MagentaBanner, 1.0f }, + { Material.MagentaWallBanner, 1.0f }, + { Material.MangroveHangingSign, 1.0f }, + { Material.MangroveSign, 1.0f }, + { Material.MangroveWallHangingSign, 1.0f }, + { Material.MangroveWallSign, 1.0f }, + { Material.NetherWartBlock, 1.0f }, + { Material.OakHangingSign, 1.0f }, + { Material.OakSign, 1.0f }, + { Material.OakWallHangingSign, 1.0f }, + { Material.OakWallSign, 1.0f }, + { Material.OrangeBanner, 1.0f }, + { Material.OrangeWallBanner, 1.0f }, + { Material.PackedMud, 1.0f }, + { Material.PaleOakHangingSign, 1.0f }, + { Material.PaleOakSign, 1.0f }, + { Material.PaleOakWallHangingSign, 1.0f }, + { Material.PaleOakWallSign, 1.0f }, + { Material.PiglinHead, 1.0f }, + { Material.PiglinWallHead, 1.0f }, + { Material.PinkBanner, 1.0f }, + { Material.PinkWallBanner, 1.0f }, + { Material.PlayerHead, 1.0f }, + { Material.PlayerWallHead, 1.0f }, + { Material.PurpleBanner, 1.0f }, + { Material.PurpleWallBanner, 1.0f }, + { Material.RedBanner, 1.0f }, + { Material.RedWallBanner, 1.0f }, + { Material.Shroomlight, 1.0f }, + { Material.SkeletonSkull, 1.0f }, + { Material.SkeletonWallSkull, 1.0f }, + { Material.SpruceHangingSign, 1.0f }, + { Material.SpruceSign, 1.0f }, + { Material.SpruceWallHangingSign, 1.0f }, + { Material.SpruceWallSign, 1.0f }, + { Material.WarpedHangingSign, 1.0f }, + { Material.WarpedSign, 1.0f }, + { Material.WarpedWallHangingSign, 1.0f }, + { Material.WarpedWallSign, 1.0f }, + { Material.WarpedWartBlock, 1.0f }, + { Material.WhiteBanner, 1.0f }, + { Material.WhiteWallBanner, 1.0f }, + { Material.WitherSkeletonSkull, 1.0f }, + { Material.WitherSkeletonWallSkull, 1.0f }, + { Material.YellowBanner, 1.0f }, + { Material.YellowWallBanner, 1.0f }, + { Material.ZombieHead, 1.0f }, + { Material.ZombieWallHead, 1.0f }, + // Hardness 1.25: 19 blocks + { Material.Basalt, 1.25f }, + { Material.BlackTerracotta, 1.25f }, + { Material.BlueTerracotta, 1.25f }, + { Material.BrownTerracotta, 1.25f }, + { Material.CyanTerracotta, 1.25f }, + { Material.GrayTerracotta, 1.25f }, + { Material.GreenTerracotta, 1.25f }, + { Material.LightBlueTerracotta, 1.25f }, + { Material.LightGrayTerracotta, 1.25f }, + { Material.LimeTerracotta, 1.25f }, + { Material.MagentaTerracotta, 1.25f }, + { Material.OrangeTerracotta, 1.25f }, + { Material.PinkTerracotta, 1.25f }, + { Material.PolishedBasalt, 1.25f }, + { Material.PurpleTerracotta, 1.25f }, + { Material.RedTerracotta, 1.25f }, + { Material.Terracotta, 1.25f }, + { Material.WhiteTerracotta, 1.25f }, + { Material.YellowTerracotta, 1.25f }, + // Hardness 1.4: 16 blocks + { Material.BlackGlazedTerracotta, 1.4f }, + { Material.BlueGlazedTerracotta, 1.4f }, + { Material.BrownGlazedTerracotta, 1.4f }, + { Material.CyanGlazedTerracotta, 1.4f }, + { Material.GrayGlazedTerracotta, 1.4f }, + { Material.GreenGlazedTerracotta, 1.4f }, + { Material.LightBlueGlazedTerracotta, 1.4f }, + { Material.LightGrayGlazedTerracotta, 1.4f }, + { Material.LimeGlazedTerracotta, 1.4f }, + { Material.MagentaGlazedTerracotta, 1.4f }, + { Material.OrangeGlazedTerracotta, 1.4f }, + { Material.PinkGlazedTerracotta, 1.4f }, + { Material.PurpleGlazedTerracotta, 1.4f }, + { Material.RedGlazedTerracotta, 1.4f }, + { Material.WhiteGlazedTerracotta, 1.4f }, + { Material.YellowGlazedTerracotta, 1.4f }, + // Hardness 1.5: 49 blocks + { Material.AmethystBlock, 1.5f }, + { Material.AmethystCluster, 1.5f }, + { Material.Andesite, 1.5f }, + { Material.Blackstone, 1.5f }, + { Material.Bookshelf, 1.5f }, + { Material.BrainCoralBlock, 1.5f }, + { Material.BubbleCoralBlock, 1.5f }, + { Material.BuddingAmethyst, 1.5f }, + { Material.ChiseledBookshelf, 1.5f }, + { Material.ChiseledPolishedBlackstone, 1.5f }, + { Material.ChiseledResinBricks, 1.5f }, + { Material.ChiseledStoneBricks, 1.5f }, + { Material.CrackedStoneBricks, 1.5f }, + { Material.Crafter, 1.5f }, + { Material.DarkPrismarine, 1.5f }, + { Material.DarkPrismarineSlab, 1.5f }, + { Material.DeadBrainCoralBlock, 1.5f }, + { Material.DeadBubbleCoralBlock, 1.5f }, + { Material.DeadFireCoralBlock, 1.5f }, + { Material.DeadHornCoralBlock, 1.5f }, + { Material.DeadTubeCoralBlock, 1.5f }, + { Material.Diorite, 1.5f }, + { Material.DripstoneBlock, 1.5f }, + { Material.FireCoralBlock, 1.5f }, + { Material.Granite, 1.5f }, + { Material.HornCoralBlock, 1.5f }, + { Material.MossyStoneBricks, 1.5f }, + { Material.MudBrickSlab, 1.5f }, + { Material.MudBricks, 1.5f }, + { Material.PistonHead, 1.5f }, + { Material.PointedDripstone, 1.5f }, + { Material.PolishedAndesite, 1.5f }, + { Material.PolishedBlackstoneBricks, 1.5f }, + { Material.PolishedDiorite, 1.5f }, + { Material.PolishedGranite, 1.5f }, + { Material.Prismarine, 1.5f }, + { Material.PrismarineBrickSlab, 1.5f }, + { Material.PrismarineBricks, 1.5f }, + { Material.PrismarineSlab, 1.5f }, + { Material.PurpurBlock, 1.5f }, + { Material.PurpurPillar, 1.5f }, + { Material.ResinBrickSlab, 1.5f }, + { Material.ResinBrickWall, 1.5f }, + { Material.ResinBricks, 1.5f }, + { Material.SculkSensor, 1.5f }, + { Material.Stone, 1.5f }, + { Material.StoneBricks, 1.5f }, + { Material.TubeCoralBlock, 1.5f }, + { Material.Tuff, 1.5f }, + // Hardness 1.8: 16 blocks + { Material.BlackConcrete, 1.8f }, + { Material.BlueConcrete, 1.8f }, + { Material.BrownConcrete, 1.8f }, + { Material.CyanConcrete, 1.8f }, + { Material.GrayConcrete, 1.8f }, + { Material.GreenConcrete, 1.8f }, + { Material.LightBlueConcrete, 1.8f }, + { Material.LightGrayConcrete, 1.8f }, + { Material.LimeConcrete, 1.8f }, + { Material.MagentaConcrete, 1.8f }, + { Material.OrangeConcrete, 1.8f }, + { Material.PinkConcrete, 1.8f }, + { Material.PurpleConcrete, 1.8f }, + { Material.RedConcrete, 1.8f }, + { Material.WhiteConcrete, 1.8f }, + { Material.YellowConcrete, 1.8f }, + // Hardness 2.0: 117 blocks + { Material.AcaciaFence, 2.0f }, + { Material.AcaciaFenceGate, 2.0f }, + { Material.AcaciaPlanks, 2.0f }, + { Material.AcaciaShelf, 2.0f }, + { Material.AcaciaSlab, 2.0f }, + { Material.AcaciaWood, 2.0f }, + { Material.BambooFence, 2.0f }, + { Material.BambooFenceGate, 2.0f }, + { Material.BambooMosaic, 2.0f }, + { Material.BambooMosaicSlab, 2.0f }, + { Material.BambooPlanks, 2.0f }, + { Material.BambooShelf, 2.0f }, + { Material.BambooSlab, 2.0f }, + { Material.BirchFence, 2.0f }, + { Material.BirchFenceGate, 2.0f }, + { Material.BirchPlanks, 2.0f }, + { Material.BirchShelf, 2.0f }, + { Material.BirchSlab, 2.0f }, + { Material.BirchWood, 2.0f }, + { Material.BlackstoneSlab, 2.0f }, + { Material.BoneBlock, 2.0f }, + { Material.BrickSlab, 2.0f }, + { Material.Bricks, 2.0f }, + { Material.Campfire, 2.0f }, + { Material.Cauldron, 2.0f }, + { Material.CherryFence, 2.0f }, + { Material.CherryFenceGate, 2.0f }, + { Material.CherryPlanks, 2.0f }, + { Material.CherryShelf, 2.0f }, + { Material.CherrySlab, 2.0f }, + { Material.CherryWood, 2.0f }, + { Material.ChiseledNetherBricks, 2.0f }, + { Material.Cobblestone, 2.0f }, + { Material.CobblestoneSlab, 2.0f }, + { Material.CrackedNetherBricks, 2.0f }, + { Material.CrimsonFence, 2.0f }, + { Material.CrimsonFenceGate, 2.0f }, + { Material.CrimsonHyphae, 2.0f }, + { Material.CrimsonPlanks, 2.0f }, + { Material.CrimsonShelf, 2.0f }, + { Material.CrimsonSlab, 2.0f }, + { Material.CutRedSandstoneSlab, 2.0f }, + { Material.CutSandstoneSlab, 2.0f }, + { Material.DarkOakFence, 2.0f }, + { Material.DarkOakFenceGate, 2.0f }, + { Material.DarkOakPlanks, 2.0f }, + { Material.DarkOakShelf, 2.0f }, + { Material.DarkOakSlab, 2.0f }, + { Material.DarkOakWood, 2.0f }, + { Material.Grindstone, 2.0f }, + { Material.Jukebox, 2.0f }, + { Material.JungleFence, 2.0f }, + { Material.JungleFenceGate, 2.0f }, + { Material.JunglePlanks, 2.0f }, + { Material.JungleShelf, 2.0f }, + { Material.JungleSlab, 2.0f }, + { Material.JungleWood, 2.0f }, + { Material.MangroveFence, 2.0f }, + { Material.MangroveFenceGate, 2.0f }, + { Material.MangrovePlanks, 2.0f }, + { Material.MangroveShelf, 2.0f }, + { Material.MangroveSlab, 2.0f }, + { Material.MangroveWood, 2.0f }, + { Material.MossyCobblestone, 2.0f }, + { Material.NetherBrickFence, 2.0f }, + { Material.NetherBrickSlab, 2.0f }, + { Material.NetherBricks, 2.0f }, + { Material.OakFence, 2.0f }, + { Material.OakFenceGate, 2.0f }, + { Material.OakPlanks, 2.0f }, + { Material.OakShelf, 2.0f }, + { Material.OakSlab, 2.0f }, + { Material.OakWood, 2.0f }, + { Material.PaleOakFence, 2.0f }, + { Material.PaleOakFenceGate, 2.0f }, + { Material.PaleOakPlanks, 2.0f }, + { Material.PaleOakShelf, 2.0f }, + { Material.PaleOakSlab, 2.0f }, + { Material.PaleOakWood, 2.0f }, + { Material.PetrifiedOakSlab, 2.0f }, + { Material.PolishedBlackstone, 2.0f }, + { Material.PolishedBlackstoneBrickSlab, 2.0f }, + { Material.PurpurSlab, 2.0f }, + { Material.QuartzSlab, 2.0f }, + { Material.RedNetherBricks, 2.0f }, + { Material.RedSandstoneSlab, 2.0f }, + { Material.SandstoneSlab, 2.0f }, + { Material.SmoothQuartz, 2.0f }, + { Material.SmoothRedSandstone, 2.0f }, + { Material.SmoothSandstone, 2.0f }, + { Material.SmoothStone, 2.0f }, + { Material.SmoothStoneSlab, 2.0f }, + { Material.SoulCampfire, 2.0f }, + { Material.SpruceFence, 2.0f }, + { Material.SpruceFenceGate, 2.0f }, + { Material.SprucePlanks, 2.0f }, + { Material.SpruceShelf, 2.0f }, + { Material.SpruceSlab, 2.0f }, + { Material.SpruceWood, 2.0f }, + { Material.StoneBrickSlab, 2.0f }, + { Material.StoneSlab, 2.0f }, + { Material.StrippedAcaciaWood, 2.0f }, + { Material.StrippedBirchWood, 2.0f }, + { Material.StrippedCherryWood, 2.0f }, + { Material.StrippedCrimsonHyphae, 2.0f }, + { Material.StrippedDarkOakWood, 2.0f }, + { Material.StrippedJungleWood, 2.0f }, + { Material.StrippedOakWood, 2.0f }, + { Material.StrippedPaleOakWood, 2.0f }, + { Material.StrippedSpruceWood, 2.0f }, + { Material.StrippedWarpedHyphae, 2.0f }, + { Material.WarpedFence, 2.0f }, + { Material.WarpedFenceGate, 2.0f }, + { Material.WarpedHyphae, 2.0f }, + { Material.WarpedPlanks, 2.0f }, + { Material.WarpedShelf, 2.0f }, + { Material.WarpedSlab, 2.0f }, + // Hardness 2.5: 9 blocks + { Material.Barrel, 2.5f }, + { Material.CartographyTable, 2.5f }, + { Material.Chest, 2.5f }, + { Material.CraftingTable, 2.5f }, + { Material.FletchingTable, 2.5f }, + { Material.Lectern, 2.5f }, + { Material.Loom, 2.5f }, + { Material.SmithingTable, 2.5f }, + { Material.TrappedChest, 2.5f }, + { Material.BlueIce, 2.8f }, + // Hardness 3.0: 53 blocks + { Material.AcaciaDoor, 3.0f }, + { Material.AcaciaTrapdoor, 3.0f }, + { Material.BambooDoor, 3.0f }, + { Material.BambooTrapdoor, 3.0f }, + { Material.Beacon, 3.0f }, + { Material.BirchDoor, 3.0f }, + { Material.BirchTrapdoor, 3.0f }, + { Material.CherryDoor, 3.0f }, + { Material.CherryTrapdoor, 3.0f }, + { Material.CoalOre, 3.0f }, + { Material.Conduit, 3.0f }, + { Material.CopperBlock, 3.0f }, + { Material.CopperBulb, 3.0f }, + { Material.CopperChest, 3.0f }, + { Material.CopperDoor, 3.0f }, + { Material.CopperGolemStatue, 3.0f }, + { Material.CopperGrate, 3.0f }, + { Material.CopperTrapdoor, 3.0f }, + { Material.CrimsonDoor, 3.0f }, + { Material.CrimsonTrapdoor, 3.0f }, + { Material.DarkOakDoor, 3.0f }, + { Material.DarkOakTrapdoor, 3.0f }, + { Material.Deepslate, 3.0f }, + { Material.DiamondOre, 3.0f }, + { Material.DragonEgg, 3.0f }, + { Material.EmeraldOre, 3.0f }, + { Material.EndStone, 3.0f }, + { Material.EndStoneBricks, 3.0f }, + { Material.GoldBlock, 3.0f }, + { Material.GoldOre, 3.0f }, + { Material.Hopper, 3.0f }, + { Material.IronOre, 3.0f }, + { Material.JungleDoor, 3.0f }, + { Material.JungleTrapdoor, 3.0f }, + { Material.LapisBlock, 3.0f }, + { Material.LapisOre, 3.0f }, + { Material.LightningRod, 3.0f }, + { Material.MangroveDoor, 3.0f }, + { Material.MangroveTrapdoor, 3.0f }, + { Material.NetherGoldOre, 3.0f }, + { Material.NetherQuartzOre, 3.0f }, + { Material.OakDoor, 3.0f }, + { Material.OakTrapdoor, 3.0f }, + { Material.Observer, 3.0f }, + { Material.PaleOakDoor, 3.0f }, + { Material.PaleOakTrapdoor, 3.0f }, + { Material.RedstoneOre, 3.0f }, + { Material.SculkCatalyst, 3.0f }, + { Material.SculkShrieker, 3.0f }, + { Material.SpruceDoor, 3.0f }, + { Material.SpruceTrapdoor, 3.0f }, + { Material.WarpedDoor, 3.0f }, + { Material.WarpedTrapdoor, 3.0f }, + // Hardness 3.5: 10 blocks + { Material.BlastFurnace, 3.5f }, + { Material.CobbledDeepslate, 3.5f }, + { Material.Dispenser, 3.5f }, + { Material.Dropper, 3.5f }, + { Material.Furnace, 3.5f }, + { Material.Lantern, 3.5f }, + { Material.Lodestone, 3.5f }, + { Material.Smoker, 3.5f }, + { Material.SoulLantern, 3.5f }, + { Material.Stonecutter, 3.5f }, + { Material.Cobweb, 4.0f }, + { Material.DeepslateCoalOre, 4.5f }, + { Material.DeepslateCopperOre, 4.5f }, + { Material.DeepslateDiamondOre, 4.5f }, + { Material.DeepslateEmeraldOre, 4.5f }, + { Material.DeepslateGoldOre, 4.5f }, + { Material.DeepslateIronOre, 4.5f }, + { Material.DeepslateLapisOre, 4.5f }, + { Material.DeepslateRedstoneOre, 4.5f }, + // Hardness 5.0: 18 blocks + { Material.Anvil, 5.0f }, + { Material.Bell, 5.0f }, + { Material.ChippedAnvil, 5.0f }, + { Material.CoalBlock, 5.0f }, + { Material.DamagedAnvil, 5.0f }, + { Material.DiamondBlock, 5.0f }, + { Material.EmeraldBlock, 5.0f }, + { Material.EnchantingTable, 5.0f }, + { Material.IronBars, 5.0f }, + { Material.IronBlock, 5.0f }, + { Material.IronChain, 5.0f }, + { Material.IronDoor, 5.0f }, + { Material.IronTrapdoor, 5.0f }, + { Material.RawCopperBlock, 5.0f }, + { Material.RawGoldBlock, 5.0f }, + { Material.RawIronBlock, 5.0f }, + { Material.RedstoneBlock, 5.0f }, + { Material.Spawner, 5.0f }, + { Material.CreakingHeart, 10.0f }, + { Material.HeavyCore, 10.0f }, + { Material.EnderChest, 22.5f }, + { Material.AncientDebris, 30.0f }, + { Material.CryingObsidian, 50.0f }, + { Material.NetheriteBlock, 50.0f }, + { Material.Obsidian, 50.0f }, + { Material.RespawnAnchor, 50.0f }, + { Material.TrialSpawner, 50.0f }, + { Material.Vault, 50.0f }, + { Material.ReinforcedDeepslate, 55.0f }, + { Material.Lava, 100.0f }, + { Material.Water, 100.0f }, + }.ToFrozenDictionary(); + + private static readonly FrozenSet RequiresCorrectToolSet = new HashSet + { + Material.AmethystBlock, + Material.AncientDebris, + Material.Andesite, + Material.Anvil, + Material.Basalt, + Material.BlackConcrete, + Material.BlackGlazedTerracotta, + Material.BlackTerracotta, + Material.Blackstone, + Material.BlastFurnace, + Material.BlueConcrete, + Material.BlueGlazedTerracotta, + Material.BlueTerracotta, + Material.BoneBlock, + Material.BrainCoralBlock, + Material.BrickSlab, + Material.Bricks, + Material.BrownConcrete, + Material.BrownGlazedTerracotta, + Material.BrownTerracotta, + Material.BubbleCoralBlock, + Material.BuddingAmethyst, + Material.Calcite, + Material.Cauldron, + Material.ChainCommandBlock, + Material.ChippedAnvil, + Material.ChiseledNetherBricks, + Material.ChiseledQuartzBlock, + Material.ChiseledRedSandstone, + Material.ChiseledResinBricks, + Material.ChiseledSandstone, + Material.ChiseledStoneBricks, + Material.CoalBlock, + Material.CoalOre, + Material.Cobblestone, + Material.CobblestoneSlab, + Material.Cobweb, + Material.CommandBlock, + Material.CopperBlock, + Material.CopperBulb, + Material.CopperChest, + Material.CopperGrate, + Material.CopperTrapdoor, + Material.CrackedNetherBricks, + Material.CrackedStoneBricks, + Material.CrimsonNylium, + Material.CryingObsidian, + Material.CutRedSandstone, + Material.CutRedSandstoneSlab, + Material.CutSandstone, + Material.CutSandstoneSlab, + Material.CyanConcrete, + Material.CyanGlazedTerracotta, + Material.CyanTerracotta, + Material.DamagedAnvil, + Material.DarkPrismarine, + Material.DarkPrismarineSlab, + Material.DeadBrainCoral, + Material.DeadBrainCoralBlock, + Material.DeadBrainCoralFan, + Material.DeadBrainCoralWallFan, + Material.DeadBubbleCoral, + Material.DeadBubbleCoralBlock, + Material.DeadBubbleCoralFan, + Material.DeadBubbleCoralWallFan, + Material.DeadFireCoral, + Material.DeadFireCoralBlock, + Material.DeadFireCoralFan, + Material.DeadFireCoralWallFan, + Material.DeadHornCoral, + Material.DeadHornCoralBlock, + Material.DeadHornCoralFan, + Material.DeadHornCoralWallFan, + Material.DeadTubeCoral, + Material.DeadTubeCoralBlock, + Material.DeadTubeCoralFan, + Material.DeadTubeCoralWallFan, + Material.Deepslate, + Material.DiamondBlock, + Material.DiamondOre, + Material.Diorite, + Material.Dispenser, + Material.DripstoneBlock, + Material.Dropper, + Material.EmeraldBlock, + Material.EmeraldOre, + Material.EnchantingTable, + Material.EndStone, + Material.EndStoneBricks, + Material.FireCoralBlock, + Material.Furnace, + Material.GoldBlock, + Material.GoldOre, + Material.Granite, + Material.GrayConcrete, + Material.GrayGlazedTerracotta, + Material.GrayTerracotta, + Material.GreenConcrete, + Material.GreenGlazedTerracotta, + Material.GreenTerracotta, + Material.Grindstone, + Material.Hopper, + Material.HornCoralBlock, + Material.IronBars, + Material.IronBlock, + Material.IronChain, + Material.IronOre, + Material.IronTrapdoor, + Material.Jigsaw, + Material.LapisBlock, + Material.LapisOre, + Material.LightBlueConcrete, + Material.LightBlueGlazedTerracotta, + Material.LightBlueTerracotta, + Material.LightGrayConcrete, + Material.LightGrayGlazedTerracotta, + Material.LightGrayTerracotta, + Material.LightningRod, + Material.LimeConcrete, + Material.LimeGlazedTerracotta, + Material.LimeTerracotta, + Material.Lodestone, + Material.MagentaConcrete, + Material.MagentaGlazedTerracotta, + Material.MagentaTerracotta, + Material.MagmaBlock, + Material.MossyCobblestone, + Material.MossyStoneBricks, + Material.MudBrickSlab, + Material.MudBricks, + Material.NetherBrickFence, + Material.NetherBrickSlab, + Material.NetherBricks, + Material.NetherGoldOre, + Material.NetherQuartzOre, + Material.NetheriteBlock, + Material.Netherrack, + Material.Observer, + Material.Obsidian, + Material.OrangeConcrete, + Material.OrangeGlazedTerracotta, + Material.OrangeTerracotta, + Material.PetrifiedOakSlab, + Material.PinkConcrete, + Material.PinkGlazedTerracotta, + Material.PinkTerracotta, + Material.PolishedAndesite, + Material.PolishedBasalt, + Material.PolishedDiorite, + Material.PolishedGranite, + Material.Prismarine, + Material.PrismarineBrickSlab, + Material.PrismarineBricks, + Material.PrismarineSlab, + Material.PurpleConcrete, + Material.PurpleGlazedTerracotta, + Material.PurpleTerracotta, + Material.PurpurBlock, + Material.PurpurPillar, + Material.PurpurSlab, + Material.QuartzBlock, + Material.QuartzPillar, + Material.QuartzSlab, + Material.RawCopperBlock, + Material.RawGoldBlock, + Material.RawIronBlock, + Material.RedConcrete, + Material.RedGlazedTerracotta, + Material.RedNetherBricks, + Material.RedSandstone, + Material.RedSandstoneSlab, + Material.RedTerracotta, + Material.RedstoneBlock, + Material.RedstoneOre, + Material.RepeatingCommandBlock, + Material.ResinBrickSlab, + Material.ResinBrickWall, + Material.ResinBricks, + Material.RespawnAnchor, + Material.Sandstone, + Material.SandstoneSlab, + Material.Smoker, + Material.SmoothQuartz, + Material.SmoothRedSandstone, + Material.SmoothSandstone, + Material.SmoothStone, + Material.SmoothStoneSlab, + Material.Snow, + Material.SnowBlock, + Material.Spawner, + Material.Stone, + Material.StoneBrickSlab, + Material.StoneBricks, + Material.StoneSlab, + Material.Stonecutter, + Material.StructureBlock, + Material.Terracotta, + Material.TubeCoralBlock, + Material.Tuff, + Material.WarpedNylium, + Material.WaxedCutCopperSlab, + Material.WaxedExposedCutCopperSlab, + Material.WaxedOxidizedCutCopperSlab, + Material.WaxedWeatheredCutCopperSlab, + Material.WhiteConcrete, + Material.WhiteGlazedTerracotta, + Material.WhiteTerracotta, + Material.YellowConcrete, + Material.YellowGlazedTerracotta, + Material.YellowTerracotta, + }.ToFrozenSet(); + } +} diff --git a/MinecraftClient/Mapping/MiningCalculator.cs b/MinecraftClient/Mapping/MiningCalculator.cs new file mode 100644 index 00000000..38cf6d18 --- /dev/null +++ b/MinecraftClient/Mapping/MiningCalculator.cs @@ -0,0 +1,493 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MinecraftClient.Inventory; +using MinecraftClient.Protocol.Handlers; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; + +namespace MinecraftClient.Mapping +{ + /// + /// Computes dig duration in ticks for survival-style block breaking. + /// Version-aware across 1.8-1.21.11+, using tool speed, enchantments, effects, and attributes. + /// + public static class MiningCalculator + { + /// + /// Compute the number of ticks required to break a block in survival mode. + /// Returns 0 for instant-break blocks, -1 for unbreakable blocks. + /// + /// The block material to break + /// The item in the player's main hand (null for empty hand) + /// The item in the player's helmet slot (null if empty, used for Aqua Affinity) + /// Currently active player effects + /// Cached player attribute values (from OnEntityProperties) + /// Whether the player's eyes are submerged in water + /// Whether the player is on the ground + /// The Minecraft protocol version + /// Ticks to break the block, 0 for instant, -1 for unbreakable + public static int ComputeDigTicks( + Material blockMaterial, + Item? heldItem, + Item? helmetItem, + Dictionary effects, + Dictionary playerAttributes, + bool isUnderwater, + bool isOnGround, + int protocolVersion) + { + float hardness = BlockHardness.GetHardness(blockMaterial); + + if (hardness < 0) + return -1; // Unbreakable + + if (hardness == 0) + return 0; // Instant break + + float destroySpeed = GetDestroySpeed( + blockMaterial, heldItem, helmetItem, effects, playerAttributes, + isUnderwater, isOnGround, protocolVersion); + + bool correctTool = HasCorrectToolForDrops(blockMaterial, heldItem, protocolVersion); + int divisor = correctTool ? 30 : 100; + + float destroyProgress = destroySpeed / hardness / divisor; + + if (destroyProgress >= 1.0f) + return 0; // Instant break + + return (int)MathF.Ceiling(1.0f / destroyProgress); + } + + /// + /// Compute the player's destroy speed for a given block, following vanilla formulas. + /// + private static float GetDestroySpeed( + Material blockMaterial, + Item? heldItem, + Item? helmetItem, + Dictionary effects, + Dictionary playerAttributes, + bool isUnderwater, + bool isOnGround, + int protocolVersion) + { + float speed = GetToolSpeed(blockMaterial, heldItem, protocolVersion); + + if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version) + { + // 1.21.11+: Efficiency is delivered via the MINING_EFFICIENCY attribute + if (speed > 1.0f && playerAttributes.TryGetValue("player.mining_efficiency", out double miningEff)) + speed += (float)miningEff; + } + else + { + // Pre-1.21.11: Efficiency enchantment adds level^2 + 1 + int effLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion); + if (speed > 1.0f && effLevel > 0) + speed += effLevel * effLevel + 1; + } + + // Haste effect: multiply by 1 + 0.2 * (amplifier + 1) + if (effects.TryGetValue(Effects.Haste, out var hasteData)) + speed *= 1.0f + (hasteData.Amplifier + 1) * 0.2f; + + // Conduit Power also grants dig speed equivalent when in water + if (effects.TryGetValue(Effects.ConduitPower, out var conduitData)) + speed *= 1.0f + (conduitData.Amplifier + 1) * 0.2f; + + // Mining Fatigue + if (effects.TryGetValue(Effects.MiningFatigue, out var fatigueData)) + { + float multiplier = fatigueData.Amplifier switch + { + 0 => 0.3f, + 1 => 0.09f, + 2 => 0.0027f, + _ => 8.1E-4f + }; + speed *= multiplier; + } + + // Attribute multipliers for modern versions + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + { + // BLOCK_BREAK_SPEED attribute (default 1.0) + if (playerAttributes.TryGetValue("player.block_break_speed", out double bbs)) + speed *= (float)bbs; + } + + // Underwater penalty + if (isUnderwater) + { + if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version) + { + // 1.21.11+: Uses SUBMERGED_MINING_SPEED attribute (default 0.2) + double submergedSpeed = 0.2; + if (playerAttributes.TryGetValue("player.submerged_mining_speed", out double sms)) + submergedSpeed = sms; + speed *= (float)submergedSpeed; + } + else + { + // Pre-1.21.11: /5 unless Aqua Affinity + bool hasAquaAffinity = GetEnchantmentLevel(helmetItem, Enchantments.AquaAffinity, protocolVersion) > 0; + if (!hasAquaAffinity) + speed /= 5.0f; + } + } + + // Airborne penalty + if (!isOnGround) + speed /= 5.0f; + + return speed; + } + + /// + /// Get the base tool mining speed for a block. + /// For 1.20.6+ with ToolComponent, uses structured component data. + /// For older versions, uses hardcoded tool speed tables. + /// + private static float GetToolSpeed(Material blockMaterial, Item? heldItem, int protocolVersion) + { + if (heldItem is null) + return 1.0f; + + // Modern path: use ToolComponent from structured components + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + { + var toolComp = heldItem.Components?.OfType().FirstOrDefault(); + if (toolComp is not null) + { + // Check rules for matching blocks + foreach (var rule in toolComp.Rules) + { + if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial)) + return rule.Speed; + } + return toolComp.DefaultMiningSpeed; + } + } + + // Legacy path: hardcoded tool speed tables + return GetLegacyToolSpeed(heldItem.Type, blockMaterial); + } + + /// + /// Check whether the tool provides correct drops for a block. + /// + private static bool HasCorrectToolForDrops(Material blockMaterial, Item? heldItem, int protocolVersion) + { + if (!BlockHardness.RequiresCorrectTool(blockMaterial)) + return true; + + if (heldItem is null) + return false; + + // Modern path: check ToolComponent rules + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + { + var toolComp = heldItem.Components?.OfType().FirstOrDefault(); + if (toolComp is not null) + { + foreach (var rule in toolComp.Rules) + { + if (rule.HasCorrectDropForBlocks && rule.CorrectDropForBlocks + && MatchesBlockSet(rule.Blocks, blockMaterial)) + return true; + } + } + return false; + } + + // Legacy path: check if Material2Tool recommends this tool type + return IsCorrectToolLegacy(heldItem.Type, blockMaterial); + } + + /// + /// Match a block material against a ToolComponent BlockSetSubcomponent. + /// + private static bool MatchesBlockSet( + Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6.BlockSetSubcomponent blockSet, + Material blockMaterial) + { + if (blockSet.BlockIds is not null) + { + // Check against explicit block state IDs + foreach (int blockId in blockSet.BlockIds) + { + if (Block.Palette.FromId(blockId) == blockMaterial) + return true; + } + } + + if (blockSet.TagName is not null) + { + // Match against tag name (e.g., "minecraft:mineable/pickaxe") + return MatchesBlockTag(blockSet.TagName, blockMaterial); + } + + return false; + } + + /// + /// Approximate block tag matching using Material2Tool categories. + /// Tags like "minecraft:mineable/pickaxe" map to the appropriate tool categories. + /// + private static bool MatchesBlockTag(string tagName, Material blockMaterial) + { + // Normalize tag name + string tag = tagName.Replace("minecraft:", ""); + + ItemType[] tools = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (tools.Length == 0) + return false; + + ItemType firstTool = tools[0]; + return tag switch + { + "mineable/pickaxe" => IsPickaxe(firstTool), + "mineable/axe" => IsAxe(firstTool), + "mineable/shovel" => IsShovel(firstTool), + "mineable/hoe" => IsHoe(firstTool), + _ => false + }; + } + + /// + /// Get the enchantment level from an item, supporting both legacy NBT and modern structured components. + /// + public static int GetEnchantmentLevel(Item? item, Enchantments enchantment, int protocolVersion) + { + if (item is null) + return 0; + + // Modern path: structured components (1.20.6+) + var enchList = item.EnchantmentList; + if (enchList is not null) + { + var ench = enchList.FirstOrDefault(e => e.Type == enchantment); + if (ench is not null) + return ench.Level; + } + + // Legacy path: NBT data + if (item.NBT is not null && + item.NBT.TryGetValue("Enchantments", out object? enchantments)) + { + try + { + string enchNameLower = GetEnchantmentResourceName(enchantment); + foreach (Dictionary enchEntry in (object[])enchantments) + { + string id = ((string)enchEntry["id"]).ToLowerInvariant(); + if (id == enchNameLower || id == "minecraft:" + enchNameLower) + return (short)enchEntry["lvl"]; + } + } + catch + { + // NBT parsing failure - return 0 + } + } + + return 0; + } + + /// + /// Map Enchantments enum to Minecraft resource name (e.g., "efficiency"). + /// + private static string GetEnchantmentResourceName(Enchantments enchantment) + { + return enchantment switch + { + Enchantments.AquaAffinity => "aqua_affinity", + Enchantments.BaneOfArthropods => "bane_of_arthropods", + Enchantments.BlastProtection => "blast_protection", + Enchantments.Efficiency => "efficiency", + Enchantments.FeatherFalling => "feather_falling", + Enchantments.FireAspect => "fire_aspect", + Enchantments.FireProtection => "fire_protection", + Enchantments.FrostWalker => "frost_walker", + Enchantments.LuckOfTheSea => "luck_of_the_sea", + Enchantments.ProjectileProtection => "projectile_protection", + Enchantments.QuickCharge => "quick_charge", + Enchantments.SilkTouch => "silk_touch", + Enchantments.SoulSpeed => "soul_speed", + Enchantments.SwiftSneak => "swift_sneak", + Enchantments.VanishingCurse => "vanishing_curse", + Enchantments.BindingCurse => "binding_curse", + Enchantments.WindBurst => "wind_burst", + _ => enchantment.ToString().ToUnderscoreCase() + }; + } + + #region Legacy Tool Speed Tables + + /// + /// Legacy tool speed for pre-1.20.6 versions using hardcoded values. + /// + private static float GetLegacyToolSpeed(ItemType toolType, Material blockMaterial) + { + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return 1.0f; + + // Check if the held tool matches the recommended tool category + ToolCategory heldCategory = GetToolCategory(toolType); + ToolCategory neededCategory = GetToolCategory(recommended[0]); + + if (heldCategory == ToolCategory.None || heldCategory != neededCategory) + { + // Special cases: sword on cobweb, shears on specific blocks + if (toolType is ItemType.Shears && IsShearable(blockMaterial)) + return 1.5f; + if (IsSword(toolType) && blockMaterial == Material.Cobweb) + return 15.0f; + return 1.0f; + } + + return GetBaseToolSpeed(toolType); + } + + private static float GetBaseToolSpeed(ItemType toolType) + { + return toolType switch + { + // Wooden tools + ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or + ItemType.WoodenSword or ItemType.WoodenHoe => 2.0f, + + // Stone tools + ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or + ItemType.StoneSword or ItemType.StoneHoe => 4.0f, + + // Iron tools + ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or + ItemType.IronSword or ItemType.IronHoe => 6.0f, + + // Diamond tools + ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or + ItemType.DiamondSword or ItemType.DiamondHoe => 8.0f, + + // Netherite tools + ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or + ItemType.NetheriteSword or ItemType.NetheriteHoe => 9.0f, + + // Golden tools + ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or + ItemType.GoldenSword or ItemType.GoldenHoe => 12.0f, + + // Shears + ItemType.Shears => 2.0f, + + _ => 1.0f + }; + } + + /// + /// Check if the held tool is the correct tool for drops in legacy versions. + /// Uses Material2Tool's recommendations to determine correctness. + /// + private static bool IsCorrectToolLegacy(ItemType toolType, Material blockMaterial) + { + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return false; + + ToolCategory heldCategory = GetToolCategory(toolType); + ToolCategory neededCategory = GetToolCategory(recommended[0]); + + if (heldCategory == ToolCategory.None || heldCategory != neededCategory) + return false; + + // Check tool tier requirement + int heldTier = GetToolTier(toolType); + int requiredTier = GetRequiredTier(blockMaterial, recommended); + + return heldTier >= requiredTier; + } + + /// + /// Get the minimum tool tier required for a block based on Material2Tool's recommendation ordering. + /// + private static int GetRequiredTier(Material blockMaterial, ItemType[] recommended) + { + if (recommended.Length == 0) + return 0; + + // Material2Tool lists tools from highest to lowest tier. + // The last tool in the array is the minimum required tier. + return GetToolTier(recommended[^1]); + } + + private enum ToolCategory + { + None, + Pickaxe, + Axe, + Shovel, + Hoe, + Sword, + Shears + } + + private static ToolCategory GetToolCategory(ItemType item) + { + if (IsPickaxe(item)) return ToolCategory.Pickaxe; + if (IsAxe(item)) return ToolCategory.Axe; + if (IsShovel(item)) return ToolCategory.Shovel; + if (IsHoe(item)) return ToolCategory.Hoe; + if (IsSword(item)) return ToolCategory.Sword; + if (item == ItemType.Shears) return ToolCategory.Shears; + return ToolCategory.None; + } + + private static int GetToolTier(ItemType item) + { + string name = item.ToString(); + if (name.StartsWith("Wooden")) return 0; + if (name.StartsWith("Golden")) return 0; + if (name.StartsWith("Stone")) return 1; + if (name.StartsWith("Iron")) return 2; + if (name.StartsWith("Diamond")) return 3; + if (name.StartsWith("Netherite")) return 4; + return 0; + } + + private static bool IsPickaxe(ItemType item) => + item is ItemType.WoodenPickaxe or ItemType.StonePickaxe or ItemType.IronPickaxe + or ItemType.GoldenPickaxe or ItemType.DiamondPickaxe or ItemType.NetheritePickaxe; + + private static bool IsAxe(ItemType item) => + item is ItemType.WoodenAxe or ItemType.StoneAxe or ItemType.IronAxe + or ItemType.GoldenAxe or ItemType.DiamondAxe or ItemType.NetheriteAxe; + + private static bool IsShovel(ItemType item) => + item is ItemType.WoodenShovel or ItemType.StoneShovel or ItemType.IronShovel + or ItemType.GoldenShovel or ItemType.DiamondShovel or ItemType.NetheriteShovel; + + private static bool IsHoe(ItemType item) => + item is ItemType.WoodenHoe or ItemType.StoneHoe or ItemType.IronHoe + or ItemType.GoldenHoe or ItemType.DiamondHoe or ItemType.NetheriteHoe; + + private static bool IsSword(ItemType item) => + item is ItemType.WoodenSword or ItemType.StoneSword or ItemType.IronSword + or ItemType.GoldenSword or ItemType.DiamondSword or ItemType.NetheriteSword; + + private static bool IsShearable(Material block) => + block is Material.Cobweb or Material.OakLeaves or Material.SpruceLeaves + or Material.BirchLeaves or Material.JungleLeaves or Material.AcaciaLeaves + or Material.DarkOakLeaves or Material.CherryLeaves or Material.MangroveLeaves + or Material.AzaleaLeaves or Material.FloweringAzaleaLeaves + or Material.WhiteWool or Material.OrangeWool or Material.MagentaWool + or Material.LightBlueWool or Material.YellowWool or Material.LimeWool + or Material.PinkWool or Material.GrayWool or Material.LightGrayWool + or Material.CyanWool or Material.PurpleWool or Material.BlueWool + or Material.BrownWool or Material.GreenWool or Material.RedWool + or Material.BlackWool or Material.Vine; + + #endregion + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 24069342..edfa9cec 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -107,6 +107,9 @@ namespace MinecraftClient // player effects private readonly Dictionary playerEffects = new(); + + // player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed) + private readonly Dictionary playerAttributes = new(); // Sneaking public bool IsSneaking { get; set; } = false; @@ -2591,6 +2594,13 @@ namespace MinecraftClient if (lookAtBlock) UpdateLocation(GetCurrentLocation(), location); + // Auto-compute dig duration for survival/adventure mode when not explicitly supplied + if (duration <= 0 && protocolversion >= Protocol18Handler.MC_1_8_Version + && gamemode is 0 or 2) // Survival or Adventure + { + duration = ComputeAutoDigDuration(location); + } + // Send dig start and dig end, will need to wait for server response to know dig result // See https://wiki.vg/How_to_Write_a_Client#Digging for more details bool result = handler.SendPlayerDigging(0, location, blockFace, sequenceId++) @@ -2608,6 +2618,52 @@ namespace MinecraftClient } } + /// + /// Compute the automatic dig duration in seconds for a block, based on held tool, + /// enchantments, effects, attributes, and player state. + /// Returns 0 for instant-break blocks. + /// + private double ComputeAutoDigDuration(Location location) + { + try + { + Block block = world.GetBlock(location); + Material blockMaterial = block.Type; + + if (blockMaterial == Material.Air) + return 0; + + // Get held item from player inventory + Item? heldItem = null; + Item? helmetItem = null; + if (inventories.TryGetValue(0, out var playerInv)) + { + int hotbarSlot = 36 + CurrentSlot; // Hotbar slots are 36-44 + playerInv.Items.TryGetValue(hotbarSlot, out heldItem); + playerInv.Items.TryGetValue(5, out helmetItem); // Slot 5 = helmet + } + + int ticks = MiningCalculator.ComputeDigTicks( + blockMaterial, + heldItem, + helmetItem, + playerEffects, + playerAttributes, + playerPhysics.InWater, + playerPhysics.OnGround, + protocolversion); + + if (ticks <= 0) + return 0; + + return (double)ticks / Settings.ClientTicksPerSecond; + } + catch + { + return 0; + } + } + /// /// Change active slot in the player inventory /// @@ -3712,6 +3768,9 @@ namespace MinecraftClient { if (EntityID == playerEntityID) { + foreach (var kvp in prop) + playerAttributes[kvp.Key] = kvp.Value; + DispatchBotEvent(bot => bot.OnPlayerProperty(prop)); } } diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index f62e1377..912b1c49 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -1089,9 +1089,10 @@ namespace MinecraftClient.Scripting /// Example: if your player is under a block that is being destroyed, use Down /// Also perform the "arm swing" animation /// Also look at the block before digging - protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true) + /// Dig duration in seconds. 0 = auto-compute for survival, or instant for creative + protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true, double duration = 0) { - return Handler.DigBlock(location, direction, swingArms, lookAtBlock); + return Handler.DigBlock(location, direction, swingArms, lookAtBlock, duration); } /// From 0881cbaa1ca0af7a4d4876cc3fad03a1c4eb1cc2 Mon Sep 17 00:00:00 2001 From: milutinke Date: Mon, 30 Mar 2026 17:25:08 +0200 Subject: [PATCH 56/76] Fix legacy achievements and add test harness --- .../scripts/ensure_offline_server.sh | 19 +- .../scripts/prepare_offline_mcc_config.sh | 10 +- .../scripts/run_achievements_matrix.sh | 157 +++++++ .../scripts/run_achievements_test.sh | 443 ++++++++++++++++++ .../scripts/summarize_achievements_matrix.sh | 57 +++ MinecraftClient/LegacyAchievementCatalog.cs | 53 +++ .../Protocol/Handlers/Protocol18.cs | 49 +- 7 files changed, 785 insertions(+), 3 deletions(-) create mode 100755 .skills/mcc-integration-testing/scripts/run_achievements_matrix.sh create mode 100755 .skills/mcc-integration-testing/scripts/run_achievements_test.sh create mode 100755 .skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh create mode 100644 MinecraftClient/LegacyAchievementCatalog.cs diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 5e67687d..1e348445 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -6,6 +6,14 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + VERSION="${1:-1.21.11-Vanilla}" SERVER_DIR="${MCC_SERVERS:?}/$VERSION" PROPS_FILE="$SERVER_DIR/server.properties" @@ -49,6 +57,15 @@ wait_for_server_stop() { sleep 1 ((elapsed += 1)) done + + # Legacy servers can leave the tmux session around after stdin stop. + # Fall back to force-killing the session so the harness can continue. + mc-kill "$VERSION" >/dev/null 2>&1 || true + + if ! server_running; then + return 0 + fi + echo "Timed out waiting for $VERSION to stop" >&2 return 1 } @@ -58,7 +75,7 @@ upsert_property() { local value="$2" if grep -Eq "^${key}=" "$PROPS_FILE"; then - sed -i "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" + sed_in_place "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" else printf '%s=%s\n' "$key" "$value" >> "$PROPS_FILE" fi diff --git a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh index f36129fa..9eae53b3 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash set -euo pipefail +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + if [[ $# -lt 3 || $# -gt 4 ]]; then echo "Usage: $0 [login]" >&2 exit 1 @@ -28,7 +36,7 @@ fi cp "$TEMPLATE_INI" "$OUTPUT_INI" -sed -i \ +sed_in_place \ -e "s#^Account = .*#Account = { Login = \"$LOGIN_NAME\", Password = \"$PASSWORD_VALUE\" }#" \ -e "s#^AccountType = .*#AccountType = \"$ACCOUNT_TYPE\"#" \ -e "s#^MinecraftVersion = \"[^\"]*\"\\(.*\\)\$#MinecraftVersion = \"$MC_VERSION\"\\1#" \ diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh new file mode 100755 index 00000000..45629525 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements/matrix" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +MATRIX_DIR="$RUN_ROOT/$RUN_ID" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +BUILD_LOG="$MATRIX_DIR/build.log" +REPORT_MD="$MATRIX_DIR/report.md" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" + +mkdir -p "$MATRIX_DIR" + +write_row() { + local fields=("$@") + + while (( ${#fields[@]} < 14 )); do + fields+=("") + done + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${fields[6]}" \ + "${fields[7]}" "${fields[8]}" "${fields[9]}" "${fields[10]}" "${fields[11]}" "${fields[12]}" \ + "${fields[13]}" >> "$RESULTS_TSV" +} + +resolve_server_dir() { + local version="$1" + local candidate + + for candidate in "$version" "$version-Vanilla"; do + if [[ -d "$MCC_SERVERS/$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +run_version() { + local version="$1" + local profile="$2" + local family="$3" + local server_dir="$4" + local summary_env + + if bash "$SCRIPT_DIR/run_achievements_test.sh" --no-build "$server_dir" "$version" "$profile"; then + : + fi + + summary_env="${TMPDIR:-/tmp}/mcc-achievements/$server_dir/latest/summary.env" + if [[ ! -f "$summary_env" ]]; then + write_row "$version" "$server_dir" "unknown" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "Summary file was not produced." "" "" "" + return + fi + + # shellcheck disable=SC1090 + source "$summary_env" + + write_row "$VERSION" "$SERVER_DIR" "$PORT" "$FAMILY" "$INITIAL_STATUS" "$GRANT_STATUS" "$REVOKE_STATUS" \ + "$API_STATUS" "$VERDICT" "$NOTE" "$RUN_DIR" "$MCC_LOG" "$COPIED_SERVER_LOG" "$COMMAND_LOG" +} + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" +} > "$PRECHECK_TXT" + +printf 'Version\tServerDir\tPort\tFamily\tInitial\tGrant\tRevoke\tAPI\tVerdict\tNote\tRunDir\tMccLog\tServerLog\tCommandLog\n' > "$RESULTS_TSV" + +JAVA_OK="yes" +TMUX_OK="yes" +DOTNET_OK="yes" +BUILD_OK="yes" + +if ! command -v dotnet >/dev/null 2>&1; then + DOTNET_OK="no" +fi + +if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then + JAVA_OK="no" +fi + +if ! command -v tmux >/dev/null 2>&1; then + TMUX_OK="no" +fi + +if [[ "$DOTNET_OK" == "yes" ]]; then + if ! dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release > "$BUILD_LOG" 2>&1; then + BUILD_OK="no" + fi +else + : > "$BUILD_LOG" +fi + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" + printf 'dotnet=%s\n' "$DOTNET_OK" + printf 'java=%s\n' "$JAVA_OK" + printf 'tmux=%s\n' "$TMUX_OK" + printf 'build=%s\n' "$BUILD_OK" +} > "$PRECHECK_TXT" + +while IFS='|' read -r version profile family; do + [[ -z "$version" ]] && continue + + if [[ "$DOTNET_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet is not available on PATH." + continue + fi + + if [[ "$BUILD_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet build failed. See $BUILD_LOG." + continue + fi + + if [[ "$JAVA_OK" != "yes" || "$TMUX_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "java or tmux is not available, so live server execution was blocked." + continue + fi + + if ! server_dir="$(resolve_server_dir "$version")"; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "⚠️ Partial" \ + "Server directory for $version was not found under $MCC_SERVERS." + continue + fi + + run_version "$version" "$profile" "$family" "$server_dir" +done <<'EOF' +1.8|legacy|Legacy 🧱 +1.11.2|legacy|Legacy 🧱 +1.12.2|modern|First advancements 🌱 +1.19.4|modern|Stable modern ✅ +1.20|modern|Telemetry edge 1 ⚠️ +1.20.2|modern|Telemetry edge 2 ⚠️ +1.20.4|modern|End of 1.20.x ⚠️ +1.20.6|modern|Post-1.20.6 🔧 +1.21.2|modern|1.21.2 family 🔧 +1.21.11|modern|showAdvancements 🆕 +26.1|modern|Latest supported 🚀 +EOF + +bash "$SCRIPT_DIR/summarize_achievements_matrix.sh" "$MATRIX_DIR" > "$REPORT_MD" +printf '%s\n' "$MATRIX_DIR" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh new file mode 100755 index 00000000..88c2b027 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -0,0 +1,443 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +usage() { + cat <<'EOF' +Usage: run_achievements_test.sh [--no-build] + +Examples: + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.8 1.8 legacy + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.21.11-Vanilla 1.21.11 modern +EOF +} + +DO_BUILD=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-build) DO_BUILD=false; shift ;; + --build) DO_BUILD=true; shift ;; + -h|--help) usage; exit 0 ;; + *) break ;; + esac +done + +if [[ $# -ne 3 ]]; then + usage >&2 + exit 1 +fi + +SERVER_DIR="$1" +MC_VERSION="$2" +PROFILE="$3" + +if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then + echo "Unsupported profile: $PROFILE" >&2 + exit 1 +fi + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +RUN_DIR="$RUN_ROOT/$SERVER_DIR/$RUN_ID" +LATEST_LINK="$RUN_ROOT/$SERVER_DIR/latest" +MCC_LOG="$RUN_DIR/mcc.log" +BUILD_LOG="$RUN_DIR/build.log" +SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" +SERVER_FILE_LOG="$RUN_DIR/server-latest.log" +COMMAND_LOG="$RUN_DIR/commands.log" +SUMMARY_ENV="$RUN_DIR/summary.env" +PROBE_SCRIPT="$RUN_DIR/achievement_probe.cs" +CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" +INPUT_FILE="$REPO_ROOT/mcc_input.txt" +SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" +TARGET_ID="minecraft:story/root" +TARGET_COMMAND_GRANT="advancement grant CursorBot only minecraft:story/root" +TARGET_COMMAND_REVOKE="advancement revoke CursorBot only minecraft:story/root" +TARGET_TYPE="Modern 🌱" +PORT="unknown" +MCC_PID="" + +INITIAL_STATUS="❌" +GRANT_STATUS="❌" +REVOKE_STATUS="❌" +API_STATUS="❌" +VERDICT="❌ Fail" +NOTE="Run did not complete." +EXECUTED="yes" + +if [[ "$PROFILE" == "legacy" ]]; then + TARGET_ID="achievement.openInventory" + TARGET_COMMAND_GRANT="achievement give achievement.openInventory CursorBot" + TARGET_COMMAND_REVOKE="achievement take achievement.openInventory CursorBot" + TARGET_TYPE="Legacy 🧱" +fi + +mkdir -p "$RUN_DIR" + +write_summary() { + { + printf 'VERSION=%q\n' "$MC_VERSION" + printf 'SERVER_DIR=%q\n' "$SERVER_DIR" + printf 'PROFILE=%q\n' "$PROFILE" + printf 'FAMILY=%q\n' "$TARGET_TYPE" + printf 'PORT=%q\n' "$PORT" + printf 'RUN_DIR=%q\n' "$RUN_DIR" + printf 'MCC_LOG=%q\n' "$MCC_LOG" + printf 'SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'SERVER_FILE_LOG=%q\n' "$SERVER_LOG_FILE" + printf 'SERVER_TMUX_LOG=%q\n' "$SERVER_TMUX_LOG" + printf 'COPIED_SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'COMMAND_LOG=%q\n' "$COMMAND_LOG" + printf 'SUMMARY_ENV=%q\n' "$SUMMARY_ENV" + printf 'TARGET_ID=%q\n' "$TARGET_ID" + printf 'INITIAL_STATUS=%q\n' "$INITIAL_STATUS" + printf 'GRANT_STATUS=%q\n' "$GRANT_STATUS" + printf 'REVOKE_STATUS=%q\n' "$REVOKE_STATUS" + printf 'API_STATUS=%q\n' "$API_STATUS" + printf 'VERDICT=%q\n' "$VERDICT" + printf 'NOTE=%q\n' "$NOTE" + printf 'EXECUTED=%q\n' "$EXECUTED" + } > "$SUMMARY_ENV" +} + +capture_server_logs() { + mc-log "$SERVER_DIR" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true + if [[ -f "$SERVER_LOG_FILE" ]]; then + cp "$SERVER_LOG_FILE" "$RUN_DIR/server-latest.log" 2>/dev/null || true + fi +} + +cleanup() { + capture_server_logs + + if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then + echo "quit" >> "$INPUT_FILE" 2>/dev/null || true + sleep 2 + kill "$MCC_PID" 2>/dev/null || true + wait "$MCC_PID" 2>/dev/null || true + fi + + mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true + ln -sfn "$RUN_DIR" "$LATEST_LINK" + write_summary +} +trap cleanup EXIT + +log_step() { + printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$COMMAND_LOG" +} + +fail() { + NOTE="$1" + VERDICT="❌ Fail" + exit 1 +} + +wait_for_file_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + local timeout="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for: $description" >&2 + return 1 +} + +wait_for_server_ready() { + local timeout="${1:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for server readiness" >&2 + return 1 +} + +disable_noisy_bots() { + sed_in_place '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" + sed_in_place '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" +} + +ensure_root_config() { + if [[ -f "$REPO_ROOT/MinecraftClient.ini" ]]; then + return + fi + + ( + cd "$REPO_ROOT" + dotnet run --project MinecraftClient -c Release --no-build -- --help >/dev/null 2>&1 + ) +} + +write_probe_script() { + cat > "$PROBE_SCRIPT" < updated, IReadOnlyList removedIds, bool reset) + { + LogToConsole($"[ACH_TEST] event reset={reset} updated={updated.Count} removed={removedIds.Count}"); + DumpState("event"); + } + + private void DumpState(string origin) + { + Achievement[] all = GetAchievements(); + Achievement[] unlocked = GetUnlockedAchievements(); + Achievement[] locked = GetLockedAchievements(); + Achievement? target = null; + + foreach (Achievement entry in all) + { + if (entry.Id == TargetId) + { + target = entry; + break; + } + } + + string titleState = "missing"; + string completionState = "missing"; + + if (target is not null) + { + titleState = target.Title is null ? "null" : "present"; + completionState = target.IsCompleted ? "done" : "todo"; + } + + LogToConsole($"[ACH_TEST] snapshot origin={origin} all={all.Length} unlocked={unlocked.Length} locked={locked.Length}"); + LogToConsole($"[ACH_TEST] target_state origin={origin} id={TargetId} title={titleState} completed={completionState}"); + } +} +EOF +} + +run_server_command() { + local cmd="$1" + local attempt + + log_step "SERVER> $cmd" + for attempt in 1 2 3 4 5; do + if mc-rcon "$cmd" >/dev/null 2>&1; then + sleep 1 + return 0 + fi + sleep 1 + done + + fail "Server command failed: $cmd" +} + +run_mcc_command() { + local name="$1" + local cmd="$2" + local delay="${3:-2}" + local start_line=0 + local end_line=0 + + if [[ -f "$MCC_LOG" ]]; then + start_line="$(wc -l < "$MCC_LOG")" + fi + + log_step "MCC> $cmd" + echo "$cmd" >> "$INPUT_FILE" + sleep "$delay" + + if [[ -f "$MCC_LOG" ]]; then + end_line="$(wc -l < "$MCC_LOG")" + fi + + if (( end_line > start_line )); then + sed -n "$((start_line + 1)),$((end_line))p" "$MCC_LOG" > "$RUN_DIR/$name.mcc.log" + else + : > "$RUN_DIR/$name.mcc.log" + fi +} + +assert_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + + grep -Fq "$pattern" "$file" || fail "$description" +} + +if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then + fail "java was not found on PATH." +fi + +if ! command -v tmux >/dev/null 2>&1; then + fail "tmux was not found on PATH." +fi + +if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then + fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" +fi + +PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" + +ensure_root_config +"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" +disable_noisy_bots +write_probe_script + +if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" +fi + +if $DO_BUILD; then + log_step "BUILD> dotnet build MinecraftClient.sln -c Release" + mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed." +else + : > "$BUILD_LOG" +fi + +: > "$INPUT_FILE" +rm -f "$MCC_LOG" + +log_step "Starting server $SERVER_DIR on port $PORT" +mc-start "$SERVER_DIR" >/dev/null +wait_for_server_ready || fail "Server did not become ready." + +log_step "Starting MCC for $MC_VERSION" +( + cd "$REPO_ROOT" + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + CursorBot \ + - \ + "localhost:$PORT" \ + "--accounttype=mojang" \ + "--minecraftversion=$MC_VERSION" \ + "--terrainandmovements=true" \ + "--inventoryhandling=true" \ + "--entityhandling=true" \ + "--autorespawn=true" \ + "--debugmessages=true" \ + > "$MCC_LOG" 2>&1 +) & +MCC_PID=$! + +wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join." +wait_for_file_pattern "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join." + +run_server_command "op CursorBot" +run_server_command "gamerule sendCommandFeedback true" +if [[ "$PROFILE" == "modern" ]]; then + run_server_command "gamerule logAdminCommands true" +fi +run_server_command "time set day" +run_server_command "weather clear" + +run_mcc_command "load_probe" "script $PROBE_SCRIPT" 3 +wait_for_file_pattern "$MCC_LOG" "[ACH_TEST] probe initialized" "probe startup" 30 || fail "Probe script did not initialize." + +run_mcc_command "baseline_debug" "debug state" 2 +run_mcc_command "baseline_all" "achievement" 2 +run_mcc_command "baseline_locked" "achievement locked" 2 +run_mcc_command "baseline_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_GRANT" +sleep 3 +run_mcc_command "after_grant_all" "achievement" 2 +run_mcc_command "after_grant_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_REVOKE" +sleep 3 +run_mcc_command "after_revoke_all" "achievement" 2 +run_mcc_command "after_revoke_locked" "achievement locked" 2 + +assert_pattern "$MCC_LOG" "Achievements/Advancements:" "Achievement command header never appeared." + +if ! grep -Fq "No achievements/advancements received yet." "$RUN_DIR/baseline_all.mcc.log"; then + INITIAL_STATUS="✅" +fi + +if grep -Fq "$TARGET_ID" "$RUN_DIR/after_grant_unlocked.mcc.log" && grep -Fq "[DONE]" "$RUN_DIR/after_grant_unlocked.mcc.log"; then + GRANT_STATUS="✅" +fi + +if [[ "$PROFILE" == "legacy" ]]; then + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + fi +else + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + elif [[ "$GRANT_STATUS" == "✅" ]] && ! grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_all.mcc.log"; then + REVOKE_STATUS="✅" + fi +fi + +if grep -Fq "[ACH_TEST] event" "$MCC_LOG" && grep -Fq "target_state origin=event id=$TARGET_ID title=" "$MCC_LOG"; then + API_STATUS="✅" +fi + +case "$INITIAL_STATUS|$GRANT_STATUS|$REVOKE_STATUS|$API_STATUS" in + "✅|✅|✅|✅") + VERDICT="✅ Pass" + NOTE="All planned achievement checks passed." + ;; + *"✅"*) + VERDICT="⚠️ Partial" + NOTE="At least one achievement phase passed, but the matrix did not fully clear." + ;; + *) + VERDICT="❌ Fail" + NOTE="Achievement checks did not produce the expected evidence." + ;; +esac + +run_mcc_command "quit" "quit" 2 +NOTE="$NOTE Artifacts saved in $RUN_DIR." diff --git a/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh new file mode 100755 index 00000000..9dbeb78c --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: summarize_achievements_matrix.sh " >&2 + exit 1 +fi + +MATRIX_DIR="$1" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" +BUILD_LOG="$MATRIX_DIR/build.log" + +if [[ ! -f "$RESULTS_TSV" ]]; then + echo "Missing results file: $RESULTS_TSV" >&2 + exit 1 +fi + +echo "# Achievements Matrix Report" +echo +echo "## Executed" +echo +if [[ -f "$PRECHECK_TXT" ]]; then + echo '```text' + cat "$PRECHECK_TXT" + echo '```' +fi +echo +echo "- Matrix artifacts: \`$MATRIX_DIR\`" +echo "- Results TSV: \`$RESULTS_TSV\`" +echo "- Build log: \`$BUILD_LOG\`" +echo "- Execution mode: sequential" +echo "- Auth mode: offline" +echo +echo "## Observed" +echo +echo "| Version | Port | Family | Initial snapshot | Grant | Revoke | API callback | Verdict |" +echo "|---|---:|---|---|---|---|---|---|" +awk -F '\t' 'NR > 1 { + printf("| `%s` | `%s` | %s | %s | %s | %s | %s | %s |\n", + $1, $3, $4, $5, $6, $7, $8, $9); +}' "$RESULTS_TSV" + +echo +echo "## Artifact Links" +echo +awk -F '\t' 'NR > 1 { + printf("- `%s`: run=`%s`, mcc=`%s`, server=`%s`, commands=`%s`\n", $1, $11, $12, $13, $14); + printf(" note: %s\n", $10); +}' "$RESULTS_TSV" + +echo +echo "## Inferred" +echo +echo "- Only rows with real MCC and server-log artifacts count as executed proof." +echo "- Rows blocked by missing Java, tmux, or server directories are environment-limited, not product pass results." +echo "- Legacy rows remain the highest-risk bucket because static inspection suggests pre-1.12 \`Statistics\` packets may not currently reach the achievements handler." diff --git a/MinecraftClient/LegacyAchievementCatalog.cs b/MinecraftClient/LegacyAchievementCatalog.cs new file mode 100644 index 00000000..bce17f8d --- /dev/null +++ b/MinecraftClient/LegacyAchievementCatalog.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace MinecraftClient +{ + internal static class LegacyAchievementCatalog + { + public static IReadOnlyList Ids { get; } = + [ + "achievement.openInventory", + "achievement.mineWood", + "achievement.buildWorkBench", + "achievement.buildPickaxe", + "achievement.buildFurnace", + "achievement.acquireIron", + "achievement.buildHoe", + "achievement.makeBread", + "achievement.bakeCake", + "achievement.buildBetterPickaxe", + "achievement.cookFish", + "achievement.onARail", + "achievement.buildSword", + "achievement.killEnemy", + "achievement.killCow", + "achievement.flyPig", + "achievement.snipeSkeleton", + "achievement.diamonds", + "achievement.diamondsToYou", + "achievement.portal", + "achievement.ghast", + "achievement.blazeRod", + "achievement.potion", + "achievement.theEnd", + "achievement.theEnd2", + "achievement.enchantments", + "achievement.overkill", + "achievement.bookcase", + "achievement.breedCow", + "achievement.spawnWither", + "achievement.killWither", + "achievement.fullBeacon", + "achievement.exploreAllBiomes", + "achievement.overpowered" + ]; + + private static readonly HashSet s_idSet = new(Ids, StringComparer.Ordinal); + + public static bool Contains(string id) + { + return s_idSet.Contains(id); + } + } +} diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 7032bcbd..eeabc8e0 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -91,6 +91,7 @@ namespace MinecraftClient.Protocol.Handlers private int currentDimension; private bool isOnlineMode = false; private readonly BlockingCollection>> packetQueue = new(); + private readonly Dictionary legacyAchievementProgress = new(StringComparer.Ordinal); private float LastYaw, LastPitch; private double lastSentX, lastSentY, lastSentZ; private float lastSentYaw, lastSentPitch; @@ -120,6 +121,7 @@ namespace MinecraftClient.Protocol.Handlers Tuple? netReader = null; // reader thread readonly ILogger log; readonly RandomNumberGenerator randomGen; + private bool legacyAchievementsInitialized; public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo, int rawProtocolVersion = 0) @@ -3132,6 +3134,11 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.RecipeBookSettings: break; + case PacketTypesIn.Statistics: + if (protocolVersion < MC_1_12_Version) + HandleLegacyStatistics(packetData); + break; + case PacketTypesIn.Advancements: HandleAdvancements(packetData); break; @@ -3147,9 +3154,39 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Handle the Statistics packet for pre-1.12 legacy achievements. + /// + private void HandleLegacyStatistics(Queue packetData) + { + int statCount = dataTypes.ReadNextVarInt(packetData); + + for (int i = 0; i < statCount; i++) + { + string statId = dataTypes.ReadNextString(packetData); + int value = dataTypes.ReadNextVarInt(packetData); + + if (statId.StartsWith("achievement.", StringComparison.Ordinal)) + legacyAchievementProgress[statId] = value > 0; + } + + List added = new(LegacyAchievementCatalog.Ids.Count + legacyAchievementProgress.Count); + + foreach (string achievementId in LegacyAchievementCatalog.Ids) + added.Add(CreateLegacyAchievement(achievementId, legacyAchievementProgress.TryGetValue(achievementId, out bool completed) && completed)); + + foreach (var (achievementId, completed) in legacyAchievementProgress) + { + if (!LegacyAchievementCatalog.Contains(achievementId)) + added.Add(CreateLegacyAchievement(achievementId, completed)); + } + + handler.OnAchievementsUpdate(added, [], reset: !legacyAchievementsInitialized); + legacyAchievementsInitialized = true; + } + /// /// Handle the Advancements packet (1.12+). - /// Also handles the Statistics packet for pre-1.12 legacy achievements. /// private void HandleAdvancements(Queue packetData) { @@ -3282,6 +3319,16 @@ namespace MinecraftClient.Protocol.Handlers handler.OnAchievementsUpdate(added, removedIds, reset); } + private static Achievement CreateLegacyAchievement(string id, bool isCompleted) + { + Dictionary criteria = new(StringComparer.Ordinal) + { + [id] = isCompleted + }; + IReadOnlyList[] requirements = [[id]]; + return new Achievement(id, null, null, AchievementType.Legacy, false, isCompleted, requirements, criteria); + } + /// /// Compute whether an advancement is completed based on AND-of-ORs requirements. /// From 62740ee94e50e1a7eb5e2bfc762e0667e8ce00f8 Mon Sep 17 00:00:00 2001 From: milutinke Date: Mon, 30 Mar 2026 17:28:15 +0200 Subject: [PATCH 57/76] Document achievements feature --- docs/guide/creating-bots.md | 52 +++++++++++++++++++++++++++++++++++++ docs/guide/usage.md | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/docs/guide/creating-bots.md b/docs/guide/creating-bots.md index e374f273..ed35d549 100644 --- a/docs/guide/creating-bots.md +++ b/docs/guide/creating-bots.md @@ -229,6 +229,58 @@ Make a built-in MCC chat bot named AutoTorch and wire it fully into the repo con Create a standalone MCC /script bot that follows private messages, uses GetVerbatim(text), and replies only to bot owners. Use the mcc-chatbot-authoring skill. ``` +## Achievements And Advancements + +Chat bots and C# scripts can read the current achievement state and react to updates. + +Useful methods: + +- `GetAchievements()` +- `GetUnlockedAchievements()` +- `GetLockedAchievements()` +- `OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset)` + +Things worth knowing: + +- On `1.8` to `1.11.2`, ids use the legacy `achievement.*` format. +- On `1.12+`, ids use advancement resource ids such as `minecraft:story/root`. +- Legacy achievements usually have `Title = null` and `Description = null` because the server does not send display metadata in the statistics packet. +- On newer versions, revoking an advancement may remove it from the current set instead of turning it into a locked entry, so `removedIds` matters. + +Example: + +```csharp +//MCCScript 1.0 + +MCC.LoadBot(new AchievementWatcher()); + +//MCCScript Extensions + +public class AchievementWatcher : ChatBot +{ + public override void AfterGameJoined() + { + Achievement[] known = GetAchievements(); + LogToConsole($"Known achievements: {known.Length}"); + } + + public override void OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset) + { + LogToConsole($"Achievement update: reset={reset}, updated={updated.Count}, removed={removedIds.Count}"); + + foreach (Achievement achievement in updated) + { + string title = achievement.Title ?? achievement.Id; + string state = achievement.IsCompleted ? "done" : "todo"; + LogToConsole($" - {title}: {state}"); + } + + foreach (string removedId in removedIds) + LogToConsole($" - removed: {removedId}"); + } +} +``` + ## C# API The authoritative reference for the C# API is [ChatBot.cs](https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Scripting/ChatBot.cs). diff --git a/docs/guide/usage.md b/docs/guide/usage.md index 0a439aff..e0e4a0ea 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -219,6 +219,54 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+achievement + +- **Description:** + + Show the achievements or advancements currently known to MCC. + + On Minecraft `1.8` to `1.11.2`, MCC tracks legacy achievements such as `achievement.openInventory`. + + On Minecraft `1.12+`, MCC tracks advancements such as `minecraft:story/root`. + +- **Usage:** + + ``` + /achievement + /achievement list + /achievement locked + /achievement unlocked + ``` + +- **Examples:** + + List everything MCC currently knows: + + ``` + /achievement + ``` + + Show only incomplete entries: + + ``` + /achievement locked + ``` + + Show only completed entries: + + ``` + /achievement unlocked + ``` + +- **Notes:** + + The command only shows data the server has already sent to MCC. + + Legacy achievements do not include titles or descriptions in the protocol, so older servers usually show the raw id instead. + +
+
bed From 76e5cab248f946fec6ff94de73bf6910fdc2dabe Mon Sep 17 00:00:00 2001 From: milutinke Date: Mon, 30 Mar 2026 18:02:10 +0200 Subject: [PATCH 58/76] Improve MCC testing workflow resilience --- .skills/mcc-dev-workflow/SKILL.md | 38 ++++-- .skills/mcc-integration-testing/SKILL.md | 23 +++- .../mcc-integration-testing/scripts/common.sh | 110 ++++++++++++++++++ .../scripts/ensure_offline_server.sh | 53 +-------- .../scripts/preflight_test_env.sh | 48 ++++++++ .../scripts/prepare_offline_mcc_config.sh | 69 +++++++++-- .../scripts/reset_shared_test_state.sh | 50 ++++++++ .../scripts/run_achievements_matrix.sh | 11 ++ .../scripts/run_achievements_test.sh | 91 ++++----------- .../scripts/run_full_spectrum_test.sh | 68 +++++------ .../scripts/summarize_achievements_matrix.sh | 2 +- .skills/mcc-version-adaptation/SKILL.md | 1 + tools/mcc-debug.sh | 43 ++++--- tools/mcc-env.sh | 4 + tools/run-creative-e2e.sh | 66 +++-------- tools/start-server.sh | 42 ++++++- 16 files changed, 464 insertions(+), 255 deletions(-) create mode 100755 .skills/mcc-integration-testing/scripts/common.sh create mode 100755 .skills/mcc-integration-testing/scripts/preflight_test_env.sh create mode 100755 .skills/mcc-integration-testing/scripts/reset_shared_test_state.sh diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index f1a3c8fe..b1a9ef01 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: mcc-dev-workflow -description: Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server in WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code. +description: Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server on Linux, macOS, or WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code. --- # MCC Development Workflow @@ -11,7 +11,7 @@ Use this skill when the task needs a real local server loop, not just code readi - Solution: `MinecraftClient.sln` - Runtime target: `.NET 10` / `net10.0` -- Environment: WSL Ubuntu, Java 21, tmux, python3 +- Environment: Linux, macOS, or WSL with Java, tmux, python3, and dotnet available - Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}` - Default validation target when the user does not specify a version: `1.21.11` @@ -30,10 +30,22 @@ Both modes support the same commands and input/output through `ConsoleIO.Backend - Prefer a real local server over static reasoning for protocol, login, movement, inventory, entity, or command-path work. - Treat tmux `mc-*` sessions as shared state. Do not run multi-version server workflows in parallel unless the harness explicitly isolates them. -- For scripted or repeatable runs, prefer a temporary config copied from `MinecraftClient.ini`. Use the repo-root config only for ad hoc manual work. +- For scripted or repeatable runs, use a generated temporary config. Do not edit the repo-root `MinecraftClient.ini` as part of the test loop. - A server log line containing `Done (` means startup finished. It does not guarantee that RCON is ready on the first attempt. Retry early `mc-rcon` commands. - When instructions, docs, and code disagree, trust current code and current tool behavior first. +## Preflight and reset + +Before scripted runs, especially on macOS or in a reused tmux environment: + +```bash +source tools/mcc-env.sh +mcc-preflight 1.21.11 +mc-reset-test-env 1.21.11 +``` + +`mcc-preflight` checks Java, tmux, dotnet, python3, and server directories. It also resolves common Homebrew Java paths on macOS. `mc-reset-test-env` clears stale tmux sessions and stale `stdin.pipe` files before they turn into misleading startup failures. + ## Build ```bash @@ -92,7 +104,7 @@ 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 +2. Creates a clean temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled and noisy bots disabled 3. Ensures server is running (starts if not, waits for `Done (`) 4. Launches MCC in the specified mode @@ -210,6 +222,9 @@ After `source tools/mcc-env.sh`: | `mc-rcon "CMD"` | Send RCON command | | `mc-kill VER` | Force-kill server tmux session | | `mc-list` | List running MC server sessions | +| `mc-wait-ready VER [SEC]` | Wait for server `Done (` | +| `mc-wait-stop VER [SEC]` | Wait for server shutdown, with force-kill fallback | +| `mc-reset-test-env [--all|VER...]` | Reset shared tmux server state and stale pipes | | `mcc-build` | Build MCC | | `mcc-run [PORT]` | Run MCC classic+FileInput on port | | `mcc-tui [PORT]` | Run MCC TUI mode in tmux | @@ -218,6 +233,7 @@ After `source tools/mcc-env.sh`: | `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 | +| `mcc-preflight [VER...]` | Verify Java, tmux, dotnet, python3, and server dirs | ## Temporary config recipe @@ -226,14 +242,10 @@ source tools/mcc-env.sh TEST_ROOT="${TMPDIR:-/tmp}/mcc-dev" CFG="$TEST_ROOT/MinecraftClient.1.21.11.ini" mkdir -p "$TEST_ROOT" -cp "$MCC_REPO/MinecraftClient.ini" "$CFG" -sed -i \ - -e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e 's/MinecraftVersion = "auto"/MinecraftVersion = "1.21.11"/' \ - -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ - -e 's/InventoryHandling = false/InventoryHandling = true/' \ - -e 's/EntityHandling = false/EntityHandling = true/' \ - "$CFG" +bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ + "$CFG" \ + "1.21.11" \ + "CursorBot" ``` For TUI mode, also add: @@ -257,6 +269,8 @@ Basic command check: mcc-cmd "inventory player list" ``` +If a scripted run fails before MCC joins, check for a harness problem before assuming a product regression. Missing `mcc.log`, a pre-join `Connection refused`, or a server that never reached `Done (` usually means shared-state cleanup or startup failed. + ## Typical debug loop 1. `source tools/mcc-env.sh` diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md index 50673abb..168b545f 100644 --- a/.skills/mcc-integration-testing/SKILL.md +++ b/.skills/mcc-integration-testing/SKILL.md @@ -56,7 +56,7 @@ If the environment cannot run a real server, say so and report the result as une - Use a real local server. - Launch MCC against an explicit `localhost:` target for repeatable local tests. - Keep version matrices sequential in shared local environments. The tmux server harness is shared state by default. -- Prefer temporary MCC configs for scripted runs so one test does not contaminate the next. +- Prefer generated temporary MCC configs for scripted runs so one test does not contaminate the next. - Default to offline auth in generated temp configs. Do not trust the repo-root `MinecraftClient.ini` account defaults. - If the user explicitly asks for Microsoft online login, honor that request and generate the temp config for Microsoft auth instead of offline mode. - For Microsoft auth, prefer an interactive TTY launch with `BasicIO-NoColor` so the device code is easy to read and relay to the user. @@ -65,8 +65,10 @@ If the environment cannot run a real server, say so and report the result as une - Legacy and modern command syntax differ. Do not assume one server-command profile fits every version. - Use actual MCC output and actual server logs for assertions. Do not invent success strings. - Treat server `Done` as startup progress, not RCON readiness. Retry the first RCON command before assuming the setup is broken. +- Run preflight before scripted test loops. On macOS, Java may be installed but not exported on PATH in the shell the harness uses. - If a change touches shared routing or a version range, test at least one adjacent version that shares that path, or explicitly mark adjacent versions as unexecuted and inferred. - For palette or version-content changes, probe at least one neighboring or existing item, entity, or block. Do not only check the headline addition. +- Separate product failures from harness failures. Missing logs, stale tmux state, stale `stdin.pipe`, or pre-join `Connection refused` errors are usually environment problems until proven otherwise. ## Choose the test mode @@ -117,11 +119,19 @@ Run them against a real server with a temp config and summarize counts from the Before running any scenario: +0. run preflight and clear stale shared state when the environment is reused 1. configure the target server for offline testing 2. ensure `eula=true` 3. ensure RCON is enabled 4. build MCC unless the task explicitly reuses a fresh build +Preflight and reset helpers: + +```bash +.skills/mcc-integration-testing/scripts/preflight_test_env.sh 1.21.11-Vanilla +.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh 1.21.11-Vanilla +``` + Offline configuration helper: ```bash @@ -141,8 +151,12 @@ Optionally override the login name with the fourth argument to the config helper - `.skills/mcc-integration-testing/scripts/ensure_offline_server.sh` - configures persistent offline mode and RCON +- `.skills/mcc-integration-testing/scripts/preflight_test_env.sh` + - verifies Java, tmux, dotnet, python3, server directories, and resolves common Java PATH issues +- `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh` + - clears stale tmux sessions and stale `stdin.pipe` files before a rerun - `.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh` - - copies `MinecraftClient.ini`, prepares offline login by default, and can switch to Microsoft auth when explicitly requested + - generates a clean temporary MCC config, prepares offline login by default, disables noisy bots, and can switch to Microsoft auth when explicitly requested - `.skills/mcc-integration-testing/scripts/get_server_port.sh` - resolves the actual local server port from `server.properties` or the latest server log - `.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh` @@ -159,6 +173,7 @@ In every report, separate: - `Executed`: exact scripts, commands, versions, auth mode, and whether the run was sequential or single-version - `Observed`: exact MCC output, exact server-log evidence, and the saved log directory - `Inferred`: conclusions not directly shown by that run's runtime evidence +- `Harness issues`: setup or runner problems such as missing Java on PATH, stale tmux sessions, stale `stdin.pipe`, missing log artifacts, or failed config generation Never upgrade inferred claims to observed facts. Absence of errors is supporting evidence only; pair it with a positive assertion for the feature under test. @@ -196,6 +211,7 @@ Always summarize: ## Troubleshooting - If the first RCON command fails, retry it before assuming the setup is broken. +- If Java is installed but the harness still says it is missing, run `preflight_test_env.sh`. This resolves common Homebrew Java paths on macOS. - If MCC reaches Microsoft device-code login during an offline test, stop and inspect the generated temp config before retrying. - If the user explicitly requests Microsoft online login, set `MCC_TEST_ACCOUNT_TYPE=microsoft` before launching the harness. - If the user explicitly requests Microsoft online login, use `BasicIO-NoColor` in a real TTY, relay the device code from the TUI, and avoid pressing empty Enter at any auth prompt. @@ -203,7 +219,8 @@ Always summarize: - If `dotnet run` cannot see an existing Microsoft session, check whether `SessionCache.db` and `ProfileKeyCache.ini` need to be synced from `MinecraftClient/bin/Release/net10.0/` to the repo root. - If Microsoft auth keeps prompting even with a valid session cache, verify `Account.Login` matches the cached username exactly. - If MCC reports `Connection refused`, verify the launched target matches the server's actual `server-port`. +- If MCC reports `Connection refused` immediately after a server start, also check for stale shared state: old tmux sessions, a stale `stdin.pipe`, or a server that never actually reached `Done (`. - If multiple versions are being tested, do not start them in parallel unless the harness isolates tmux sessions and input files. - If a test assertion fails, inspect the real MCC output before changing the code or weakening the assertion. - If an older server behaves oddly on Linux, check `use-native-transport=false` in `server.properties`. -- If a test should be repeatable, avoid mutating the repo-root `MinecraftClient.ini`. +- If a matrix row fails before producing `mcc.log` or a command transcript, treat it as a harness failure, fix the environment, and rerun that row before drawing product conclusions. diff --git a/.skills/mcc-integration-testing/scripts/common.sh b/.skills/mcc-integration-testing/scripts/common.sh new file mode 100755 index 00000000..973b5da3 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/common.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +ensure_java_in_path() { + if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then + return 0 + fi + + local candidate + for candidate in \ + "${JAVA_BIN:-}" \ + "/opt/homebrew/opt/openjdk/bin/java" \ + "/usr/local/opt/openjdk/bin/java" \ + "/usr/lib/jvm/default-java/bin/java" + do + [[ -z "$candidate" ]] && continue + if [[ -x "$candidate" ]]; then + export PATH="$(dirname "$candidate"):$PATH" + export JAVA_BIN="$candidate" + if java -version >/dev/null 2>&1; then + return 0 + fi + fi + done + + echo "java was not found on PATH. Install Java or set JAVA_BIN." >&2 + return 1 +} + +server_session_name() { + printf 'mc-%s\n' "${1//./_}" +} + +server_running() { + local version="$1" + mc-list | grep -Fq "$(server_session_name "$version")" +} + +wait_for_server_ready() { + local version="$1" + local timeout="${2:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if mc-log "$version" 250 2>/dev/null | grep -Fq "Done ("; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for $version to become ready" >&2 + return 1 +} + +wait_for_server_stop() { + local version="$1" + local timeout="${2:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if ! server_running "$version"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + mc-kill "$version" >/dev/null 2>&1 || true + + if ! server_running "$version"; then + return 0 + fi + + echo "Timed out waiting for $version to stop" >&2 + return 1 +} + +disable_noisy_bots_in_ini() { + local ini_file="$1" + local section + + for section in \ + ScriptScheduler \ + DiscordRpc \ + AntiAFK \ + AutoDig \ + AutoAttack \ + PlayerListLogger \ + ReplayCapture + do + sed_in_place "/^\\[ChatBot\\.${section}\\]/,/^\\[/ { s/^Enabled = true/Enabled = false/; }" "$ini_file" + done +} + +remove_stale_stdin_pipe() { + local version="$1" + local pipe_path="$MCC_SERVERS/$version/stdin.pipe" + + if [[ -e "$pipe_path" && ! -p "$pipe_path" ]]; then + rm -f "$pipe_path" + fi +} diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 1e348445..38e73978 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -5,14 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" - -sed_in_place() { - if [[ "$(uname)" == "Darwin" ]]; then - sed -i '' "$@" - else - sed -i "$@" - fi -} +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" VERSION="${1:-1.21.11-Vanilla}" SERVER_DIR="${MCC_SERVERS:?}/$VERSION" @@ -33,43 +27,6 @@ server_running() { mc-list | grep -Fq "$SESSION_NAME" } -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - while (( elapsed < timeout )); do - if mc-log "$VERSION" 200 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - echo "Timed out waiting for $VERSION to become ready" >&2 - return 1 -} - -wait_for_server_stop() { - local timeout="${1:-60}" - local elapsed=0 - while (( elapsed < timeout )); do - if ! server_running; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - # Legacy servers can leave the tmux session around after stdin stop. - # Fall back to force-killing the session so the harness can continue. - mc-kill "$VERSION" >/dev/null 2>&1 || true - - if ! server_running; then - return 0 - fi - - echo "Timed out waiting for $VERSION to stop" >&2 - return 1 -} - upsert_property() { local key="$1" local value="$2" @@ -83,14 +40,14 @@ upsert_property() { if [[ ! -f "$PROPS_FILE" ]]; then mc-start "$VERSION" - wait_for_server_ready + wait_for_server_ready "$VERSION" mc-stop "$VERSION" - wait_for_server_stop + wait_for_server_stop "$VERSION" fi if server_running; then mc-stop "$VERSION" - wait_for_server_stop + wait_for_server_stop "$VERSION" fi upsert_property "online-mode" "false" diff --git a/.skills/mcc-integration-testing/scripts/preflight_test_env.sh b/.skills/mcc-integration-testing/scripts/preflight_test_env.sh new file mode 100755 index 00000000..22376026 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/preflight_test_env.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' +Usage: preflight_test_env.sh [server-dir...] + +Checks the local MCC test environment and resolves common Java path issues. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +ensure_java_in_path +command -v tmux >/dev/null 2>&1 || { echo "tmux was not found on PATH." >&2; exit 1; } +command -v dotnet >/dev/null 2>&1 || { echo "dotnet was not found on PATH." >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "python3 was not found on PATH." >&2; exit 1; } + +if [[ ! -d "$MCC_SERVERS" ]]; then + echo "Server root not found: $MCC_SERVERS" >&2 + exit 1 +fi + +for server_dir in "$@"; do + [[ -z "$server_dir" ]] && continue + if [[ ! -d "$MCC_SERVERS/$server_dir" ]]; then + echo "Server directory not found: $MCC_SERVERS/$server_dir" >&2 + exit 1 + fi + + remove_stale_stdin_pipe "$server_dir" +done + +printf 'MCC_REPO=%s\n' "$MCC_REPO" +printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" +printf 'JAVA=%s\n' "$(command -v java)" +printf 'TMUX=%s\n' "$(command -v tmux)" +printf 'DOTNET=%s\n' "$(command -v dotnet)" diff --git a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh index 9eae53b3..64727a58 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -1,23 +1,40 @@ #!/usr/bin/env bash set -euo pipefail -sed_in_place() { - if [[ "$(uname)" == "Darwin" ]]; then - sed -i '' "$@" - else - sed -i "$@" - fi +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' >&2 +Usage: + prepare_offline_mcc_config.sh [login] + prepare_offline_mcc_config.sh [login] +EOF } -if [[ $# -lt 3 || $# -gt 4 ]]; then - echo "Usage: $0 [login]" >&2 +if [[ $# -lt 2 || $# -gt 4 ]]; then + usage exit 1 fi -TEMPLATE_INI="$1" -OUTPUT_INI="$2" -MC_VERSION="$3" -LOGIN_NAME="${4:-CursorBot}" +TEMPLATE_INI="" +OUTPUT_INI="" +MC_VERSION="" +LOGIN_NAME="" + +if [[ $# -ge 3 && -f "$1" ]]; then + TEMPLATE_INI="$1" + OUTPUT_INI="$2" + MC_VERSION="$3" + LOGIN_NAME="${4:-CursorBot}" +else + OUTPUT_INI="$1" + MC_VERSION="$2" + LOGIN_NAME="${3:-CursorBot}" +fi + ACCOUNT_TYPE="${MCC_TEST_ACCOUNT_TYPE:-mojang}" PASSWORD_VALUE="${MCC_TEST_PASSWORD-}" @@ -34,6 +51,32 @@ if [[ -z "${MCC_TEST_PASSWORD+x}" ]]; then fi fi +generate_template_ini() { + local template_root + template_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-config-template.XXXXXX")" + + if [[ ! -f "$REPO_ROOT/MinecraftClient/bin/Release/net10.0/MinecraftClient.dll" ]]; then + dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo >/dev/null + fi + + ( + cd "$template_root" + dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build -- --help >/dev/null 2>&1 + ) + + if [[ ! -f "$template_root/MinecraftClient.ini" ]]; then + echo "Failed to generate a temporary MCC config template." >&2 + exit 1 + fi + + TEMPLATE_INI="$template_root/MinecraftClient.ini" +} + +if [[ -z "$TEMPLATE_INI" ]]; then + generate_template_ini +fi + +mkdir -p "$(dirname "$OUTPUT_INI")" cp "$TEMPLATE_INI" "$OUTPUT_INI" sed_in_place \ @@ -46,6 +89,8 @@ sed_in_place \ -e 's#^AutoRespawn = false#AutoRespawn = true#' \ "$OUTPUT_INI" +disable_noisy_bots_in_ini "$OUTPUT_INI" + grep -Fq "AccountType = \"$ACCOUNT_TYPE\"" "$OUTPUT_INI" || { echo "Failed to enforce account type $ACCOUNT_TYPE in $OUTPUT_INI" >&2 exit 1 diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh new file mode 100755 index 00000000..2d84ac1b --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' +Usage: reset_shared_test_state.sh [--all | ...] + +Kills shared tmux test sessions and removes stale stdin pipes. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +kill_named_session() { + local session_name="$1" + tmux kill-session -t "$session_name" 2>/dev/null || true +} + +kill_named_session "mcc-debug" + +if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then + while IFS= read -r session_name; do + [[ -z "$session_name" ]] && continue + kill_named_session "$session_name" + done < <(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true) + + while IFS= read -r pipe_path; do + [[ -z "$pipe_path" ]] && continue + if [[ ! -p "$pipe_path" ]]; then + rm -f "$pipe_path" + fi + done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) +else + for version in "$@"; do + kill_named_session "$(server_session_name "$version")" + remove_stale_stdin_pipe "$version" + done +fi + +rm -f "$MCC_REPO/mcc_input.txt" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh index 45629525..65ff7d3f 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh @@ -64,6 +64,16 @@ run_version() { # shellcheck disable=SC1090 source "$summary_env" + if [[ -n "${MCC_LOG:-}" && ! -f "$MCC_LOG" ]]; then + NOTE="Harness failure: MCC log was not produced." + VERDICT="❌ Fail" + fi + + if [[ -n "${COMMAND_LOG:-}" && ! -f "$COMMAND_LOG" ]]; then + NOTE="Harness failure: command transcript was not produced." + VERDICT="❌ Fail" + fi + write_row "$VERSION" "$SERVER_DIR" "$PORT" "$FAMILY" "$INITIAL_STATUS" "$GRANT_STATUS" "$REVOKE_STATUS" \ "$API_STATUS" "$VERDICT" "$NOTE" "$RUN_DIR" "$MCC_LOG" "$COPIED_SERVER_LOG" "$COMMAND_LOG" } @@ -94,6 +104,7 @@ if ! command -v tmux >/dev/null 2>&1; then fi if [[ "$DOTNET_OK" == "yes" ]]; then + bash "$SCRIPT_DIR/preflight_test_env.sh" >/dev/null 2>&1 || true if ! dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release > "$BUILD_LOG" 2>&1; then BUILD_OK="no" fi diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh index 88c2b027..df0236e0 100755 --- a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -5,14 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" - -sed_in_place() { - if [[ "$(uname)" == "Darwin" ]]; then - sed -i '' "$@" - else - sed -i "$@" - fi -} +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" usage() { cat <<'EOF' @@ -131,6 +125,7 @@ cleanup() { fi mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true + wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true ln -sfn "$RUN_DIR" "$LATEST_LINK" write_summary } @@ -165,43 +160,6 @@ wait_for_file_pattern() { return 1 } -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for server readiness" >&2 - return 1 -} - -disable_noisy_bots() { - sed_in_place '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" - sed_in_place '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$REPO_ROOT/MinecraftClient.ini" -} - -ensure_root_config() { - if [[ -f "$REPO_ROOT/MinecraftClient.ini" ]]; then - return - fi - - ( - cd "$REPO_ROOT" - dotnet run --project MinecraftClient -c Release --no-build -- --help >/dev/null 2>&1 - ) -} - write_probe_script() { cat > "$PROBE_SCRIPT" </dev/null 2>&1 || ! java -version >/dev/null 2>&1; then - fail "java was not found on PATH." -fi - -if ! command -v tmux >/dev/null 2>&1; then - fail "tmux was not found on PATH." -fi - -if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then - fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" -fi - -PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" - -ensure_root_config -"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" -disable_noisy_bots -write_probe_script - -if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then - sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" -fi - if $DO_BUILD; then log_step "BUILD> dotnet build MinecraftClient.sln -c Release" mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed." @@ -344,17 +279,35 @@ else : > "$BUILD_LOG" fi +bash "$SCRIPT_DIR/preflight_test_env.sh" "$SERVER_DIR" >/dev/null || fail "Test environment preflight failed." +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null || fail "Failed to reset shared test state." + +if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then + fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" +fi + +bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null || fail "Failed to prepare temporary MCC config." +PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" + +"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" +write_probe_script + +if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" +fi + : > "$INPUT_FILE" rm -f "$MCC_LOG" log_step "Starting server $SERVER_DIR on port $PORT" mc-start "$SERVER_DIR" >/dev/null -wait_for_server_ready || fail "Server did not become ready." +wait_for_server_ready "$SERVER_DIR" || fail "Server did not become ready." log_step "Starting MCC for $MC_VERSION" ( cd "$REPO_ROOT" MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" \ CursorBot \ - \ "localhost:$PORT" \ diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 2db7d2a8..51021974 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" VERSION="${1:-1.21.11-Vanilla}" MC_VERSION="${VERSION%-Vanilla}" @@ -34,46 +36,12 @@ cleanup() { fi mc-stop "$VERSION" >/dev/null 2>&1 || true + wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true } trap cleanup EXIT prepare_config() { - bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" >/dev/null -} - -wait_for_file_pattern() { - local file="$1" - local pattern="$2" - local description="$3" - local timeout="${4:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for: $description" >&2 - return 1 -} - -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if mc-log "$VERSION" 250 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for server readiness" >&2 - return 1 + bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null } wait_for_server_log_pattern() { @@ -101,6 +69,25 @@ capture_server_logs() { fi } +wait_for_file_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + local timeout="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for: $description" >&2 + return 1 +} + fail() { capture_server_logs echo "FAIL: $1" >&2 @@ -146,18 +133,19 @@ run_mcc_command() { sleep 2 } +bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null "$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" +echo "Building MCC..." +mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" prepare_config SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")" : > "$INPUT_FILE" -echo "Building MCC..." -mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" - echo "Starting server..." mc-start "$VERSION" >/dev/null -wait_for_server_ready || fail "Server did not become ready" +wait_for_server_ready "$VERSION" || fail "Server did not become ready" echo "Starting MCC..." ( diff --git a/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh index 9dbeb78c..6ef72397 100755 --- a/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh +++ b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh @@ -54,4 +54,4 @@ echo "## Inferred" echo echo "- Only rows with real MCC and server-log artifacts count as executed proof." echo "- Rows blocked by missing Java, tmux, or server directories are environment-limited, not product pass results." -echo "- Legacy rows remain the highest-risk bucket because static inspection suggests pre-1.12 \`Statistics\` packets may not currently reach the achievements handler." +echo "- Rows with missing MCC or command-log artifacts should be treated as harness failures until rerun confirms a product issue." diff --git a/.skills/mcc-version-adaptation/SKILL.md b/.skills/mcc-version-adaptation/SKILL.md index 195b5592..706399cb 100644 --- a/.skills/mcc-version-adaptation/SKILL.md +++ b/.skills/mcc-version-adaptation/SKILL.md @@ -15,6 +15,7 @@ Systematic workflow for updating Minecraft Console Client to support a new Minec $MCC_REPO/tools/decompile.sh --version ``` This auto-downloads `MinecraftDecompiler.jar` if needed, produces the decompiled source, and downloads `server.jar` into `$MCC_SERVERS//`. +- `tools/decompile.sh` depends on official mappings. For older versions where it refuses to decompile, fall back to a raw Java decompiler such as `cfr-decompiler` against `$MCC_SERVERS//server.jar`. That fallback is good enough for packet inspection and registration order checks even when the output is obfuscated. - A test server of the target version in `$MCC_SERVERS//` (see `mcc-dev-workflow` skill) ## Step 0: Generate Server Reports (CRITICAL since 1.21.9) diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index b29f4b20..c5c6205e 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -32,6 +32,7 @@ EOF VERSION="1.21.11-Vanilla" MODE="classic" PORT="25565" +PORT_SET_BY_USER=false DO_BUILD=true DEBUG_ON=false FILE_INPUT=false @@ -40,7 +41,7 @@ while [[ $# -gt 0 ]]; do case "$1" in -v|--version) VERSION="$2"; shift 2 ;; -m|--mode) MODE="$2"; shift 2 ;; - -p|--port) PORT="$2"; shift 2 ;; + -p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;; --no-build) DO_BUILD=false; shift ;; --debug-on) DEBUG_ON=true; shift ;; --file-input) FILE_INPUT=true; shift ;; @@ -54,6 +55,10 @@ CFG="$TEST_ROOT/MinecraftClient.debug.ini" MCC_LOG="$TEST_ROOT/mcc-debug.log" INPUT_FILE="$REPO_ROOT/mcc_input.txt" SESSION_NAME="mc-${VERSION//\./_}" +PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" +ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" +PREFLIGHT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" +GET_PORT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" mkdir -p "$TEST_ROOT" @@ -64,6 +69,8 @@ echo " Config: $CFG" echo " Log: $MCC_LOG" echo "" +bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null + # --- Build --- if $DO_BUILD; then echo "[1/4] Building MCC..." @@ -75,21 +82,22 @@ fi # --- Prepare config --- echo "[2/4] Preparing config..." -cp "$REPO_ROOT/MinecraftClient.ini" "$CFG" - -sed -i \ - -e 's/Account = { Login = "[^"]*", Password = "[^"]*" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ - -e 's/InventoryHandling = false/InventoryHandling = true/' \ - -e 's/EntityHandling = false/EntityHandling = true/' \ - "$CFG" +bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" CursorBot >/dev/null if [[ "$MODE" == "tui" ]]; then - sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + else + sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + fi fi if $DEBUG_ON; then - sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG" + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' 's/DebugMessages = false/DebugMessages = true/' "$CFG" + else + sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG" + fi fi echo " Config ready" @@ -99,14 +107,7 @@ echo "[3/4] Starting server $VERSION..." if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo " Server already running" else - # Ensure offline mode - SERVER_DIR="$MCC_SERVERS/$VERSION" - if [[ -f "$SERVER_DIR/server.properties" ]]; then - sed -i 's/^online-mode=.*/online-mode=false/' "$SERVER_DIR/server.properties" - grep -q "^enable-rcon=" "$SERVER_DIR/server.properties" || echo "enable-rcon=true" >> "$SERVER_DIR/server.properties" - grep -q "^rcon.password=" "$SERVER_DIR/server.properties" || echo "rcon.password=test123" >> "$SERVER_DIR/server.properties" - grep -q "^rcon.port=" "$SERVER_DIR/server.properties" || echo "rcon.port=25575" >> "$SERVER_DIR/server.properties" - fi + bash "$ENSURE_SERVER_SCRIPT" "$VERSION" >/dev/null mc-start "$VERSION" >/dev/null echo -n " Waiting for server..." @@ -125,6 +126,10 @@ else done fi +if ! $PORT_SET_BY_USER; then + PORT="$(bash "$GET_PORT_SCRIPT" "$VERSION")" +fi + # --- Launch MCC --- echo "[4/4] Launching MCC in $MODE mode..." : > "$INPUT_FILE" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 6ddca998..904a8a00 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -26,6 +26,9 @@ mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; } mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; } mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; } mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; } +mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; } +mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; } +mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; } # --- RCON --- mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } @@ -59,3 +62,4 @@ mcc-tui() { mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; } mcc-log-mcc() { tail -f "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null || echo "No MCC log found"; } mcc-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; } +mcc-preflight() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$@"; } diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index 35555ad9..83fdb42a 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$REPO_ROOT/.skills/mcc-integration-testing/scripts/common.sh" usage() { cat <<'EOF' @@ -37,6 +39,7 @@ MCC_LOG="$TEST_ROOT/mcc.log" SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" INPUT_FILE="$REPO_ROOT/mcc_input.txt" MCC_PID="" +SERVER_PORT="25565" mkdir -p "$TEST_ROOT" @@ -59,33 +62,6 @@ wait_for_file_pattern() { return 1 } -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for server readiness" >&2 - return 1 -} - -kill_other_servers() { - local sessions - sessions="$(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)" - if [[ -n "$sessions" ]]; then - while IFS= read -r session; do - [[ -z "$session" ]] && continue - tmux kill-session -t "$session" 2>/dev/null || true - done <<< "$sessions" - fi -} - cleanup() { if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then echo "quit" >> "$INPUT_FILE" 2>/dev/null || true @@ -96,7 +72,7 @@ cleanup() { if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true - sleep 2 + wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true fi tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true @@ -105,24 +81,7 @@ cleanup() { trap cleanup EXIT prepare_config() { - cp "$REPO_ROOT/MinecraftClient.ini" "$CFG" - - sed -i \ - -e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e "s/MinecraftVersion = \"auto\"/MinecraftVersion = \"$MC_VERSION\"/" \ - -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ - -e 's/InventoryHandling = false/InventoryHandling = true/' \ - -e 's/EntityHandling = false/EntityHandling = true/' \ - -e 's/AutoRespawn = false/AutoRespawn = true/' \ - "$CFG" - - sed -i '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" + bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null } send_mcc_command() { @@ -195,23 +154,30 @@ modern_mob_and_effects() { run_server_command "effect give CursorBot minecraft:regeneration 10 1 true" } +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null prepare_config -kill_other_servers rm -f "$MCC_LOG" "$INPUT_FILE" bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null +SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR")" if [[ -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then - sed -i 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" fi mc-start "$SERVER_DIR" >/dev/null -wait_for_server_ready || exit 1 +wait_for_server_ready "$SERVER_DIR" || exit 1 : > "$INPUT_FILE" ( cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" > "$MCC_LOG" 2>&1 + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" \ + CursorBot \ + - \ + "localhost:$SERVER_PORT" \ + > "$MCC_LOG" 2>&1 ) & MCC_PID=$! diff --git a/tools/start-server.sh b/tools/start-server.sh index 9debbae9..63e5eed3 100644 --- a/tools/start-server.sh +++ b/tools/start-server.sh @@ -1,12 +1,38 @@ #!/bin/bash # Start a Minecraft server in a tmux session with named pipe for stdin # Servers live under $MCC_SERVERS or default to MinecraftOfficial/downloads//. +resolve_java_bin() { + if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then + command -v java + return 0 + fi + + local candidate + for candidate in \ + "${JAVA_BIN:-}" \ + "/opt/homebrew/opt/openjdk/bin/java" \ + "/usr/local/opt/openjdk/bin/java" \ + "/usr/lib/jvm/default-java/bin/java" + do + [[ -z "$candidate" ]] && continue + if [[ -x "$candidate" ]]; then + if "$candidate" -version >/dev/null 2>&1; then + printf '%s\n' "$candidate" + return 0 + fi + fi + done + + return 1 +} + VERSION="${1}" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DOWNLOADS="${MCC_SERVERS:-$REPO_ROOT/MinecraftOfficial/downloads}" DIR="$DOWNLOADS/$VERSION" PIPE="$DIR/stdin.pipe" SESSION="mc-${VERSION//\./_}" +JAVA_BIN="$(resolve_java_bin || true)" if [ -z "$VERSION" ] || [ ! -d "$DIR" ]; then echo "Error: Server directory not found${VERSION:+: $DIR}" @@ -20,6 +46,16 @@ if [ ! -f "$DIR/server.jar" ]; then exit 1 fi +if ! command -v tmux >/dev/null 2>&1; then + echo "Error: tmux is required to start local test servers" + exit 1 +fi + +if [[ -z "$JAVA_BIN" ]]; then + echo "Error: Java was not found on PATH. Install Java or set JAVA_BIN." >&2 + exit 1 +fi + if tmux has-session -t "$SESSION" 2>/dev/null; then echo "Server $VERSION already running in tmux session '$SESSION'" echo "View output: tmux capture-pane -t '$SESSION' -p -S -50" @@ -29,10 +65,14 @@ fi rm -f "$DIR/world/session.lock" +if [[ -e "$PIPE" && ! -p "$PIPE" ]]; then + rm -f "$PIPE" +fi + [ -p "$PIPE" ] || mkfifo "$PIPE" tmux new-session -d -s "$SESSION" -c "$DIR" \ - "tail -f $PIPE | java -Xmx2G -Xms2G -jar server.jar nogui 2>&1" + "tail -f $PIPE | '$JAVA_BIN' -Xmx2G -Xms2G -jar server.jar nogui 2>&1" echo "Server $VERSION started in tmux session '$SESSION'" echo "Send commands: echo 'say hello' > $PIPE" From eaf4704473113c01f6b954bb4314e6db08dc3ef0 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 00:00:50 +0800 Subject: [PATCH 59/76] Add icon banner display option and refactor startup banner logic - Introduced a configuration option `Display_Icon_Banner` to control the visibility of the startup icon banner. - Refactored `ProcessStartupState` to utilize TUI for displaying the banner if enabled, falling back to a classic banner display otherwise. - Added new methods for building the banner panel and icon grid for improved visual representation. - Updated translations and resource comments to support the new banner features. --- MinecraftClient/Program.cs | 30 +- .../ConfigComments/ConfigComments.Designer.cs | 4384 +++++++++-------- .../ConfigComments/ConfigComments.resx | 3 + .../Translations/Translations.Designer.cs | 12 + .../Resources/Translations/Translations.resx | 6 + MinecraftClient/Settings.cs | 3 + MinecraftClient/Tui/IconGridBuilder.cs | 125 + MinecraftClient/Tui/MccBannerPanelBuilder.cs | 180 + .../Tui/ServerStatusPanelBuilder.cs | 113 +- 9 files changed, 2557 insertions(+), 2299 deletions(-) create mode 100644 MinecraftClient/Tui/IconGridBuilder.cs create mode 100644 MinecraftClient/Tui/MccBannerPanelBuilder.cs diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 8c1e7644..9e71ad8e 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -228,9 +228,26 @@ namespace MinecraftClient /// True if startup can continue; false if config load failed and user chose to exit. internal static bool ProcessStartupState(StartupState state) { - ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - if (BuildInfo is not null) - ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + if (Config.Console.General.Display_Icon_Banner && ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBanner) + { + var view = tuiBanner.GetView(); + if (view is not null) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + var panel = Tui.MccBannerPanelBuilder.Build(BuildInfo); + view.AppendControlToLog(panel); + }); + } + else + { + ShowClassicBanner(); + } + } + else + { + ShowClassicBanner(); + } var cfg = state.ConfigResult; @@ -271,6 +288,13 @@ namespace MinecraftClient return true; } + private static void ShowClassicBanner() + { + ConsoleIO.WriteLine(string.Format(Translations.mcc_banner_classic, Version, MCLowestVersion, MCHighestVersion, "Github.com/MCCTeam")); + if (BuildInfo is not null) + ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + } + private static void MaybePrintClassicModeTuiRecommendation() { if (ConsoleIO.BasicIO diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 66213cc9..d94cab66 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1,1848 +1,1858 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MinecraftClient { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ConfigComments { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ConfigComments() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MinecraftClient.Resources.ConfigComments.ConfigComments", typeof(ConfigComments).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to can be used in some other fields as %yourvar% - ///%username% and %serverip% are reserved variables.. - /// - internal static string AppVars_Variables { - get { - return ResourceManager.GetString("AppVars.Variables", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to =============================== # - /// Minecraft Console Client Bots # - ///=============================== #. - /// - internal static string ChatBot { - get { - return ResourceManager.GetString("ChatBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Get alerted when specified words are detected in chat - ///Useful for moderating your server or detecting when someone is talking to you. - /// - internal static string ChatBot_Alerts { - get { - return ResourceManager.GetString("ChatBot.Alerts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Play a beep sound when a word is detected in addition to highlighting.. - /// - internal static string ChatBot_Alerts_Beep_Enabled { - get { - return ResourceManager.GetString("ChatBot.Alerts.Beep_Enabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to List of words/strings to NOT alert you on.. - /// - internal static string ChatBot_Alerts_Excludes { - get { - return ResourceManager.GetString("ChatBot.Alerts.Excludes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The name of a file where alers logs will be written.. - /// - internal static string ChatBot_Alerts_Log_File { - get { - return ResourceManager.GetString("ChatBot.Alerts.Log_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log alerts info a file.. - /// - internal static string ChatBot_Alerts_Log_To_File { - get { - return ResourceManager.GetString("ChatBot.Alerts.Log_To_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to List of words/strings to alert you on.. - /// - internal static string ChatBot_Alerts_Matches { - get { - return ResourceManager.GetString("ChatBot.Alerts.Matches", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Trigger alerts when it rains and when it stops.. - /// - internal static string ChatBot_Alerts_Trigger_By_Rain { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Rain", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Triggers alerts at the beginning and end of thunderstorms.. - /// - internal static string ChatBot_Alerts_Trigger_By_Thunderstorm { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Thunderstorm", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Triggers an alert after receiving a specified keyword.. - /// - internal static string ChatBot_Alerts_Trigger_By_Words { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Words", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send a command on a regular or random basis or make the bot walk around randomly to avoid automatic AFK disconnection - /// /!\ Make sure your server rules do not forbid anti-AFK mechanisms! - /// /!\ Make sure you keep the bot in an enclosure to prevent it wandering off if you're using terrain handling! (Recommended size 5x5x5). - /// - internal static string ChatBot_AntiAfk { - get { - return ResourceManager.GetString("ChatBot.AntiAfk", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command to send to the server.. - /// - internal static string ChatBot_AntiAfk_Command { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Command", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The time interval for execution. (in seconds). - /// - internal static string ChatBot_AntiAfk_Delay { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sneak when sending the command.. - /// - internal static string ChatBot_AntiAfk_Use_Sneak { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Use_Sneak", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use terrain handling to enable the bot to move around.. - /// - internal static string ChatBot_AntiAfk_Use_Terrain_Handling { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Use_Terrain_Handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The range the bot can move around randomly (Note: the bigger the range, the slower the bot will be). - /// - internal static string ChatBot_AntiAfk_Walk_Range { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Range", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How many times can the bot fail trying to move before using the command method.. - /// - internal static string ChatBot_AntiAfk_Walk_Retries { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Retries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically attack hostile mobs around you - ///You need to enable Entity Handling to use this bot - /// /!\ Make sure server rules allow your planned use of AutoAttack - /// /!\ SERVER PLUGINS may consider AutoAttack to be a CHEAT MOD and TAKE ACTION AGAINST YOUR ACCOUNT so DOUBLE CHECK WITH SERVER RULES!. - /// - internal static string ChatBot_AutoAttack { - get { - return ResourceManager.GetString("ChatBot.AutoAttack", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow attacking hostile mobs.. - /// - internal static string ChatBot_AutoAttack_Attack_Hostile { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Hostile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow attacking passive mobs.. - /// - internal static string ChatBot_AutoAttack_Attack_Passive { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Passive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Capped between 1 to 4. - /// - internal static string ChatBot_AutoAttack_Attack_Range { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Range", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait between each attack. Set "Custom = false" to let MCC calculate it.. - /// - internal static string ChatBot_AutoAttack_Cooldown_Time { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Cooldown_Time", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All entity types can be found here: https://mccteam.github.io/r/entity/#L15. - /// - internal static string ChatBot_AutoAttack_Entites_List { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Entites_List", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Possible values: "Interact", "Attack" (default), "InteractAt" (Interact and Attack).. - /// - internal static string ChatBot_AutoAttack_Interaction { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Interaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wether to treat the entities list as a "whitelist" or as a "blacklist".. - /// - internal static string ChatBot_AutoAttack_List_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.List_Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "single" or "multi". single target one mob per attack. multi target all mobs in range per attack. - /// - internal static string ChatBot_AutoAttack_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "health" or "distance". Only needed when using single mode. - /// - internal static string ChatBot_AutoAttack_Priority { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Priority", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically craft items in your inventory - ///See https://mccteam.github.io/g/bots/#auto-craft for how to use - ///You need to enable Inventory Handling to use this bot - ///You should also enable Terrain and Movements if you need to use a crafting table. - /// - internal static string ChatBot_AutoCraft { - get { - return ResourceManager.GetString("ChatBot.AutoCraft", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Location of the crafting table if you intended to use it. Terrain and movements must be enabled.. - /// - internal static string ChatBot_AutoCraft_CraftingTable { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.CraftingTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to What to do on crafting failure, "abort" or "wait".. - /// - internal static string ChatBot_AutoCraft_OnFailure { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.OnFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Recipes.Name: The name can be whatever you like and it is used to represent the recipe. - ///Recipes.Type: crafting table type: "player" or "table" - ///Recipes.Result: the resulting item - ///Recipes.Slots: All slots, counting from left to right, top to bottom. Please fill in "Null" for empty slots. - ///For the naming of the items, please see: https://mccteam.github.io/r/item/#L12. - /// - internal static string ChatBot_AutoCraft_Recipes { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.Recipes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Auto-digging blocks. - ///You need to enable Terrain Handling to use this bot - ///You can use "/digbot start" and "/digbot stop" to control the start and stop of AutoDig. - ///Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead. - ///For the naming of the block, please see https://mccteam.github.io/r/block/#L15. - /// - internal static string ChatBot_AutoDig { - get { - return ResourceManager.GetString("ChatBot.AutoDig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How many seconds to wait after entering the game to start digging automatically, set to -1 to disable automatic start.. - /// - internal static string ChatBot_AutoDig_Auto_Start_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Auto_Start_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically switch to the appropriate tool.. - /// - internal static string ChatBot_AutoDig_Auto_Tool_Switch { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Auto_Tool_Switch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mining a block for more than "Dig_Timeout" seconds will be considered a timeout.. - /// - internal static string ChatBot_AutoDig_Dig_Timeout { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Dig_Timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to drop the current tool when its durability is too low.. - /// - internal static string ChatBot_AutoDig_Drop_Low_Durability_Tools { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Drop_Low_Durability_Tools", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Will not use tools with less durability than this. Set to zero to disable this feature.. - /// - internal static string ChatBot_AutoDig_Durability_Limit { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Durability_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wether to treat the blocks list as a "whitelist" or as a "blacklist".. - /// - internal static string ChatBot_AutoDig_List_Type { - get { - return ResourceManager.GetString("ChatBot.AutoDig.List_Type", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "distance" or "index", When using the "fixedpos" mode, the blocks are determined by distance to the player, or by the order in the list.. - /// - internal static string ChatBot_AutoDig_Location_Order { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Location_Order", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The position of the blocks when using "fixedpos" or "both" mode.. - /// - internal static string ChatBot_AutoDig_Locations { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Locations", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to output logs when digging blocks.. - /// - internal static string ChatBot_AutoDig_Log_Block_Dig { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Log_Block_Dig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "lookat", "fixedpos" or "both". Digging the block being looked at, the block in a fixed position, or the block that needs to be all met.. - /// - internal static string ChatBot_AutoDig_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically drop items in inventory - ///You need to enable Inventory Handling to use this bot - ///See this file for an up-to-date list of item types you can use with this bot: https://mccteam.github.io/r/item/#L12. - /// - internal static string ChatBot_AutoDrop { - get { - return ResourceManager.GetString("ChatBot.AutoDrop", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "include", "exclude" or "everything". Include: drop item IN the list. Exclude: drop item NOT IN the list. - /// - internal static string ChatBot_AutoDrop_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoDrop.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically eat food when your Hunger value is low - ///You need to enable Inventory Handling to use this bot. - /// - internal static string ChatBot_AutoEat { - get { - return ResourceManager.GetString("ChatBot.AutoEat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically catch fish using a fishing rod - ///Guide: https://mccteam.github.io/g/bots/#auto-fishing - ///You can use "/fish" to control the bot manually. - /// /!\ Make sure server rules allow automated farming before using this bot. - /// - internal static string ChatBot_AutoFishing { - get { - return ResourceManager.GetString("ChatBot.AutoFishing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep it as false if you have not changed it before.. - /// - internal static string ChatBot_AutoFishing_Antidespawn { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Antidespawn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch to a new rod from inventory after the current rod is unavailable.. - /// - internal static string ChatBot_AutoFishing_Auto_Rod_Switch { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Rod_Switch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to start fishing automatically after entering a world.. - /// - internal static string ChatBot_AutoFishing_Auto_Start { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Start", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How soon to re-cast after successful fishing.. - /// - internal static string ChatBot_AutoFishing_Cast_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Cast_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Will not use rods with less durability than this (full durability is 64). Set to zero to disable this feature.. - /// - internal static string ChatBot_AutoFishing_Durability_Limit { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Durability_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This allows the player to change position/facing after each fish caught.. - /// - internal static string ChatBot_AutoFishing_Enable_Move { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Enable_Move", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long after entering the game to start fishing (seconds).. - /// - internal static string ChatBot_AutoFishing_Fishing_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fishing timeout (seconds). Timeout will trigger a re-cast.. - /// - internal static string ChatBot_AutoFishing_Fishing_Timeout { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.. - /// - internal static string ChatBot_AutoFishing_Hook_Threshold { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Hook_Threshold", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet.. - /// - internal static string ChatBot_AutoFishing_Log_Fish_Bobber { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Log_Fish_Bobber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use the mainhand or the offhand to hold the rod.. - /// - internal static string ChatBot_AutoFishing_Mainhand { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Mainhand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to It will move in order "1->2->3->4->3->2->1->2->..." and can change position or facing or both each time. It is recommended to change the facing only.. - /// - internal static string ChatBot_AutoFishing_Movements { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Movements", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hook movement in the X and Z axis less than this value will be considered stationary.. - /// - internal static string ChatBot_AutoFishing_Stationary_Threshold { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Stationary_Threshold", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically relog when disconnected by server, for example because the server is restating - /// /!\ Use Ignore_Kick_Message=true at own risk! Server staff might not appreciate if you auto-relog on manual kicks. - /// - internal static string ChatBot_AutoRelog { - get { - return ResourceManager.GetString("ChatBot.AutoRelog", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The delay time before joining the server. (in seconds). - /// - internal static string ChatBot_AutoRelog_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When set to true, autorelog will reconnect regardless of kick messages.. - /// - internal static string ChatBot_AutoRelog_Ignore_Kick_Message { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Ignore_Kick_Message", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If the kickout message matches any of the strings, then autorelog will be triggered.. - /// - internal static string ChatBot_AutoRelog_Kick_Messages { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Kick_Messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Retries when failing to relog to the server. use -1 for unlimited retries.. - /// - internal static string ChatBot_AutoRelog_Retries { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Retries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Run commands or send messages automatically when a specified pattern is detected in chat - ///Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules - /// /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam. - /// - internal static string ChatBot_AutoRespond { - get { - return ResourceManager.GetString("ChatBot.AutoRespond", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not remove colors from text (Note: Your matches will have to include color codes (ones using the § character) in order to work). - /// - internal static string ChatBot_AutoRespond_Match_Colors { - get { - return ResourceManager.GetString("ChatBot.AutoRespond.Match_Colors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Logs chat messages in a file on disk.. - /// - internal static string ChatBot_ChatLog { - get { - return ResourceManager.GetString("ChatBot.ChatLog", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This bot allows you to send and recieve messages and commands via a Discord channel. - ///For Setup you can either use the documentation or read here (Documentation has images). - ///Documentation: https://mccteam.github.io/g/bots/#discord-bridge - ///Setup: - ///First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA . - /// /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent [rest of string was truncated]";. - /// - internal static string ChatBot_DiscordBridge { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ID of a channel where you want to interact with the MCC using the bot.. - /// - internal static string ChatBot_DiscordBridge_ChannelId { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.ChannelId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message formats - ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! - ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. - ///For Discord message formatting, check the following: https://mccteam.github.io/r/dc-fmt.html. - /// - internal static string ChatBot_DiscordBridge_Formats { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.Formats", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ID of a server/guild where you have invited the bot to.. - /// - internal static string ChatBot_DiscordBridge_GuildId { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.GuildId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second).. - /// - internal static string ChatBot_DiscordBridge_MessageSendTimeout { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.MessageSendTimeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A list of IDs of people you want to be able to interact with the MCC using the bot.. - /// - internal static string ChatBot_DiscordBridge_OwnersIds { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.OwnersIds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Discord Bot token.. - /// - internal static string ChatBot_DiscordBridge_Token { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.Token", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat.. - /// - internal static string ChatBot_DiscordBridge_AllowOtherBotMessages { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.AllowOtherBotMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically farms cropsfor you (plants, breaks and bonemeals them). - ///Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. - ///Usage: "/farmer start" command and "/farmer stop" command. - ///NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes. - ///or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this. [rest of string was truncated]";. - /// - internal static string ChatBot_Farmer { - get { - return ResourceManager.GetString("ChatBot.Farmer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delay between tasks in seconds (Minimum 1 second). - /// - internal static string ChatBot_Farmer_Delay_Between_Tasks { - get { - return ResourceManager.GetString("ChatBot.Farmer.Delay_Between_Tasks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enabled you to make the bot follow you - ///NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you - ///It's similar to making animals follow you when you're holding food in your hand. - ///This is due to a slow pathfinding algorithm, we're working on getting a better one - ///You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, /// [rest of string was truncated]";. - /// - internal static string ChatBot_FollowPlayer { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not follow the player if he is in the range of 3 blocks (prevents the bot from pushing a player in an infinite loop). - /// - internal static string ChatBot_FollowPlayer_Stop_At_Distance { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer.Stop_At_Distance", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The rate at which the bot does calculations (in seconds) (You can tweak this if you feel the bot is too slow). - /// - internal static string ChatBot_FollowPlayer_Update_Limit { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer.Update_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A small game to demonstrate chat interactions. Players can guess mystery words one letter at a time. - ///You need to have ChatFormat working correctly and add yourself in botowners to start the game with /tell <bot username> start - /// /!\ This bot may get a bit spammy if many players are interacting with it. - /// - internal static string ChatBot_HangmanGame { - get { - return ResourceManager.GetString("ChatBot.HangmanGame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A Chat Bot that collects items on the ground. - /// - internal static string ChatBot_ItemsCollector { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will return to it's starting position after there are no items to collect. - /// - internal static string ChatBot_ItemsCollector_Always_Return_To_Start { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Always_Return_To_Start", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will collect all items, regardless of their type. If you want to use the whitelisted item types, disable this by setting it to false. - /// - internal static string ChatBot_ItemsCollector_Collect_All_Item_Types { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Collect_All_Item_Types", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The radius in which bot will look for items to collect (Default: 30). - /// - internal static string ChatBot_ItemsCollector_Collection_Radius { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Collection_Radius", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delay in milliseconds between bot scanning items (Recommended: 300-500). - /// - internal static string ChatBot_ItemsCollector_Delay_Between_Tasks { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Delay_Between_Tasks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to In this list you can specify which items the bot will collect. To enable this, set the Collect_All_Item_Types to false. (NOTE: This does not prevent the bot from accidentally picking up other items, it only goes to positions where it finds the whitelisted items)\nYou can see the list of item types here: https://raw.githubusercontent.com/MCCTeam/Minecraft-Console-Client/master/MinecraftClient/Inventory/ItemType.cs. - /// - internal static string ChatBot_ItemsCollector_Items_Whitelist { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Items_Whitelist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will go after clustered items instead for the closest ones. - /// - internal static string ChatBot_ItemsCollector_Prioritize_Clusters { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Prioritize_Clusters", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info. - ///Setup: - ///1. Go to https://discord.com/developers/applications and log in with your Discord account. [rest of string was truncated]";. - /// - internal static string ChatBot_DiscordRpc { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Discord Application ID.. - /// - internal static string ChatBot_DiscordRpc_ApplicationId { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ApplicationId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The top line of the Rich Presence display. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_PresenceDetails { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceDetails", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The second line of the Rich Presence display. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_PresenceState { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceState", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The key of the large image asset uploaded to your Discord application.. - /// - internal static string ChatBot_DiscordRpc_LargeImageKey { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tooltip text for the large image. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_LargeImageText { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The key of the small image asset uploaded to your Discord application (leave empty to hide).. - /// - internal static string ChatBot_DiscordRpc_SmallImageKey { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tooltip text for the small image. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_SmallImageText { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the server address (host and port) in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowServerAddress { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowServerAddress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the player coordinates in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowCoordinates { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowCoordinates", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show health and food level in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowHealth { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowHealth", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the current dimension in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowDimension { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowDimension", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the current gamemode in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowGamemode { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowGamemode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show elapsed session time in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowElapsedTime { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowElapsedTime", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the online player count as a party size in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowPlayerCount { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowPlayerCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How often (in seconds) to refresh the Discord presence. Minimum: 1. - /// - internal static string ChatBot_DiscordRpc_UpdateIntervalSeconds { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.UpdateIntervalSeconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Relay messages between players and servers, like a mail plugin - ///This bot can store messages when the recipients are offline, and send them when they join the server - /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins. - /// - internal static string ChatBot_Mailer { - get { - return ResourceManager.GetString("ChatBot.Mailer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot) - ///This is useful for solving captchas which use maps - ///The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled. - ///NOTE: - ///If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console. - /// /!\ Make sure server rules allow bots to be used on the server, or you risk being punished.. - /// - internal static string ChatBot_Map { - get { - return ResourceManager.GetString("ChatBot.Map", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically render the map once it is received or updated from/by the server. - /// - internal static string ChatBot_Map_Auto_Render_On_Update { - get { - return ResourceManager.GetString("ChatBot.Map.Auto_Render_On_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delete all rendered maps on unload/reload or when you launch the MCC again.. - /// - internal static string ChatBot_Map_Delete_All_On_Unload { - get { - return ResourceManager.GetString("ChatBot.Map.Delete_All_On_Unload", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Get a notification when you have gotten a map from the server for the first time. - /// - internal static string ChatBot_Map_Notify_On_First_Update { - get { - return ResourceManager.GetString("ChatBot.Map.Notify_On_First_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord.. - /// - internal static string ChatBot_Map_Rasize_Rendered_Image { - get { - return ResourceManager.GetString("ChatBot.Map.Rasize_Rendered_Image", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to render the map in the console.. - /// - internal static string ChatBot_Map_Render_In_Console { - get { - return ResourceManager.GetString("ChatBot.Map.Render_In_Console", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The size that a rendered image should be resized to, in pixels (eg. 512).. - /// - internal static string ChatBot_Map_Resize_To { - get { - return ResourceManager.GetString("ChatBot.Map.Resize_To", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge).. - /// - internal static string ChatBot_Map_Save_To_File { - get { - return ResourceManager.GetString("ChatBot.Map.Save_To_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!) - ///You need to enable Save_To_File in order for this to work. - ///We also recommend turning on resizing.. - /// - internal static string ChatBot_Map_Send_Rendered_To_Bridges { - get { - return ResourceManager.GetString("ChatBot.Map.Send_Rendered_To_Bridges", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log the list of players periodically into a textual file.. - /// - internal static string ChatBot_PlayerListLogger { - get { - return ResourceManager.GetString("ChatBot.PlayerListLogger", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to (In seconds). - /// - internal static string ChatBot_PlayerListLogger_Delay { - get { - return ResourceManager.GetString("ChatBot.PlayerListLogger.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send MCC console commands to your bot through server PMs (/tell) - ///You need to have ChatFormat working correctly and add yourself in botowners to use the bot - /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable RemoteControl only if you trust server admins. - /// - internal static string ChatBot_RemoteControl { - get { - return ResourceManager.GetString("ChatBot.RemoteControl", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable recording of the game (/replay start) and replay it later using the Replay Mod (https://www.replaymod.com/) - ///Please note that due to technical limitations, the client player (you) will not be shown in the replay file - /// /!\ You SHOULD use /replay stop or exit the program gracefully with /quit OR THE REPLAY FILE MAY GET CORRUPT!. - /// - internal static string ChatBot_ReplayCapture { - get { - return ResourceManager.GetString("ChatBot.ReplayCapture", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long should replay file be auto-saved, in seconds. Use -1 to disable.. - /// - internal static string ChatBot_ReplayCapture_Backup_Interval { - get { - return ResourceManager.GetString("ChatBot.ReplayCapture.Backup_Interval", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Schedule commands and scripts to launch on various events such as server join, date/time or time interval - ///See https://mccteam.github.io/g/bots/#script-scheduler for more info. - /// - internal static string ChatBot_ScriptScheduler { - get { - return ResourceManager.GetString("ChatBot.ScriptScheduler", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel. - /// /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel. - ///----------------------------------------------------------- - ///Setup: - ///First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather - ///Click on "Start" button and re [rest of string was truncated]";. - /// - internal static string ChatBot_TelegramBridge { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram.. - /// - internal static string ChatBot_TelegramBridge_Authorized_Chat_Ids { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Authorized_Chat_Ids", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An ID of a channel where you want to interact with the MCC using the bot.. - /// - internal static string ChatBot_TelegramBridge_ChannelId { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.ChannelId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message formats - ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! - ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. - ///For Telegram message formatting, check the following: https://mccteam.github.io/r/tg-fmt.html. - /// - internal static string ChatBot_TelegramBridge_Formats { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Formats", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second).. - /// - internal static string ChatBot_TelegramBridge_MessageSendTimeout { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.MessageSendTimeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Telegram Bot token.. - /// - internal static string ChatBot_TelegramBridge_Token { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Token", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon.. - /// - internal static string ChatBot_WebSocketBot { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow IP aliases, such as "localhost" or if using containers then the container name can be used.... - /// - internal static string ChatBot_WebSocketBot_AllowIpAlias { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.AllowIpAlias", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This setting is for developers who are developing a library that uses this chat bot to remotely execute procedures/commands/functions.. - /// - internal static string ChatBot_WebSocketBot_DebugMode { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.DebugMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The IP address that Websocket server will be bound to.. - /// - internal static string ChatBot_WebSocketBot_Ip { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Ip", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A password that will be used to authenticate on thw Websocket server (It is recommended to change the default password and to set a strong one).. - /// - internal static string ChatBot_WebSocketBot_Password { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Port that Websocket server will be bounded to.. - /// - internal static string ChatBot_WebSocketBot_Port { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Port", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MCC does it best to detect chat messages, but some server have unusual chat formats - ///When this happens, you'll need to configure chat format below, see https://mccteam.github.io/g/conf/#chat-format-section. - /// - internal static string ChatFormat { - get { - return ResourceManager.GetString("ChatFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MCC support for common message formats. Set "false" to avoid conflicts with custom formats.. - /// - internal static string ChatFormat_Builtins { - get { - return ResourceManager.GetString("ChatFormat.Builtins", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to use the custom regular expressions below for detection.. - /// - internal static string ChatFormat_UserDefined { - get { - return ResourceManager.GetString("ChatFormat.UserDefined", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Console-related settings.. - /// - internal static string Console { - get { - return ResourceManager.GetString("Console", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The settings for command completion suggestions. - ///Custom colors are only available when using "vt100_24bit" color mode.. - /// - internal static string Console_CommandSuggestion { - get { - return ResourceManager.GetString("Console.CommandSuggestion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to display command suggestions in the console.. - /// - internal static string Console_CommandSuggestion_Enable { - get { - return ResourceManager.GetString("Console.CommandSuggestion.Enable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable this option if the arrows in the command suggestions are not displayed properly in your terminal.. - /// - internal static string Console_CommandSuggestion_Use_Basic_Arrow { - get { - return ResourceManager.GetString("Console.CommandSuggestion.Use_Basic_Arrow", resourceCulture); - } - } - - /// - /// 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.. - /// - internal static string Console_General_ConsoleColorMode { - get { - return ResourceManager.GetString("Console.General.ConsoleColorMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can use "Ctrl+P" to print out the current input and cursor position.. - /// - internal static string Console_General_Display_Input { - get { - return ResourceManager.GetString("Console.General.Display_Input", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Startup Config File - ///Please do not record extraneous data in this file as it will be overwritten by MCC. - /// - ///New to Minecraft Console Client? Check out this document: https://mccteam.github.io/g/conf.html - ///Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Console-Client/#download. - /// - internal static string Head { - get { - return ResourceManager.GetString("Head", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This setting affects only the messages in the console.. - /// - internal static string Logging { - get { - return ResourceManager.GetString("Logging", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regex for filtering chat message.. - /// - internal static string Logging_ChatFilter { - get { - return ResourceManager.GetString("Logging.ChatFilter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show server chat messages.. - /// - internal static string Logging_ChatMessages { - get { - return ResourceManager.GetString("Logging.ChatMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regex for filtering debug message.. - /// - internal static string Logging_DebugFilter { - get { - return ResourceManager.GetString("Logging.DebugFilter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please enable this before submitting bug reports. Thanks!. - /// - internal static string Logging_DebugMessages { - get { - return ResourceManager.GetString("Logging.DebugMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show error messages.. - /// - internal static string Logging_ErrorMessages { - get { - return ResourceManager.GetString("Logging.ErrorMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "disable" or "blacklist" OR "whitelist". Blacklist hide message match regex. Whitelist show message match regex.. - /// - internal static string Logging_FilterMode { - get { - return ResourceManager.GetString("Logging.FilterMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Informative messages. (i.e Most of the message from MCC). - /// - internal static string Logging_InfoMessages { - get { - return ResourceManager.GetString("Logging.InfoMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log file name.. - /// - internal static string Logging_LogFile { - get { - return ResourceManager.GetString("Logging.LogFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Write log messages to file.. - /// - internal static string Logging_LogToFile { - get { - return ResourceManager.GetString("Logging.LogToFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prepend timestamp to messages in log file.. - /// - internal static string Logging_PrependTimestamp { - get { - return ResourceManager.GetString("Logging.PrependTimestamp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep color codes in the saved text.(look like "§b"). - /// - internal static string Logging_SaveColorCodes { - get { - return ResourceManager.GetString("Logging.SaveColorCodes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show warning messages.. - /// - internal static string Logging_WarningMessages { - get { - return ResourceManager.GetString("Logging.WarningMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Make sure you understand what each setting does before changing anything!. - /// - internal static string Main_Advanced { - get { - return ResourceManager.GetString("Main.Advanced", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AccountList: It allows a fast account switching without directly using the credentials - ///Usage examples: "/tell <mybot> reco Player2", "/connect <serverip> Player1". - /// - internal static string Main_Advanced_account_list { - get { - return ResourceManager.GetString("Main.Advanced.account_list", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle auto respawn if client player was dead (make sure your spawn point is safe).. - /// - internal static string Main_Advanced_auto_respawn { - get { - return ResourceManager.GetString("Main.Advanced.auto_respawn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set the owner of the bot. /!\ Server admins can impersonate owners!. - /// - internal static string Main_Advanced_bot_owners { - get { - return ResourceManager.GetString("Main.Advanced.bot_owners", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "mcc", "vanilla" or "none". This is how MCC identifies itself to the server.. - /// - internal static string Main_Advanced_brand_info { - get { - return ResourceManager.GetString("Main.Advanced.brand_info", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Leave empty for no logfile.. - /// - internal static string Main_Advanced_chatbot_log_file { - get { - return ResourceManager.GetString("Main.Advanced.chatbot_log_file", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If turned off, the emoji will be replaced with a simpler character (for /chunk status).. - /// - internal static string Main_Advanced_enable_emoji { - get { - return ResourceManager.GetString("Main.Advanced.enable_emoji", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set to false to opt-out of Sentry error logging.. - /// - internal static string Main_Advanced_enable_sentry { - get { - return ResourceManager.GetString("Main.Advanced.enable_sentry", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle entity handling.. - /// - internal static string Main_Advanced_entity_handling { - get { - return ResourceManager.GetString("Main.Advanced.entity_handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to exit directly when an error occurs, for using MCC in non-interactive scripts.. - /// - internal static string Main_Advanced_exit_on_failure { - get { - return ResourceManager.GetString("Main.Advanced.exit_on_failure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ignore invalid player name. - /// - internal static string Main_Advanced_ignore_invalid_playername { - get { - return ResourceManager.GetString("Main.Advanced.ignore_invalid_playername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "none", "slash"(/) or "backslash"(\).. - /// - internal static string Main_Advanced_internal_cmd_char { - get { - return ResourceManager.GetString("Main.Advanced.internal_cmd_char", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle inventory handling.. - /// - internal static string Main_Advanced_inventory_handling { - get { - return ResourceManager.GetString("Main.Advanced.inventory_handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fill in with in-game locale code, check https://mccteam.github.io/r/l-code.html. - /// - internal static string Main_Advanced_language { - get { - return ResourceManager.GetString("Main.Advanced.language", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Load translations applied to MCC when available, turn it off to use English only.. - /// - internal static string Main_Advanced_LoadMccTrans { - get { - return ResourceManager.GetString("Main.Advanced.LoadMccTrans", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "auto", "no" or "force". Force-enabling only works for MC 1.13+.. - /// - internal static string Main_Advanced_mc_forge { - get { - return ResourceManager.GetString("Main.Advanced.mc_forge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "auto" or "1.X.X" values. Allows to skip server info retrieval.. - /// - internal static string Main_Advanced_mc_version { - get { - return ResourceManager.GetString("Main.Advanced.mc_version", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Controls the minimum interval (in seconds) between sending each message to the server.. - /// - internal static string Main_Advanced_message_cooldown { - get { - return ResourceManager.GetString("Main.Advanced.message_cooldown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Override the maximum chat message length. Set to 0 to use the default (100 for 1.10 and below, 256 for 1.11+). WARNING: Setting this incorrectly may cause you to be kicked from the server.. - /// - internal static string Main_Advanced_max_chat_message_length { - get { - return ResourceManager.GetString("Main.Advanced.max_chat_message_length", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable support for joining Minecraft Realms worlds.. - /// - internal static string Main_Advanced_minecraft_realms { - get { - return ResourceManager.GetString("Main.Advanced.minecraft_realms", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The minimum height to use when calculating the image size from the height of the terminal.. - /// - internal static string Main_Advanced_MinTerminalHeight { - get { - return ResourceManager.GetString("Main.Advanced.MinTerminalHeight", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The minimum width used when calculating the image size from the width of the terminal.. - /// - internal static string Main_Advanced_MinTerminalWidth { - get { - return ResourceManager.GetString("Main.Advanced.MinTerminalWidth", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable head movement while walking to avoid anti-cheat triggers.. - /// - internal static string Main_Advanced_move_head_while_walking { - get { - return ResourceManager.GetString("Main.Advanced.move_head_while_walking", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A movement speed higher than 2 may be considered cheating.. - /// - internal static string Main_Advanced_movement_speed { - get { - return ResourceManager.GetString("Main.Advanced.movement_speed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only works on Windows XP-8 or Windows 10 with old console.. - /// - internal static string Main_Advanced_player_head_icon { - get { - return ResourceManager.GetString("Main.Advanced.player_head_icon", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to For remote control of the bot.. - /// - internal static string Main_Advanced_private_msgs_cmd_name { - get { - return ResourceManager.GetString("Main.Advanced.private_msgs_cmd_name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How to retain profile key. Use "none", "memory" or "disk".. - /// - internal static string Main_Advanced_profilekey_cache { - get { - return ResourceManager.GetString("Main.Advanced.profilekey_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "no", "fast" (5s timeout), or "yes". Required for joining some servers.. - /// - internal static string Main_Advanced_resolve_srv_records { - get { - return ResourceManager.GetString("Main.Advanced.resolve_srv_records", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cache compiled scripts for faster load on low-end devices.. - /// - internal static string Main_Advanced_script_cache { - get { - return ResourceManager.GetString("Main.Advanced.script_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ServerList: It allows an easier and faster server switching with short aliases instead of full server IP - ///Aliases cannot contain dots or spaces, and the name "localhost" cannot be used as an alias. - ///Usage examples: "/tell <mybot> connect Server1", "/connect Server2". - /// - internal static string Main_Advanced_server_list { - get { - return ResourceManager.GetString("Main.Advanced.server_list", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How to retain session tokens. Use "none", "memory" or "disk".. - /// - internal static string Main_Advanced_session_cache { - get { - return ResourceManager.GetString("Main.Advanced.session_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decode links embedded in chat messages and show them in console.. - /// - internal static string Main_Advanced_show_chat_links { - get { - return ResourceManager.GetString("Main.Advanced.show_chat_links", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. - /// +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MinecraftClient { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ConfigComments { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ConfigComments() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MinecraftClient.Resources.ConfigComments.ConfigComments", typeof(ConfigComments).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to can be used in some other fields as %yourvar% + ///%username% and %serverip% are reserved variables.. + /// + internal static string AppVars_Variables { + get { + return ResourceManager.GetString("AppVars.Variables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to =============================== # + /// Minecraft Console Client Bots # + ///=============================== #. + /// + internal static string ChatBot { + get { + return ResourceManager.GetString("ChatBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get alerted when specified words are detected in chat + ///Useful for moderating your server or detecting when someone is talking to you. + /// + internal static string ChatBot_Alerts { + get { + return ResourceManager.GetString("ChatBot.Alerts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play a beep sound when a word is detected in addition to highlighting.. + /// + internal static string ChatBot_Alerts_Beep_Enabled { + get { + return ResourceManager.GetString("ChatBot.Alerts.Beep_Enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List of words/strings to NOT alert you on.. + /// + internal static string ChatBot_Alerts_Excludes { + get { + return ResourceManager.GetString("ChatBot.Alerts.Excludes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of a file where alers logs will be written.. + /// + internal static string ChatBot_Alerts_Log_File { + get { + return ResourceManager.GetString("ChatBot.Alerts.Log_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log alerts info a file.. + /// + internal static string ChatBot_Alerts_Log_To_File { + get { + return ResourceManager.GetString("ChatBot.Alerts.Log_To_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List of words/strings to alert you on.. + /// + internal static string ChatBot_Alerts_Matches { + get { + return ResourceManager.GetString("ChatBot.Alerts.Matches", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Trigger alerts when it rains and when it stops.. + /// + internal static string ChatBot_Alerts_Trigger_By_Rain { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Rain", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Triggers alerts at the beginning and end of thunderstorms.. + /// + internal static string ChatBot_Alerts_Trigger_By_Thunderstorm { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Thunderstorm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Triggers an alert after receiving a specified keyword.. + /// + internal static string ChatBot_Alerts_Trigger_By_Words { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Words", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send a command on a regular or random basis or make the bot walk around randomly to avoid automatic AFK disconnection + /// /!\ Make sure your server rules do not forbid anti-AFK mechanisms! + /// /!\ Make sure you keep the bot in an enclosure to prevent it wandering off if you're using terrain handling! (Recommended size 5x5x5). + /// + internal static string ChatBot_AntiAfk { + get { + return ResourceManager.GetString("ChatBot.AntiAfk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command to send to the server.. + /// + internal static string ChatBot_AntiAfk_Command { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The time interval for execution. (in seconds). + /// + internal static string ChatBot_AntiAfk_Delay { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sneak when sending the command.. + /// + internal static string ChatBot_AntiAfk_Use_Sneak { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Use_Sneak", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use terrain handling to enable the bot to move around.. + /// + internal static string ChatBot_AntiAfk_Use_Terrain_Handling { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Use_Terrain_Handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The range the bot can move around randomly (Note: the bigger the range, the slower the bot will be). + /// + internal static string ChatBot_AntiAfk_Walk_Range { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How many times can the bot fail trying to move before using the command method.. + /// + internal static string ChatBot_AntiAfk_Walk_Retries { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically attack hostile mobs around you + ///You need to enable Entity Handling to use this bot + /// /!\ Make sure server rules allow your planned use of AutoAttack + /// /!\ SERVER PLUGINS may consider AutoAttack to be a CHEAT MOD and TAKE ACTION AGAINST YOUR ACCOUNT so DOUBLE CHECK WITH SERVER RULES!. + /// + internal static string ChatBot_AutoAttack { + get { + return ResourceManager.GetString("ChatBot.AutoAttack", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow attacking hostile mobs.. + /// + internal static string ChatBot_AutoAttack_Attack_Hostile { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Hostile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow attacking passive mobs.. + /// + internal static string ChatBot_AutoAttack_Attack_Passive { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Passive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Capped between 1 to 4. + /// + internal static string ChatBot_AutoAttack_Attack_Range { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait between each attack. Set "Custom = false" to let MCC calculate it.. + /// + internal static string ChatBot_AutoAttack_Cooldown_Time { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Cooldown_Time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity types can be found here: https://mccteam.github.io/r/entity/#L15. + /// + internal static string ChatBot_AutoAttack_Entites_List { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Entites_List", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Possible values: "Interact", "Attack" (default), "InteractAt" (Interact and Attack).. + /// + internal static string ChatBot_AutoAttack_Interaction { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Interaction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wether to treat the entities list as a "whitelist" or as a "blacklist".. + /// + internal static string ChatBot_AutoAttack_List_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.List_Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "single" or "multi". single target one mob per attack. multi target all mobs in range per attack. + /// + internal static string ChatBot_AutoAttack_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "health" or "distance". Only needed when using single mode. + /// + internal static string ChatBot_AutoAttack_Priority { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Priority", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically craft items in your inventory + ///See https://mccteam.github.io/g/bots/#auto-craft for how to use + ///You need to enable Inventory Handling to use this bot + ///You should also enable Terrain and Movements if you need to use a crafting table. + /// + internal static string ChatBot_AutoCraft { + get { + return ResourceManager.GetString("ChatBot.AutoCraft", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location of the crafting table if you intended to use it. Terrain and movements must be enabled.. + /// + internal static string ChatBot_AutoCraft_CraftingTable { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.CraftingTable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to What to do on crafting failure, "abort" or "wait".. + /// + internal static string ChatBot_AutoCraft_OnFailure { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.OnFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recipes.Name: The name can be whatever you like and it is used to represent the recipe. + ///Recipes.Type: crafting table type: "player" or "table" + ///Recipes.Result: the resulting item + ///Recipes.Slots: All slots, counting from left to right, top to bottom. Please fill in "Null" for empty slots. + ///For the naming of the items, please see: https://mccteam.github.io/r/item/#L12. + /// + internal static string ChatBot_AutoCraft_Recipes { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.Recipes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Auto-digging blocks. + ///You need to enable Terrain Handling to use this bot + ///You can use "/digbot start" and "/digbot stop" to control the start and stop of AutoDig. + ///Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead. + ///For the naming of the block, please see https://mccteam.github.io/r/block/#L15. + /// + internal static string ChatBot_AutoDig { + get { + return ResourceManager.GetString("ChatBot.AutoDig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How many seconds to wait after entering the game to start digging automatically, set to -1 to disable automatic start.. + /// + internal static string ChatBot_AutoDig_Auto_Start_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Auto_Start_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically switch to the appropriate tool.. + /// + internal static string ChatBot_AutoDig_Auto_Tool_Switch { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Auto_Tool_Switch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mining a block for more than "Dig_Timeout" seconds will be considered a timeout.. + /// + internal static string ChatBot_AutoDig_Dig_Timeout { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Dig_Timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to drop the current tool when its durability is too low.. + /// + internal static string ChatBot_AutoDig_Drop_Low_Durability_Tools { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Drop_Low_Durability_Tools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will not use tools with less durability than this. Set to zero to disable this feature.. + /// + internal static string ChatBot_AutoDig_Durability_Limit { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Durability_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wether to treat the blocks list as a "whitelist" or as a "blacklist".. + /// + internal static string ChatBot_AutoDig_List_Type { + get { + return ResourceManager.GetString("ChatBot.AutoDig.List_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "distance" or "index", When using the "fixedpos" mode, the blocks are determined by distance to the player, or by the order in the list.. + /// + internal static string ChatBot_AutoDig_Location_Order { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Location_Order", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The position of the blocks when using "fixedpos" or "both" mode.. + /// + internal static string ChatBot_AutoDig_Locations { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Locations", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to output logs when digging blocks.. + /// + internal static string ChatBot_AutoDig_Log_Block_Dig { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Log_Block_Dig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "lookat", "fixedpos" or "both". Digging the block being looked at, the block in a fixed position, or the block that needs to be all met.. + /// + internal static string ChatBot_AutoDig_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically drop items in inventory + ///You need to enable Inventory Handling to use this bot + ///See this file for an up-to-date list of item types you can use with this bot: https://mccteam.github.io/r/item/#L12. + /// + internal static string ChatBot_AutoDrop { + get { + return ResourceManager.GetString("ChatBot.AutoDrop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "include", "exclude" or "everything". Include: drop item IN the list. Exclude: drop item NOT IN the list. + /// + internal static string ChatBot_AutoDrop_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoDrop.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically eat food when your Hunger value is low + ///You need to enable Inventory Handling to use this bot. + /// + internal static string ChatBot_AutoEat { + get { + return ResourceManager.GetString("ChatBot.AutoEat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically catch fish using a fishing rod + ///Guide: https://mccteam.github.io/g/bots/#auto-fishing + ///You can use "/fish" to control the bot manually. + /// /!\ Make sure server rules allow automated farming before using this bot. + /// + internal static string ChatBot_AutoFishing { + get { + return ResourceManager.GetString("ChatBot.AutoFishing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep it as false if you have not changed it before.. + /// + internal static string ChatBot_AutoFishing_Antidespawn { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Antidespawn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to a new rod from inventory after the current rod is unavailable.. + /// + internal static string ChatBot_AutoFishing_Auto_Rod_Switch { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Rod_Switch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to start fishing automatically after entering a world.. + /// + internal static string ChatBot_AutoFishing_Auto_Start { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How soon to re-cast after successful fishing.. + /// + internal static string ChatBot_AutoFishing_Cast_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Cast_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will not use rods with less durability than this (full durability is 64). Set to zero to disable this feature.. + /// + internal static string ChatBot_AutoFishing_Durability_Limit { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Durability_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This allows the player to change position/facing after each fish caught.. + /// + internal static string ChatBot_AutoFishing_Enable_Move { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Enable_Move", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long after entering the game to start fishing (seconds).. + /// + internal static string ChatBot_AutoFishing_Fishing_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fishing timeout (seconds). Timeout will trigger a re-cast.. + /// + internal static string ChatBot_AutoFishing_Fishing_Timeout { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.. + /// + internal static string ChatBot_AutoFishing_Hook_Threshold { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Hook_Threshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet.. + /// + internal static string ChatBot_AutoFishing_Log_Fish_Bobber { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Log_Fish_Bobber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the mainhand or the offhand to hold the rod.. + /// + internal static string ChatBot_AutoFishing_Mainhand { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Mainhand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to It will move in order "1->2->3->4->3->2->1->2->..." and can change position or facing or both each time. It is recommended to change the facing only.. + /// + internal static string ChatBot_AutoFishing_Movements { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Movements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hook movement in the X and Z axis less than this value will be considered stationary.. + /// + internal static string ChatBot_AutoFishing_Stationary_Threshold { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Stationary_Threshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically relog when disconnected by server, for example because the server is restating + /// /!\ Use Ignore_Kick_Message=true at own risk! Server staff might not appreciate if you auto-relog on manual kicks. + /// + internal static string ChatBot_AutoRelog { + get { + return ResourceManager.GetString("ChatBot.AutoRelog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The delay time before joining the server. (in seconds). + /// + internal static string ChatBot_AutoRelog_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When set to true, autorelog will reconnect regardless of kick messages.. + /// + internal static string ChatBot_AutoRelog_Ignore_Kick_Message { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Ignore_Kick_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If the kickout message matches any of the strings, then autorelog will be triggered.. + /// + internal static string ChatBot_AutoRelog_Kick_Messages { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Kick_Messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Retries when failing to relog to the server. use -1 for unlimited retries.. + /// + internal static string ChatBot_AutoRelog_Retries { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run commands or send messages automatically when a specified pattern is detected in chat + ///Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules + /// /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam. + /// + internal static string ChatBot_AutoRespond { + get { + return ResourceManager.GetString("ChatBot.AutoRespond", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not remove colors from text (Note: Your matches will have to include color codes (ones using the § character) in order to work). + /// + internal static string ChatBot_AutoRespond_Match_Colors { + get { + return ResourceManager.GetString("ChatBot.AutoRespond.Match_Colors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logs chat messages in a file on disk.. + /// + internal static string ChatBot_ChatLog { + get { + return ResourceManager.GetString("ChatBot.ChatLog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This bot allows you to send and recieve messages and commands via a Discord channel. + ///For Setup you can either use the documentation or read here (Documentation has images). + ///Documentation: https://mccteam.github.io/g/bots/#discord-bridge + ///Setup: + ///First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA . + /// /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent [rest of string was truncated]";. + /// + internal static string ChatBot_DiscordBridge { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ID of a channel where you want to interact with the MCC using the bot.. + /// + internal static string ChatBot_DiscordBridge_ChannelId { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.ChannelId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message formats + ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! + ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. + ///For Discord message formatting, check the following: https://mccteam.github.io/r/dc-fmt.html. + /// + internal static string ChatBot_DiscordBridge_Formats { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.Formats", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ID of a server/guild where you have invited the bot to.. + /// + internal static string ChatBot_DiscordBridge_GuildId { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.GuildId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second).. + /// + internal static string ChatBot_DiscordBridge_MessageSendTimeout { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.MessageSendTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of IDs of people you want to be able to interact with the MCC using the bot.. + /// + internal static string ChatBot_DiscordBridge_OwnersIds { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.OwnersIds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord Bot token.. + /// + internal static string ChatBot_DiscordBridge_Token { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.Token", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat.. + /// + internal static string ChatBot_DiscordBridge_AllowOtherBotMessages { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.AllowOtherBotMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically farms cropsfor you (plants, breaks and bonemeals them). + ///Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. + ///Usage: "/farmer start" command and "/farmer stop" command. + ///NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes. + ///or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this. [rest of string was truncated]";. + /// + internal static string ChatBot_Farmer { + get { + return ResourceManager.GetString("ChatBot.Farmer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delay between tasks in seconds (Minimum 1 second). + /// + internal static string ChatBot_Farmer_Delay_Between_Tasks { + get { + return ResourceManager.GetString("ChatBot.Farmer.Delay_Between_Tasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enabled you to make the bot follow you + ///NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you + ///It's similar to making animals follow you when you're holding food in your hand. + ///This is due to a slow pathfinding algorithm, we're working on getting a better one + ///You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, + /// [rest of string was truncated]";. + /// + internal static string ChatBot_FollowPlayer { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not follow the player if he is in the range of 3 blocks (prevents the bot from pushing a player in an infinite loop). + /// + internal static string ChatBot_FollowPlayer_Stop_At_Distance { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer.Stop_At_Distance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The rate at which the bot does calculations (in seconds) (You can tweak this if you feel the bot is too slow). + /// + internal static string ChatBot_FollowPlayer_Update_Limit { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer.Update_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A small game to demonstrate chat interactions. Players can guess mystery words one letter at a time. + ///You need to have ChatFormat working correctly and add yourself in botowners to start the game with /tell <bot username> start + /// /!\ This bot may get a bit spammy if many players are interacting with it. + /// + internal static string ChatBot_HangmanGame { + get { + return ResourceManager.GetString("ChatBot.HangmanGame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A Chat Bot that collects items on the ground. + /// + internal static string ChatBot_ItemsCollector { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will return to it's starting position after there are no items to collect. + /// + internal static string ChatBot_ItemsCollector_Always_Return_To_Start { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Always_Return_To_Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will collect all items, regardless of their type. If you want to use the whitelisted item types, disable this by setting it to false. + /// + internal static string ChatBot_ItemsCollector_Collect_All_Item_Types { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Collect_All_Item_Types", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The radius in which bot will look for items to collect (Default: 30). + /// + internal static string ChatBot_ItemsCollector_Collection_Radius { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Collection_Radius", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delay in milliseconds between bot scanning items (Recommended: 300-500). + /// + internal static string ChatBot_ItemsCollector_Delay_Between_Tasks { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Delay_Between_Tasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In this list you can specify which items the bot will collect. To enable this, set the Collect_All_Item_Types to false. (NOTE: This does not prevent the bot from accidentally picking up other items, it only goes to positions where it finds the whitelisted items)\nYou can see the list of item types here: https://raw.githubusercontent.com/MCCTeam/Minecraft-Console-Client/master/MinecraftClient/Inventory/ItemType.cs. + /// + internal static string ChatBot_ItemsCollector_Items_Whitelist { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Items_Whitelist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will go after clustered items instead for the closest ones. + /// + internal static string ChatBot_ItemsCollector_Prioritize_Clusters { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Prioritize_Clusters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info. + ///Setup: + ///1. Go to https://discord.com/developers/applications and log in with your Discord account. [rest of string was truncated]";. + /// + internal static string ChatBot_DiscordRpc { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord Application ID.. + /// + internal static string ChatBot_DiscordRpc_ApplicationId { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ApplicationId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The top line of the Rich Presence display. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_PresenceDetails { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceDetails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The second line of the Rich Presence display. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_PresenceState { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key of the large image asset uploaded to your Discord application.. + /// + internal static string ChatBot_DiscordRpc_LargeImageKey { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tooltip text for the large image. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_LargeImageText { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key of the small image asset uploaded to your Discord application (leave empty to hide).. + /// + internal static string ChatBot_DiscordRpc_SmallImageKey { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tooltip text for the small image. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_SmallImageText { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the server address (host and port) in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowServerAddress { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowServerAddress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the player coordinates in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowCoordinates { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowCoordinates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show health and food level in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowHealth { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowHealth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the current dimension in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowDimension { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowDimension", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the current gamemode in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowGamemode { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowGamemode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show elapsed session time in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowElapsedTime { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowElapsedTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the online player count as a party size in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowPlayerCount { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowPlayerCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How often (in seconds) to refresh the Discord presence. Minimum: 1. + /// + internal static string ChatBot_DiscordRpc_UpdateIntervalSeconds { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.UpdateIntervalSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Relay messages between players and servers, like a mail plugin + ///This bot can store messages when the recipients are offline, and send them when they join the server + /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins. + /// + internal static string ChatBot_Mailer { + get { + return ResourceManager.GetString("ChatBot.Mailer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot) + ///This is useful for solving captchas which use maps + ///The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled. + ///NOTE: + ///If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console. + /// /!\ Make sure server rules allow bots to be used on the server, or you risk being punished.. + /// + internal static string ChatBot_Map { + get { + return ResourceManager.GetString("ChatBot.Map", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically render the map once it is received or updated from/by the server. + /// + internal static string ChatBot_Map_Auto_Render_On_Update { + get { + return ResourceManager.GetString("ChatBot.Map.Auto_Render_On_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete all rendered maps on unload/reload or when you launch the MCC again.. + /// + internal static string ChatBot_Map_Delete_All_On_Unload { + get { + return ResourceManager.GetString("ChatBot.Map.Delete_All_On_Unload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get a notification when you have gotten a map from the server for the first time. + /// + internal static string ChatBot_Map_Notify_On_First_Update { + get { + return ResourceManager.GetString("ChatBot.Map.Notify_On_First_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord.. + /// + internal static string ChatBot_Map_Rasize_Rendered_Image { + get { + return ResourceManager.GetString("ChatBot.Map.Rasize_Rendered_Image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to render the map in the console.. + /// + internal static string ChatBot_Map_Render_In_Console { + get { + return ResourceManager.GetString("ChatBot.Map.Render_In_Console", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The size that a rendered image should be resized to, in pixels (eg. 512).. + /// + internal static string ChatBot_Map_Resize_To { + get { + return ResourceManager.GetString("ChatBot.Map.Resize_To", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge).. + /// + internal static string ChatBot_Map_Save_To_File { + get { + return ResourceManager.GetString("ChatBot.Map.Save_To_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!) + ///You need to enable Save_To_File in order for this to work. + ///We also recommend turning on resizing.. + /// + internal static string ChatBot_Map_Send_Rendered_To_Bridges { + get { + return ResourceManager.GetString("ChatBot.Map.Send_Rendered_To_Bridges", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log the list of players periodically into a textual file.. + /// + internal static string ChatBot_PlayerListLogger { + get { + return ResourceManager.GetString("ChatBot.PlayerListLogger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (In seconds). + /// + internal static string ChatBot_PlayerListLogger_Delay { + get { + return ResourceManager.GetString("ChatBot.PlayerListLogger.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send MCC console commands to your bot through server PMs (/tell) + ///You need to have ChatFormat working correctly and add yourself in botowners to use the bot + /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable RemoteControl only if you trust server admins. + /// + internal static string ChatBot_RemoteControl { + get { + return ResourceManager.GetString("ChatBot.RemoteControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable recording of the game (/replay start) and replay it later using the Replay Mod (https://www.replaymod.com/) + ///Please note that due to technical limitations, the client player (you) will not be shown in the replay file + /// /!\ You SHOULD use /replay stop or exit the program gracefully with /quit OR THE REPLAY FILE MAY GET CORRUPT!. + /// + internal static string ChatBot_ReplayCapture { + get { + return ResourceManager.GetString("ChatBot.ReplayCapture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long should replay file be auto-saved, in seconds. Use -1 to disable.. + /// + internal static string ChatBot_ReplayCapture_Backup_Interval { + get { + return ResourceManager.GetString("ChatBot.ReplayCapture.Backup_Interval", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Schedule commands and scripts to launch on various events such as server join, date/time or time interval + ///See https://mccteam.github.io/g/bots/#script-scheduler for more info. + /// + internal static string ChatBot_ScriptScheduler { + get { + return ResourceManager.GetString("ChatBot.ScriptScheduler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel. + /// /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel. + ///----------------------------------------------------------- + ///Setup: + ///First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather + ///Click on "Start" button and re [rest of string was truncated]";. + /// + internal static string ChatBot_TelegramBridge { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram.. + /// + internal static string ChatBot_TelegramBridge_Authorized_Chat_Ids { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Authorized_Chat_Ids", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ID of a channel where you want to interact with the MCC using the bot.. + /// + internal static string ChatBot_TelegramBridge_ChannelId { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.ChannelId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message formats + ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! + ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. + ///For Telegram message formatting, check the following: https://mccteam.github.io/r/tg-fmt.html. + /// + internal static string ChatBot_TelegramBridge_Formats { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Formats", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second).. + /// + internal static string ChatBot_TelegramBridge_MessageSendTimeout { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.MessageSendTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Telegram Bot token.. + /// + internal static string ChatBot_TelegramBridge_Token { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Token", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon.. + /// + internal static string ChatBot_WebSocketBot { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow IP aliases, such as "localhost" or if using containers then the container name can be used.... + /// + internal static string ChatBot_WebSocketBot_AllowIpAlias { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.AllowIpAlias", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting is for developers who are developing a library that uses this chat bot to remotely execute procedures/commands/functions.. + /// + internal static string ChatBot_WebSocketBot_DebugMode { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.DebugMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IP address that Websocket server will be bound to.. + /// + internal static string ChatBot_WebSocketBot_Ip { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Ip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A password that will be used to authenticate on thw Websocket server (It is recommended to change the default password and to set a strong one).. + /// + internal static string ChatBot_WebSocketBot_Password { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Port that Websocket server will be bounded to.. + /// + internal static string ChatBot_WebSocketBot_Port { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Port", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MCC does it best to detect chat messages, but some server have unusual chat formats + ///When this happens, you'll need to configure chat format below, see https://mccteam.github.io/g/conf/#chat-format-section. + /// + internal static string ChatFormat { + get { + return ResourceManager.GetString("ChatFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MCC support for common message formats. Set "false" to avoid conflicts with custom formats.. + /// + internal static string ChatFormat_Builtins { + get { + return ResourceManager.GetString("ChatFormat.Builtins", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to use the custom regular expressions below for detection.. + /// + internal static string ChatFormat_UserDefined { + get { + return ResourceManager.GetString("ChatFormat.UserDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Console-related settings.. + /// + internal static string Console { + get { + return ResourceManager.GetString("Console", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The settings for command completion suggestions. + ///Custom colors are only available when using "vt100_24bit" color mode.. + /// + internal static string Console_CommandSuggestion { + get { + return ResourceManager.GetString("Console.CommandSuggestion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display command suggestions in the console.. + /// + internal static string Console_CommandSuggestion_Enable { + get { + return ResourceManager.GetString("Console.CommandSuggestion.Enable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable this option if the arrows in the command suggestions are not displayed properly in your terminal.. + /// + internal static string Console_CommandSuggestion_Use_Basic_Arrow { + get { + return ResourceManager.GetString("Console.CommandSuggestion.Use_Basic_Arrow", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string Console_General_ConsoleColorMode { + get { + return ResourceManager.GetString("Console.General.ConsoleColorMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display the MCC startup banner with version info and icon.. + /// + internal static string Console_General_Display_Icon_Banner { + get { + return ResourceManager.GetString("Console.General.Display_Icon_Banner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can use "Ctrl+P" to print out the current input and cursor position.. + /// + internal static string Console_General_Display_Input { + get { + return ResourceManager.GetString("Console.General.Display_Input", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Startup Config File + ///Please do not record extraneous data in this file as it will be overwritten by MCC. + /// + ///New to Minecraft Console Client? Check out this document: https://mccteam.github.io/g/conf.html + ///Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Console-Client/#download. + /// + internal static string Head { + get { + return ResourceManager.GetString("Head", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting affects only the messages in the console.. + /// + internal static string Logging { + get { + return ResourceManager.GetString("Logging", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regex for filtering chat message.. + /// + internal static string Logging_ChatFilter { + get { + return ResourceManager.GetString("Logging.ChatFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show server chat messages.. + /// + internal static string Logging_ChatMessages { + get { + return ResourceManager.GetString("Logging.ChatMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regex for filtering debug message.. + /// + internal static string Logging_DebugFilter { + get { + return ResourceManager.GetString("Logging.DebugFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enable this before submitting bug reports. Thanks!. + /// + internal static string Logging_DebugMessages { + get { + return ResourceManager.GetString("Logging.DebugMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show error messages.. + /// + internal static string Logging_ErrorMessages { + get { + return ResourceManager.GetString("Logging.ErrorMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "disable" or "blacklist" OR "whitelist". Blacklist hide message match regex. Whitelist show message match regex.. + /// + internal static string Logging_FilterMode { + get { + return ResourceManager.GetString("Logging.FilterMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Informative messages. (i.e Most of the message from MCC). + /// + internal static string Logging_InfoMessages { + get { + return ResourceManager.GetString("Logging.InfoMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log file name.. + /// + internal static string Logging_LogFile { + get { + return ResourceManager.GetString("Logging.LogFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write log messages to file.. + /// + internal static string Logging_LogToFile { + get { + return ResourceManager.GetString("Logging.LogToFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prepend timestamp to messages in log file.. + /// + internal static string Logging_PrependTimestamp { + get { + return ResourceManager.GetString("Logging.PrependTimestamp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep color codes in the saved text.(look like "§b"). + /// + internal static string Logging_SaveColorCodes { + get { + return ResourceManager.GetString("Logging.SaveColorCodes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show warning messages.. + /// + internal static string Logging_WarningMessages { + get { + return ResourceManager.GetString("Logging.WarningMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make sure you understand what each setting does before changing anything!. + /// + internal static string Main_Advanced { + get { + return ResourceManager.GetString("Main.Advanced", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AccountList: It allows a fast account switching without directly using the credentials + ///Usage examples: "/tell <mybot> reco Player2", "/connect <serverip> Player1". + /// + internal static string Main_Advanced_account_list { + get { + return ResourceManager.GetString("Main.Advanced.account_list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle auto respawn if client player was dead (make sure your spawn point is safe).. + /// + internal static string Main_Advanced_auto_respawn { + get { + return ResourceManager.GetString("Main.Advanced.auto_respawn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set the owner of the bot. /!\ Server admins can impersonate owners!. + /// + internal static string Main_Advanced_bot_owners { + get { + return ResourceManager.GetString("Main.Advanced.bot_owners", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "mcc", "vanilla" or "none". This is how MCC identifies itself to the server.. + /// + internal static string Main_Advanced_brand_info { + get { + return ResourceManager.GetString("Main.Advanced.brand_info", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Leave empty for no logfile.. + /// + internal static string Main_Advanced_chatbot_log_file { + get { + return ResourceManager.GetString("Main.Advanced.chatbot_log_file", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If turned off, the emoji will be replaced with a simpler character (for /chunk status).. + /// + internal static string Main_Advanced_enable_emoji { + get { + return ResourceManager.GetString("Main.Advanced.enable_emoji", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set to false to opt-out of Sentry error logging.. + /// + internal static string Main_Advanced_enable_sentry { + get { + return ResourceManager.GetString("Main.Advanced.enable_sentry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle entity handling.. + /// + internal static string Main_Advanced_entity_handling { + get { + return ResourceManager.GetString("Main.Advanced.entity_handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to exit directly when an error occurs, for using MCC in non-interactive scripts.. + /// + internal static string Main_Advanced_exit_on_failure { + get { + return ResourceManager.GetString("Main.Advanced.exit_on_failure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ignore invalid player name. + /// + internal static string Main_Advanced_ignore_invalid_playername { + get { + return ResourceManager.GetString("Main.Advanced.ignore_invalid_playername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "none", "slash"(/) or "backslash"(\).. + /// + internal static string Main_Advanced_internal_cmd_char { + get { + return ResourceManager.GetString("Main.Advanced.internal_cmd_char", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle inventory handling.. + /// + internal static string Main_Advanced_inventory_handling { + get { + return ResourceManager.GetString("Main.Advanced.inventory_handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fill in with in-game locale code, check https://mccteam.github.io/r/l-code.html. + /// + internal static string Main_Advanced_language { + get { + return ResourceManager.GetString("Main.Advanced.language", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Load translations applied to MCC when available, turn it off to use English only.. + /// + internal static string Main_Advanced_LoadMccTrans { + get { + return ResourceManager.GetString("Main.Advanced.LoadMccTrans", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "auto", "no" or "force". Force-enabling only works for MC 1.13+.. + /// + internal static string Main_Advanced_mc_forge { + get { + return ResourceManager.GetString("Main.Advanced.mc_forge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "auto" or "1.X.X" values. Allows to skip server info retrieval.. + /// + internal static string Main_Advanced_mc_version { + get { + return ResourceManager.GetString("Main.Advanced.mc_version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Controls the minimum interval (in seconds) between sending each message to the server.. + /// + internal static string Main_Advanced_message_cooldown { + get { + return ResourceManager.GetString("Main.Advanced.message_cooldown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Override the maximum chat message length. Set to 0 to use the default (100 for 1.10 and below, 256 for 1.11+). WARNING: Setting this incorrectly may cause you to be kicked from the server.. + /// + internal static string Main_Advanced_max_chat_message_length { + get { + return ResourceManager.GetString("Main.Advanced.max_chat_message_length", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable support for joining Minecraft Realms worlds.. + /// + internal static string Main_Advanced_minecraft_realms { + get { + return ResourceManager.GetString("Main.Advanced.minecraft_realms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum height to use when calculating the image size from the height of the terminal.. + /// + internal static string Main_Advanced_MinTerminalHeight { + get { + return ResourceManager.GetString("Main.Advanced.MinTerminalHeight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum width used when calculating the image size from the width of the terminal.. + /// + internal static string Main_Advanced_MinTerminalWidth { + get { + return ResourceManager.GetString("Main.Advanced.MinTerminalWidth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable head movement while walking to avoid anti-cheat triggers.. + /// + internal static string Main_Advanced_move_head_while_walking { + get { + return ResourceManager.GetString("Main.Advanced.move_head_while_walking", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A movement speed higher than 2 may be considered cheating.. + /// + internal static string Main_Advanced_movement_speed { + get { + return ResourceManager.GetString("Main.Advanced.movement_speed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only works on Windows XP-8 or Windows 10 with old console.. + /// + internal static string Main_Advanced_player_head_icon { + get { + return ResourceManager.GetString("Main.Advanced.player_head_icon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For remote control of the bot.. + /// + internal static string Main_Advanced_private_msgs_cmd_name { + get { + return ResourceManager.GetString("Main.Advanced.private_msgs_cmd_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How to retain profile key. Use "none", "memory" or "disk".. + /// + internal static string Main_Advanced_profilekey_cache { + get { + return ResourceManager.GetString("Main.Advanced.profilekey_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "no", "fast" (5s timeout), or "yes". Required for joining some servers.. + /// + internal static string Main_Advanced_resolve_srv_records { + get { + return ResourceManager.GetString("Main.Advanced.resolve_srv_records", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cache compiled scripts for faster load on low-end devices.. + /// + internal static string Main_Advanced_script_cache { + get { + return ResourceManager.GetString("Main.Advanced.script_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ServerList: It allows an easier and faster server switching with short aliases instead of full server IP + ///Aliases cannot contain dots or spaces, and the name "localhost" cannot be used as an alias. + ///Usage examples: "/tell <mybot> connect Server1", "/connect Server2". + /// + internal static string Main_Advanced_server_list { + get { + return ResourceManager.GetString("Main.Advanced.server_list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How to retain session tokens. Use "none", "memory" or "disk".. + /// + internal static string Main_Advanced_session_cache { + get { + return ResourceManager.GetString("Main.Advanced.session_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Decode links embedded in chat messages and show them in console.. + /// + internal static string Main_Advanced_show_chat_links { + get { + return ResourceManager.GetString("Main.Advanced.show_chat_links", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. + /// internal static string Main_Advanced_show_inventory_layout { get { return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); @@ -1862,345 +1872,345 @@ namespace MinecraftClient { /// Looks up a localized string similar to System messages for server ops.. /// internal static string Main_Advanced_show_system_messages { - get { - return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Messages displayed above xp bar, set this to false in case of xp bar spam.. - /// - internal static string Main_Advanced_show_xpbar_messages { - get { - return ResourceManager.GetString("Main.Advanced.show_xpbar_messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Temporary fix for Badpacket issue on some servers. Need to enable "TerrainAndMovements" first.. - /// - internal static string Main_Advanced_temporary_fix_badpacket { - get { - return ResourceManager.GetString("Main.Advanced.temporary_fix_badpacket", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Uses more ram, cpu, bandwidth but allows you to move around.. - /// - internal static string Main_Advanced_terrain_and_movements { - get { - return ResourceManager.GetString("Main.Advanced.terrain_and_movements", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Customize the TCP connection timeout with the server. (in seconds). - /// - internal static string Main_Advanced_timeout { - get { - return ResourceManager.GetString("Main.Advanced.timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prepend timestamps to chat messages.. - /// - internal static string Main_Advanced_timestamps { - get { - return ResourceManager.GetString("Main.Advanced.timestamps", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Login=Email or Name. Use "-" as password for offline mode. Leave blank to prompt user on startup.. - /// - internal static string Main_General_account { - get { - return ResourceManager.GetString("Main.General.account", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yggdrasil authlib server domain name and port.. - /// - internal static string Main_General_AuthlibServer { - get { - return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yggdrasil authlib multi-user selection.. - /// - internal static string Main_General_AuthlibUser { - get { - return ResourceManager.GetString("Main.General.AuthlibUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The address of the game server, "Host" can be filled in with domain name or IP address. (The "Port" field can be deleted, it will be resolved automatically). - /// - internal static string Main_General_login { - get { - return ResourceManager.GetString("Main.General.login", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Microsoft Account sign-in method: "mcc" (device code, supports 2FA) OR "browser" (manual browser login).. - /// - internal static string Main_General_method { - get { - return ResourceManager.GetString("Main.General.method", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Account type: "mojang" OR "microsoft" OR "yggdrasil". Also affects interactive login in console.. - /// - internal static string Main_General_server_info { - get { - return ResourceManager.GetString("Main.General.server_info", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings below are sent to the server and only affect server-side things like your skin.. - /// - internal static string MCSettings { - get { - return ResourceManager.GetString("MCSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows disabling chat colors server-side.. - /// - internal static string MCSettings_ChatColors { - get { - return ResourceManager.GetString("MCSettings.ChatColors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "enabled", "commands", or "disabled". Allows to mute yourself.... - /// - internal static string MCSettings_ChatMode { - get { - return ResourceManager.GetString("MCSettings.ChatMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MC 1.7- difficulty. "peaceful", "easy", "normal", "difficult".. - /// - internal static string MCSettings_Difficulty { - get { - return ResourceManager.GetString("MCSettings.Difficulty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If disabled, settings below are not sent to the server.. - /// - internal static string MCSettings_Enabled { - get { - return ResourceManager.GetString("MCSettings.Enabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use any language implemented in Minecraft.. - /// - internal static string MCSettings_Locale { - get { - return ResourceManager.GetString("MCSettings.Locale", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MC 1.9+ main hand. "left" or "right".. - /// - internal static string MCSettings_MainHand { - get { - return ResourceManager.GetString("MCSettings.MainHand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value range: [0 - 255].. - /// - internal static string MCSettings_RenderDistance { - get { - return ResourceManager.GetString("MCSettings.RenderDistance", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly - ///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. - ///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. - /// /!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!. - /// - internal static string Proxy { - get { - return ResourceManager.GetString("Proxy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to connect to the game server through a proxy.. - /// - internal static string Proxy_Enabled_Ingame { - get { - return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to connect to the login server through a proxy.. - /// - internal static string Proxy_Enabled_Login { - get { - return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to download MCC updates via proxy.. - /// - internal static string Proxy_Enabled_Update { - get { - return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only required for password-protected proxies.. - /// - internal static string Proxy_Password { - get { - return ResourceManager.GetString("Proxy.Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Supported types: "HTTP", "SOCKS4", "SOCKS4a", "SOCKS5".. - /// - internal static string Proxy_Proxy_Type { - get { - return ResourceManager.GetString("Proxy.Proxy_Type", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Proxy server must allow HTTPS for login, and non-443 ports for playing.. - /// - internal static string Proxy_Server { - get { - return ResourceManager.GetString("Proxy.Server", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only required for password-protected proxies.. - /// - internal static string Proxy_Username { - get { - return ResourceManager.GetString("Proxy.Username", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chat signature related settings (affects minecraft 1.19+). - /// - internal static string Signature { - get { - return ResourceManager.GetString("Signature", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Microsoft accounts only. If disabled, will not be able to sign chat and join servers configured with "enforce-secure-profile=true". - /// - internal static string Signature_LoginWithSecureProfile { - get { - return ResourceManager.GetString("Signature.LoginWithSecureProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use red    color block to mark chat without legitimate signature. - /// - internal static string Signature_MarkIllegallySignedMsg { - get { - return ResourceManager.GetString("Signature.MarkIllegallySignedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use green  color block to mark chat with legitimate signatures. - /// - internal static string Signature_MarkLegallySignedMsg { - get { - return ResourceManager.GetString("Signature.MarkLegallySignedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use yellow color block to mark chat that have been modified by the server.. - /// - internal static string Signature_MarkModifiedMsg { - get { - return ResourceManager.GetString("Signature.MarkModifiedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use gray   color block to mark system message (always without signature). - /// - internal static string Signature_MarkSystemMessage { - get { - return ResourceManager.GetString("Signature.MarkSystemMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to display chat and messages in commands without legal signatures. - /// - internal static string Signature_ShowIllegalSignedChat { - get { - return ResourceManager.GetString("Signature.ShowIllegalSignedChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set to true to display messages modified by the server, false to display the original signed messages. - /// - internal static string Signature_ShowModifiedChat { - get { - return ResourceManager.GetString("Signature.ShowModifiedChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sign the chat send from MCC. - /// - internal static string Signature_SignChat { - get { - return ResourceManager.GetString("Signature.SignChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sign the messages contained in the commands sent by MCC. For example, the message in "/msg" and "/me". - /// - internal static string Signature_SignMessageInCommand { - get { - return ResourceManager.GetString("Signature.SignMessageInCommand", resourceCulture); - } - } - } -} + get { + return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Messages displayed above xp bar, set this to false in case of xp bar spam.. + /// + internal static string Main_Advanced_show_xpbar_messages { + get { + return ResourceManager.GetString("Main.Advanced.show_xpbar_messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Temporary fix for Badpacket issue on some servers. Need to enable "TerrainAndMovements" first.. + /// + internal static string Main_Advanced_temporary_fix_badpacket { + get { + return ResourceManager.GetString("Main.Advanced.temporary_fix_badpacket", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uses more ram, cpu, bandwidth but allows you to move around.. + /// + internal static string Main_Advanced_terrain_and_movements { + get { + return ResourceManager.GetString("Main.Advanced.terrain_and_movements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Customize the TCP connection timeout with the server. (in seconds). + /// + internal static string Main_Advanced_timeout { + get { + return ResourceManager.GetString("Main.Advanced.timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prepend timestamps to chat messages.. + /// + internal static string Main_Advanced_timestamps { + get { + return ResourceManager.GetString("Main.Advanced.timestamps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login=Email or Name. Use "-" as password for offline mode. Leave blank to prompt user on startup.. + /// + internal static string Main_General_account { + get { + return ResourceManager.GetString("Main.General.account", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yggdrasil authlib server domain name and port.. + /// + internal static string Main_General_AuthlibServer { + get { + return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yggdrasil authlib multi-user selection.. + /// + internal static string Main_General_AuthlibUser { + get { + return ResourceManager.GetString("Main.General.AuthlibUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The address of the game server, "Host" can be filled in with domain name or IP address. (The "Port" field can be deleted, it will be resolved automatically). + /// + internal static string Main_General_login { + get { + return ResourceManager.GetString("Main.General.login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Account sign-in method: "mcc" (device code, supports 2FA) OR "browser" (manual browser login).. + /// + internal static string Main_General_method { + get { + return ResourceManager.GetString("Main.General.method", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account type: "mojang" OR "microsoft" OR "yggdrasil". Also affects interactive login in console.. + /// + internal static string Main_General_server_info { + get { + return ResourceManager.GetString("Main.General.server_info", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings below are sent to the server and only affect server-side things like your skin.. + /// + internal static string MCSettings { + get { + return ResourceManager.GetString("MCSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows disabling chat colors server-side.. + /// + internal static string MCSettings_ChatColors { + get { + return ResourceManager.GetString("MCSettings.ChatColors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "enabled", "commands", or "disabled". Allows to mute yourself.... + /// + internal static string MCSettings_ChatMode { + get { + return ResourceManager.GetString("MCSettings.ChatMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MC 1.7- difficulty. "peaceful", "easy", "normal", "difficult".. + /// + internal static string MCSettings_Difficulty { + get { + return ResourceManager.GetString("MCSettings.Difficulty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If disabled, settings below are not sent to the server.. + /// + internal static string MCSettings_Enabled { + get { + return ResourceManager.GetString("MCSettings.Enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use any language implemented in Minecraft.. + /// + internal static string MCSettings_Locale { + get { + return ResourceManager.GetString("MCSettings.Locale", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MC 1.9+ main hand. "left" or "right".. + /// + internal static string MCSettings_MainHand { + get { + return ResourceManager.GetString("MCSettings.MainHand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value range: [0 - 255].. + /// + internal static string MCSettings_RenderDistance { + get { + return ResourceManager.GetString("MCSettings.RenderDistance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly + ///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. + ///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. + /// /!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!. + /// + internal static string Proxy { + get { + return ResourceManager.GetString("Proxy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to connect to the game server through a proxy.. + /// + internal static string Proxy_Enabled_Ingame { + get { + return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to connect to the login server through a proxy.. + /// + internal static string Proxy_Enabled_Login { + get { + return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to download MCC updates via proxy.. + /// + internal static string Proxy_Enabled_Update { + get { + return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only required for password-protected proxies.. + /// + internal static string Proxy_Password { + get { + return ResourceManager.GetString("Proxy.Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Supported types: "HTTP", "SOCKS4", "SOCKS4a", "SOCKS5".. + /// + internal static string Proxy_Proxy_Type { + get { + return ResourceManager.GetString("Proxy.Proxy_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy server must allow HTTPS for login, and non-443 ports for playing.. + /// + internal static string Proxy_Server { + get { + return ResourceManager.GetString("Proxy.Server", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only required for password-protected proxies.. + /// + internal static string Proxy_Username { + get { + return ResourceManager.GetString("Proxy.Username", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chat signature related settings (affects minecraft 1.19+). + /// + internal static string Signature { + get { + return ResourceManager.GetString("Signature", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft accounts only. If disabled, will not be able to sign chat and join servers configured with "enforce-secure-profile=true". + /// + internal static string Signature_LoginWithSecureProfile { + get { + return ResourceManager.GetString("Signature.LoginWithSecureProfile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use red    color block to mark chat without legitimate signature. + /// + internal static string Signature_MarkIllegallySignedMsg { + get { + return ResourceManager.GetString("Signature.MarkIllegallySignedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use green  color block to mark chat with legitimate signatures. + /// + internal static string Signature_MarkLegallySignedMsg { + get { + return ResourceManager.GetString("Signature.MarkLegallySignedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use yellow color block to mark chat that have been modified by the server.. + /// + internal static string Signature_MarkModifiedMsg { + get { + return ResourceManager.GetString("Signature.MarkModifiedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use gray   color block to mark system message (always without signature). + /// + internal static string Signature_MarkSystemMessage { + get { + return ResourceManager.GetString("Signature.MarkSystemMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display chat and messages in commands without legal signatures. + /// + internal static string Signature_ShowIllegalSignedChat { + get { + return ResourceManager.GetString("Signature.ShowIllegalSignedChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set to true to display messages modified by the server, false to display the original signed messages. + /// + internal static string Signature_ShowModifiedChat { + get { + return ResourceManager.GetString("Signature.ShowModifiedChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sign the chat send from MCC. + /// + internal static string Signature_SignChat { + get { + return ResourceManager.GetString("Signature.SignChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sign the messages contained in the commands sent by MCC. For example, the message in "/msg" and "/me". + /// + internal static string Signature_SignMessageInCommand { + get { + return ResourceManager.GetString("Signature.SignMessageInCommand", resourceCulture); + } + } + } +} diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 2b09765e..4816d2de 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -566,6 +566,9 @@ Custom colors are only available when using "vt100_24bit" color mode. 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. + + Whether to display the MCC startup icon banner. + You can use "Ctrl+P" to print out the current input and cursor position. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index b77b0f39..d2883376 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -2269,6 +2269,18 @@ namespace MinecraftClient { } } + internal static string mcc_banner_classic { + get { + return ResourceManager.GetString("mcc.banner.classic", resourceCulture); + } + } + + internal static string mcc_banner_label_mc_versions { + get { + return ResourceManager.GetString("mcc.banner.label_mc_versions", resourceCulture); + } + } + internal static string mcc_server_info_label_server { get { return ResourceManager.GetString("mcc.server_info.label_server", resourceCulture); diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 48a07470..15895ca8 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -830,6 +830,12 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file TestBot + + Minecraft Console Client v{0} - for MC {1} to {2} - {3} + + + MC Versions: + Server: diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 299fe990..8f556a5c 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1210,6 +1210,9 @@ namespace MinecraftClient [TomlInlineComment("$Console.General.ConsoleColorMode$")] public ConsoleColorModeType ConsoleColorMode = ConsoleColorModeType.vt100_24bit; + [TomlInlineComment("$Console.General.Display_Icon_Banner$")] + public bool Display_Icon_Banner = true; + [TomlInlineComment("$Console.General.Display_Input$")] public bool Display_Input = true; diff --git a/MinecraftClient/Tui/IconGridBuilder.cs b/MinecraftClient/Tui/IconGridBuilder.cs new file mode 100644 index 00000000..3d13a8e2 --- /dev/null +++ b/MinecraftClient/Tui/IconGridBuilder.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class IconGridBuilder + { + internal static Grid BuildFromRgba(byte[] rgba, int srcWidth, int srcHeight, int displaySize) + { + int cellCols = displaySize; + int cellRows = displaySize / 2; + + var grid = new Grid(); + for (int c = 0; c < cellCols; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); + for (int r = 0; r < cellRows; r++) + grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); + + for (int row = 0; row < cellRows; row++) + { + for (int col = 0; col < cellCols; col++) + { + int topPixelY = row * 2; + int bottomPixelY = row * 2 + 1; + + var topColor = SamplePixel(rgba, srcWidth, srcHeight, col, topPixelY, cellCols, displaySize); + var bottomColor = SamplePixel(rgba, srcWidth, srcHeight, col, bottomPixelY, cellCols, displaySize); + + var cell = new TextBlock + { + Text = "\u2580", + Foreground = new SolidColorBrush(topColor), + Background = new SolidColorBrush(bottomColor), + Padding = new Thickness(0), + Margin = new Thickness(0), + }; + + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + grid.Children.Add(cell); + } + } + + return grid; + } + + internal static Grid BuildFromBase64(string base64Data, int displaySize) + { + byte[] imageBytes; + try + { + imageBytes = Convert.FromBase64String(base64Data); + } + catch + { + return new Grid(); + } + + return BuildFromImageBytes(imageBytes, displaySize) ?? new Grid(); + } + + internal static Grid? BuildFromImageBytes(byte[] imageBytes, int displaySize) + { + int srcWidth, srcHeight; + byte[] rgba; + try + { + (srcWidth, srcHeight, rgba) = DecodeImageToRgba(imageBytes); + } + catch + { + return null; + } + + return BuildFromRgba(rgba, srcWidth, srcHeight, displaySize); + } + + internal static (int Width, int Height, byte[] Rgba) DecodeImageToRgba(byte[] imageData) + { + using var image = new ImageMagick.MagickImage(imageData); + int w = (int)image.Width; + int h = (int)image.Height; + + using var pixels = image.GetPixelsUnsafe(); + var rgba = new byte[w * h * 4]; + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var pixel = pixels.GetPixel(x, y)!; + int idx = (y * w + x) * 4; + var color = pixel.ToColor()!; + rgba[idx] = (byte)(color.R >> 8); + rgba[idx + 1] = (byte)(color.G >> 8); + rgba[idx + 2] = (byte)(color.B >> 8); + rgba[idx + 3] = (byte)(color.A >> 8); + } + } + + return (w, h, rgba); + } + + private static Color SamplePixel(byte[] rgba, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH) + { + int srcX = dstX * srcW / dstW; + int srcY = dstY * srcH / dstH; + srcX = Math.Clamp(srcX, 0, srcW - 1); + srcY = Math.Clamp(srcY, 0, srcH - 1); + + int idx = (srcY * srcW + srcX) * 4; + if (idx + 3 >= rgba.Length) + return Color.FromRgb(0, 0, 0); + + byte r = rgba[idx]; + byte g = rgba[idx + 1]; + byte b = rgba[idx + 2]; + byte a = rgba[idx + 3]; + + return a < 128 ? Color.FromRgb(0, 0, 0) : Color.FromRgb(r, g, b); + } + } +} diff --git a/MinecraftClient/Tui/MccBannerPanelBuilder.cs b/MinecraftClient/Tui/MccBannerPanelBuilder.cs new file mode 100644 index 00000000..67d5ccaf --- /dev/null +++ b/MinecraftClient/Tui/MccBannerPanelBuilder.cs @@ -0,0 +1,180 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Layout; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class MccBannerPanelBuilder + { + internal static Border Build(string? buildInfo) + { + var contentPanel = new DockPanel { Background = Brushes.Black }; + + var icon = BuildIcon(); + icon.VerticalAlignment = VerticalAlignment.Center; + DockPanel.SetDock(icon, Dock.Left); + contentPanel.Children.Add(icon); + + var infoPanel = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(1, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + + AddTitle(infoPanel); + AddVersionRange(infoPanel); + AddGithub(infoPanel); + + if (buildInfo is not null) + AddBuildInfo(infoPanel, buildInfo); + + contentPanel.Children.Add(infoPanel); + + return new Border + { + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)), + Padding = new Thickness(1, 0), + Child = contentPanel, + Margin = new Thickness(0), + }; + } + + private static void AddTitle(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(new Run("Minecraft Console Client") + { Foreground = Pal.Gold, FontWeight = FontWeight.Bold }); + row.Inlines.Add(new Run($" v{Program.Version}") { Foreground = Pal.Aqua }); + panel.Children.Add(row); + } + + private static void AddVersionRange(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(Lbl(Translations.mcc_banner_label_mc_versions)); + row.Inlines.Add(Val(Program.MCLowestVersion, Pal.Green)); + row.Inlines.Add(new Run(" - ") { Foreground = Pal.Gray }); + row.Inlines.Add(Val(Program.MCHighestVersion, Pal.Green)); + panel.Children.Add(row); + } + + private static void AddGithub(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(Val("Github.com/MCCTeam", Pal.Gray)); + panel.Children.Add(row); + } + + private static void AddBuildInfo(StackPanel panel, string buildInfo) + { + panel.Children.Add(new TextBlock + { + Text = buildInfo, + Foreground = Pal.DarkGray, + }); + } + + #region Icon + + private static readonly Color B1 = Color.FromRgb(200, 200, 200); // bezel bright + private static readonly Color B2 = Color.FromRgb(160, 160, 160); // bezel mid + private static readonly Color B3 = Color.FromRgb(120, 120, 120); // bezel dark + private static readonly Color Sc = Color.FromRgb(32, 32, 32); // screen + private static readonly Color Sd = Color.FromRgb(26, 26, 26); // screen (dark) + private static readonly Color S = Color.FromRgb(20, 20, 20); // screen bg + private static readonly Color C = Color.FromRgb(55, 200, 55); // creeper green + + // @formatter:off + private static readonly Color[,] Pixels = + { + { B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B2 }, + { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, B3 }, + { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, B3 }, + { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, B3 }, + { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, Sd, S, S, S, B3 }, + { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, S, S, S, S, S, S, B3 }, + { B1, Sc, Sc, Sc, Sc, Sd, Sd, S, S, C, C, S, S, C, C, S, B3 }, + { B1, Sc, Sc, Sd, Sd, S, S, S, S, C, C, S, S, C, C, S, B3 }, + { B1, Sd, Sd, S, S, S, S, S, S, S, S, C, C, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, S, S, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B2, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3 }, + }; + // @formatter:on + + private static Control BuildIcon() + { + int cols = Pixels.GetLength(1); + int textRows = Pixels.GetLength(0) / 2; + + var pixelGrid = new Grid(); + for (int c = 0; c < cols; c++) + pixelGrid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); + for (int r = 0; r < textRows; r++) + pixelGrid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); + + for (int row = 0; row < textRows; row++) + { + for (int col = 0; col < cols; col++) + { + var topColor = Pixels[row * 2, col]; + var bottomColor = Pixels[row * 2 + 1, col]; + + var cell = new TextBlock + { + Text = "\u2580", + Foreground = new SolidColorBrush(topColor), + Background = new SolidColorBrush(bottomColor), + Padding = new Thickness(0), + Margin = new Thickness(0), + }; + + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + pixelGrid.Children.Add(cell); + } + } + + var prompt = new TextBlock + { + Text = " >_", + Foreground = new SolidColorBrush(Color.FromRgb(220, 220, 220)), + Background = new SolidColorBrush(Sc), + Padding = new Thickness(0), + Margin = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + }; + Grid.SetRow(prompt, 1); + Grid.SetColumn(prompt, 1); + Grid.SetColumnSpan(prompt, 4); + pixelGrid.Children.Add(prompt); + + return pixelGrid; + } + + #endregion + + private static Run Lbl(string text) => + new(text + " ") { Foreground = Pal.Gray }; + + private static Run Val(string text, IBrush color) => + new(text) { Foreground = color }; + + private static class Pal + { + public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170)); + public static readonly IBrush DarkGray = new SolidColorBrush(Color.FromRgb(85, 85, 85)); + public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255)); + public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85)); + public static readonly IBrush Gold = new SolidColorBrush(Color.FromRgb(255, 170, 0)); + } + } +} diff --git a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs index dd859242..2c8daf6f 100644 --- a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs +++ b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs @@ -28,6 +28,7 @@ namespace MinecraftClient.Tui { Orientation = Orientation.Vertical, Margin = new Thickness(1, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, }; AddMotd(infoPanel, info); @@ -47,7 +48,7 @@ namespace MinecraftClient.Tui Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)), Padding = new Thickness(1, 0), Child = contentPanel, - Margin = new Thickness(0, 1), + Margin = new Thickness(0), }; } @@ -180,114 +181,8 @@ namespace MinecraftClient.Tui private static Run Value(string text, IBrush color) => new(text) { Foreground = color }; - #region Favicon Rendering - - private static Grid BuildFaviconGrid(string base64Png, int displaySize) - { - byte[] pngBytes; - try - { - pngBytes = Convert.FromBase64String(base64Png); - } - catch - { - return new Grid(); - } - - int srcWidth, srcHeight; - byte[] rgba; - try - { - (srcWidth, srcHeight, rgba) = DecodePngToRgba(pngBytes); - } - catch - { - return new Grid(); - } - - int cellCols = displaySize; - int cellRows = displaySize / 2; - - var grid = new Grid(); - for (int c = 0; c < cellCols; c++) - grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); - for (int r = 0; r < cellRows; r++) - grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); - - for (int row = 0; row < cellRows; row++) - { - for (int col = 0; col < cellCols; col++) - { - int topPixelY = row * 2; - int bottomPixelY = row * 2 + 1; - - var topColor = SamplePixel(rgba, srcWidth, srcHeight, col, topPixelY, cellCols, displaySize); - var bottomColor = SamplePixel(rgba, srcWidth, srcHeight, col, bottomPixelY, cellCols, displaySize); - - var cell = new TextBlock - { - Text = "\u2580", - Foreground = new SolidColorBrush(topColor), - Background = new SolidColorBrush(bottomColor), - Padding = new Thickness(0), - Margin = new Thickness(0), - }; - - Grid.SetRow(cell, row); - Grid.SetColumn(cell, col); - grid.Children.Add(cell); - } - } - - return grid; - } - - private static Color SamplePixel(byte[] rgba, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH) - { - int srcX = dstX * srcW / dstW; - int srcY = dstY * srcH / dstH; - srcX = Math.Clamp(srcX, 0, srcW - 1); - srcY = Math.Clamp(srcY, 0, srcH - 1); - - int idx = (srcY * srcW + srcX) * 4; - if (idx + 3 >= rgba.Length) - return Color.FromRgb(0, 0, 0); - - byte r = rgba[idx]; - byte g = rgba[idx + 1]; - byte b = rgba[idx + 2]; - byte a = rgba[idx + 3]; - - return a < 128 ? Color.FromRgb(0, 0, 0) : Color.FromRgb(r, g, b); - } - - private static (int Width, int Height, byte[] Rgba) DecodePngToRgba(byte[] png) - { - using var image = new ImageMagick.MagickImage(png); - int w = (int)image.Width; - int h = (int)image.Height; - - using var pixels = image.GetPixelsUnsafe(); - var rgba = new byte[w * h * 4]; - - for (int y = 0; y < h; y++) - { - for (int x = 0; x < w; x++) - { - var pixel = pixels.GetPixel(x, y)!; - int idx = (y * w + x) * 4; - var color = pixel.ToColor()!; - rgba[idx] = (byte)(color.R >> 8); - rgba[idx + 1] = (byte)(color.G >> 8); - rgba[idx + 2] = (byte)(color.B >> 8); - rgba[idx + 3] = (byte)(color.A >> 8); - } - } - - return (w, h, rgba); - } - - #endregion + private static Grid BuildFaviconGrid(string base64Png, int displaySize) => + IconGridBuilder.BuildFromBase64(base64Png, displaySize); private static class McColors { From f7bc8174083a9e571a2540fee49a2abe114998c0 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 00:02:59 +0800 Subject: [PATCH 60/76] Refactor color definitions in MccBannerPanelBuilder for improved clarity - Removed unused color definitions for screen and dark screen. - Updated pixel array to use the new screen background color consistently. - Adjusted prompt text colors for better visibility in the TUI. --- MinecraftClient/Tui/MccBannerPanelBuilder.cs | 22 +++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/MinecraftClient/Tui/MccBannerPanelBuilder.cs b/MinecraftClient/Tui/MccBannerPanelBuilder.cs index 67d5ccaf..36d35b40 100644 --- a/MinecraftClient/Tui/MccBannerPanelBuilder.cs +++ b/MinecraftClient/Tui/MccBannerPanelBuilder.cs @@ -84,8 +84,6 @@ namespace MinecraftClient.Tui private static readonly Color B1 = Color.FromRgb(200, 200, 200); // bezel bright private static readonly Color B2 = Color.FromRgb(160, 160, 160); // bezel mid private static readonly Color B3 = Color.FromRgb(120, 120, 120); // bezel dark - private static readonly Color Sc = Color.FromRgb(32, 32, 32); // screen - private static readonly Color Sd = Color.FromRgb(26, 26, 26); // screen (dark) private static readonly Color S = Color.FromRgb(20, 20, 20); // screen bg private static readonly Color C = Color.FromRgb(55, 200, 55); // creeper green @@ -93,14 +91,14 @@ namespace MinecraftClient.Tui private static readonly Color[,] Pixels = { { B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B2 }, - { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, B3 }, - { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, B3 }, - { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, B3 }, - { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, Sd, S, S, S, B3 }, - { B1, Sc, Sc, Sc, Sc, Sc, Sc, Sc, Sd, Sd, S, S, S, S, S, S, B3 }, - { B1, Sc, Sc, Sc, Sc, Sd, Sd, S, S, C, C, S, S, C, C, S, B3 }, - { B1, Sc, Sc, Sd, Sd, S, S, S, S, C, C, S, S, C, C, S, B3 }, - { B1, Sd, Sd, S, S, S, S, S, S, S, S, C, C, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 }, + { B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, C, C, S, S, S, B3 }, { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, { B1, S, S, S, S, S, S, S, S, S, C, S, S, C, S, S, B3 }, @@ -145,8 +143,8 @@ namespace MinecraftClient.Tui var prompt = new TextBlock { Text = " >_", - Foreground = new SolidColorBrush(Color.FromRgb(220, 220, 220)), - Background = new SolidColorBrush(Sc), + Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255)), + Background = new SolidColorBrush(S), Padding = new Thickness(0), Margin = new Thickness(0), HorizontalAlignment = HorizontalAlignment.Left, From 435887cc045b125483fe5eb5721765a42c92aaa0 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 00:05:22 +0800 Subject: [PATCH 61/76] Update translation for banner label to clarify supported Minecraft versions --- MinecraftClient/Resources/Translations/Translations.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 15895ca8..b67f453b 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -834,7 +834,7 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file Minecraft Console Client v{0} - for MC {1} to {2} - {3} - MC Versions: + Supported MC Versions: Server: From eae96a8fbcfc7adf15de2f4cb06f3fae2a88e785 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 00:40:26 +0800 Subject: [PATCH 62/76] Cave mode for minimap --- MinecraftClient/Commands/Minimap.cs | 30 +- .../ConfigComments/ConfigComments.resx | 3 + .../Translations/Translations.Designer.cs | 18 ++ .../Resources/Translations/Translations.resx | 6 + MinecraftClient/Settings.cs | 3 + MinecraftClient/Tui/MainTuiView.cs | 9 + MinecraftClient/Tui/MinimapColorMap.cs | 27 +- MinecraftClient/Tui/MinimapControl.cs | 304 ++++++++++++++++-- 8 files changed, 379 insertions(+), 21 deletions(-) diff --git a/MinecraftClient/Commands/Minimap.cs b/MinecraftClient/Commands/Minimap.cs index 897c88ae..0b6ca00f 100644 --- a/MinecraftClient/Commands/Minimap.cs +++ b/MinecraftClient/Commands/Minimap.cs @@ -11,7 +11,7 @@ namespace MinecraftClient.Commands class Minimap : Command { public override string CmdName => "minimap"; - public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right]"; + public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right] | minimap cave [auto|on|off]"; public override string CmdDesc => Translations.cmd_minimap_desc; public override void RegisterCommand(CommandDispatcher dispatcher) @@ -78,6 +78,14 @@ namespace MinecraftClient.Commands .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_left))) .Then(l => l.Literal("bottom_right") .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_right)))) + .Then(l => l.Literal("cave") + .Executes(r => DoCaveInfo(r.Source)) + .Then(l => l.Literal("auto") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.auto))) + .Then(l => l.Literal("on") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.on))) + .Then(l => l.Literal("off") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.off)))) .Then(l => l.Literal("_help") .Executes(r => GetUsage(r.Source, string.Empty)) .Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName))) @@ -251,6 +259,26 @@ namespace MinecraftClient.Commands string.Format(Translations.cmd_minimap_position_set, pos)); } + private static int DoCaveInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var mode = view.GetMinimapCaveMode(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_cave_current, mode)); + } + + private static int DoCaveSet(CmdResult r, CaveModeOption mode) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapCaveMode(mode)); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_cave_set, mode)); + } + private static string BoolStr(bool v) => v ? "ON" : "OFF"; } } diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 4816d2de..8f3e4964 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -975,6 +975,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be Minimap refresh interval in milliseconds (100-5000). + + Cave rendering mode: "auto" (detect ceiling), "on" (always cave view), "off" (always surface view). + Yggdrasil authlib multi-user selection. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index d2883376..de6784ae 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -7187,6 +7187,24 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Current cave mode: {0}. + /// + internal static string cmd_minimap_cave_current { + get { + return ResourceManager.GetString("cmd.minimap.cave_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cave mode set to: {0}. + /// + internal static string cmd_minimap_cave_set { + get { + return ResourceManager.GetString("cmd.minimap.cave_set", resourceCulture); + } + } + /// /// Looks up a localized string similar to list achievements/advancements from the server.. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index b67f453b..fddf6400 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -2533,6 +2533,12 @@ see item details. Minimap position set to: {0} + + Current cave mode: {0} + + + Cave mode set to: {0} + list achievements/advancements from the server. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 8f556a5c..ededd636 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1286,6 +1286,9 @@ namespace MinecraftClient [TomlInlineComment("$Console.Minimap.RefreshInterval$")] public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs; + [TomlInlineComment("$Console.Minimap.CaveMode$")] + public Tui.CaveModeOption CaveMode = Tui.CaveModeOption.auto; + public void OnSettingUpdate() { Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom); diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index 1106f763..d8155d48 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -167,6 +167,7 @@ namespace MinecraftClient.Tui _minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames; _minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames; _minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames; + _minimapControl.CaveMode = mmCfg.CaveMode; var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position); _minimapBorder = new Border @@ -1097,6 +1098,14 @@ namespace MinecraftClient.Tui public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position; + public void SetMinimapCaveMode(CaveModeOption mode) + { + _minimapControl.CaveMode = mode; + Settings.Config.Console.Minimap.CaveMode = mode; + } + + public CaveModeOption GetMinimapCaveMode() => _minimapControl.CaveMode; + private static (HorizontalAlignment h, VerticalAlignment v, Thickness margin) GetMinimapAlignment(MinimapPosition pos) => pos switch { MinimapPosition.top_left => (HorizontalAlignment.Left, VerticalAlignment.Top, new Thickness(1, 1, 0, 0)), diff --git a/MinecraftClient/Tui/MinimapColorMap.cs b/MinecraftClient/Tui/MinimapColorMap.cs index ae1bf21c..b0b09596 100644 --- a/MinecraftClient/Tui/MinimapColorMap.cs +++ b/MinecraftClient/Tui/MinimapColorMap.cs @@ -20,6 +20,8 @@ namespace MinecraftClient.Tui public static readonly Color LavaColor = Color.FromRgb(255, 100, 0); public static readonly Color DefaultColor = Color.FromRgb(60, 60, 60); public static readonly Color VoidColor = Color.FromRgb(0, 0, 0); + public static readonly Color CaveBorderColor = Color.FromRgb(16, 16, 16); + public static readonly Color CaveSolidColor = Color.FromRgb(24, 20, 18); private static readonly FrozenDictionary ColorTable; private static readonly FrozenSet FullyTransparentMats; @@ -114,6 +116,14 @@ namespace MinecraftClient.Tui public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m); + /// + /// Returns true for materials that block light propagation (solid, liquids), + /// used by cave mode to find the surface from the player's Y level. + /// Mirrors VoxelMap's lightDampening > 0 check. + /// + public static bool IsLightBlocking(Material m) + => (m == Material.Lava) || (!FullyTransparentMats.Contains(m) && m.IsSolid()); + public static bool IsWater(Material m) => WaterMats.Contains(m); public static bool IsIce(Material m) => IceMats.Contains(m); @@ -137,8 +147,8 @@ namespace MinecraftClient.Tui int multiplier = heightDelta switch { > 0 => 255, // higher than neighbor: brightest - 0 => 220, // same height: normal - _ => 180, // lower than neighbor: darker + 0 => 220, // same height: normal + _ => 180, // lower than neighbor: darker }; byte r = (byte)(baseColor.R * multiplier / 255); byte g = (byte)(baseColor.G * multiplier / 255); @@ -157,6 +167,19 @@ namespace MinecraftClient.Tui return Blend(IceColor, bottomColor, 0.35); } + /// + /// Darken a color to simulate underground lighting. Cave floors receive + /// a minimum brightness of ~32/255 for non-solid blocks (matching VoxelMap), + /// while solid/unreachable columns render as near-black. + /// + public static Color ApplyCaveDarkening(Color baseColor, double factor = 0.55) + { + byte r = (byte)(baseColor.R * factor); + byte g = (byte)(baseColor.G * factor); + byte b = (byte)(baseColor.B * factor); + return Color.FromRgb(r, g, b); + } + private static Color Blend(Color top, Color bottom, double topAlpha) { byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha)); diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs index 33f25465..616b44e5 100644 --- a/MinecraftClient/Tui/MinimapControl.cs +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -13,6 +13,8 @@ using MinecraftClient.Mapping; namespace MinecraftClient.Tui { + public enum CaveModeOption { auto, on, off } + /// /// TUI minimap control rendered as a grid of TextBlocks using half-block characters. /// Zoom is expressed as blocks-per-pixel (1 = 1:1, 16 = 16 blocks per pixel). @@ -63,6 +65,8 @@ namespace MinecraftClient.Tui public MinimapPosition Position { get; set; } = MinimapPosition.top_right; + public CaveModeOption CaveMode { get; set; } = CaveModeOption.auto; + public int MapPixelWidth => _mapWidth; public int MapPixelHeight => _mapHeight; @@ -177,19 +181,21 @@ namespace MinecraftClient.Tui bool showHostile = _nameConfig.Hostile; bool showNeutral = _nameConfig.Neutral; bool showPassive = _nameConfig.Passive; + var caveOpt = CaveMode; Task.Run(() => { try { var result = SampleTerrain(client, bpp, w, h, - showPlayers, showHostile, showNeutral, showPassive, ct); + showPlayers, showHostile, showNeutral, showPassive, caveOpt, ct); if (ct.IsCancellationRequested) return; Dispatcher.UIThread.Post(() => { ApplyPixelBuffer(result, w, h); - UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w); + UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w, + result.CaveModeActive); }); } catch (OperationCanceledException) { } @@ -236,6 +242,7 @@ namespace MinecraftClient.Tui public int CenterX; public int CenterY; public int Bpp; + public bool CaveModeActive; } private static bool ShouldShowNameLocal(MobCategory cat, @@ -253,7 +260,7 @@ namespace MinecraftClient.Tui private static SampleResult SampleTerrain(McClient client, int bpp, int mapW, int mapH, bool showPlayers, bool showHostile, bool showNeutral, bool showPassive, - CancellationToken ct) + CaveModeOption caveOpt, CancellationToken ct) { var result = new SampleResult { @@ -281,6 +288,9 @@ namespace MinecraftClient.Tui int minY = dim.minY; int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1); + bool caveMode = ResolveCaveMode(caveOpt, world, dim, playerBlockX, playerBlockY, playerBlockZ, scanTop); + result.CaveModeActive = caveMode; + var entities = client.GetEntityHandlingEnabled() ? client.GetEntities() : null; @@ -374,6 +384,8 @@ namespace MinecraftClient.Tui ChunkColumn? cachedColumn = null; int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue; + bool[,]? caveMask = caveMode ? new bool[mapW, mapH] : null; + for (int px = 0; px < mapW; px++) { for (int py = 0; py < mapH; py++) @@ -383,23 +395,51 @@ namespace MinecraftClient.Tui int baseX = playerBlockX + (px - centerX) * bpp; int baseZ = playerBlockZ + (py - centerY) * bpp; - if (bpp == 1) + if (caveMode) { - var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY, - ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); - result.Pixels[px, py] = color; - result.Heights[px, py] = surfY; - result.BlockTypes![px, py] = surfMat; + if (bpp == 1) + { + var (color, surfY, surfMat, inCave) = SampleColumnCave( + world, baseX, baseZ, playerBlockY, minY, dim.maxY - 1, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + result.BlockTypes![px, py] = surfMat; + caveMask![px, py] = inCave; + } + else + { + var (color, surfY, matSum, inCave) = SampleAreaDominantCave( + world, baseX, baseZ, bpp, playerBlockY, minY, dim.maxY - 1, + result.BlockSummary is not null, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + if (result.BlockSummary is not null) + result.BlockSummary[px, py] = matSum; + caveMask![px, py] = inCave; + } } else { - var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp, - scanTop, minY, result.BlockSummary is not null, - ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); - result.Pixels[px, py] = color; - result.Heights[px, py] = surfY; - if (result.BlockSummary is not null) - result.BlockSummary[px, py] = matSum; + if (bpp == 1) + { + var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + result.BlockTypes![px, py] = surfMat; + } + else + { + var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp, + scanTop, minY, result.BlockSummary is not null, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + if (result.BlockSummary is not null) + result.BlockSummary[px, py] = matSum; + } } } } @@ -416,6 +456,9 @@ namespace MinecraftClient.Tui } } + if (caveMask is not null) + ApplyCaveBorder(result, caveMask, mapW, mapH, entityPixels); + foreach (var (key, info) in entityPixels) { var (px, py) = key; @@ -640,6 +683,230 @@ namespace MinecraftClient.Tui return (best, avgY, summary); } + /// + /// Determine whether cave mode should be active for this frame. + /// Mirrors VoxelMap's detection: hasCeiling dimensions always use cave mode, + /// otherwise check whether the player's column has a solid block above. + /// + private static bool ResolveCaveMode(CaveModeOption opt, World world, Dimension dim, + int playerX, int playerY, int playerZ, int scanTop) + { + if (opt == CaveModeOption.off) return false; + if (opt == CaveModeOption.on) return true; + + if (dim.hasCeiling) return true; + + for (int y = playerY + 2; y <= scanTop; y++) + { + var mat = world.GetBlock(new Mapping.Location(playerX, y, playerZ)).Type; + if (MinimapColorMap.IsLightBlocking(mat)) + return true; + } + return false; + } + + /// + /// Cave-mode column sampler. Starting from playerY, scans down through air + /// to find the first light-blocking block (the cave floor), or scans up if + /// the player is embedded in solid. Returns the floor block color with cave + /// darkening applied, plus an inCave flag indicating the column has a reachable + /// air pocket at the player's Y level. + /// + private static (Color color, int surfaceY, Material surfaceMat, bool inCave) SampleColumnCave( + World world, int x, int z, int playerY, int minY, int maxY, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + int chunkX = x >> 4; + int chunkZ = z >> 4; + if (chunkX != cachedChunkX || chunkZ != cachedChunkZ) + { + cachedColumn = world[chunkX, chunkZ]; + cachedChunkX = chunkX; + cachedChunkZ = chunkZ; + } + + if (cachedColumn is null) + return (MinimapColorMap.VoidColor, minY, Material.Air, false); + + int caveFloorY = FindCaveFloorY(cachedColumn, x, z, playerY, minY, maxY); + + if (caveFloorY == int.MinValue) + { + var fallback = SampleColumn(world, x, z, maxY, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + return (MinimapColorMap.CaveSolidColor, fallback.surfaceY, fallback.surfaceMat, false); + } + + var loc = new Mapping.Location(x, caveFloorY, z); + var chunk = cachedColumn.GetChunk(loc); + if (chunk is null) + return (MinimapColorMap.CaveSolidColor, caveFloorY, Material.Air, false); + + var block = chunk.GetBlock(loc); + var mat = block.Type; + var color = MinimapColorMap.GetBaseColor(mat); + color = MinimapColorMap.ApplyCaveDarkening(color); + + return (color, caveFloorY, mat, true); + } + + /// + /// Find the cave floor Y at (x, z) by scanning from playerY. + /// If the block at playerY is air-like, scan down for the first solid block. + /// If the block at playerY is solid, scan up (up to playerY + 10) for the + /// first air block, then return that Y (the cave ceiling opening). + /// Returns int.MinValue if no cave floor is found. + /// + private static int FindCaveFloorY(ChunkColumn column, int x, int z, int playerY, int minY, int maxY) + { + var startLoc = new Mapping.Location(x, playerY, z); + var startChunk = column.GetChunk(startLoc); + + bool startIsAir; + if (startChunk is null) + { + startIsAir = true; + } + else + { + var startMat = startChunk.GetBlock(startLoc).Type; + startIsAir = !MinimapColorMap.IsLightBlocking(startMat); + } + + if (startIsAir) + { + for (int y = playerY - 1; y >= minY; y--) + { + var loc = new Mapping.Location(x, y, z); + var chunk = column.GetChunk(loc); + if (chunk is null) continue; + + var mat = chunk.GetBlock(loc).Type; + if (MinimapColorMap.IsLightBlocking(mat)) + return y; + } + return minY; + } + else + { + int upLimit = Math.Min(playerY + 10, maxY); + for (int y = playerY + 1; y <= upLimit; y++) + { + var loc = new Mapping.Location(x, y, z); + var chunk = column.GetChunk(loc); + if (chunk is null) continue; + + var mat = chunk.GetBlock(loc).Type; + if (!MinimapColorMap.IsLightBlocking(mat)) + { + for (int y2 = y - 1; y2 >= minY; y2--) + { + var loc2 = new Mapping.Location(x, y2, z); + var chunk2 = column.GetChunk(loc2); + if (chunk2 is null) continue; + + var mat2 = chunk2.GetBlock(loc2).Type; + if (MinimapColorMap.IsLightBlocking(mat2)) + return y2; + } + return minY; + } + } + return int.MinValue; + } + } + + private static (Color color, int surfaceY, List<(Material Mat, int Count)>? matSummary, bool inCave) + SampleAreaDominantCave(World world, int baseX, int baseZ, + int size, int playerY, int minY, int maxY, bool collectMats, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + var colorCounts = new Dictionary(); + Dictionary? matCounts = collectMats ? [] : null; + int caveCount = 0; + + int step = Math.Max(1, size / 3); + for (int dx = 0; dx < size; dx += step) + { + for (int dz = 0; dz < size; dz += step) + { + var (c, surfY, surfMat, inCave) = SampleColumnCave( + world, baseX + dx, baseZ + dz, playerY, minY, maxY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + + if (inCave) caveCount++; + + if (colorCounts.TryGetValue(c, out var existing)) + colorCounts[c] = (existing.Count + 1, existing.SumY + surfY); + else + colorCounts[c] = (1, surfY); + + if (matCounts is not null) + { + if (matCounts.TryGetValue(surfMat, out int mc)) + matCounts[surfMat] = mc + 1; + else + matCounts[surfMat] = 1; + } + } + } + + Color best = MinimapColorMap.VoidColor; + int bestCount = 0; + int avgY = minY; + foreach (var kvp in colorCounts) + { + if (kvp.Value.Count > bestCount) + { + bestCount = kvp.Value.Count; + best = kvp.Key; + avgY = kvp.Value.SumY / kvp.Value.Count; + } + } + + List<(Material, int)>? summary = null; + if (matCounts is not null && matCounts.Count > 0) + { + summary = matCounts + .OrderByDescending(kv => kv.Value) + .Select(kv => (kv.Key, kv.Value)) + .ToList(); + } + + int totalSamples = 0; + foreach (var kvp in colorCounts) + totalSamples += kvp.Value.Count; + + bool majorityInCave = caveCount * 2 >= totalSamples; + return (best, avgY, summary, majorityInCave); + } + + /// + /// Draw a 1-pixel dark border around the boundary between cave-reachable pixels + /// and non-cave (solid/surface) pixels, giving the cave region a visible edge. + /// + private static void ApplyCaveBorder(SampleResult result, bool[,] caveMask, + int mapW, int mapH, Dictionary<(int, int), (Color, int)> entityPixels) + { + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (entityPixels.ContainsKey((px, py))) continue; + if (caveMask[px, py]) continue; + + bool neighborInCave = false; + if (px > 0 && caveMask[px - 1, py]) neighborInCave = true; + if (!neighborInCave && px < mapW - 1 && caveMask[px + 1, py]) neighborInCave = true; + if (!neighborInCave && py > 0 && caveMask[px, py - 1]) neighborInCave = true; + if (!neighborInCave && py < mapH - 1 && caveMask[px, py + 1]) neighborInCave = true; + + if (neighborInCave) + result.Pixels[px, py] = MinimapColorMap.CaveBorderColor; + } + } + } + private void ApplyPixelBuffer(SampleResult result, int w, int h) { int rows = h / 2; @@ -898,7 +1165,7 @@ namespace MinecraftClient.Tui } private void UpdateInfoBarAndLegend(McClient client, int bpp, - HashSet categories, int mapW) + HashSet categories, int mapW, bool caveModeActive) { var loc = client.GetCurrentLocation(); float yaw = client.GetYaw(); @@ -908,7 +1175,8 @@ namespace MinecraftClient.Tui int y = (int)Math.Floor(loc.Y); int z = (int)Math.Floor(loc.Z); - string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1"; + string caveSuffix = caveModeActive ? " \u25bc" : ""; + string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1{caveSuffix}"; var legendParts = new List(); var legendColors = new List(); From 35dd3c4f069c64fb2583632c7c49b061f68d10e1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 01:27:27 +0800 Subject: [PATCH 63/76] Add support for ItemStackTemplate in DataTypes and Protocol18 - Implemented ReadNextItemStackTemplate method to read ItemStackTemplate data with item-first encoding. - Updated Protocol18 to utilize ReadItemStackTemplateLabel for improved item display handling. - Enhanced item component parsing to accommodate new structured components. --- .../Protocol/Handlers/DataTypes.cs | 31 +++++++++++++++++++ .../Protocol/Handlers/Protocol18.cs | 11 ++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 669ba5b3..6a0d27aa 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers return ReadNextNbt(cache, true); } + /// + /// Read an ItemStackTemplate (26.1+) from a cache of bytes. + /// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch. + /// ItemStackTemplate is always non-empty (no count=0 sentinel). + /// + public Item ReadNextItemStackTemplate(Queue cache, ItemPalette itemPalette) + { + var itemId = ReadNextVarInt(cache); + var itemCount = ReadNextVarInt(cache); + var item = new Item(itemPalette.FromId(itemId), itemCount, null); + + var numberOfComponentsToAdd = ReadNextVarInt(cache); + var numberofComponentsToRemove = ReadNextVarInt(cache); + var structuredComponentHandler = new StructuredComponentsHandler(protocolversion, this, itemPalette); + var strcturedComponentsToAdd = new List(numberOfComponentsToAdd); + + for (var i = 0; i < numberOfComponentsToAdd; i++) + { + var componentTypeId = ReadNextVarInt(cache); + strcturedComponentsToAdd.Add(structuredComponentHandler.Parse(componentTypeId, cache)); + } + + for (var i = 0; i < numberofComponentsToRemove; i++) + ReadNextVarInt(cache); + + if (strcturedComponentsToAdd.Count > 0) + item.Components = strcturedComponentsToAdd; + + return item; + } + /// /// Read a single item slot from a cache of bytes and remove it from the cache /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index eeabc8e0..45bde28f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3530,7 +3530,7 @@ namespace MinecraftClient.Protocol.Handlers 2 => ReadWithAnyPotionSlotDisplayLabel(packetData), 3 => ReadOnlyWithComponentSlotDisplayLabel(packetData), 4 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))), - 5 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty", + 5 => ReadItemStackTemplateLabel(packetData), 6 => "#" + dataTypes.ReadNextString(packetData), 7 => ReadDyedSlotDisplayLabel(packetData), 8 => ReadSmithingTrimSlotDisplayLabel(packetData), @@ -3612,6 +3612,15 @@ namespace MinecraftClient.Protocol.Handlers return label; } + /// + /// Read an ItemStackTemplate (26.1+) which encodes fields in a different order + /// than ItemStack: item_id (VarInt), count (VarInt), DataComponentPatch. + /// + private string ReadItemStackTemplateLabel(Queue packetData) + { + return dataTypes.ReadNextItemStackTemplate(packetData, itemPalette).GetTypeString(); + } + private void SkipOptionalCraftingRequirements(Queue packetData) { if (!dataTypes.ReadNextBool(packetData)) From b9b7160e19d1b985c6e682650b84652d1947d79d Mon Sep 17 00:00:00 2001 From: BruceChen Date: Tue, 31 Mar 2026 01:27:34 +0800 Subject: [PATCH 64/76] Enhance decompile.sh to support version metadata resolution and improved decompilation handling - Added functionality to fetch version metadata from Mojang's manifest. - Implemented checks for the presence of Proguard mappings to determine decompilation method. - Enhanced the script to handle unobfuscated versions by downloading and extracting inner jars when necessary. - Improved error handling for missing dependencies and added informative output messages during the decompilation process. --- tools/decompile.sh | 101 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 20 deletions(-) diff --git a/tools/decompile.sh b/tools/decompile.sh index cd17ab7a..791431ba 100644 --- a/tools/decompile.sh +++ b/tools/decompile.sh @@ -86,18 +86,90 @@ fi mkdir -p "$MC_OFFICIAL/remapped_jar" +# --- Resolve version metadata from Mojang manifest --- +MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json" +VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c " +import json, sys +data = json.load(sys.stdin) +for v in data['versions']: + if v['id'] == '$VERSION': + print(v['url']) + break +") +if [[ -z "$VERSION_URL" ]]; then + echo "Error: version $VERSION not found in Mojang launcher manifest." + exit 1 +fi + +VERSION_META=$(curl -sL "$VERSION_URL") +MAPPING_KEY="${SIDE_LOWER}_mappings" +HAS_MAPPINGS=$(echo "$VERSION_META" | python3 -c " +import json, sys +data = json.load(sys.stdin) +print('true' if '$MAPPING_KEY' in data.get('downloads', {}) else 'false') +") + echo "=== Decompiling Minecraft $VERSION ($SIDE) ===" echo " Remapped JAR: $REMAPPED_JAR" echo " Decompiled: $DECOMPILED_DIR" +echo " Obfuscated: $HAS_MAPPINGS" echo "" cd "$MC_OFFICIAL" -java -jar "$DECOMPILER_JAR" \ - --version "$VERSION" \ - --side "$SIDE" \ - --decompile \ - --output "$REMAPPED_JAR" \ - --decompiled-output "$DECOMPILED_DIR" + +if [[ "$HAS_MAPPINGS" == "true" ]]; then + # Obfuscated version: use --version/--side to auto-download jar + mappings + deobfuscate + java -jar "$DECOMPILER_JAR" \ + --version "$VERSION" \ + --side "$SIDE" \ + --decompile \ + --output "$REMAPPED_JAR" \ + --decompiled-output "$DECOMPILED_DIR" +else + # Unobfuscated version (26.1+): download jar, extract inner jar from bundle, decompile directly. + # MinecraftDecompiler requires --mapping-path with --input, but unobfuscated versions + # have no mappings. We use Vineflower directly instead. + echo "No Proguard mappings for $VERSION; decompiling without deobfuscation." + + JAR_URL=$(echo "$VERSION_META" | python3 -c " +import json, sys +data = json.load(sys.stdin) +print(data['downloads']['${SIDE_LOWER}']['url']) +") + ORIGINAL_JAR="$MC_OFFICIAL/remapped_jar/${VERSION}-${SIDE_LOWER}-original.jar" + if [[ ! -f "$ORIGINAL_JAR" ]]; then + echo "Downloading ${SIDE_LOWER}.jar ..." + curl -L -o "$ORIGINAL_JAR" "$JAR_URL" + fi + + # Since 1.18, server.jar is a bundled jar containing the actual game jar inside + # META-INF/versions//server-.jar. Extract it if present. + DECOMPILE_TARGET="$ORIGINAL_JAR" + EXTRACT_DIR=$(mktemp -d) + trap "rm -rf '$EXTRACT_DIR'" EXIT + if unzip -q -o "$ORIGINAL_JAR" "META-INF/versions.list" -d "$EXTRACT_DIR" 2>/dev/null; then + INNER_PATH=$(awk '{print $NF}' "$EXTRACT_DIR/META-INF/versions.list" | head -1) + if [[ -n "$INNER_PATH" ]]; then + unzip -q -o "$ORIGINAL_JAR" "META-INF/versions/$INNER_PATH" -d "$EXTRACT_DIR" + DECOMPILE_TARGET="$EXTRACT_DIR/META-INF/versions/$INNER_PATH" + echo "Extracted inner jar: $INNER_PATH" + fi + fi + + # Use Vineflower directly (bundled with MinecraftDecompiler, or standalone) + VINEFLOWER_JAR="$MC_OFFICIAL/downloads/decompiler/vineflower.jar" + if [[ ! -f "$VINEFLOWER_JAR" ]]; then + # Fall back to vineflower bundled inside MinecraftDecompiler's cache + VINEFLOWER_JAR=$(find "$MC_OFFICIAL" -name "vineflower*.jar" -not -name "MinecraftDecompiler.jar" 2>/dev/null | head -1) + fi + if [[ -z "$VINEFLOWER_JAR" || ! -f "$VINEFLOWER_JAR" ]]; then + echo "Error: vineflower.jar not found. Place it at $MC_OFFICIAL/downloads/decompiler/vineflower.jar" + exit 1 + fi + + echo "Decompiling with Vineflower: $VINEFLOWER_JAR" + java -jar "$VINEFLOWER_JAR" "$DECOMPILE_TARGET" "$DECOMPILED_DIR" +fi echo "" echo "=== Done ===" @@ -108,29 +180,18 @@ if [[ "$SIDE" == "SERVER" ]]; then DOWNLOADS_DIR="$MC_OFFICIAL/downloads/$VERSION" if [[ ! -f "$DOWNLOADS_DIR/server.jar" ]]; then mkdir -p "$DOWNLOADS_DIR" - # MinecraftDecompiler downloads the original jar into its cache; - # extract it from the bundled remapped jar or re-download via manifest. echo "" echo "Downloading server.jar for $VERSION into $DOWNLOADS_DIR ..." - MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json" - VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for v in data['versions']: - if v['id'] == '$VERSION': - print(v['url']) - break -") - if [[ -n "$VERSION_URL" ]]; then - SERVER_JAR_URL=$(curl -sL "$VERSION_URL" | python3 -c " + SERVER_JAR_URL=$(echo "$VERSION_META" | python3 -c " import json, sys data = json.load(sys.stdin) print(data['downloads']['server']['url']) ") + if [[ -n "$SERVER_JAR_URL" ]]; then curl -L -o "$DOWNLOADS_DIR/server.jar" "$SERVER_JAR_URL" echo "Downloaded server.jar" else - echo "Warning: could not find version $VERSION in Mojang manifest; server.jar not downloaded." + echo "Warning: could not download server.jar for $VERSION." fi else echo "server.jar already exists: $DOWNLOADS_DIR/server.jar" From d158bfdf21a7b8b9c6df90e159c09db0c3166395 Mon Sep 17 00:00:00 2001 From: Anon Date: Mon, 30 Mar 2026 20:20:58 +0200 Subject: [PATCH 65/76] Fixed bugs --- MinecraftClient/Mapping/BlockHardness.cs | 4 +- MinecraftClient/Mapping/MiningCalculator.cs | 199 ++++++++++++++------ 2 files changed, 141 insertions(+), 62 deletions(-) diff --git a/MinecraftClient/Mapping/BlockHardness.cs b/MinecraftClient/Mapping/BlockHardness.cs index 311507b0..84cd8f92 100644 --- a/MinecraftClient/Mapping/BlockHardness.cs +++ b/MinecraftClient/Mapping/BlockHardness.cs @@ -249,8 +249,8 @@ namespace MinecraftClient.Mapping { Material.NetherSprouts, 0.0f }, { Material.NetherWart, 0.0f }, { Material.OakButton, 0.0f }, - { Material.OakLeaves, 0.0f }, - { Material.OakLog, 0.0f }, + { Material.OakLeaves, 0.2f }, + { Material.OakLog, 2.0f }, { Material.OakSapling, 0.0f }, { Material.OpenEyeblossom, 0.0f }, { Material.OrangeCandle, 0.0f }, diff --git a/MinecraftClient/Mapping/MiningCalculator.cs b/MinecraftClient/Mapping/MiningCalculator.cs index 38cf6d18..6f49fd4a 100644 --- a/MinecraftClient/Mapping/MiningCalculator.cs +++ b/MinecraftClient/Mapping/MiningCalculator.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using MinecraftClient.Inventory; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_5; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6; namespace MinecraftClient.Mapping { @@ -74,27 +77,14 @@ namespace MinecraftClient.Mapping { float speed = GetToolSpeed(blockMaterial, heldItem, protocolVersion); - if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version) + if (speed > 1.0f) { - // 1.21.11+: Efficiency is delivered via the MINING_EFFICIENCY attribute - if (speed > 1.0f && playerAttributes.TryGetValue("player.mining_efficiency", out double miningEff)) - speed += (float)miningEff; - } - else - { - // Pre-1.21.11: Efficiency enchantment adds level^2 + 1 - int effLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion); - if (speed > 1.0f && effLevel > 0) - speed += effLevel * effLevel + 1; + speed += GetEfficiencyBonus(heldItem, playerAttributes, protocolVersion); } - // Haste effect: multiply by 1 + 0.2 * (amplifier + 1) - if (effects.TryGetValue(Effects.Haste, out var hasteData)) - speed *= 1.0f + (hasteData.Amplifier + 1) * 0.2f; - - // Conduit Power also grants dig speed equivalent when in water - if (effects.TryGetValue(Effects.ConduitPower, out var conduitData)) - speed *= 1.0f + (conduitData.Amplifier + 1) * 0.2f; + int digSpeedAmplifier = GetDigSpeedAmplifier(effects); + if (digSpeedAmplifier >= 0) + speed *= 1.0f + (digSpeedAmplifier + 1) * 0.2f; // Mining Fatigue if (effects.TryGetValue(Effects.MiningFatigue, out var fatigueData)) @@ -155,19 +145,19 @@ namespace MinecraftClient.Mapping return 1.0f; // Modern path: use ToolComponent from structured components - if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version + && TryGetToolRules(heldItem, out List? rules, out float defaultMiningSpeed)) { - var toolComp = heldItem.Components?.OfType().FirstOrDefault(); - if (toolComp is not null) + foreach (var rule in rules) { - // Check rules for matching blocks - foreach (var rule in toolComp.Rules) - { - if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial)) - return rule.Speed; - } - return toolComp.DefaultMiningSpeed; + if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial)) + return rule.Speed; } + + // Structured tool data covers modern mining rules, but keep the legacy fallback for + // explicit block holder-sets that MCC cannot resolve yet (for example cobweb). + if (defaultMiningSpeed > 1.0f) + return defaultMiningSpeed; } // Legacy path: hardcoded tool speed tables @@ -186,25 +176,48 @@ namespace MinecraftClient.Mapping return false; // Modern path: check ToolComponent rules - if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version + && TryGetToolRules(heldItem, out List? rules, out _)) { - var toolComp = heldItem.Components?.OfType().FirstOrDefault(); - if (toolComp is not null) + foreach (var rule in rules) { - foreach (var rule in toolComp.Rules) - { - if (rule.HasCorrectDropForBlocks && rule.CorrectDropForBlocks - && MatchesBlockSet(rule.Blocks, blockMaterial)) - return true; - } + if (rule.HasCorrectDropForBlocks && MatchesBlockSet(rule.Blocks, blockMaterial)) + return rule.CorrectDropForBlocks; } - return false; } - // Legacy path: check if Material2Tool recommends this tool type + // Legacy path, plus a modern fallback for direct block holder-sets MCC cannot resolve yet. return IsCorrectToolLegacy(heldItem.Type, blockMaterial); } + private static bool TryGetToolRules( + Item heldItem, + [NotNullWhen(true)] out List? rules, + out float defaultMiningSpeed) + { + rules = null; + defaultMiningSpeed = 1.0f; + + if (heldItem.Components is null) + return false; + + if (heldItem.Components.OfType().FirstOrDefault() is ToolComponent toolComponent) + { + rules = toolComponent.Rules; + defaultMiningSpeed = toolComponent.DefaultMiningSpeed; + return true; + } + + if (heldItem.Components.OfType().FirstOrDefault() is ToolComponent1215 toolComponent1215) + { + rules = toolComponent1215.Rules; + defaultMiningSpeed = toolComponent1215.DefaultMiningSpeed; + return true; + } + + return false; + } + /// /// Match a block material against a ToolComponent BlockSetSubcomponent. /// @@ -241,20 +254,34 @@ namespace MinecraftClient.Mapping string tag = tagName.Replace("minecraft:", ""); ItemType[] tools = Material2Tool.GetCorrectToolForBlock(blockMaterial); - if (tools.Length == 0) - return false; - - ItemType firstTool = tools[0]; return tag switch { - "mineable/pickaxe" => IsPickaxe(firstTool), - "mineable/axe" => IsAxe(firstTool), - "mineable/shovel" => IsShovel(firstTool), - "mineable/hoe" => IsHoe(firstTool), + "mineable/pickaxe" => tools.Length > 0 && IsPickaxe(tools[0]), + "mineable/axe" => tools.Length > 0 && IsAxe(tools[0]), + "mineable/shovel" => tools.Length > 0 && IsShovel(tools[0]), + "mineable/hoe" => tools.Length > 0 && IsHoe(tools[0]), + "leaves" => IsLeaf(blockMaterial), + "wool" => IsWool(blockMaterial), + "incorrect_for_wooden_tool" => RequiresHigherTier(blockMaterial, 0), + "incorrect_for_gold_tool" => RequiresHigherTier(blockMaterial, 0), + "incorrect_for_stone_tool" => RequiresHigherTier(blockMaterial, 1), + "incorrect_for_copper_tool" => RequiresHigherTier(blockMaterial, 1), + "incorrect_for_iron_tool" => RequiresHigherTier(blockMaterial, 2), + "incorrect_for_diamond_tool" => RequiresHigherTier(blockMaterial, 3), + "incorrect_for_netherite_tool" => RequiresHigherTier(blockMaterial, 4), _ => false }; } + private static bool RequiresHigherTier(Material blockMaterial, int tier) + { + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return false; + + return GetRequiredTier(blockMaterial, recommended) > tier; + } + /// /// Get the enchantment level from an item, supporting both legacy NBT and modern structured components. /// @@ -330,6 +357,16 @@ namespace MinecraftClient.Mapping /// private static float GetLegacyToolSpeed(ItemType toolType, Material blockMaterial) { + float specialToolSpeed = toolType switch + { + ItemType.Shears => GetShearsSpeed(blockMaterial), + _ when IsSword(toolType) && blockMaterial == Material.Cobweb => 15.0f, + _ => 1.0f + }; + + if (specialToolSpeed > 1.0f) + return specialToolSpeed; + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); if (recommended.Length == 0) return 1.0f; @@ -339,14 +376,7 @@ namespace MinecraftClient.Mapping ToolCategory neededCategory = GetToolCategory(recommended[0]); if (heldCategory == ToolCategory.None || heldCategory != neededCategory) - { - // Special cases: sword on cobweb, shears on specific blocks - if (toolType is ItemType.Shears && IsShearable(blockMaterial)) - return 1.5f; - if (IsSword(toolType) && blockMaterial == Material.Cobweb) - return 15.0f; return 1.0f; - } return GetBaseToolSpeed(toolType); } @@ -400,7 +430,13 @@ namespace MinecraftClient.Mapping ToolCategory neededCategory = GetToolCategory(recommended[0]); if (heldCategory == ToolCategory.None || heldCategory != neededCategory) + { + if (toolType == ItemType.Shears && blockMaterial == Material.Cobweb) + return true; + if (IsSword(toolType) && blockMaterial == Material.Cobweb) + return true; return false; + } // Check tool tier requirement int heldTier = GetToolTier(toolType); @@ -476,17 +512,60 @@ namespace MinecraftClient.Mapping item is ItemType.WoodenSword or ItemType.StoneSword or ItemType.IronSword or ItemType.GoldenSword or ItemType.DiamondSword or ItemType.NetheriteSword; + private static float GetShearsSpeed(Material block) + { + return block switch + { + Material.Cobweb => 15.0f, + Material.Vine or Material.GlowLichen => 2.0f, + _ when IsLeaf(block) => 15.0f, + _ when IsWool(block) => 5.0f, + _ => 1.0f + }; + } + private static bool IsShearable(Material block) => - block is Material.Cobweb or Material.OakLeaves or Material.SpruceLeaves - or Material.BirchLeaves or Material.JungleLeaves or Material.AcaciaLeaves - or Material.DarkOakLeaves or Material.CherryLeaves or Material.MangroveLeaves - or Material.AzaleaLeaves or Material.FloweringAzaleaLeaves - or Material.WhiteWool or Material.OrangeWool or Material.MagentaWool + block == Material.Cobweb || IsLeaf(block) || IsWool(block) || block is Material.Vine or Material.GlowLichen; + + private static bool IsLeaf(Material block) => + block is Material.OakLeaves or Material.SpruceLeaves or Material.BirchLeaves + or Material.JungleLeaves or Material.AcaciaLeaves or Material.DarkOakLeaves + or Material.CherryLeaves or Material.MangroveLeaves or Material.AzaleaLeaves + or Material.FloweringAzaleaLeaves or Material.PaleOakLeaves; + + private static bool IsWool(Material block) => + block is Material.WhiteWool or Material.OrangeWool or Material.MagentaWool or Material.LightBlueWool or Material.YellowWool or Material.LimeWool or Material.PinkWool or Material.GrayWool or Material.LightGrayWool or Material.CyanWool or Material.PurpleWool or Material.BlueWool or Material.BrownWool or Material.GreenWool or Material.RedWool - or Material.BlackWool or Material.Vine; + or Material.BlackWool; + + private static float GetEfficiencyBonus(Item? heldItem, Dictionary playerAttributes, int protocolVersion) + { + if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version + && playerAttributes.TryGetValue("player.mining_efficiency", out double miningEfficiency) + && miningEfficiency > 0.0) + { + return (float)miningEfficiency; + } + + int efficiencyLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion); + return efficiencyLevel > 0 ? efficiencyLevel * efficiencyLevel + 1 : 0.0f; + } + + private static int GetDigSpeedAmplifier(Dictionary effects) + { + int amplifier = -1; + + if (effects.TryGetValue(Effects.Haste, out var hasteData)) + amplifier = Math.Max(amplifier, hasteData.Amplifier); + + if (effects.TryGetValue(Effects.ConduitPower, out var conduitData)) + amplifier = Math.Max(amplifier, conduitData.Amplifier); + + return amplifier; + } #endregion } From 5de72de234a32262398e75c19e4e4c7b3888b0c9 Mon Sep 17 00:00:00 2001 From: Anon Date: Mon, 30 Mar 2026 21:37:53 +0200 Subject: [PATCH 66/76] Fixed a wrong effect decoding on 1.20.4 --- MinecraftClient/Protocol/Handlers/Protocol18.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index eeabc8e0..8fd192a7 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2514,7 +2514,7 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entityId = dataTypes.ReadNextVarInt(packetData); - var effectId = protocolVersion >= MC_1_18_2_Version + var effectId = protocolVersion >= MC_1_20_4_Version ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); @@ -2544,7 +2544,7 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entityId = dataTypes.ReadNextVarInt(packetData); - var effectId = protocolVersion >= MC_1_18_2_Version + var effectId = protocolVersion >= MC_1_20_4_Version ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); From c9b0913c1a7fbff36c640a78614eed1e77f26990 Mon Sep 17 00:00:00 2001 From: Anon Date: Tue, 31 Mar 2026 00:18:31 +0200 Subject: [PATCH 67/76] feat(autofishing): add velocity and sound bite detection --- MinecraftClient/ChatBots/AutoFishing.cs | 100 ++++++++++++++++-- MinecraftClient/McClient.cs | 38 +++++++ .../Protocol/Handlers/DataTypes.cs | 42 ++++++-- .../Protocol/Handlers/Protocol18.cs | 97 +++++++++++++++++ .../Protocol/IMinecraftComHandler.cs | 21 ++++ .../ConfigComments/ConfigComments.resx | 15 +++ MinecraftClient/Scripting/ChatBot.cs | 23 ++++ docs/guide/chat-bots.md | 61 +++++++++++ 8 files changed, 380 insertions(+), 17 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 9711b86c..cf381561 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -62,6 +62,21 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$ChatBot.AutoFishing.Hook_Threshold$")] public double Hook_Threshold = 0.2; + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Velocity_Detection$")] + public bool Enable_Velocity_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Velocity_Hook_Threshold$")] + public double Velocity_Hook_Threshold = -0.2; + + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Sound_Detection$")] + public bool Enable_Sound_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Sound_Distance$")] + public double Sound_Distance = 5.0; + + [TomlInlineComment("$ChatBot.AutoFishing.Detection_Warmup$")] + public double Detection_Warmup = 1.0; + [TomlInlineComment("$ChatBot.AutoFishing.Log_Fish_Bobber$")] public bool Log_Fish_Bobber = false; @@ -97,6 +112,15 @@ namespace MinecraftClient.ChatBots if (Hook_Threshold < 0) Hook_Threshold = -Hook_Threshold; + + if (Velocity_Hook_Threshold > 0) + Velocity_Hook_Threshold = -Velocity_Hook_Threshold; + + if (Sound_Distance < 0) + Sound_Distance = -Sound_Distance; + + if (Detection_Warmup < 0) + Detection_Warmup = 0; } public struct LocationConfig @@ -171,6 +195,7 @@ namespace MinecraftClient.ChatBots private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; + private DateTime BobberSpawnTime = DateTime.MinValue; private int fishItemCounter = 15; private Dictionary fishItemCnt = new(); private Entity fishItem = new(-1, EntityType.Item, Location.Zero); @@ -464,6 +489,7 @@ namespace MinecraftClient.ChatBots fishingBobber = entity; LastPos = entity.Location; isFishing = true; + BobberSpawnTime = DateTime.Now; castTimeout = 24; counter = 0; @@ -500,7 +526,7 @@ namespace MinecraftClient.ChatBots public override void OnEntityMove(Entity entity) { if (isFishing && entity is not null && fishingBobber!.ID == entity.ID && - (state == FishingState.WaitingFishToBite || state == FishingState.WaitingFishingBobber)) + state == FishingState.WaitingFishToBite) { Location Pos = entity.Location; double Dx = LastPos.X - Pos.X; @@ -515,13 +541,7 @@ namespace MinecraftClient.ChatBots Math.Abs(Dz) < Math.Abs(Config.Stationary_Threshold) && Math.Abs(Dy) > Math.Abs(Config.Hook_Threshold)) { - // prevent triggering multiple time - if ((DateTime.Now - CaughtTime).TotalSeconds > 1) - { - isFishing = false; - CaughtTime = DateTime.Now; - OnCaughtFish(); - } + TryCatchFish(); } } } @@ -540,6 +560,38 @@ namespace MinecraftClient.ChatBots } } + public override void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) + { + if (!Config.Enable_Velocity_Detection || !CanUseAdvancedDetection()) + return; + + if (fishingBobber is null || entity.ID != fishingBobber.ID) + return; + + if (velocityY <= Config.Velocity_Hook_Threshold) + TryCatchFish(); + } + + public override void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) + { + if (!Config.Enable_Sound_Detection || !CanUseAdvancedDetection()) + return; + + if (!IsFishingBobberSplashSound(soundName)) + return; + + Location? soundLocation = location; + if (soundLocation is null && sourceEntity is not null) + soundLocation = sourceEntity.Location; + + if (soundLocation is null || fishingBobber is null) + return; + + if (soundLocation.Value.Distance(fishingBobber.Location) <= Config.Sound_Distance) + TryCatchFish(); + } + public override void AfterGameJoined() { StartFishing(); @@ -562,10 +614,42 @@ namespace MinecraftClient.ChatBots fishingBobber = null; LastPos = Location.Zero; CaughtTime = DateTime.Now; + BobberSpawnTime = DateTime.MinValue; return base.OnDisconnect(reason, message); } + private bool CanUseAdvancedDetection() + { + if (!isFishing || fishingBobber is null || state != FishingState.WaitingFishToBite) + return false; + + return (DateTime.Now - BobberSpawnTime).TotalSeconds >= Config.Detection_Warmup; + } + + private void TryCatchFish() + { + if (!CanUseAdvancedDetection()) + return; + + // Prevent repeated catches from multiple packets of the same bite. + if ((DateTime.Now - CaughtTime).TotalSeconds <= 1) + return; + + isFishing = false; + CaughtTime = DateTime.Now; + OnCaughtFish(); + } + + private static bool IsFishingBobberSplashSound(string? soundName) + { + return string.Equals(soundName, "minecraft:entity.fishing_bobber.splash", + StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.fishing_bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "minecraft:entity.bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.bobber.splash", StringComparison.OrdinalIgnoreCase); + } + /// /// Called when detected a fish is caught /// diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index bd84d562..e1079c3c 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -3798,6 +3798,44 @@ namespace MinecraftClient } } + /// + /// Called when an entity velocity update is received. + /// + /// Entity ID + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ) + { + if (entities.TryGetValue(entityID, out Entity? entity)) + DispatchBotEvent(bot => bot.OnEntityVelocity(entity, velocityX, velocityY, velocityZ)); + } + + /// + /// Called when a sound packet is received. + /// + /// Sound key when available, otherwise null + /// Sound location when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity id for entity sound packets, if any + public void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + int? entityID) + { + Entity? sourceEntity = null; + Location? resolvedLocation = location; + + if (entityID is int id && entities.TryGetValue(id, out Entity? entity)) + { + sourceEntity = entity; + resolvedLocation ??= entity.Location; + } + + DispatchBotEvent(bot => bot.OnSoundEffect(soundName, resolvedLocation, category, volume, pitch, + sourceEntity)); + } + /// /// Called when received entity properties from server. /// diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 6a0d27aa..bdbdecea 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -1054,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers } } + private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4; + + private static double UnpackLpVec3(long packedAxis) + { + return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0; + } + /// - /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+). - /// Variable-length encoding: first byte 0 = zero vector; otherwise - /// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation. + /// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+). + /// Returned vector is expressed in blocks per tick. /// - public void ReadNextLpVec3(Queue cache) + public (double X, double Y, double Z) ReadNextLpVec3Values(Queue cache) { int first = ReadNextByte(cache); if (first == 0) - return; - ReadNextByte(cache); // second byte - ReadData(4, cache); // uint32 - if ((first & 4) == 4) // continuation bit set - ReadNextVarInt(cache); + return (0.0, 0.0, 0.0); + + int second = ReadNextByte(cache); + uint high = (uint)ReadNextInt(cache); + long packed = ((long)high << 16) | (long)(second << 8) | (uint)first; + + long scale = first & 3; + if (HasLpVec3Continuation(first)) + scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2; + + return ( + UnpackLpVec3(packed >> 3) * scale, + UnpackLpVec3(packed >> 18) * scale, + UnpackLpVec3(packed >> 33) * scale + ); + } + + /// + /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it. + /// + public void ReadNextLpVec3(Queue cache) + { + ReadNextLpVec3Values(cache); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 88c26690..a27c4b0a 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -2642,6 +2642,27 @@ namespace MinecraftClient.Protocol.Handlers handler.OnEntityRotation(entityId, yaw, pitch, isOnGround); } + break; + case PacketTypesIn.EntityVelocity: + if (handler.GetEntityHandlingEnabled()) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + double velocityX, velocityY, velocityZ; + + if (protocolVersion >= MC_1_21_9_Version) + { + (velocityX, velocityY, velocityZ) = dataTypes.ReadNextLpVec3Values(packetData); + } + else + { + velocityX = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityY = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityZ = dataTypes.ReadNextShort(packetData) / 8000.0D; + } + + handler.OnEntityVelocity(entityId, velocityX, velocityY, velocityZ); + } + break; case PacketTypesIn.EntityProperties: if (handler.GetEntityHandlingEnabled()) @@ -2892,6 +2913,65 @@ namespace MinecraftClient.Protocol.Handlers handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); break; + case PacketTypesIn.NamedSoundEffect: + { + string? soundName = dataTypes.ReadNextString(packetData); + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.SoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.EntitySoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + int entityId = dataTypes.ReadNextVarInt(packetData); + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, null, category, volume, pitch, entityId); + break; + } case PacketTypesIn.HeldItemChange: case PacketTypesIn.SetHeldSlot: handler.OnHeldItemChange(dataTypes.ReadNextByte(packetData)); // Slot @@ -3154,6 +3234,23 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Read a Holder<SoundEvent> from packet data and return its key when inline. + /// Returns null when the holder is a registry reference. + /// + private string? ReadSoundEventHolderName(Queue packetData) + { + int soundHolderId = dataTypes.ReadNextVarInt(packetData); + if (soundHolderId != 0) + return null; + + string soundName = dataTypes.ReadNextString(packetData); + bool hasFixedRange = dataTypes.ReadNextBool(packetData); + if (hasFixedRange) + dataTypes.ReadNextFloat(packetData); + return soundName; + } + /// /// Handle the Statistics packet for pre-1.12 legacy achievements. /// diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 9bfa44e8..13f5a628 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol /// TRUE if on ground void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround); + /// + /// Called when an entity velocity update packet is received. + /// Velocity values are in blocks per tick. + /// + /// Entity ID + /// Velocity X + /// Velocity Y + /// Velocity Z + void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ); + /// /// Called when additional properties have been received for an entity /// @@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol /// Amount of affected blocks void OnExplosion(Location location, float strength, int affectedBlocks); + /// + /// Called when a sound packet is received. + /// + /// Sound key if available, otherwise null + /// Sound location for world sounds, or null if unavailable + /// Sound category id + /// Sound volume + /// Sound pitch + /// Source entity id for entity-sound packets, if any + void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID); + /// /// Called when a player's game mode has changed /// diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 8f3e4964..825c7e76 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually. A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish. + + Enable fish bite detection using fishing bobber velocity packets. + + + Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative. + + + Enable fish bite detection using splash sounds near the fishing bobber. + + + Maximum distance (blocks) between splash sound and bobber to treat it as a bite. + + + Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion. + Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet. diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 2776cda9..c5950334 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -199,6 +199,29 @@ namespace MinecraftClient.Scripting /// Entity with updated location public virtual void OnEntityMove(Entity entity) { } + /// + /// Called when a tracked entity receives a velocity update packet. + /// Velocity is expressed in blocks per tick. + /// + /// Entity with updated velocity + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { } + + /// + /// Called when a sound packet is received. + /// The sound name is null when the protocol provides only a registry id. + /// + /// Sound key when available, otherwise null + /// Sound position when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity for entity-sound packets when tracked + public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) { } + /// /// Called when an entity rotates /// diff --git a/docs/guide/chat-bots.md b/docs/guide/chat-bots.md index 512f1664..e8b6c711 100644 --- a/docs/guide/chat-bots.md +++ b/docs/guide/chat-bots.md @@ -927,6 +927,7 @@ redirectFrom: - **Description:** Automatically catch fish using a fishing rod. + Bite detection combines bobber movement, bobber velocity, and splash sounds.

Note

@@ -1103,6 +1104,66 @@ redirectFrom: - **Default:** `0.2` + #### `Enable_Velocity_Detection` + + - **Description:** + + Enables bite detection using the fishing bobber velocity packet. + + This improves reliability when bobber X/Z movement is constrained (for example by blocks near the water surface). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Velocity_Hook_Threshold` + + - **Description:** + + Velocity Y threshold in blocks/tick for velocity-based bite detection. + + Values below this threshold are considered a bite. Keep this value negative. + + - **Type:** `float` + + - **Default:** `-0.2` + + #### `Enable_Sound_Detection` + + - **Description:** + + Enables bite detection using nearby splash sounds (`entity.fishing_bobber.splash`). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Sound_Distance` + + - **Description:** + + Maximum distance in blocks between a splash sound and the tracked bobber to treat it as a bite. + + - **Type:** `float` + + - **Default:** `5.0` + + #### `Detection_Warmup` + + - **Description:** + + Delay in seconds after bobber spawn before bite detection starts. + + This helps ignore the initial cast-entry splash/motion. + + - **Type:** `float` + + - **Default:** `1.0` + #### `Log_Fish_Bobber` - **Description:** From cf65c6f2418662de420e2f6a824c6035812034d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:31:09 +0000 Subject: [PATCH 68/76] Initial plan From fdbffcc5bb4ec2d6c820e769d3136e281d9e18c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:41:11 +0000 Subject: [PATCH 69/76] feat: add Teams packet support (parsing, state tracking, /teams command, bot API) Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/72ea891e-ba62-4dfc-bbba-f197cd1d0404 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/Commands/Teams.cs | 77 ++++++++++++++ MinecraftClient/Mapping/PlayerTeam.cs | 49 +++++++++ MinecraftClient/McClient.cs | 100 +++++++++++++++++- .../Protocol/Handlers/Protocol18.cs | 78 ++++++++++++++ .../Protocol/IMinecraftComHandler.cs | 17 +++ .../Translations/Translations.Designer.cs | 45 ++++++++ .../Resources/Translations/Translations.resx | 15 +++ MinecraftClient/Scripting/ChatBot.cs | 17 +++ 8 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 MinecraftClient/Commands/Teams.cs create mode 100644 MinecraftClient/Mapping/PlayerTeam.cs diff --git a/MinecraftClient/Commands/Teams.cs b/MinecraftClient/Commands/Teams.cs new file mode 100644 index 00000000..af7a06d1 --- /dev/null +++ b/MinecraftClient/Commands/Teams.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Commands +{ + public class Teams : Command + { + public override string CmdName => "teams"; + public override string CmdUsage => "teams"; + public override string CmdDesc => Translations.cmd_teams_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => DoListTeams(r.Source)) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private static int DoListTeams(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + Dictionary snapshot = handler.GetTeams(); + + if (snapshot.Count == 0) + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_teams_no_teams); + + var sb = new StringBuilder(); + foreach (var team in snapshot.Values.OrderBy(static t => t.Name, StringComparer.Ordinal)) + { + sb.AppendLine(string.Format(Translations.cmd_teams_team_header, + team.Name, + team.DisplayName, + team.Color, + team.Prefix, + team.Suffix, + team.NameTagVisibility, + team.CollisionRule, + team.AllowFriendlyFire, + team.SeeFriendlyInvisibles)); + + if (team.Members.Count == 0) + sb.AppendLine(Translations.cmd_teams_team_no_members); + else + sb.AppendLine(string.Format(Translations.cmd_teams_team_members, + team.Members.Count, + string.Join(", ", team.Members.OrderBy(static m => m, StringComparer.OrdinalIgnoreCase)))); + } + + return r.SetAndReturn(CmdResult.Status.Done, sb.ToString().TrimEnd()); + } + } +} diff --git a/MinecraftClient/Mapping/PlayerTeam.cs b/MinecraftClient/Mapping/PlayerTeam.cs new file mode 100644 index 00000000..4ad7ca64 --- /dev/null +++ b/MinecraftClient/Mapping/PlayerTeam.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace MinecraftClient.Mapping +{ + /// + /// Represents a Minecraft scoreboard team and its current state. + /// + public class PlayerTeam + { + /// Team internal name (up to 16 chars) + public string Name { get; set; } = string.Empty; + + /// Display name component (formatted text) + public string DisplayName { get; set; } = string.Empty; + + /// Friendly fire is allowed between team members + public bool AllowFriendlyFire { get; set; } + + /// Team members can see invisible teammates + public bool SeeFriendlyInvisibles { get; set; } + + /// + /// Nametag visibility rule. + /// Values: "always", "hideForOtherTeams", "hideForOwnTeam", "never" + /// + public string NameTagVisibility { get; set; } = string.Empty; + + /// + /// Collision rule. + /// Values: "always", "pushOtherTeams", "pushOwnTeam", "never" + /// + public string CollisionRule { get; set; } = string.Empty; + + /// + /// Team color as ChatFormatting enum ordinal (-1 = RESET/none, + /// 0–15 = BLACK … WHITE). + /// + public int Color { get; set; } = -1; + + /// Prefix displayed before member names (formatted text) + public string Prefix { get; set; } = string.Empty; + + /// Suffix displayed after member names (formatted text) + public string Suffix { get; set; } = string.Empty; + + /// Current set of player / entity names on this team + public HashSet Members { get; } = new(System.StringComparer.OrdinalIgnoreCase); + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index e1079c3c..394d01cd 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -113,6 +113,9 @@ namespace MinecraftClient // player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed) private readonly Dictionary playerAttributes = new(); + + // scoreboard teams (key = team name) + private readonly Dictionary teams = new(StringComparer.Ordinal); // Sneaking public bool IsSneaking { get; set; } = false; @@ -162,6 +165,30 @@ namespace MinecraftClient return new Dictionary(playerEffects); } + /// + /// Get a snapshot of all known scoreboard teams. + /// + /// Dictionary mapping team name to + public Dictionary GetTeams() + { + lock (teams) + return new Dictionary(teams, StringComparer.Ordinal); + } + + /// + /// Get the team that contains the given player/entity name, or null if not found. + /// + public PlayerTeam? GetPlayerTeam(string playerName) + { + lock (teams) + { + foreach (var team in teams.Values) + if (team.Members.Contains(playerName)) + return team; + return null; + } + } + public int GetLevel() { return playerLevel; } public int GetTotalExperience() { return playerTotalExperience; } public byte GetCurrentSlot() { return CurrentSlot; } @@ -4053,7 +4080,78 @@ namespace MinecraftClient { DispatchBotEvent(bot => bot.OnUpdateScore(entityName, action, objectiveName, objectiveDisplayName, objectiveValue, numberFormat)); } - + + /// + /// Called when a Teams packet is received. Updates the internal team state and notifies bots. + /// + public void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players) + { + lock (teams) + { + switch (method) + { + case 0: // create + var newTeam = new PlayerTeam + { + Name = teamName, + DisplayName = displayName, + AllowFriendlyFire = (friendlyFlags & 0x01) != 0, + SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0, + NameTagVisibility = nameTagVisibility, + CollisionRule = collisionRule, + Color = color, + Prefix = prefix, + Suffix = suffix + }; + foreach (var p in players) + newTeam.Members.Add(p); + teams[teamName] = newTeam; + break; + + case 1: // remove + teams.Remove(teamName); + break; + + case 2: // update parameters + if (!teams.TryGetValue(teamName, out var updateTeam)) + { + updateTeam = new PlayerTeam { Name = teamName }; + teams[teamName] = updateTeam; + } + updateTeam.DisplayName = displayName; + updateTeam.AllowFriendlyFire = (friendlyFlags & 0x01) != 0; + updateTeam.SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0; + updateTeam.NameTagVisibility = nameTagVisibility; + updateTeam.CollisionRule = collisionRule; + updateTeam.Color = color; + updateTeam.Prefix = prefix; + updateTeam.Suffix = suffix; + break; + + case 3: // add players + if (!teams.TryGetValue(teamName, out var addTeam)) + { + addTeam = new PlayerTeam { Name = teamName }; + teams[teamName] = addTeam; + } + foreach (var p in players) + addTeam.Members.Add(p); + break; + + case 4: // remove players + if (teams.TryGetValue(teamName, out var removeTeam)) + foreach (var p in players) + removeTeam.Members.Remove(p); + break; + } + } + DispatchBotEvent(bot => bot.OnTeam(teamName, method, displayName, friendlyFlags, + nameTagVisibility, collisionRule, color, prefix, suffix, players)); + } + + /// /// Called when the client received the Tab Header and Footer /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index a27c4b0a..0085725b 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3035,6 +3035,84 @@ namespace MinecraftClient.Protocol.Handlers handler.OnUpdateScore(entityName, action3, objectiveName3, objectiveDisplayName3, objectiveValue2, numberFormat2); break; + case PacketTypesIn.Teams: + // Wire format per version: + // All versions: name (string), method (byte) + // method 0/2: displayName (component), options (byte), + // nameTagVisibility, collisionRule, color (VarInt), + // prefix (component), suffix (component) + // method 0/3/4: players list (VarInt count + strings) + // 1.21.9+ (protocol 773): nameTagVisibility and collisionRule are + // VarInt-encoded enum IDs instead of UTF strings. + var teamName = dataTypes.ReadNextString(packetData); + var teamMethod = dataTypes.ReadNextByte(packetData); + + var teamDisplayName = string.Empty; + byte teamFriendlyFlags = 0; + var teamNameTagVisibility = string.Empty; + var teamCollisionRule = string.Empty; + var teamColor = -1; + var teamPrefix = string.Empty; + var teamSuffix = string.Empty; + + if (teamMethod is 0 or 2) + { + teamDisplayName = dataTypes.ReadNextChat(packetData); + teamFriendlyFlags = dataTypes.ReadNextByte(packetData); + + // nameTagVisibility + if (protocolVersion >= MC_1_21_9_Version) + { + // STREAM_CODEC: 0=always, 1=never, 2=hideForOtherTeams, 3=hideForOwnTeam + teamNameTagVisibility = dataTypes.ReadNextVarInt(packetData) switch + { + 0 => "always", + 1 => "never", + 2 => "hideForOtherTeams", + 3 => "hideForOwnTeam", + _ => "always" + }; + } + else + { + teamNameTagVisibility = dataTypes.ReadNextString(packetData); + } + + // collisionRule + if (protocolVersion >= MC_1_21_9_Version) + { + // STREAM_CODEC: 0=always, 1=never, 2=pushOtherTeams, 3=pushOwnTeam + teamCollisionRule = dataTypes.ReadNextVarInt(packetData) switch + { + 0 => "always", + 1 => "never", + 2 => "pushOtherTeams", + 3 => "pushOwnTeam", + _ => "always" + }; + } + else + { + teamCollisionRule = dataTypes.ReadNextString(packetData); + } + + teamColor = dataTypes.ReadNextVarInt(packetData); + teamPrefix = dataTypes.ReadNextChat(packetData); + teamSuffix = dataTypes.ReadNextChat(packetData); + } + + var teamPlayers = new List(); + if (teamMethod is 0 or 3 or 4) + { + int playerCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < playerCount; i++) + teamPlayers.Add(dataTypes.ReadNextString(packetData)); + } + + handler.OnTeam(teamName, teamMethod, teamDisplayName, teamFriendlyFlags, + teamNameTagVisibility, teamCollisionRule, teamColor, + teamPrefix, teamSuffix, teamPlayers); + break; case PacketTypesIn.BlockChangedAck: handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(packetData)); break; diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 13f5a628..bfaebe1d 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -489,6 +489,23 @@ namespace MinecraftClient.Protocol /// Number format: 0 - blank, 1 - styled, 2 - fixed void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat); + /// + /// Called when a Teams packet is received from the server. + /// + /// Internal team name (up to 16 chars) + /// 0=create, 1=remove, 2=update, 3=add players, 4=remove players + /// Display name (formatted). Present when method is 0 or 2. + /// Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2. + /// Nametag visibility rule string. Present when method is 0 or 2. + /// Collision rule string. Present when method is 0 or 2. + /// ChatFormatting color value (-1=none). Present when method is 0 or 2. + /// Member name prefix (formatted). Present when method is 0 or 2. + /// Member name suffix (formatted). Present when method is 0 or 2. + /// Player/entity names. Present when method is 0, 3, or 4. + void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players); + /// /// Called when the client received the Tab Header and Footer /// diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index de6784ae..96e143d3 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -4660,6 +4660,51 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to List all scoreboard teams and their members. + /// + internal static string cmd_teams_desc { + get { + return ResourceManager.GetString("cmd.teams.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No teams are currently tracked. + /// + internal static string cmd_teams_no_teams { + get { + return ResourceManager.GetString("cmd.teams.no_teams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Team '{0}' (display: {1}, ...). + /// + internal static string cmd_teams_team_header { + get { + return ResourceManager.GetString("cmd.teams.team_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Members ({0}): {1}. + /// + internal static string cmd_teams_team_members { + get { + return ResourceManager.GetString("cmd.teams.team_members", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No members. + /// + internal static string cmd_teams_team_no_members { + get { + return ResourceManager.GetString("cmd.teams.team_no_members", resourceCulture); + } + } + /// /// Looks up a localized string similar to Place a block or open chest. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index fddf6400..f25a6165 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -1562,6 +1562,21 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Display server current tps (tick per second). May not be accurate + + List all scoreboard teams and their members. + + + No teams are currently tracked. + + + Team '{0}' (display: {1}, color: {2}, prefix: '{3}', suffix: '{4}', nameTagVisibility: {5}, collisionRule: {6}, friendlyFire: {7}, seeInvisibles: {8}) + + + Members ({0}): {1} + + + No members. + Place a block or open chest diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index c5950334..3dc27c40 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -384,6 +384,23 @@ namespace MinecraftClient.Scripting /// Number format: 0 - blank, 1 - styled, 2 - fixed public virtual void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int value, int numberFormat) { } + /// + /// Called when a Teams packet is received from the server. + /// + /// Internal team name (up to 16 chars) + /// 0=create, 1=remove, 2=update, 3=add players, 4=remove players + /// Display name (formatted). Present when method is 0 or 2. + /// Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2. + /// Nametag visibility rule. Present when method is 0 or 2. + /// Collision rule. Present when method is 0 or 2. + /// ChatFormatting color value (-1=none). Present when method is 0 or 2. + /// Member name prefix (formatted). Present when method is 0 or 2. + /// Member name suffix (formatted). Present when method is 0 or 2. + /// Player/entity names. Present when method is 0, 3, or 4. + public virtual void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players) { } + /// /// Called when the client received the Tab Header and Footer /// From 4c54d2bbb71bafd88442874b0a066f43d58a5997 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:02:38 +0000 Subject: [PATCH 70/76] docs: add /teams command entry and scoreboard teams bot API section Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/5dff6cc2-149d-431b-b0b1-2b29128a428e Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- docs/guide/creating-bots.md | 62 +++++++++++++++++++++++++++++++++++++ docs/guide/usage.md | 24 ++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/docs/guide/creating-bots.md b/docs/guide/creating-bots.md index ed35d549..076f4600 100644 --- a/docs/guide/creating-bots.md +++ b/docs/guide/creating-bots.md @@ -281,6 +281,68 @@ public class AchievementWatcher : ChatBot } ``` +## Scoreboard teams + +Chat bots and C# scripts can read the current team state and react to team changes. + +Useful methods and events: + +- `GetTeams()` - returns a snapshot of all teams the server has sent +- `GetPlayerTeam(playerName)` - returns the team a specific player is on, or `null` +- `OnTeam(teamName, method, displayName, friendlyFlags, nameTagVisibility, collisionRule, color, prefix, suffix, players)` - called whenever a team packet arrives + +The `method` byte tells you what changed: + +- `0` - team created (includes full parameters and initial member list) +- `1` - team removed +- `2` - team parameters updated (display name, colors, rules) +- `3` - players added to the team +- `4` - players removed from the team + +The `color` field is a `ChatFormatting` enum ordinal. Common values: `0`=black, `9`=blue, `10`=green, `12`=red, `14`=yellow, `-1`=none/reset. + +The `nameTagVisibility` and `collisionRule` strings take values from the Minecraft wiki: `"always"`, `"never"`, `"hideForOtherTeams"`, `"hideForOwnTeam"` (visibility) or `"pushOtherTeams"`, `"pushOwnTeam"` (collision). + +Example: + +```csharp +//MCCScript 1.0 + +MCC.LoadBot(new TeamWatcher()); + +//MCCScript Extensions + +public class TeamWatcher : ChatBot +{ + public override void AfterGameJoined() + { + foreach (var team in GetTeams().Values) + LogToConsole($"Team '{team.Name}' has {team.Members.Count} member(s)"); + } + + public override void OnTeam(string teamName, byte method, string displayName, + byte friendlyFlags, string nameTagVisibility, string collisionRule, + int color, string prefix, string suffix, List players) + { + switch (method) + { + case 0: + LogToConsole($"Team '{teamName}' created with {players.Count} member(s)"); + break; + case 1: + LogToConsole($"Team '{teamName}' removed"); + break; + case 3: + LogToConsole($"{string.Join(", ", players)} joined team '{teamName}'"); + break; + case 4: + LogToConsole($"{string.Join(", ", players)} left team '{teamName}'"); + break; + } + } +} +``` + ## C# API The authoritative reference for the C# API is [ChatBot.cs](https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Scripting/ChatBot.cs). diff --git a/docs/guide/usage.md b/docs/guide/usage.md index e0e4a0ea..24478acb 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -953,6 +953,30 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+teams + +- **Description:** + + List all scoreboard teams the server has sent, along with their members and settings. + +- **Usage:** + + ``` + /teams + ``` + +- **Example output:** + + ``` + Team 'RedTeam' (display: RedTeam, color: 12, prefix: '', suffix: '', nameTagVisibility: always, collisionRule: always, friendlyFire: True, seeInvisibles: True) + Members (2): Steve, Alex + Team 'BlueTeam' (display: BlueTeam, color: 9, prefix: '', suffix: '', nameTagVisibility: always, collisionRule: always, friendlyFire: True, seeInvisibles: True) + No members. + ``` + +
+
useitem From 65dec596872fe32c836d417fcf1e1ef14ee2cef0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:14:25 +0000 Subject: [PATCH 71/76] Initial plan From 1a86655dfbab6ebe90cea41fd8abb9b12bbbc0d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:44:23 +0000 Subject: [PATCH 72/76] feat: add autodig tool switching Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/e83f9c2a-a85c-4763-821b-c5b4a0db75a6 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/ChatBots/AutoDig.cs | 112 +++++++++++++++++- .../Translations/Translations.Designer.cs | 18 +++ .../Resources/Translations/Translations.resx | 6 + docs/guide/chat-bots.md | 40 +++++++ 4 files changed, 173 insertions(+), 3 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoDig.cs b/MinecraftClient/ChatBots/AutoDig.cs index 67d0b199..4af2d90d 100644 --- a/MinecraftClient/ChatBots/AutoDig.cs +++ b/MinecraftClient/ChatBots/AutoDig.cs @@ -5,7 +5,9 @@ using System.Threading; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.Inventory; using MinecraftClient.Mapping; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; using MinecraftClient.Scripting; using Tomlet.Attributes; @@ -25,15 +27,12 @@ namespace MinecraftClient.ChatBots public bool Enabled = false; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Auto_Tool_Switch$")] public bool Auto_Tool_Switch = false; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Durability_Limit$")] public int Durability_Limit = 2; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Drop_Low_Durability_Tools$")] public bool Drop_Low_Durability_Tools = false; @@ -65,6 +64,8 @@ namespace MinecraftClient.ChatBots public void OnSettingUpdate() { + Durability_Limit = Math.Max(0, Durability_Limit); + if (Auto_Start_Delay >= 0) Auto_Start_Delay = Math.Max(0.1, Auto_Start_Delay); @@ -225,6 +226,102 @@ namespace MinecraftClient.ChatBots } } + private static int GetLegacyMaxDamage(ItemType itemType) + { + return itemType switch + { + ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or ItemType.WoodenSword or ItemType.WoodenHoe => 59, + ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or ItemType.StoneSword or ItemType.StoneHoe => 131, + ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or ItemType.IronSword or ItemType.IronHoe => 250, + ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or ItemType.GoldenSword or ItemType.GoldenHoe => 32, + ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or ItemType.DiamondSword or ItemType.DiamondHoe => 1561, + ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or ItemType.NetheriteSword or ItemType.NetheriteHoe => 2031, + ItemType.Shears => 238, + _ => 0 + }; + } + + private static int GetMaxDamage(Item item) + { + if (item.Components is not null) + { + var maxDamageComponent = item.Components.OfType().FirstOrDefault(); + if (maxDamageComponent is not null) + return maxDamageComponent.MaxDamage; + } + + return GetLegacyMaxDamage(item.Type); + } + + private static int GetRemainingDurability(Item item) + { + int maxDamage = GetMaxDamage(item); + return maxDamage > 0 ? maxDamage - item.Damage : int.MaxValue; + } + + private bool HasEnoughDurability(Item item) + { + return Config.Durability_Limit <= 0 || GetRemainingDurability(item) >= Config.Durability_Limit; + } + + private bool IsBelowDurabilityLimit(Item? item) + { + return item is not null && Config.Durability_Limit > 0 && GetRemainingDurability(item) < Config.Durability_Limit; + } + + private static bool IsRecommendedTool(Item? item, ItemType[] recommendedTools) + { + return item is not null && recommendedTools.Contains(item.Type); + } + + private bool SwapToolIntoHand(int sourceSlot, int handSlot) + { + return WindowAction(0, sourceSlot, WindowActionType.LeftClick) + && WindowAction(0, handSlot, WindowActionType.LeftClick) + && WindowAction(0, sourceSlot, WindowActionType.LeftClick); + } + + private bool EnsureSuitableTool(Material blockType) + { + if (!inventoryEnabled || !Config.Auto_Tool_Switch) + return true; + + ItemType[] recommendedTools = Material2Tool.GetCorrectToolForBlock(blockType); + if (recommendedTools.Length == 0) + return true; + + Container container = GetPlayerInventory(); + int handSlot = 36 + GetCurrentSlot(); + container.Items.TryGetValue(handSlot, out Item? currentTool); + + if (IsRecommendedTool(currentTool, recommendedTools) && currentTool is not null && HasEnoughDurability(currentTool)) + return true; + + foreach (ItemType recommendedTool in recommendedTools) + { + foreach ((int slot, Item item) in container.Items) + { + if (slot == handSlot || item.Type != recommendedTool || !HasEnoughDurability(item)) + continue; + + if (!SwapToolIntoHand(slot, handSlot)) + return false; + + LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_switch, slot, item.GetTypeString())); + + if (Config.Drop_Low_Durability_Tools && IsBelowDurabilityLimit(currentTool) && + WindowAction(0, slot, WindowActionType.DropItemStack)) + { + LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_drop_low_durability, currentTool!.GetTypeString(), slot)); + } + + return true; + } + } + + return !IsBelowDurabilityLimit(currentTool); + } + public override void Update() { lock (stateLock) @@ -293,6 +390,9 @@ namespace MinecraftClient.ChatBots if (Config.Mode == Configs.ModeType.lookat || (Config.Mode == Configs.ModeType.both && Config._Locations.Contains(blockLoc))) { + if (!EnsureSuitableTool(block.Type)) + return false; + if (DigBlock(blockLoc, Direction.Down, lookAtBlock: false)) { currentDig = blockLoc; @@ -354,6 +454,9 @@ namespace MinecraftClient.ChatBots if (minDistance <= 6.0) { + if (!EnsureSuitableTool(targetBlock.Type)) + return false; + if (DigBlock(target, Direction.Down, lookAtBlock: true)) { currentDig = target; @@ -388,6 +491,9 @@ namespace MinecraftClient.ChatBots ((Config.List_Type == Configs.ListType.whitelist && Config.Blocks.Contains(block.Type)) || (Config.List_Type == Configs.ListType.blacklist && !Config.Blocks.Contains(block.Type)))) { + if (!EnsureSuitableTool(block.Type)) + return false; + if (DigBlock(blockLoc, Direction.Down, lookAtBlock: true)) { currentDig = blockLoc; diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 96e143d3..8259988c 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -437,6 +437,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Dropped low durability {0} from slot {1}.. + /// + internal static string bot_autodig_drop_low_durability { + get { + return ResourceManager.GetString("bot.autodig.drop_low_durability", resourceCulture); + } + } + /// /// Looks up a localized string similar to The block currently pointed to is not in the allowed list.. /// @@ -473,6 +482,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Switch to {1} from slot {0}.. + /// + internal static string bot_autodig_switch { + get { + return ResourceManager.GetString("bot.autodig.switch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Added item {0}. /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index f25a6165..31e29fc8 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -243,6 +243,9 @@ Inventory handling is not enabled. Unable to switch tools automatically. + + Dropped low durability {0} from slot {1}. + Automatic digging has started. @@ -252,6 +255,9 @@ Auto-digging has been stopped. + + Switch to {1} from slot {0}. + Added item {0} diff --git a/docs/guide/chat-bots.md b/docs/guide/chat-bots.md index e8b6c711..3c725a7a 100644 --- a/docs/guide/chat-bots.md +++ b/docs/guide/chat-bots.md @@ -748,6 +748,46 @@ redirectFrom: - **Default:** `3.0` + #### `Auto_Tool_Switch` + + - **Description:** + + Automatically switch to a more suitable tool from your inventory before digging. + + When `Durability_Limit` is above zero, tools below that durability threshold are skipped. + + - **Available values:** `true` and `false` + + - **Type:** `boolean` + + - **Default:** `false` + + #### `Durability_Limit` + + - **Description:** + + Will not use tools with less durability than this. + + Set to `0` to disable this durability check. + + - **Type:** `integer` + + - **Default:** `2` + + #### `Drop_Low_Durability_Tools` + + - **Description:** + + Drop the replaced tool if its remaining durability is below `Durability_Limit`. + + This setting is only useful when `Auto_Tool_Switch` is enabled. + + - **Available values:** `true` and `false` + + - **Type:** `boolean` + + - **Default:** `false` + #### `Dig_Timeout` - **Description:** From 737a94475ad1613f73d031ac777f2dcaa8911b6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:50:43 +0000 Subject: [PATCH 73/76] fix: clarify autodig switch log formatting Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/e83f9c2a-a85c-4763-821b-c5b4a0db75a6 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com> --- MinecraftClient/ChatBots/AutoDig.cs | 4 ++-- .../Resources/Translations/Translations.Designer.cs | 2 +- MinecraftClient/Resources/Translations/Translations.resx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoDig.cs b/MinecraftClient/ChatBots/AutoDig.cs index 4af2d90d..148335d5 100644 --- a/MinecraftClient/ChatBots/AutoDig.cs +++ b/MinecraftClient/ChatBots/AutoDig.cs @@ -294,7 +294,7 @@ namespace MinecraftClient.ChatBots int handSlot = 36 + GetCurrentSlot(); container.Items.TryGetValue(handSlot, out Item? currentTool); - if (IsRecommendedTool(currentTool, recommendedTools) && currentTool is not null && HasEnoughDurability(currentTool)) + if (currentTool is not null && IsRecommendedTool(currentTool, recommendedTools) && HasEnoughDurability(currentTool)) return true; foreach (ItemType recommendedTool in recommendedTools) @@ -307,7 +307,7 @@ namespace MinecraftClient.ChatBots if (!SwapToolIntoHand(slot, handSlot)) return false; - LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_switch, slot, item.GetTypeString())); + LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_switch, item.GetTypeString(), slot)); if (Config.Drop_Low_Durability_Tools && IsBelowDurabilityLimit(currentTool) && WindowAction(0, slot, WindowActionType.DropItemStack)) diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 8259988c..02447274 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -483,7 +483,7 @@ namespace MinecraftClient { } /// - /// Looks up a localized string similar to Switch to {1} from slot {0}.. + /// Looks up a localized string similar to Switch to {0} from slot {1}.. /// internal static string bot_autodig_switch { get { diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 31e29fc8..c9c53ae6 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -256,7 +256,7 @@ Auto-digging has been stopped. - Switch to {1} from slot {0}. + Switch to {0} from slot {1}. Added item {0} From 2441c671781422a243f46007965b00ec69fcf222 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 3 Apr 2026 01:10:06 +0800 Subject: [PATCH 74/76] fix: work around Consolonia libcoreclr.so DllNotFoundException on Linux single-file publish Consolonia's Unix.Terminal uses [DllImport("libcoreclr.so")] to load dlopen/dlsym on .NET Core. It ships a SetDllImportResolver that maps the library name to the current process handle, but the resolver is guarded by #if NET6_0 (exact TFM match) instead of NET6_0_OR_GREATER. Since Consolonia targets net8.0, the resolver is never compiled in. On self-contained single-file publishes, libcoreclr.so is bundled inside the host binary and does not exist on disk. Without the resolver the OS linker cannot find it, causing a DllNotFoundException that crashes the TUI on startup. This primarily affects ARM64 Linux users (e.g. Raspberry Pi / Debian Trixie) who almost exclusively use self-contained publishes. This commit registers an AssemblyLoadContext.Default.ResolvingUnmanagedDll handler in Program.Main (before TUI init) that returns (IntPtr)(-1) for libcoreclr.so, which the runtime interprets as the current process. This is a temporary workaround until the upstream fix lands: https://github.com/Consolonia/Consolonia/pull/605 Made-with: Cursor --- MinecraftClient/Program.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 9e71ad8e..6f505644 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -174,6 +176,9 @@ namespace MinecraftClient }; // --- Determine console mode and initialize backend --- + if (!OperatingSystem.IsWindows()) + InstallCursesNativeResolver(); + if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) { ConsoleIO.Backend?.Shutdown(); @@ -206,6 +211,26 @@ namespace MinecraftClient RunStartupSequence(args); } + /// + /// Consolonia's Unix.Terminal uses [DllImport("libcoreclr.so")] to reach + /// dlopen/dlsym on .NET Core. The library ships a + /// SetDllImportResolver that maps libcoreclr.so to the current + /// process, but it is compiled under #if NET6_0 (exact TFM match) instead + /// of NET6_0_OR_GREATER, so it is dead code when the consuming project + /// targets net8.0+. On a self-contained single-file publish the physical + /// libcoreclr.so does not exist on the search path, causing a + /// DllNotFoundException that crashes the TUI. + /// + /// We work around this by registering our own resolver before any Consolonia + /// code runs: if any assembly asks for libcoreclr.so we return + /// (IntPtr)(-1) which the runtime interprets as "the current process". + /// + private static void InstallCursesNativeResolver() + { + AssemblyLoadContext.Default.ResolvingUnmanagedDll += (assembly, libraryName) => + libraryName == "libcoreclr.so" ? (IntPtr)(-1) : IntPtr.Zero; + } + private static void HandleTuiStartupFailure(Exception exception) { Config.Console.General.ConsoleMode = ConsoleModeType.classic; From 2003786608d08712dfb84df8e214ac42b19b6bdf Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 3 Apr 2026 01:26:59 +0800 Subject: [PATCH 75/76] fix: resolve AutoRelog reconnect errors (#3036) - Reset _BotRecoAttempts on successful game join so retry counter does not carry stale state across sessions - Display "unlimited" instead of near-int.MaxValue retry count when Retries is set to -1 (infinite) - Guard SendText with CanSendMessage check to prevent NullReferenceException when bots call send after disconnect - Catch SocketException/IOException in Protocol18 Updater thread so a closed socket triggers OnConnectionLost gracefully instead of an unhandled exception Made-with: Cursor --- MinecraftClient/ChatBots/AutoRelog.cs | 22 +++++++++++---- MinecraftClient/McClient.cs | 6 +++++ .../Protocol/Handlers/Protocol18.cs | 6 +++++ .../Translations/Translations.Designer.cs | 27 +++++++++++++++++++ .../Resources/Translations/Translations.resx | 9 +++++++ 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoRelog.cs b/MinecraftClient/ChatBots/AutoRelog.cs index 316c5ab5..8a42a26c 100644 --- a/MinecraftClient/ChatBots/AutoRelog.cs +++ b/MinecraftClient/ChatBots/AutoRelog.cs @@ -1,4 +1,4 @@ -using System; +using System; using MinecraftClient.Scripting; using Tomlet.Attributes; @@ -95,6 +95,11 @@ namespace MinecraftClient.ChatBots _Initialize(); } + public override void AfterGameJoined() + { + Configs._BotRecoAttempts = 0; + } + private void _Initialize() { McClient.ReconnectionAttemptsLeft = Config.Retries; @@ -144,10 +149,17 @@ namespace MinecraftClient.ChatBots { double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min; LogDebugToConsole(string.Format(string.IsNullOrEmpty(msg) ? Translations.bot_autoRelog_reconnect_always : Translations.bot_autoRelog_reconnect, msg)); - - // TODO: Change this translation string to add the retries left text - LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay) + $" ({Config.Retries - Configs._BotRecoAttempts} retries left)"); - ReconnectToTheServer(Config.Retries - Configs._BotRecoAttempts, (int)Math.Floor(delay), true); + + int retriesLeft = Config.Retries - Configs._BotRecoAttempts; + if (retriesLeft < 0) + retriesLeft = 0; + + string retriesDisplay = Config.Retries == int.MaxValue + ? Translations.bot_autoRelog_retries_unlimited + : retriesLeft.ToString(); + + LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay)); + ReconnectToTheServer(retriesLeft, (int)Math.Floor(delay), true); } public static bool OnDisconnectStatic(DisconnectReason reason, string message) diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 394d01cd..ae5e427b 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -1579,6 +1579,12 @@ namespace MinecraftClient if (String.IsNullOrEmpty(text)) return; + if (!CanSendMessage) + { + Log.Warn(Translations.mcc_send_text_not_connected); + return; + } + int maxLength = handler.GetMaxChatMessageLength(); lock (chatQueue) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 0085725b..d883e65f 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -324,6 +324,12 @@ namespace MinecraftClient.Protocol.Handlers catch (NullReferenceException) { } + catch (SocketException) + { + } + catch (System.IO.IOException) + { + } if (cancelToken.IsCancellationRequested) return; diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index 02447274..4647c2f1 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -897,6 +897,24 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Waiting {0:0.000} seconds before reconnecting... ({1} retries left). + /// + internal static string bot_autoRelog_wait_with_retries { + get { + return ResourceManager.GetString("bot.autoRelog.wait_with_retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unlimited. + /// + internal static string bot_autoRelog_retries_unlimited { + get { + return ResourceManager.GetString("bot.autoRelog.retries_unlimited", resourceCulture); + } + } + /// /// Looks up a localized string similar to File not found: '{0}'. /// @@ -6158,6 +6176,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Cannot send text: not connected to a server.. + /// + internal static string mcc_send_text_not_connected { + get { + return ResourceManager.GetString("mcc.send_text_not_connected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Waiting {0} seconds before restarting.... /// diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index c9c53ae6..f4b92069 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -394,6 +394,12 @@ Waiting {0:0.000} seconds before reconnecting... + + Waiting {0:0.000} seconds before reconnecting... ({1} retries left) + + + unlimited + File not found: '{0}' @@ -2058,6 +2064,9 @@ Type '{0}quit' to leave the server. Restarting Minecraft Console Client... + + Cannot send text: not connected to a server. + Waiting {0} seconds before restarting... From 512445cb1d6489cd7297ef8f43ca6a0cd407e071 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 3 Apr 2026 03:01:15 +0800 Subject: [PATCH 76/76] fix: prevent TUI StackOverflowException from excessive log controls The TUI log view used an ItemsControl with 5000 max entries and no UI virtualization. Avalonia's composition renderer traverses the entire visual tree on each frame -- with thousands of TextBlock controls, the recursive Render/RenderCore calls exceed the thread stack size on constrained devices (especially ARM where each stack frame is larger due to ABI differences), causing a StackOverflowException in ServerCompositionContainerVisual.Render. Changes: - Enable VirtualizingStackPanel on the log ItemsControl so Avalonia only creates visuals for the rows currently in the viewport. - Add [Console.General] TUI_Log_Scrollback config option so users can control max log lines in TUI mode. Default is 0 (automatic: 3000 on x86/x64, 500 on ARM/ARM64). Made-with: Cursor --- .../ConfigComments/ConfigComments.Designer.cs | 18 ++++++++++++++++++ .../ConfigComments/ConfigComments.resx | 6 ++++++ MinecraftClient/Settings.cs | 3 +++ MinecraftClient/Tui/MainTuiView.cs | 15 ++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index d94cab66..36687b26 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1429,6 +1429,24 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Maximum number of input history records to keep.. + /// + internal static string Console_General_History_Input_Records { + get { + return ResourceManager.GetString("Console.General.History_Input_Records", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic (3000 on x86/x64, 500 on ARM).. + /// + internal static string Console_General_TUI_Log_Scrollback { + get { + return ResourceManager.GetString("Console.General.TUI_Log_Scrollback", resourceCulture); + } + } + /// /// Looks up a localized string similar to Startup Config File ///Please do not record extraneous data in this file as it will be overwritten by MCC. diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index 825c7e76..b1f7e220 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -587,6 +587,12 @@ Custom colors are only available when using "vt100_24bit" color mode. You can use "Ctrl+P" to print out the current input and cursor position. + + Maximum number of input history records to keep. + + + Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic. + Startup Config File Please do not record extraneous data in this file as it will be overwritten by MCC. diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index ededd636..ce1524fc 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1218,6 +1218,9 @@ namespace MinecraftClient [TomlInlineComment("$Console.General.History_Input_Records$")] public int History_Input_Records = 32; + + [TomlInlineComment("$Console.General.TUI_Log_Scrollback$")] + public int TUI_Log_Scrollback = 0; } [TomlDoNotInlineObject] diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index d8155d48..9affb94c 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -16,9 +17,20 @@ namespace MinecraftClient.Tui { public class MainTuiView : UserControl { - private const int MaxLogLines = 5000; + private static readonly int MaxLogLines = ResolveMaxLogLines(); private const int CtrlCDoublePressMsec = 1500; + private static int ResolveMaxLogLines() + { + int configured = Settings.Config.Console.General.TUI_Log_Scrollback; + if (configured > 0) + return configured; + + bool isArm = RuntimeInformation.ProcessArchitecture + is Architecture.Arm or Architecture.Arm64; + return isArm ? 500 : 3000; + } + private readonly ObservableCollection _logLines = new(); private readonly ObservableCollection _logControls = new(); private readonly ItemsControl _logItemsControl; @@ -78,6 +90,7 @@ namespace MinecraftClient.Tui { ItemsSource = _logControls, Focusable = false, + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()), }; _logScrollViewer = new ScrollViewer