diff --git a/MinecraftClient/ChatBots/Farmer.cs b/MinecraftClient/ChatBots/Farmer.cs new file mode 100644 index 00000000..0892b855 --- /dev/null +++ b/MinecraftClient/ChatBots/Farmer.cs @@ -0,0 +1,826 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using MinecraftClient.Inventory; +using MinecraftClient.Mapping; +using MinecraftClient.Protocol.Handlers; +using Tomlet.Attributes; + +namespace MinecraftClient.ChatBots +{ + enum State + { + SearchingForCropsToBreak = 0, + SearchingForFarmlandToPlant, + PlantingCrops, + BonemealingCrops + } + + enum CropType + { + Beetroot, + Carrot, + Melon, + Netherwart, + Pumpkin, + Potato, + Wheat + } + + public class Farmer : ChatBot + { + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "Farmer"; + + public bool Enabled = false; + + [TomlInlineComment("$config.ChatBot.Farmer.Delay_Between_Tasks$")] + public int Delay_Between_Tasks = 1; + + public void OnSettingUpdate() + { + if (Delay_Between_Tasks <= 0) + Delay_Between_Tasks = 1; + } + } + + private State state = State.SearchingForCropsToBreak; + 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(Translations.TryGet("bot.farmer.not_implemented")); + return; + } + + if (!GetTerrainEnabled()) + { + LogToConsole("Terrain handling needed!"); + return; + } + + if (!GetInventoryEnabled()) + { + LogToConsole("Inventory handling needed!"); + return; + } + + RegisterChatBotCommand("farmer", "bot.farmer.desc", commandDescription, OnFarmCommand); + } + + private string OnFarmCommand(string cmd, string[] args) + { + if (args.Length > 0) + { + if (args[0].Equals("stop", StringComparison.OrdinalIgnoreCase)) + { + if (!running) + return Translations.TryGet("bot.farmer.already_stopped"); + + running = false; + return Translations.TryGet("bot.farmer.stopping"); + } + + if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) + { + if (args.Length >= 2) + { + if (running) + return Translations.TryGet("bot.farmer.already_running"); + + if (!Enum.TryParse(args[1], true, out CropType whatToFarm)) + return Translations.TryGet("bot.farmer.invalid_crop_type"); + + int radius = 30; + + state = State.SearchingForFarmlandToPlant; + cropType = whatToFarm; + 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 ""; + } + } + } + + return Translations.TryGet("bot.farmer.desc") + ": " + commandDescription; + } + + public override void AfterGameJoined() + { + running = false; + } + + public override bool OnDisconnect(DisconnectReason reason, string message) + { + running = false; + return true; + } + + private void MainPorcess() + { + 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) + { + LogDebug("Eating..."); + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + switch (state) + { + case State.SearchingForFarmlandToPlant: + 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(cropTypeToPlant)) + { + LogDebug("No seeds, trying to find some crops to break"); + state = State.SearchingForCropsToBreak; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + List farmlandToPlantOn = findEmptyFarmland(farmingRadius); + + if (farmlandToPlantOn.Count == 0) + { + LogDebug("Could not find any farmland, trying to find some crops to break"); + state = State.SearchingForCropsToBreak; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + int i = 0; + foreach (Location location in farmlandToPlantOn) + { + if (!running) break; + + // Check only every second iteration, minor optimization xD + if (i % 2 == 0) + { + if (!HasItemOfTypeInInventory(cropTypeToPlant)) + { + LogDebug("Ran out of seeds, looking for crops to break..."); + state = State.SearchingForCropsToBreak; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + } + + double yValue = Math.Floor(location.Y) + 1; + + // 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) + 0.5, yValue, Math.Floor(location.Z) + 0.5); + + if (WaitForMoveToLocation(location2)) + { + LogDebug("Moving to: " + location2); + + // Stop if we do not have any more seeds left + if (!SwitchToItem(GetSeedItemTypeForCropType(cropType))) + { + 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)); + LogDebug("Sending placeblock to: " + loc); + + SendPlaceBlock(loc, Direction.Up); + Thread.Sleep(300); + } + else LogDebug("Can't move to: " + location2); + + i++; + } + + LogDebug("Finished planting crops!"); + state = State.SearchingForCropsToBreak; + break; + + case State.SearchingForCropsToBreak: + LogDebug("Searching for crops to break..."); + + List cropsToCollect = findCrops(farmingRadius, cropType, true); + + if (cropsToCollect.Count == 0) + { + LogToConsole("No crops to break, trying to bonemeal ungrown ones"); + state = State.BonemealingCrops; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + 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 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)((location.Y - 1) + (double)0.93750), Math.Floor(location.Z) + 0.5) + + if (WaitForMoveToLocation(location)) + WaitForDigBlock(location); + + // Allow some time to pickup the item + Thread.Sleep(cropType == CropType.Melon || cropType == CropType.Pumpkin ? 400 : 200); + } + + LogDebug("Finished breaking crops!"); + state = State.BonemealingCrops; + break; + + case State.BonemealingCrops: + // Can't be bonemealed + if (cropType == CropType.Netherwart) + { + state = State.SearchingForFarmlandToPlant; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + // If we don't have any bonemeal on our hotbar, skip this step + if (!SwitchToItem(ItemType.BoneMeal)) + { + LogDebug("No bonemeal, searching for some farmland to plant seeds on"); + state = State.SearchingForFarmlandToPlant; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + List cropsToBonemeal = findCrops(farmingRadius, cropType, false); + + if (cropsToBonemeal.Count == 0) + { + LogDebug("No crops to bonemeal, searching for farmland to plant seeds on"); + state = State.SearchingForFarmlandToPlant; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + + int i2 = 0; + foreach (Location location in cropsToBonemeal) + { + if (!running) break; + + // Check only every second iteration, minor optimization xD + if (i2 % 2 == 0) + { + if (!HasItemOfTypeInInventory(ItemType.BoneMeal)) + { + LogDebug("Ran out of Bone Meal, looking for farmland to plant on..."); + state = State.SearchingForFarmlandToPlant; + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + continue; + } + } + + if (WaitForMoveToLocation(location)) + { + // Stop if we do not have any more bonemeal left + if (!SwitchToItem(ItemType.BoneMeal)) + { + LogDebug("No bonemeal, searching for some farmland to plant seeds on..."); + break; + } + + 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(location2, Direction.Down); + } + + Thread.Sleep(100); + } + + i2++; + } + + LogDebug("Finished bonemealing crops!"); + state = State.SearchingForFarmlandToPlant; + break; + } + + LogDebug("Waiting for " + Config.Delay_Between_Tasks + " seconds for next cycle."); + Thread.Sleep(Config.Delay_Between_Tasks * 1000); + } + + LogToConsole(Translations.TryGet("bot.farmer.stopped")); + } + + private Material GetMaterialForCropType(CropType type) + { + switch (type) + { + case CropType.Beetroot: + return Material.Beetroots; + + case CropType.Carrot: + return Material.Carrots; + + case CropType.Melon: + return Material.Melon; + + case CropType.Netherwart: + return Material.NetherWart; + + case CropType.Pumpkin: + return Material.Pumpkin; + + case CropType.Potato: + return Material.Potatoes; + + case CropType.Wheat: + return Material.Wheat; + } + + throw new Exception("Material type for " + type.GetType().Name + " has not been mapped!"); + } + + private ItemType GetSeedItemTypeForCropType(CropType type) + { + switch (type) + { + case CropType.Beetroot: + return ItemType.BeetrootSeeds; + + case CropType.Carrot: + return ItemType.Carrot; + + case CropType.Melon: + return ItemType.MelonSeeds; + + case CropType.Netherwart: + return ItemType.NetherWart; + + case CropType.Pumpkin: + return ItemType.PumpkinSeeds; + + case CropType.Potato: + return ItemType.Potato; + + case CropType.Wheat: + return ItemType.WheatSeeds; + } + + throw new Exception("Seed type for " + type.GetType().Name + " has not been mapped!"); + } + + private List findEmptyFarmland(int radius) + { + return GetWorld() + .FindBlock(GetCurrentLocation(), cropType == CropType.Netherwart ? Material.SoulSand : Material.Farmland, radius) + .Where(location => GetWorld().GetBlock(new Location(location.X, location.Y + 1, location.Z)).Type == Material.Air) + .ToList(); + } + + private List findCrops(int radius, CropType cropType, bool fullyGrown) + { + Material material = GetMaterialForCropType(cropType); + + // A bit of a hack to enable bonemealing melon and pumpkin stems + if (!fullyGrown && (cropType == CropType.Melon || cropType == CropType.Pumpkin)) + material = cropType == CropType.Melon ? Material.MelonStem : Material.PumpkinStem; + + return GetWorld() + .FindBlock(GetCurrentLocation(), material, radius) + .Where(location => + { + if (fullyGrown && (material == Material.Melon || material == Material.Pumpkin)) + return true; + + bool isFullyGrown = IsCropFullyGrown(GetWorld().GetBlock(location), cropType); + return fullyGrown ? isFullyGrown : !isFullyGrown; + }) + .ToList(); + } + + private bool IsCropFullyGrown(Block block, CropType cropType) + { + int protocolVersion = GetProtocolVersion(); + + switch (cropType) + { + case CropType.Beetroot: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 10103) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 9472) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 9226) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 8686) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 8162) + return true; + } + + break; + + case CropType.Carrot: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 6930) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 6543) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 6341) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 5801) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 5295) + return true; + } + + break; + + // Checkin for stems and attached stems instead of Melons themselves + case CropType.Melon: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 5166 || block.BlockId == 5150) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 4860 || block.BlockId == 4844) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 4791 || block.BlockId == 4775) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 4771 || block.BlockId == 4755) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 4268 || block.BlockId == 4252) + return true; + } + break; + + // Checkin for stems and attached stems instead of Melons themselves + case CropType.Netherwart: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 5718) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 5332) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 5135) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 5115) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 4612) + return true; + } + break; + + // Checkin for stems and attached stems instead of Pumpkins themselves + case CropType.Pumpkin: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 5158 || block.BlockId == 5146) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 4852 || block.BlockId == 4840) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 4783 || block.BlockId == 4771) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 4763 || block.BlockId == 4751) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 4260 || block.BlockId == 4248) + return true; + } + break; + + case CropType.Potato: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 6938) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 6551) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 6349) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 5809) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 5303) + return true; + } + + break; + + case CropType.Wheat: + if (protocolVersion >= Protocol18Handler.MC_1_19_Version && protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + if (block.BlockId == 3619) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_17_Version && protocolVersion <= Protocol18Handler.MC_1_18_2_Version) + { + if (block.BlockId == 3421) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_16_Version && protocolVersion <= Protocol18Handler.MC_1_16_5_Version) + { + if (block.BlockId == 3364) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_14_Version && protocolVersion <= Protocol18Handler.MC_1_15_2_Version) + { + if (block.BlockId == 3362) + return true; + } + else if (protocolVersion >= Protocol18Handler.MC_1_13_Version && protocolVersion < Protocol18Handler.MC_1_14_Version) + { + if (block.BlockId == 3059) + return true; + } + + break; + } + + return false; + } + + // 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(); + + if (playerInventory.Items.ContainsKey(GetCurrentSlot() - 36) + && playerInventory.Items[GetCurrentSlot() - 36].Type == itemType) + return true; // Already selected + + // Search the full inventory + List fullInventorySearch = new List(playerInventory.SearchItem(itemType)); + + // 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; + + 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(location: pos, allowUnsafe: allowUnsafe, allowDirectTeleport: allowTeleport)) + { + LogDebug("Moving to: " + pos); + + while (GetCurrentLocation().Distance(pos) > tolerance) + Thread.Sleep(200); + + return true; + } + else LogDebug("Can't move to: " + pos); + + return false; + } + + // Yoinked from Daenges's Sugarcane Farmer + private bool WaitForDigBlock(Location block, int digTimeout = 1000) + { + if (DigBlock(block.ToFloor())) + { + short i = 0; // Maximum wait time of 10 sec. + while (GetWorld().GetBlock(block).Type != Material.Air && i <= digTimeout) + { + Thread.Sleep(100); + i++; + } + + return i <= digTimeout; + } + + return false; + } + + private bool HasItemOfTypeInInventory(ItemType itemType) + { + return GetPlayerInventory().SearchItem(itemType).Length > 0; + } + 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/McClient.cs b/MinecraftClient/McClient.cs index c70ecb5c..1618b464 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -223,7 +223,7 @@ namespace MinecraftClient return; - Retry: + Retry: if (timeoutdetector != null) { timeoutdetector.Item2.Cancel(); @@ -263,6 +263,7 @@ namespace MinecraftClient if (Config.ChatBot.AutoRelog.Enabled) { BotLoad(new AutoRelog()); } if (Config.ChatBot.AutoRespond.Enabled) { BotLoad(new AutoRespond()); } if (Config.ChatBot.ChatLog.Enabled) { BotLoad(new ChatLog()); } + if (Config.ChatBot.Farmer.Enabled) { BotLoad(new Farmer()); } if (Config.ChatBot.FollowPlayer.Enabled) { BotLoad(new FollowPlayer()); } if (Config.ChatBot.HangmanGame.Enabled) { BotLoad(new HangmanGame()); } if (Config.ChatBot.Mailer.Enabled) { BotLoad(new Mailer()); } diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 8347d5a8..138ca879 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) @@ -845,12 +861,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 5bfd337c..9f4d2f27 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -242,16 +242,16 @@ namespace MinecraftClient { switch (positionalIndex) { - case 0: + case 0: Config.Main.General.Account.Login = argument; break; - case 1: - InternalConfig.Password = argument; + case 1: + InternalConfig.Password = argument; break; - case 2: - Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true); + case 2: + Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true); break; - case 3: + case 3: // SingleCommand = argument; break; } @@ -269,7 +269,7 @@ namespace MinecraftClient { [TomlProperty("Current Version")] public string CurrentVersion { get; set; } = Program.BuildInfo ?? "Development Build"; - + [TomlProperty("Latest Version")] public string LatestVersion { get; set; } = "Unknown"; @@ -297,15 +297,15 @@ namespace MinecraftClient [NonSerialized] public static readonly string[] AvailableLang = { - "af_za", "ar_sa", "ast_es", "az_az", "ba_ru", "bar", "be_by", "bg_bg", "br_fr", "brb", "bs_ba", "ca_es", - "cs_cz", "cy_gb", "da_dk", "de_at", "de_ch", "de_de", "el_gr", "en_au", "en_ca", "en_gb", "en_nz", "eo_uy", - "es_ar", "es_cl", "es_ec", "es_es", "es_mx", "es_uy", "es_ve", "esan", "et_ee", "eu_es", "fa_ir", "fi_fi", - "fil_ph", "fo_fo", "fr_ca", "fr_fr", "fra_de", "fur_it", "fy_nl", "ga_ie", "gd_gb", "gl_es", "haw_us", "he_il", - "hi_in", "hr_hr", "hu_hu", "hy_am", "id_id", "ig_ng", "io_en", "is_is", "isv", "it_it", "ja_jp", "jbo_en", - "ka_ge", "kk_kz", "kn_in", "ko_kr", "ksh", "kw_gb", "la_la", "lb_lu", "li_li", "lmo", "lt_lt", "lv_lv", "lzh", - "mk_mk", "mn_mn", "ms_my", "mt_mt", "nds_de", "nl_be", "nl_nl", "nn_no", "oc_fr", "ovd", "pl_pl", "pt_br", - "pt_pt", "qya_aa", "ro_ro", "rpr", "ru_ru", "se_no", "sk_sk", "sl_si", "so_so", "sq_al", "sr_sp", "sv_se", - "sxu", "szl", "ta_in", "th_th", "tl_ph", "tlh_aa", "tok", "tr_tr", "tt_ru", "uk_ua", "val_es", "vec_it", + "af_za", "ar_sa", "ast_es", "az_az", "ba_ru", "bar", "be_by", "bg_bg", "br_fr", "brb", "bs_ba", "ca_es", + "cs_cz", "cy_gb", "da_dk", "de_at", "de_ch", "de_de", "el_gr", "en_au", "en_ca", "en_gb", "en_nz", "eo_uy", + "es_ar", "es_cl", "es_ec", "es_es", "es_mx", "es_uy", "es_ve", "esan", "et_ee", "eu_es", "fa_ir", "fi_fi", + "fil_ph", "fo_fo", "fr_ca", "fr_fr", "fra_de", "fur_it", "fy_nl", "ga_ie", "gd_gb", "gl_es", "haw_us", "he_il", + "hi_in", "hr_hr", "hu_hu", "hy_am", "id_id", "ig_ng", "io_en", "is_is", "isv", "it_it", "ja_jp", "jbo_en", + "ka_ge", "kk_kz", "kn_in", "ko_kr", "ksh", "kw_gb", "la_la", "lb_lu", "li_li", "lmo", "lt_lt", "lv_lv", "lzh", + "mk_mk", "mn_mn", "ms_my", "mt_mt", "nds_de", "nl_be", "nl_nl", "nn_no", "oc_fr", "ovd", "pl_pl", "pt_br", + "pt_pt", "qya_aa", "ro_ro", "rpr", "ru_ru", "se_no", "sk_sk", "sl_si", "so_so", "sq_al", "sr_sp", "sv_se", + "sxu", "szl", "ta_in", "th_th", "tl_ph", "tlh_aa", "tok", "tr_tr", "tt_ru", "uk_ua", "val_es", "vec_it", "vi_vn", "yi_de", "yo_ng", "zh_cn", "zh_hk", "zh_tw", "zlm_arab" }; @@ -750,7 +750,7 @@ namespace MinecraftClient } else { - return false; + return false; } } @@ -799,7 +799,7 @@ namespace MinecraftClient /// A IDictionary containing a name and a vlaue key pairs of variables public Dictionary GetVariables() { - Dictionary res = new(VarObject); + Dictionary res = new(VarObject); foreach ((string varName, string varData) in VarStirng) res.Add(varName, varData); return res; @@ -1054,6 +1054,13 @@ 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; } + set { ChatBots.Farmer.Config = value; ChatBots.Farmer.Config.OnSettingUpdate(); } + } + [TomlPrecedingComment("$config.ChatBot.FollowPlayer$")] public ChatBots.FollowPlayer.Configs FollowPlayer { @@ -1228,7 +1235,7 @@ namespace MinecraftClient }; } } - + public static class BrandInfoTypeExtensions { public static string? ToBrandString(this BrandInfoType info)