using System; using System.Collections.Generic; using System.Linq; using System.Text; using MinecraftClient.Mapping; using MinecraftClient.Inventory; namespace MinecraftClient.ChatBots { /// /// The AutoFishing bot semi-automates fishing. /// The player needs to have a fishing rod in hand, then manually send it using the UseItem command. /// class AutoFishing : ChatBot { private int fishCount = 0; private bool inventoryEnabled; private int castTimeout = 12; private bool isFishing = false; private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; private int counter = 0; private readonly object stateLock = new(); private FishingState state = FishingState.WaitJoinGame; private int curLocationIdx = 0, moveDir = 1; float nextYaw = 0, nextPitch = 0; private enum FishingState { WaitJoinGame, WaitingToCast, CastingRod, WaitingFishingBobber, WaitingFishToBite, DurabilityCheck, ChangeLocation, Stopping, } public override void Initialize() { if (!GetEntityHandlingEnabled()) { LogToConsoleTranslated("extra.entity_required"); LogToConsoleTranslated("general.bot_unload"); UnloadBot(); } inventoryEnabled = GetInventoryEnabled(); if (!inventoryEnabled) LogToConsoleTranslated("bot.autoFish.no_inv_handle"); } 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.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) { switch (state) { case FishingState.WaitJoinGame: break; case FishingState.WaitingToCast: if (AutoEat.Eating) counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); else if (--counter < 0) state = FishingState.CastingRod; break; case FishingState.CastingRod: UseFishRod(); counter = 0; state = FishingState.WaitingFishingBobber; break; case FishingState.WaitingFishingBobber: if (++counter > castTimeout) { if (castTimeout < 6000) 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.WaitingFishToBite: if (++counter > (int)(Settings.AutoFishing_FishingTimeout * 10)) { LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); state = FishingState.WaitingToCast; } break; 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; } } } public override void OnEntitySpawn(Entity entity) { if (entity.Type == EntityType.FishingBobber && entity.ObjectData == GetPlayerEntityID()) { LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); lock (stateLock) { fishingBobber = entity; LastPos = entity.Location; isFishing = true; castTimeout = 24; counter = 0; state = FishingState.WaitingFishToBite; } } } public override void OnEntityDespawn(Entity entity) { if (isFishing && entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) { isFishing = false; if (Settings.AutoFishing_Antidespawn) { LogToConsoleTranslated("bot.autoFish.despawn"); lock (stateLock) { counter = (int)(Settings.AutoFishing_FishingCastDelay * 10); state = FishingState.WaitingToCast; } } } } public override void OnEntityMove(Entity entity) { if (isFishing && fishingBobber!.ID == entity.ID) { Location Pos = entity.Location; double Dx = LastPos.X - Pos.X; double Dy = LastPos.Y - Pos.Y; double Dz = LastPos.Z - Pos.Z; LastPos = Pos; // check if fishing hook is stationary if (Dx == 0 && Dz == 0) { if (Math.Abs(Dy) > Settings.AutoFishing_FishingHookThreshold) { // caught // prevent triggering multiple time if ((DateTime.Now - CaughtTime).TotalSeconds > 1) { isFishing = false; CaughtTime = DateTime.Now; OnCaughtFish(); } } } } } public override void OnDeath() { StopFishing(); } public override bool OnDisconnect(DisconnectReason reason, string message) { StopFishing(); fishingBobber = null; LastPos = Location.Zero; CaughtTime = DateTime.Now; return base.OnDisconnect(reason, message); } private void UseFishRod() { if (Settings.AutoFishing_Mainhand) UseItemInHand(); else UseItemInLeftHand(); } private void UpdateLocation(double[,] locationList) { if (curLocationIdx >= locationList.GetLength(0)) { curLocationIdx = Math.Max(0, locationList.GetLength(0) - 2); moveDir = -1; } else if (curLocationIdx < 0) { curLocationIdx = Math.Min(locationList.GetLength(0) - 1, 1); moveDir = 1; } int locationType = locationList.GetLength(1); 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) { UseFishRod(); counter = 0; state = FishingState.DurabilityCheck; } } /// /// Check whether the player has a fishing rod in inventory /// /// TRUE if the player has a fishing rod public bool HasFishingRod() { if (!inventoryEnabled) return false; int start = 36; int end = 44; Container container = GetPlayerInventory(); foreach (KeyValuePair a in container.Items) { if (a.Key < start || a.Key > end) continue; if (a.Value.Type == ItemType.FishingRod) return true; } return false; } } }