feat: Implemented the Recipe Book into MCC

This commit is contained in:
Anon 2026-03-29 21:05:38 +02:00 committed by GitHub
commit ef106c80c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 714 additions and 0 deletions

View file

@ -0,0 +1,98 @@
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 <list|craft|craftall> [recipe id]";
public override string CmdDesc => Translations.cmd_recipebook_desc;
public override void RegisterCommand(CommandDispatcher<CmdResult> 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);
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 (RecipeBookRecipeEntry recipe in recipes)
response.AppendLine("- " + recipe.DisplayText);
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 (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 = 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)
? r.SetAndReturn(CmdResult.Status.Done, successMessage)
: r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, normalizedRecipeId));
}
}
}

View file

@ -44,10 +44,12 @@ namespace MinecraftClient
private readonly Queue<Action> threadTasks = new();
private readonly Lock threadTasksLock = new();
private readonly Lock recipeBookLock = new();
private readonly List<ChatBot> bots = new();
private static readonly List<ChatBot> botsOnHold = new();
private static readonly Dictionary<int, Container> inventories = new();
private readonly Dictionary<string, RecipeBookRecipeEntry> unlockedRecipes = new(StringComparer.Ordinal);
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new();
private readonly List<string> 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;
}
/// <summary>
/// Get all unlocked recipe book recipe identifiers.
/// </summary>
/// <returns>Unlocked recipe identifiers sorted alphabetically</returns>
public RecipeBookRecipeEntry[] GetUnlockedRecipes()
{
lock (recipeBookLock)
{
return unlockedRecipes.Values.OrderBy(static recipe => recipe.CommandId, StringComparer.Ordinal).ToArray();
}
}
/// <summary>
/// Get all Entities
/// </summary>
@ -1384,6 +1399,22 @@ namespace MinecraftClient
return GetInventory(0)!;
}
/// <summary>
/// Get the currently active inventory if it supports recipe book crafting.
/// </summary>
/// <returns>Active recipe book inventory, or null if the active inventory does not support recipe book crafting</returns>
public Container? GetActiveRecipeBookInventory()
{
if (InvokeRequired)
return InvokeOnMainThread(() => GetActiveRecipeBookInventory());
if (inventories.Count == 0)
return null;
Container activeInventory = inventories.MaxBy(static pair => pair.Key).Value;
return SupportsRecipeBook(activeInventory.Type) ? activeInventory : null;
}
/// <summary>
/// Get a set of online player names
/// </summary>
@ -2476,6 +2507,7 @@ namespace MinecraftClient
inventories.Clear();
inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory");
ClearUnlockedRecipes();
return true;
}
@ -2677,6 +2709,31 @@ namespace MinecraftClient
return handler.SendRenameItem(itemName);
}
/// <summary>
/// Send a recipe book craft request for the currently active crafting inventory.
/// </summary>
/// <param name="recipeId">Recipe identifier to craft</param>
/// <param name="makeAll">True to craft as many items as possible</param>
/// <returns>True if the packet was sent</returns>
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;
string normalizedRecipeId = NormalizeRecipeArgument(recipeId, protocolversion);
if (normalizedRecipeId.Length == 0)
return false;
return handler.SendPlaceRecipe(activeInventory.ID, normalizedRecipeId, makeAll);
}
#endregion
#region Event handlers: An event occurs on the Server
@ -4054,6 +4111,34 @@ namespace MinecraftClient
Log.Debug("CanSendMessage = " + canSendMessage);
}
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace)
{
lock (recipeBookLock)
{
if (replace)
unlockedRecipes.Clear();
foreach (RecipeBookRecipeEntry recipe in recipes)
{
// Guard against malformed server packets that send empty display IDs.
if (!string.IsNullOrWhiteSpace(recipe.CommandId))
unlockedRecipes[recipe.CommandId] = recipe;
}
}
}
public void OnRecipeBookRemove(string[] recipeIds)
{
lock (recipeBookLock)
{
foreach (string recipeId in recipeIds)
{
if (!string.IsNullOrWhiteSpace(recipeId))
unlockedRecipes.Remove(recipeId);
}
}
}
/// <summary>
/// Send a click container button packet to the server.
/// Used for Enchanting table, Lectern, stone cutter and loom
@ -4067,6 +4152,51 @@ 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();
}
}
/// <summary>
/// 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.
/// </summary>
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)
return string.Empty;
return trimmedRecipeId.Contains(':', StringComparison.Ordinal)
? trimmedRecipeId
: "minecraft:" + trimmedRecipeId;
}
#endregion
}
}

View file

@ -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

View file

@ -3116,8 +3116,19 @@ namespace MinecraftClient.Protocol.Handlers
}
break;
case PacketTypesIn.UnlockRecipes:
if (protocolVersion >= MC_1_13_Version)
HandleUnlockRecipes(packetData);
break;
case PacketTypesIn.RecipeBookAdd:
if (protocolVersion >= MC_1_21_2_Version)
HandleRecipeBookAdd(packetData);
break;
case PacketTypesIn.RecipeBookRemove:
if (protocolVersion >= MC_1_21_2_Version)
handler.OnRecipeBookRemove(ReadRecipeBookDisplayIds(packetData));
break;
case PacketTypesIn.RecipeBookSettings:
break;
@ -3128,6 +3139,238 @@ namespace MinecraftClient.Protocol.Handlers
return true; //Packet processed
}
private void HandleUnlockRecipes(Queue<byte> packetData)
{
int action = dataTypes.ReadNextVarInt(packetData);
if (!SkipRecipeBookSettings(packetData))
return;
string[] recipeIds = ReadRecipeBookRecipeIds(packetData);
RecipeBookRecipeEntry[] recipeEntries = recipeIds.Select(static recipeId => new RecipeBookRecipeEntry(recipeId, recipeId)).ToArray();
switch (action)
{
case 0:
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:
case 3:
// Action 3 is the silent-add variant, so MCC tracks it like a regular add.
handler.OnRecipeBookAdd(recipeEntries, replace: false);
break;
case 2:
handler.OnRecipeBookRemove(recipeIds);
break;
}
}
private void HandleRecipeBookAdd(Queue<byte> packetData)
{
int entryCount = dataTypes.ReadNextVarInt(packetData);
RecipeBookRecipeEntry[] recipeEntries = new RecipeBookRecipeEntry[entryCount];
// 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++)
{
recipeEntries[i] = ReadRecipeBookDisplayEntry(packetData);
_ = dataTypes.ReadNextByte(packetData); // flags
}
bool replace = dataTypes.ReadNextBool(packetData);
handler.OnRecipeBookAdd(recipeEntries, replace);
}
private string[] ReadRecipeBookRecipeIds(Queue<byte> 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 string[] ReadRecipeBookDisplayIds(Queue<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> packetData)
{
_ = ReadSlotDisplayLabel(packetData); // input
string result = ReadSlotDisplayLabel(packetData);
_ = ReadSlotDisplayLabel(packetData); // crafting station
return result;
}
private string ReadSmithingRecipeDisplayResultLabel(Queue<byte> packetData)
{
_ = ReadSlotDisplayLabel(packetData); // template
_ = ReadSlotDisplayLabel(packetData); // base
_ = ReadSlotDisplayLabel(packetData); // addition
string result = ReadSlotDisplayLabel(packetData);
_ = ReadSlotDisplayLabel(packetData); // crafting station
return result;
}
private string ReadSlotDisplayLabel(Queue<byte> 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<byte> packetData)
{
string baseLabel = ReadSlotDisplayLabel(packetData);
_ = ReadSlotDisplayLabel(packetData); // material
_ = dataTypes.ReadNextVarInt(packetData); // trim pattern registry id
return baseLabel;
}
private string ReadWithRemainderSlotDisplayLabel(Queue<byte> packetData)
{
string inputLabel = ReadSlotDisplayLabel(packetData);
_ = ReadSlotDisplayLabel(packetData); // remainder
return inputLabel;
}
private string ReadCompositeSlotDisplayLabel(Queue<byte> 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<byte> packetData)
{
if (!dataTypes.ReadNextBool(packetData))
return;
int ingredientCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < ingredientCount; i++)
SkipItemHolderSet(packetData);
}
private void SkipItemHolderSet(Queue<byte> 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<byte> 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;
for (int i = 0; i < boolCount; i++)
_ = dataTypes.ReadNextBool(packetData);
return true;
}
/// <summary>
/// Start the updating thread. Should be called after login success.
/// </summary>
@ -5018,6 +5261,37 @@ namespace MinecraftClient.Protocol.Handlers
}
}
public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll)
{
try
{
List<byte> packet = new();
if (protocolVersion < MC_1_13_Version)
return false;
packet.AddRange(DataTypes.GetVarInt(windowId));
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;
}
catch (SocketException)
{
return false;
}
catch (System.IO.IOException)
{
return false;
}
catch (ObjectDisposedException)
{
return false;
}
}
public bool SendAnimation(int animation, int playerId)
{
try

View file

@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol
bool ClickContainerButton(int windowId, int buttonId);
/// <summary>
/// Send a place recipe packet to the server for the active recipe book container.
/// </summary>
/// <param name="windowId">Id of the window being clicked</param>
/// <param name="recipeId">Recipe identifier to craft</param>
/// <param name="makeAll">True to craft as many items as possible</param>
/// <returns>True if packet was successfully sent</returns>
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
/// <summary>
/// Plays animation
/// </summary>

View file

@ -517,6 +517,19 @@ namespace MinecraftClient.Protocol
public void SetCanSendMessage(bool canSendMessage);
/// <summary>
/// Called when recipe book recipes are added or replaced.
/// </summary>
/// <param name="recipes">Recipe entries to add</param>
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
/// <summary>
/// Called when recipe book recipes are removed.
/// </summary>
/// <param name="recipeIds">Recipe identifiers to remove</param>
public void OnRecipeBookRemove(string[] recipeIds);
/// <summary>
/// Send a click container button packet to the server.
/// Used for Enchanting table, Lectern, stone cutter and loom

View file

@ -0,0 +1,4 @@
namespace MinecraftClient
{
public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText);
}

View file

@ -4359,6 +4359,87 @@ namespace MinecraftClient {
return ResourceManager.GetString("cmd.nameitem.successful", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to send recipe book craft request for {0}..
/// </summary>
internal static string cmd_recipebook_craft_failed {
get {
return ResourceManager.GetString("cmd.recipebook.craft.failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Requested recipe {0}..
/// </summary>
internal static string cmd_recipebook_craft_sent {
get {
return ResourceManager.GetString("cmd.recipebook.craft.sent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Requested recipe {0} with craft-all..
/// </summary>
internal static string cmd_recipebook_craftall_sent {
get {
return ResourceManager.GetString("cmd.recipebook.craftall.sent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory..
/// </summary>
internal static string cmd_recipebook_desc {
get {
return ResourceManager.GetString("cmd.recipebook.desc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unlocked recipe book recipes.
/// </summary>
internal static string cmd_recipebook_list {
get {
return ResourceManager.GetString("cmd.recipebook.list", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory..
/// </summary>
internal static string cmd_recipebook_no_active_inventory {
get {
return ResourceManager.GetString("cmd.recipebook.no.active.inventory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No unlocked recipe book recipes are currently tracked..
/// </summary>
internal static string cmd_recipebook_no_recipes {
get {
return ResourceManager.GetString("cmd.recipebook.no.recipes", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The recipe identifier cannot be empty..
/// </summary>
internal static string cmd_recipebook_recipe_id_empty {
get {
return ResourceManager.GetString("cmd.recipebook.recipe.id.empty", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer..
/// </summary>
internal static string cmd_recipebook_unsupported {
get {
return ResourceManager.GetString("cmd.recipebook.unsupported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to restart and reconnect to the server..

View file

@ -2208,6 +2208,33 @@ Logging in...</value>
<data name="cmd.nameitem.desc" xml:space="preserve">
<value>Set an item name when an Anvil inventory is active and the item is in the first slot.</value>
</data>
<data name="cmd.recipebook.craft.failed" xml:space="preserve">
<value>Failed to send recipe book craft request for {0}.</value>
</data>
<data name="cmd.recipebook.craft.sent" xml:space="preserve">
<value>Requested recipe {0}.</value>
</data>
<data name="cmd.recipebook.craftall.sent" xml:space="preserve">
<value>Requested recipe {0} with craft-all.</value>
</data>
<data name="cmd.recipebook.desc" xml:space="preserve">
<value>List unlocked recipe book recipes and craft them through the active recipe book inventory.</value>
</data>
<data name="cmd.recipebook.list" xml:space="preserve">
<value>Unlocked recipe book recipes</value>
</data>
<data name="cmd.recipebook.no.active.inventory" xml:space="preserve">
<value>You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.</value>
</data>
<data name="cmd.recipebook.no.recipes" xml:space="preserve">
<value>No unlocked recipe book recipes are currently tracked.</value>
</data>
<data name="cmd.recipebook.recipe.id.empty" xml:space="preserve">
<value>The recipe identifier cannot be empty.</value>
</data>
<data name="cmd.recipebook.unsupported" xml:space="preserve">
<value>Recipe book crafting is only supported on Minecraft 1.13 and newer.</value>
</data>
<data name="bot.antiafk.may.not.move" xml:space="preserve">
<value>Bot movement lock is held by bot {0}, so the Anti AFK bot might not move!</value>
</data>

View file

@ -650,6 +650,79 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
</details>
<details>
<summary><code>recipebook</code></summary>
- **Description:**
List unlocked recipe book entries and ask the server to place one of them into the active crafting inventory.
<div class="custom-container note"><p class="custom-container-title">Note</p>
**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this command to work.**
</div>
<div class="custom-container note"><p class="custom-container-title">Note</p>
**`craft` and `craftall` need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.**
</div>
<div class="custom-container warning"><p class="custom-container-title">Warning</p>
**Recipe book crafting is supported on Minecraft `1.13+`.**
</div>
`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 <recipe id>
```
```
/recipebook craftall <recipe id>
```
- **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
```
</details>
<details>
<summary><code>connect</code></summary>