diff --git a/MinecraftClient/ChatBots/Farmer.cs b/MinecraftClient/ChatBots/Farmer.cs index 41bd4f48..89163ec8 100644 --- a/MinecraftClient/ChatBots/Farmer.cs +++ b/MinecraftClient/ChatBots/Farmer.cs @@ -42,6 +42,7 @@ namespace MinecraftClient.ChatBots public bool Enabled = false; + [TomlInlineComment("$config.ChatBot.Farmer.Delay_Between_Tasks$")] public int Delay_Between_Tasks = 1; public void OnSettingUpdate() @@ -55,12 +56,17 @@ namespace MinecraftClient.ChatBots private CropType cropType = CropType.Wheat; private int farmingRadius = 30; private bool running = false; + private bool allowUnsafe = false; + private bool allowTeleport = false; + private bool debugEnabled = false; + + private const string commandDescription = "farmer [radius:|unsafe:|teleport:|debug:]|stop>"; public override void Initialize() { if (GetProtocolVersion() < Protocol18Handler.MC_1_13_Version) { - LogToConsole("Not implemented bellow 1.13!"); + LogToConsole(Translations.TryGet("bot.farmer.not_implemented")); return; } @@ -76,7 +82,7 @@ namespace MinecraftClient.ChatBots return; } - RegisterChatBotCommand("farmer", "cmd.farmer.desc", "farmer |stop>", OnFarmCommand); + RegisterChatBotCommand("farmer", "bot.farmer.desc", commandDescription, OnFarmCommand); } private string OnFarmCommand(string cmd, string[] args) @@ -86,38 +92,120 @@ namespace MinecraftClient.ChatBots if (args[0].Equals("stop", StringComparison.OrdinalIgnoreCase)) { if (!running) - return Translations.TryGet("cmd.farmer.already_stopped"); + return Translations.TryGet("bot.farmer.already_stopped"); running = false; - return Translations.TryGet("cmd.farmer.stopping"); + return Translations.TryGet("bot.farmer.stopping"); } if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { - if (args.Length == 3) + if (args.Length >= 2) { if (running) - return Translations.TryGet("cmd.farmer.already_running"); + return Translations.TryGet("bot.farmer.already_running"); if (!Enum.TryParse(args[1], true, out CropType whatToFarm)) - return Translations.TryGet("cmd.farmer.invalid_crop_type"); + return Translations.TryGet("bot.farmer.invalid_crop_type"); - if (!int.TryParse(args[2], NumberStyles.Any, CultureInfo.CurrentCulture, out int radius)) - return Translations.TryGet("cmd.farmer.invalid_radius"); + int radius = 30; state = State.SearchingForFarmlandToPlant; cropType = whatToFarm; - farmingRadius = radius <= 0 ? 30 : radius; + allowUnsafe = false; + allowTeleport = false; + debugEnabled = false; + if (args.Length >= 3) + { + for (int i = 2; i < args.Length; i++) + { + string currentArg = args[i].Trim().ToLower(); + + if (!currentArg.Contains(':')) + { + LogToConsole("§x§1§0" + Translations.TryGet("bot.farmer.warining_invalid_parameter", currentArg)); + continue; + } + + string[] parts = currentArg.Split(":", StringSplitOptions.TrimEntries); + + if (parts.Length != 2) + { + LogToConsole("§x§1§0" + Translations.TryGet("bot.farmer.warining_invalid_parameter", currentArg)); + continue; + } + + switch (parts[0]) + { + case "r": + case "radius": + if (!int.TryParse(parts[1], NumberStyles.Any, CultureInfo.CurrentCulture, out radius)) + LogToConsole("§x§1§0" + Translations.TryGet("bot.farmer.invalid_radius")); + + if (radius <= 0) + { + LogToConsole("§x§1§0" + Translations.TryGet("bot.farmer.invalid_radius")); + radius = 30; + } + + break; + + case "f": + case "unsafe": + if (allowUnsafe) + break; + + if (parts[1].Equals("true") || parts[1].Equals("1")) + { + LogToConsole("§x§1§0" + Translations.TryGet("bot.farmer.warining_force_unsafe")); + allowUnsafe = true; + } + else allowUnsafe = false; + + break; + + case "t": + case "teleport": + if (allowTeleport) + break; + + if (parts[1].Equals("true") || parts[1].Equals("1")) + { + LogToConsole("§w§1§f" + Translations.TryGet("bot.farmer.warining_allow_teleport")); + allowTeleport = true; + } + else allowTeleport = false; + + break; + + case "d": + case "debug": + if (debugEnabled) + break; + + if (parts[1].Equals("true") || parts[1].Equals("1")) + { + LogToConsole("Debug enabled!"); + debugEnabled = true; + } + else debugEnabled = false; + + break; + } + } + } + + farmingRadius = radius; running = true; new Thread(() => MainPorcess()).Start(); - return Translations.TryGet("cmd.farmer.staring", args[1], farmingRadius); + return ""; } } } - return Translations.TryGet("cmd.farmer.desc") + ": " + Translations.TryGet("cmd.farmer.usage"); + return Translations.TryGet("bot.farmer.desc") + ": " + commandDescription; } public override void AfterGameJoined() @@ -133,14 +221,16 @@ namespace MinecraftClient.ChatBots private void MainPorcess() { - LogToConsole("Started"); + LogToConsole("§y§1§f" + Translations.TryGet("bot.farmer.started")); + LogToConsole("§y§1§f " + Translations.TryGet("bot.farmer.crop_type") + ": " + cropType); + LogToConsole("§y§1§f " + Translations.TryGet("bot.farmer.radius") + ": " + farmingRadius); while (running) { // Don't do anything if the bot is currently eating, we wait for 1 second if (AutoEat.Eating) { - LogToConsole("Eating."); + LogDebug("Eating..."); Thread.Sleep(Config.Delay_Between_Tasks * 1000); continue; } @@ -148,12 +238,14 @@ namespace MinecraftClient.ChatBots switch (state) { case State.SearchingForFarmlandToPlant: - LogToConsole("Looking for farmland"); + LogDebug("Looking for farmland..."); + + ItemType cropTypeToPlant = GetSeedItemTypeForCropType(cropType); // If we don't have any seeds on our hotbar, skip this step and try collecting some - if (!SwitchToItem(GetSeedItemTypeForCropType(cropType))) + if (!SwitchToItem(cropTypeToPlant)) { - LogToConsole("No seeds, trying to find some crops to break"); + LogDebug("No seeds, trying to find some crops to break"); state = State.SearchingForCropsToBreak; Thread.Sleep(Config.Delay_Between_Tasks * 1000); continue; @@ -163,7 +255,7 @@ namespace MinecraftClient.ChatBots if (farmlandToPlantOn.Count == 0) { - LogToConsole("Could not find any farmland, trying to find some crops to break"); + LogDebug("Could not find any farmland, trying to find some crops to break"); state = State.SearchingForCropsToBreak; Thread.Sleep(Config.Delay_Between_Tasks * 1000); continue; @@ -175,40 +267,39 @@ namespace MinecraftClient.ChatBots double yValue = Math.Floor(location.Y) + 1; - if (cropType == CropType.Netherwart) - yValue = (double)(Math.Floor(location.Y) - 1.0) + (double)0.87500; + // TODO: Figure out why this is not working. + // Why we need this: sometimes the server kicks the player for "invalid movement" packets. + /*if (cropType == CropType.Netherwart) + yValue = (double)(Math.Floor(location.Y) - 1.0) + (double)0.87500;*/ - Location location2 = new Location(Math.Floor(location.X), yValue, Math.Floor(location.Z)); + Location location2 = new Location(Math.Floor(location.X) + 0.5, yValue, Math.Floor(location.Z) + 0.5); if (WaitForMoveToLocation(location2)) { - LogToConsole("Moving to: " + location2); + LogDebug("Moving to: " + location2); // Stop if we do not have any more seeds left if (!SwitchToItem(GetSeedItemTypeForCropType(cropType))) { - LogToConsole("No seeds, trying to find some crops to break"); + LogDebug("No seeds, trying to find some crops to break"); break; } Location loc = new Location(Math.Floor(location.X), Math.Floor(location2.Y), Math.Floor(location.Z)); - LogToConsole("Sending placeblock to: " + loc); + LogDebug("Sending placeblock to: " + loc); SendPlaceBlock(loc, Direction.Up); Thread.Sleep(300); } - else - { - LogToConsole("Can't move to: " + location2); - } + else LogDebug("Can't move to: " + location2); } - LogToConsole("Finished planting"); + LogDebug("Finished planting crops!"); state = State.SearchingForCropsToBreak; break; case State.SearchingForCropsToBreak: - LogToConsole("Searching for crops to break"); + LogDebug("Searching for crops to break..."); List cropsToCollect = findCrops(farmingRadius, cropType, true); @@ -220,13 +311,31 @@ namespace MinecraftClient.ChatBots continue; } + // Switch to an axe for faster breaking if the bot has one in his inventory + if (cropType == CropType.Melon || cropType == CropType.Pumpkin) + { + // Start from Diamond axe, if not found, try a tier lower axe + bool switched = SwitchToItem(ItemType.DiamondAxe); + + if (!switched) + switched = SwitchToItem(ItemType.IronAxe); + + if (!switched) + switched = SwitchToItem(ItemType.GoldenAxe); + + if (!switched) + SwitchToItem(ItemType.StoneAxe); + } + foreach (Location location in cropsToCollect) { if (!running) break; - // God damn C# rounding Y to (Y - 1) + 0.94 + // God damn C# rounding it to 0.94 + // This will be needed when bot bonemeals carrots or potatoes which are at the first stage of growth, + // because sometimes the bot walks over crops and breaks them // TODO: Figure out a fix - // new Location(Math.Floor(location.X) + 0.5, (double)((double)(location.Y - 1) + (double)0.93750), Math.Floor(location.Z) + 0.5) + // new Location(Math.Floor(location.X) + 0.5, (double)((location.Y - 1) + (double)0.93750), Math.Floor(location.Z) + 0.5) if (WaitForMoveToLocation(location)) WaitForDigBlock(location); @@ -235,7 +344,7 @@ namespace MinecraftClient.ChatBots Thread.Sleep(cropType == CropType.Melon || cropType == CropType.Pumpkin ? 400 : 200); } - LogToConsole("Finished breaking crops"); + LogDebug("Finished breaking crops!"); state = State.BonemealingCrops; break; @@ -251,7 +360,7 @@ namespace MinecraftClient.ChatBots // If we don't have any bonemeal on our hotbar, skip this step if (!SwitchToItem(ItemType.BoneMeal)) { - LogToConsole("No bonemeal, searching for some farmland to plant seeds on"); + LogDebug("No bonemeal, searching for some farmland to plant seeds on"); state = State.SearchingForFarmlandToPlant; Thread.Sleep(Config.Delay_Between_Tasks * 1000); continue; @@ -261,7 +370,7 @@ namespace MinecraftClient.ChatBots if (cropsToBonemeal.Count == 0) { - LogToConsole("No crops to bonemeal, searching for farmland to plant seeds on"); + LogDebug("No crops to bonemeal, searching for farmland to plant seeds on"); state = State.SearchingForFarmlandToPlant; Thread.Sleep(Config.Delay_Between_Tasks * 1000); continue; @@ -269,39 +378,42 @@ namespace MinecraftClient.ChatBots foreach (Location location in cropsToBonemeal) { + if (!running) break; + if (WaitForMoveToLocation(location)) { - if (!running) break; - // Stop if we do not have any more bonemeal left if (!SwitchToItem(ItemType.BoneMeal)) { - LogToConsole("No bonemeal, searching for some farmland to plant seeds on"); + LogDebug("No bonemeal, searching for some farmland to plant seeds on..."); break; } - // Send like 5 bonemeal attempts, it should do the job with 2-3, but sometimes doesn't do + Location location2 = new Location(Math.Floor(location.X) + 0.5, location.Y, Math.Floor(location.Z) + 0.5); + LogDebug("Trying to bonemeal: " + location2); + + // Send like 4 bonemeal attempts, it should do the job with 2-3, but sometimes doesn't do for (int boneMealTimes = 0; boneMealTimes < 4; boneMealTimes++) { // TODO: Do a check if the carrot/potato is on the first growth stage // if so, use: new Location(location.X, (double)(location.Y - 1) + (double)0.93750, location.Z) - SendPlaceBlock(location, Direction.Down); + SendPlaceBlock(location2, Direction.Down); } Thread.Sleep(100); } } - LogToConsole("Finished bonemealing crops"); + LogDebug("Finished bonemealing crops!"); state = State.SearchingForFarmlandToPlant; break; } - LogToConsole("Waiting for " + Config.Delay_Between_Tasks + " seconds for next cycle."); + LogDebug("Waiting for " + Config.Delay_Between_Tasks + " seconds for next cycle."); Thread.Sleep(Config.Delay_Between_Tasks * 1000); } - LogToConsole("Stopped farming!"); + LogToConsole(Translations.TryGet("bot.farmer.stopped")); } private Material GetMaterialForCropType(CropType type) @@ -604,9 +716,8 @@ namespace MinecraftClient.ChatBots return false; } - // Yoinked from ReinforceZwei's AutoTree - // TODO: Rewrite to support the whole inventory and moving to the hotbar - public bool SwitchToItem(ItemType itemType) + // Yoinked from ReinforceZwei's AutoTree and adapted to search the whole of inventory in additon to the hotbar + private bool SwitchToItem(ItemType itemType) { Container playerInventory = GetPlayerInventory(); @@ -614,31 +725,41 @@ namespace MinecraftClient.ChatBots && playerInventory.Items[GetCurrentSlot() - 36].Type == itemType) return true; // Already selected - // Search for the seed in the hotbar - List result = new List(playerInventory.SearchItem(itemType)) - .Where(slot => slot >= 36 && slot <= 44) - .ToList(); + // Search the full inventory + List fullInventorySearch = new List(playerInventory.SearchItem(itemType)); - if (result.Count <= 0) + // Search for the seed in the hotbar + List hotbarSerch = fullInventorySearch.Where(slot => slot >= 36 && slot <= 44).ToList(); + + if (hotbarSerch.Count > 0) + { + ChangeSlot((short)(hotbarSerch[0] - 36)); + return true; + } + + if (fullInventorySearch.Count == 0) return false; - ChangeSlot((short)(result[0] - 36)); + ItemMovingHelper movingHelper = new ItemMovingHelper(playerInventory, Handler); + movingHelper.Swap(fullInventorySearch[0], 36); + ChangeSlot(0); + return true; } // Yoinked from Daenges's Sugarcane Farmer private bool WaitForMoveToLocation(Location pos, float tolerance = 2f) { - if (MoveToLocation(pos)) + if (MoveToLocation(location: pos, allowUnsafe: allowUnsafe, allowDirectTeleport: allowTeleport)) { - LogToConsole("Moving to: " + pos); + LogDebug("Moving to: " + pos); while (GetCurrentLocation().Distance(pos) > tolerance) Thread.Sleep(200); return true; } - else LogToConsole("Can't move to: " + pos); + else LogDebug("Can't move to: " + pos); return false; } @@ -660,5 +781,12 @@ namespace MinecraftClient.ChatBots return false; } + + private void LogDebug(object text) + { + if (debugEnabled) + LogToConsole(text); + else LogDebugToConsole(text); + } } } diff --git a/MinecraftClient/ChatBots/BotsCommand.cs b/MinecraftClient/Commands/Bots.cs similarity index 98% rename from MinecraftClient/ChatBots/BotsCommand.cs rename to MinecraftClient/Commands/Bots.cs index 72913de3..46864dd1 100644 --- a/MinecraftClient/ChatBots/BotsCommand.cs +++ b/MinecraftClient/Commands/Bots.cs @@ -4,7 +4,7 @@ using System.Text; namespace MinecraftClient.Commands { - class BotsCommand : Command + class Bots : Command { public override string CmdName { get { return "bots"; } } public override string CmdUsage { get { return "bots [list|unload ]"; } } diff --git a/MinecraftClient/ChatBots/Reload.cs b/MinecraftClient/Commands/Reload.cs similarity index 100% rename from MinecraftClient/ChatBots/Reload.cs rename to MinecraftClient/Commands/Reload.cs diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 8bb1e67d..f526e38f 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -612,6 +612,22 @@ bot.replayCapture.created=Replay file created. bot.replayCapture.stopped=Record stopped. bot.replayCapture.restart=Record was stopped. Restart the program to start another record. +# Farmer +bot.farmer.desc=Farming bot +bot.farmer.not_implemented=Not implemented bellow 1.13! +bot.farmer.already_stopped=The bot has already stopped farming! +bot.farmer.stopping=Stoping farming, this might take a second... +bot.farmer.stopped=Stopped farming! +bot.farmer.already_running=The bot is already farming! +bot.farmer.invalid_crop_type=Invalid crop type provided (Types which you can use: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat)! +bot.farmer.warining_invalid_parameter=Invalid parameter "{0}" provided (Use format: "key:value")! +bot.farmer.invalid_radius=Invalid radius provided, you must provide a valid integer number greater than 0! +bot.farmer.warining_force_unsafe=You have enabled un-safe movement, the bot might get hurt! +bot.farmer.warining_allow_teleport=You have enabled teleporting, this might get your bot account kicked and in the worst case scenario banned! Use with caution! +bot.farmer.started=Started farming! +bot.farmer.crop_type=Crop type +bot.farmer.radius=Radius + # Follow player cmd.follow.desc=Makes the bot follow a specified player cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) @@ -798,7 +814,7 @@ config.ChatBot.AutoCraft.CraftingTable=Location of the crafting table if you int config.ChatBot.AutoCraft.OnFailure=What to do on crafting failure, "abort" or "wait". config.ChatBot.AutoCraft.Recipes=Recipes.Name: The name can be whatever you like and it is used to represent the recipe.\n# Recipes.Type: crafting table type: "player" or "table"\n# Recipes.Result: the resulting item\n# Recipes.Slots: All slots, counting from left to right, top to bottom. Please fill in "Null" for empty slots.\n# For the naming of the items, please see:\n# https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Inventory/ItemType.cs -# AutoDig +# ChatBot.AutoDig config.ChatBot.AutoDig=Auto-digging blocks.\n# You can use "/digbot start" and "/digbot stop" to control the start and stop of AutoDig.\n# Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead.\n# For the naming of the block, please see https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Mapping/Material.cs config.ChatBot.AutoDig.Auto_Tool_Switch=Automatically switch to the appropriate tool. config.ChatBot.AutoDig.Durability_Limit=Will not use tools with less durability than this. Set to zero to disable this feature. @@ -842,12 +858,16 @@ config.ChatBot.AutoRelog.Ignore_Kick_Message=When set to true, autorelog will re config.ChatBot.AutoRelog.Kick_Messages=If the kickout message matches any of the strings, then autorelog will be triggered. # ChatBot.AutoRespond -config.ChatBot.AutoRespond=Run commands or send messages automatically when a specified pattern is detected in chat\n# /!\ Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules\n# /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam +config.ChatBot.AutoRespond=Run commands or send messages automatically when a specified pattern is detected in chat\n# Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules\n# /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam config.ChatBot.AutoRespond.Match_Colors=Do not remove colors from text (Note: Your matches will have to include color codes (ones using the § character) in order to work) # ChatBot.ChatLog config.ChatBot.ChatLog=Logs chat messages in a file on disk. +# ChatBot.Farmer +config.ChatBot.Farmer=Automatically farms crops for you (plants, breaks and bonemeals them).\n# Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.\n# Usage: "/farmer start" command and "/farmer stop" command.\n# NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes.\n# or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this.\n# It is recommended to keep the farming area walled off and flat to avoid the bot jumping.\n# Also, if you have your farmland that is one block high, make it 2 or more blocks high so the bot does not fall through, as it can happen sometimes when the bot reconnects.\n# The bot also does not pickup all items if they fly off to the side, we have a plan to implement this option in the future as well as drop off and bonemeal refill chest(s). +config.ChatBot.Farmer.Delay_Between_Tasks=Delay between tasks in seconds (Minimum 1 second) + # ChatBot.FollowPlayer config.ChatBot.FollowPlayer=Enabled you to make the bot follow you\n# NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you\n# It's similar to making animals follow you when you're holding food in your hand.\n# This is due to a slow pathfinding algorithm, we're working on getting a better one\n# You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite,\n# this might clog the thread for terain handling) and thus slow the bot even more.\n# /!\ Make sure server rules allow an option like this in the rules of the server before using this bot config.ChatBot.FollowPlayer.Update_Limit=The rate at which the bot does calculations (in seconds) (You can tweak this if you feel the bot is too slow) diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 7730d274..92c4a35f 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -43,7 +43,7 @@ namespace MinecraftClient private readonly List registeredCommands = new(); private readonly object delayTasksLock = new(); private readonly List delayedTasks = new(); - private McClient Handler + protected McClient Handler { get { diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index aada22b7..d3ebdbed 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1038,6 +1038,7 @@ namespace MinecraftClient set { ChatBots.ChatLog.Config = value; ChatBots.ChatLog.Config.OnSettingUpdate(); } } + [TomlPrecedingComment("$config.ChatBot.Farmer$")] public ChatBots.Farmer.Configs Farmer { get { return ChatBots.Farmer.Config; }