mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
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>
This commit is contained in:
parent
3a28634592
commit
d5308ba8c6
8 changed files with 407 additions and 0 deletions
92
MinecraftClient/Commands/RecipeBook.cs
Normal file
92
MinecraftClient/Commands/RecipeBook.cs
Normal file
|
|
@ -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 <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);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 HashSet<string> 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 string[] GetUnlockedRecipes()
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
return [.. unlockedRecipes.OrderBy(static recipeId => recipeId, StringComparer.Ordinal)];
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.Values.Last();
|
||||
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,27 @@ 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<byte> 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<byte> 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<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 void SkipRecipeBookSettings(Queue<byte> packetData)
|
||||
{
|
||||
int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4;
|
||||
for (int i = 0; i < boolCount; i++)
|
||||
_ = dataTypes.ReadNextBool(packetData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the updating thread. Should be called after login success.
|
||||
/// </summary>
|
||||
|
|
@ -5018,6 +5084,34 @@ 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));
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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="recipeIds">Recipe identifiers to add</param>
|
||||
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
|
||||
public void OnRecipeBookAdd(string[] recipeIds, 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
|
||||
|
|
|
|||
|
|
@ -4359,6 +4359,69 @@ 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} (craft all: {1})..
|
||||
/// </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 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 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..
|
||||
|
|
|
|||
|
|
@ -2208,6 +2208,27 @@ 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} (craft all: {1}).</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.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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue