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

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