From 967f67190cc9f05c5d4d47d297252ef7cceb8522 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Thu, 19 Mar 2026 00:45:51 +0800 Subject: [PATCH] Add FileInputBot for non-interactive debugging and fix NbtToString crash FileInputBot (ChatBots/FileInputBot.cs): - New ChatBot that monitors a text file (default: mcc_input.txt) for commands, enabling MCC control from Cursor Shell or any non-interactive environment where stdin is not available - Activated by setting MCC_FILE_INPUT env var (e.g. MCC_FILE_INPUT=1) - Polls every ~500ms for new lines appended to the file - Lines starting with "/" are sent as server chat/commands - Other lines are executed as MCC internal commands (same as console input) - File path overridable via MCC_INPUT_FILE env var McClient.cs: - Load FileInputBot when MCC_FILE_INPUT environment variable is set ChatParser.cs - NbtToString: - Fix InvalidCastException when NBT "text" or nameless root tag values are Int32 instead of String (happens with 1.20.6 SystemChat packets containing numeric values in the chat component tree) - Replace direct (string) casts with ?.ToString() ?? string.Empty Made-with: Cursor --- MinecraftClient/ChatBots/FileInputBot.cs | 105 ++++++++++++++++++ MinecraftClient/McClient.cs | 4 +- .../Protocol/Message/ChatParser.cs | 7 +- 3 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 MinecraftClient/ChatBots/FileInputBot.cs diff --git a/MinecraftClient/ChatBots/FileInputBot.cs b/MinecraftClient/ChatBots/FileInputBot.cs new file mode 100644 index 00000000..16c65cd8 --- /dev/null +++ b/MinecraftClient/ChatBots/FileInputBot.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Threading; +using MinecraftClient.CommandHandler; +using MinecraftClient.Scripting; + +namespace MinecraftClient.ChatBots +{ + /// + /// Debug-only ChatBot that monitors a text file for commands. + /// Write lines to the file from any external tool (e.g. Cursor Shell) + /// and this bot will execute them as MCC internal commands. + /// + /// Usage from Cursor Shell: + /// Add-Content mcc_input.txt "inventory" + /// Add-Content mcc_input.txt "send /give @s diamond_sword 1" + /// + /// Lines starting with "/" are sent as server chat; others are treated + /// as MCC internal commands (same as typing in the MCC console). + /// + public class FileInputBot : ChatBot + { + private const string BotName = "FileInput"; + private string _filePath = string.Empty; + private long _lastPosition; + private int _tickCounter; + + public override void Initialize() + { + _filePath = Path.GetFullPath( + Environment.GetEnvironmentVariable("MCC_INPUT_FILE") ?? "mcc_input.txt"); + + if (File.Exists(_filePath)) + _lastPosition = new FileInfo(_filePath).Length; + else + File.WriteAllText(_filePath, ""); + + LogToConsole(BotName, $"Watching: {_filePath}"); + LogToConsole(BotName, "Write commands to this file to execute them."); + } + + public override void Update() + { + // Poll every ~500ms (Update is called every ~100ms) + if (++_tickCounter < 5) + return; + _tickCounter = 0; + + try + { + if (!File.Exists(_filePath)) + return; + + var info = new FileInfo(_filePath); + if (info.Length <= _lastPosition) + return; + + string newContent; + using (var fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + fs.Seek(_lastPosition, SeekOrigin.Begin); + using var reader = new StreamReader(fs); + newContent = reader.ReadToEnd(); + } + _lastPosition = info.Length; + + foreach (var rawLine in newContent.Split('\n')) + { + var line = rawLine.Trim(); + if (string.IsNullOrEmpty(line)) + continue; + + LogToConsole(BotName, $"> {line}"); + + if (line.StartsWith("/")) + { + SendText(line); + } + else + { + CmdResult result = new(); + if (PerformInternalCommand(line, ref result)) + { + if (!string.IsNullOrEmpty(result.ToString())) + LogToConsole(BotName, result.ToString()); + } + else + { + // Not an internal command — send as chat + SendText(line); + } + } + } + } + catch (IOException) + { + // File may be temporarily locked by the writer + } + catch (Exception ex) + { + LogToConsole(BotName, $"Error: {ex.Message}"); + } + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index e9b0b38c..c632569b 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -425,8 +425,8 @@ namespace MinecraftClient if (Config.ChatBot.ScriptScheduler.Enabled) { BotLoad(new ScriptScheduler()); } if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); } if (Config.ChatBot.ItemsCollector.Enabled) { BotLoad(new ItemsCollector()); } - //Add your ChatBot here by uncommenting and adapting - //BotLoad(new ChatBots.YourBot()); + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MCC_FILE_INPUT"))) + BotLoad(new FileInputBot()); } /// diff --git a/MinecraftClient/Protocol/Message/ChatParser.cs b/MinecraftClient/Protocol/Message/ChatParser.cs index d3db4e38..f907eadd 100644 --- a/MinecraftClient/Protocol/Message/ChatParser.cs +++ b/MinecraftClient/Protocol/Message/ChatParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -510,8 +510,7 @@ namespace MinecraftClient.Protocol.Message { if (nbt.Count == 1 && nbt.TryGetValue("", out object? rootMessage)) { - // Nameless root tag - return (string)rootMessage; + return rootMessage?.ToString() ?? string.Empty; } string message = string.Empty; @@ -526,7 +525,7 @@ namespace MinecraftClient.Protocol.Message { case "text": { - message = (string)value; + message = value?.ToString() ?? string.Empty; } break; case "extra":