mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
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>
This commit is contained in:
parent
dfc1164839
commit
b05c8cfe0d
5 changed files with 209 additions and 30 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ namespace MinecraftClient
|
|||
private readonly List<ChatBot> bots = new();
|
||||
private static readonly List<ChatBot> botsOnHold = new();
|
||||
private static readonly Dictionary<int, Container> inventories = new();
|
||||
private readonly HashSet<string> unlockedRecipes = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, RecipeBookRecipeEntry> unlockedRecipes = new(StringComparer.Ordinal);
|
||||
|
||||
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new();
|
||||
private readonly List<string> registeredServerPluginChannels = new();
|
||||
|
|
@ -1345,11 +1345,11 @@ namespace MinecraftClient
|
|||
/// Get all unlocked recipe book recipe identifiers.
|
||||
/// </summary>
|
||||
/// <returns>Unlocked recipe identifiers sorted alphabetically</returns>
|
||||
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)
|
||||
/// <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)
|
||||
|
|
|
|||
|
|
@ -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<byte> 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<byte> packetData)
|
||||
|
|
@ -3198,6 +3195,168 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
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.
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -520,9 +520,9 @@ namespace MinecraftClient.Protocol
|
|||
/// <summary>
|
||||
/// Called when recipe book recipes are added or replaced.
|
||||
/// </summary>
|
||||
/// <param name="recipeIds">Recipe identifiers to add</param>
|
||||
/// <param name="recipes">Recipe entries to add</param>
|
||||
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
|
||||
public void OnRecipeBookAdd(string[] recipeIds, bool replace);
|
||||
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are removed.
|
||||
|
|
|
|||
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace MinecraftClient
|
||||
{
|
||||
public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue