diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index d92d07ab..69617b36 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -15,6 +15,7 @@ namespace MinecraftClient.ChatBots { private int fishCount = 0; private bool inventoryEnabled; + private int castTimeout = 12; private bool isFishing = false; private Entity? fishingBobber; @@ -25,7 +26,8 @@ namespace MinecraftClient.ChatBots private readonly object stateLock = new(); private FishingState state = FishingState.WaitJoinGame; - private int castTimeout = 12; + private int curLocationIdx = 0, moveDir = 1; + float nextYaw = 0, nextPitch = 0; private enum FishingState { @@ -33,8 +35,9 @@ namespace MinecraftClient.ChatBots WaitingToCast, CastingRod, WaitingFishingBobber, - WaitingFishBite, - Preparing, + WaitingFishToBite, + DurabilityCheck, + ChangeLocation, Stopping, } @@ -47,19 +50,41 @@ namespace MinecraftClient.ChatBots UnloadBot(); } inventoryEnabled = GetInventoryEnabled(); + if (!inventoryEnabled) + LogToConsoleTranslated("bot.autoFish.no_inv_handle"); } - public override void AfterGameJoined() + private void StartFishing() { + isFishing = false; double delay = Settings.AutoFishing_FishingDelay; LogToConsole(Translations.Get("bot.autoFish.start", delay)); lock (stateLock) { counter = (int)(delay * 10); - state = FishingState.WaitingToCast; + state = FishingState.DurabilityCheck; } } + private void StopFishing() + { + isFishing = false; + lock (stateLock) + { + state = FishingState.Stopping; + } + } + + public override void AfterGameJoined() + { + StartFishing(); + } + + public override void OnRespawn() + { + StartFishing(); + } + public override void Update() { lock (stateLock) @@ -69,7 +94,9 @@ namespace MinecraftClient.ChatBots case FishingState.WaitJoinGame: break; case FishingState.WaitingToCast: - if (--counter < 0) + if (AutoEat.Eating) + counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); + else if (--counter < 0) state = FishingState.CastingRod; break; case FishingState.CastingRod: @@ -81,14 +108,14 @@ namespace MinecraftClient.ChatBots if (++counter > castTimeout) { if (castTimeout < 6000) - castTimeout *= 2; + castTimeout *= 2; // Exponential backoff LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); state = FishingState.WaitingToCast; } break; - case FishingState.WaitingFishBite: + case FishingState.WaitingFishToBite: if (++counter > (int)(Settings.AutoFishing_FishingTimeout * 10)) { LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); @@ -97,7 +124,21 @@ namespace MinecraftClient.ChatBots state = FishingState.WaitingToCast; } break; - case FishingState.Preparing: + case FishingState.DurabilityCheck: + if (--counter < 0) + { + DurabilityCheckAndMove(); + } + break; + case FishingState.ChangeLocation: + if (!ClientIsMoving()) + { + LookAtLocation(nextYaw, nextPitch); + LogToConsole(Translations.Get("bot.autoFish.update_lookat", nextYaw, nextPitch)); + + counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); + state = FishingState.WaitingToCast; + } break; case FishingState.Stopping: break; @@ -118,7 +159,7 @@ namespace MinecraftClient.ChatBots castTimeout = 24; counter = 0; - state = FishingState.WaitingFishBite; + state = FishingState.WaitingFishToBite; } } } @@ -161,6 +202,7 @@ namespace MinecraftClient.ChatBots // prevent triggering multiple time if ((DateTime.Now - CaughtTime).TotalSeconds > 1) { + isFishing = false; CaughtTime = DateTime.Now; OnCaughtFish(); } @@ -169,15 +211,14 @@ namespace MinecraftClient.ChatBots } } + public override void OnDeath() + { + StopFishing(); + } + public override bool OnDisconnect(DisconnectReason reason, string message) { - lock (stateLock) - { - isFishing = false; - - counter = 0; - state = FishingState.Stopping; - } + StopFishing(); fishingBobber = null; LastPos = Location.Zero; @@ -194,34 +235,123 @@ namespace MinecraftClient.ChatBots UseItemInLeftHand(); } - /// - /// Called when detected a fish is caught - /// - public void OnCaughtFish() + private void UpdateLocation(double[,] locationList) { - lock (stateLock) + if (curLocationIdx >= locationList.GetLength(0)) { - state = FishingState.Preparing; + curLocationIdx = Math.Max(0, locationList.GetLength(0) - 2); + moveDir = -1; + } + else if (curLocationIdx < 0) + { + curLocationIdx = Math.Min(locationList.GetLength(0) - 1, 1); + moveDir = 1; } - UseFishRod(); + int locationType = locationList.GetLength(1); - ++fishCount; - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); + if (locationType == 2) + { + nextYaw = (float)locationList[curLocationIdx, 0]; + nextPitch = (float)locationList[curLocationIdx, 1]; + } + else if (locationType == 3) + { + nextYaw = GetYaw(); + nextPitch = GetPitch(); + } + else if (locationType == 5) + { + nextYaw = (float)locationList[curLocationIdx, 3]; + nextPitch = (float)locationList[curLocationIdx, 4]; + } + if (locationType == 3 || locationType == 5) + { + Location current = GetCurrentLocation(); + Location goal = new(locationList[curLocationIdx, 0], locationList[curLocationIdx, 1], locationList[curLocationIdx, 2]); + + bool isMoveSuccessed; + if (!Movement.CheckChunkLoading(GetWorld(), current, goal)) + { + LogToConsole(Translations.Get("cmd.move.chunk_not_loaded", goal.X, goal.Y, goal.Z)); + isMoveSuccessed = false; + } + else + { + isMoveSuccessed = MoveToLocation(goal, allowUnsafe: false, allowDirectTeleport: false); + } + + if (!isMoveSuccessed) + { + nextYaw = GetYaw(); + nextPitch = GetPitch(); + LogToConsole(Translations.Get("cmd.move.fail", goal)); + } + else + { + LogToConsole(Translations.Get("cmd.move.walk", goal, current)); + } + } + + curLocationIdx += moveDir; + } + + + private void DurabilityCheckAndMove() + { if (inventoryEnabled) { if (!HasFishingRod()) { LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.no_rod")); + + lock (stateLock) + { + state = FishingState.Stopping; + } + return; } } + double[,]? locationList = Settings.AutoFishing_Location; + if (locationList != null) + { + UpdateLocation(locationList); + lock (stateLock) + { + state = FishingState.ChangeLocation; + } + } + else + { + lock (stateLock) + { + counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); + state = FishingState.WaitingToCast; + } + } + } + + /// + /// Called when detected a fish is caught + /// + public void OnCaughtFish() + { + ++fishCount; + if (Settings.AutoFishing_Location != null) + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught_at", + fishingBobber!.Location.X, fishingBobber!.Location.Y, fishingBobber!.Location.Z, fishCount)); + else + LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); + lock (stateLock) { - counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); - state = FishingState.WaitingToCast; + UseFishRod(); + + counter = 0; + state = FishingState.DurabilityCheck; } } @@ -233,6 +363,7 @@ namespace MinecraftClient.ChatBots { if (!inventoryEnabled) return false; + int start = 36; int end = 44; Container container = GetPlayerInventory(); diff --git a/MinecraftClient/Commands/Move.cs b/MinecraftClient/Commands/Move.cs index 1b4f85ee..da58e76b 100644 --- a/MinecraftClient/Commands/Move.cs +++ b/MinecraftClient/Commands/Move.cs @@ -74,8 +74,7 @@ namespace MinecraftClient.Commands Location goal = Movement.Move(handler.GetCurrentLocation(), direction); - ChunkColumn? chunkColumn = handler.GetWorld().GetChunkColumn(goal); - if (chunkColumn == null || chunkColumn.FullyLoaded == false) + if (!Movement.CheckChunkLoading(handler.GetWorld(), handler.GetCurrentLocation(), goal)) return Translations.Get("cmd.move.chunk_not_loaded", goal.X, goal.Y, goal.Z); if (Movement.CanMove(handler.GetWorld(), handler.GetCurrentLocation(), direction)) @@ -98,8 +97,7 @@ namespace MinecraftClient.Commands double z = args[2].StartsWith('~') ? current.Z + (args[2].Length > 1 ? double.Parse(args[2][1..]) : 0) : double.Parse(args[2]); Location goal = new(x, y, z); - ChunkColumn? chunkColumn = handler.GetWorld().GetChunkColumn(goal); - if (chunkColumn == null || chunkColumn.FullyLoaded == false) + if (!Movement.CheckChunkLoading(handler.GetWorld(), current, goal)) return Translations.Get("cmd.move.chunk_not_loaded", x, y, z); if (takeRisk || Movement.PlayerFitsHere(handler.GetWorld(), goal)) diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index 8389cdd7..21811c8d 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -643,5 +643,25 @@ namespace MinecraftClient.Mapping throw new ArgumentException("Unknown direction", "direction"); } } + + /// + /// Check that the chunks at both the start and destination locations have been loaded + /// + /// Current world + /// Start location + /// Destination location + /// Is loading complete + public static bool CheckChunkLoading(World world, Location start, Location dest) + { + ChunkColumn? chunkColumn = world.GetChunkColumn(dest); + if (chunkColumn == null || chunkColumn.FullyLoaded == false) + return false; + + chunkColumn = world.GetChunkColumn(start); + if (chunkColumn == null || chunkColumn.FullyLoaded == false) + return false; + + return true; + } } } diff --git a/MinecraftClient/Resources/config/MinecraftClient.ini b/MinecraftClient/Resources/config/MinecraftClient.ini index 2a9d7436..4632cd00 100644 --- a/MinecraftClient/Resources/config/MinecraftClient.ini +++ b/MinecraftClient/Resources/config/MinecraftClient.ini @@ -207,11 +207,16 @@ interaction=Attack # Possible values: Interact, Attack (default) # /!\ Make sure server rules allow automated farming before using this bot enabled=false antidespawn=false -main_hand=true # Use the main hand or the second hand to hold the rod +main_hand=true # Use the main hand or the second hand to hold the rod. fishing_delay=3.0 # How long after entering the game to start fishing (seconds). -fishing_timeout=600.0 # Fishing timeout (seconds). Timeout will re-cast the rod +fishing_timeout=300.0 # Fishing timeout (seconds). Timeout will trigger a re-cast. fishing_hook_threshold=0.2 # Fish hooks moving on the Y-axis above this threshold will be considered to have caught a fish. fishing_cast_delay=0.4 # How soon to re-cast after successful fishing. +location= # Some plugins do not allow the player to fish in one place. This allows the player to change position/angle after each fish caught. + # Floating point numbers can be used for both coordinates and angles. Leave blank to disable this function. + # Change the angle only (recommended): location=yaw_1,pitch_1;yaw_2,pitch_2;...;yaw_n,pitch_n + # Change position only: location=x1,y1,z1;x2,y2,z2;...;xn,yn,zn + # Change both angle and position: location=x1,y1,z1,yaw_1,pitch_1;x2,y2,z2,yaw_2,pitch_2;...;xn,yn,zn,yaw_n,pitch_n [AutoEat] # Automatically eat food when your Hunger value is low diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 4a30ee1c..15b6d2e9 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -88,6 +88,8 @@ error.connection_timeout=§8A timeout occured while attempting to connect to thi error.forge=§8Forge Login Handshake did not complete successfully error.forge_encrypt=§8Forge StartEncryption Handshake did not complete successfully error.setting.str2int=Failed to convert '{0}' into an integer. Please check your settings. +error.setting.str2locationList.convert_fail=Failed to convert '{0}' to a floating point number. Please check your settings. +error.setting.str2locationList.format_err=Wrong format, can't parse '{0}' into position data.. Please check your settings. error.setting.argument_syntax={0}: Invalid syntax, expecting --argname=value or --section.argname=value error.setting.unknown_section={0}: Unknown setting section '{1}' error.setting.unknown_or_invalid={0}: Unknown setting or invalid value @@ -447,13 +449,16 @@ bot.autoDrop.no_mode=Cannot read drop mode from config. Using include mode. bot.autoDrop.no_inventory=Cannot find inventory {0}! # AutoFish +bot.autoFish.no_inv_handle=Inventory handling is not enabled. Cannot check rod durability and switch rods. bot.autoFish.start=Fishing will start in {0:0.0} second(s). bot.autoFish.throw=Casting successfully. bot.autoFish.caught=Caught a fish! (Count: {0}) +bot.autoFish.caught_at=Caught a fish at ({0:0.0},{1:0.0},{2:0.0})! (Count: {3}) bot.autoFish.no_rod=No Fishing Rod on hand. Maybe broken? bot.autoFish.despawn=Fish floating despawn, will re-cast. bot.autoFish.fishing_timeout=Fishing timeout, will soon re-cast. bot.autoFish.cast_timeout=Casting timeout and will soon retry. (Timeout increased to {0:0.0} sec). +bot.autoFish.update_lookat=Update yaw = {0:0.00}, pitch = {1:0.00}. # AutoRelog bot.autoRelog.launch=Launching with {0} reconnection attempts diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 3e5515e9..4d4ac0ac 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -1012,6 +1012,16 @@ namespace MinecraftClient Handler.UpdateLocation(Handler.GetCurrentLocation(), location); } + /// + /// Look at the specified location + /// + /// Yaw to look at + /// Pitch to look at + protected void LookAtLocation(float yaw, float pitch) + { + Handler.UpdateLocation(Handler.GetCurrentLocation(), yaw, pitch); + } + /// /// Get a Y-M-D h:m:s timestamp representing the current system date and time /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 380f7154..30e46cc0 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -208,6 +208,7 @@ namespace MinecraftClient public static double AutoFishing_FishingTimeout = 600.0; public static double AutoFishing_FishingHookThreshold = 0.2; public static double AutoFishing_FishingCastDelay = 0.4; + public static double[,]? AutoFishing_Location = null; //Auto Eating public static bool AutoEat_Enabled = false; @@ -720,6 +721,7 @@ namespace MinecraftClient case "fishing_timeout": AutoFishing_FishingTimeout = str2double(argValue); return true; case "fishing_hook_threshold": AutoFishing_FishingHookThreshold = str2double(argValue); return true; case "fishing_cast_delay": AutoFishing_FishingCastDelay = str2double(argValue); return true; + case "location": AutoFishing_Location = str2locationList(argValue); return true; } break; @@ -872,8 +874,8 @@ namespace MinecraftClient /// Float number public static float str2float(string str) { - if (float.TryParse(str.Trim(), out float f)) - return f; + if (float.TryParse(str.Trim(), out float num)) + return num; else { ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2int", str)); @@ -888,8 +890,8 @@ namespace MinecraftClient /// Double number public static double str2double(string str) { - if (double.TryParse(str.Trim(), out double f)) - return f; + if (double.TryParse(str.Trim(), out double num)) + return num; else { ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2int", str)); @@ -910,6 +912,46 @@ namespace MinecraftClient return str == "true" || str == "1"; } + /// + /// Convert the specified string to a list of location, returning null if invalid argument + /// + /// String to parse as a location list + /// Location list (null or double[*,5] or double[*,3] or double[*,2]) + public static double[,]? str2locationList(string str) + { + string[] locationStrList = str.Split(';', StringSplitOptions.RemoveEmptyEntries); + double[,]? res = null; + int codLen = 0; + for (int i = 0; i < locationStrList.Length; ++i) + { + string[] coordinates_str_list = locationStrList[i].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + int curCodLen = coordinates_str_list.Length; + if ((curCodLen == 2 || curCodLen == 3 || curCodLen == 5) && (i == 0 || curCodLen == codLen)) + { + if (i == 0) + { + res = new double[locationStrList.Length, curCodLen]; + codLen = curCodLen; + } + + for (int j = 0; j < curCodLen; ++j) + { + if (!double.TryParse(coordinates_str_list[j], out res![i, j])) + { + ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2locationList.convert_fail", coordinates_str_list[j])); + return null; + } + } + } + else + { + ConsoleIO.WriteLogLine(Translations.Get("error.setting.str2locationList.format_err", locationStrList[i])); + return null; + } + } + return res; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static string ToLowerIfNeed(string str) {