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 1/6] 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 2/6] 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 3/6] 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 4/6] 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 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 5/6] 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 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 6/6] 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