using System; using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using LLib.Fishing; using Microsoft.Extensions.Logging; using Questionable.Controller.CustomDelivery; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Common; using Questionable.Data; using Questionable.External; using Questionable.Navigation; namespace Questionable.Controller; internal sealed class FishingController : MiniTaskController { public sealed record FishingRequest(uint ItemId, int Quantity, ushort Collectability); public enum EFishingState { Idle, Navigating, Orienting, Preparing, Fishing, Complete } private const long FishingTimeoutMs = 180000L; private const uint CollectorsGloveActionId = 4101u; private const uint CollectorsGloveStatusId = 805u; private const uint CastActionId = 289u; private const uint VersatileLureId = 29717u; private const int LurePurchaseQuantity = 99; private readonly FishingData _fishingData; private readonly AutoHookIpc _autoHookIpc; private readonly VendorResolverService _vendorResolver; private readonly NavmeshIpc _navmeshIpc; private readonly SmartNavRouteEnqueuer _routeEnqueuer; private readonly IObjectTable _objectTable; private readonly ICondition _condition; private FishingRequest? _currentRequest; private long _fishStartMs; private int _lastFishCount; private long _actionThrottle; private bool _baitReady; public EFishingState State { get; private set; } public bool IsRunning { get { if (State != EFishingState.Idle) { return State != EFishingState.Complete; } return false; } } public TaskQueue TaskQueue => _taskQueue; public uint? CurrentItemId => _currentRequest?.ItemId; public FishingController(FishingData fishingData, AutoHookIpc autoHookIpc, VendorResolverService vendorResolver, NavmeshIpc navmeshIpc, SmartNavRouteEnqueuer routeEnqueuer, IObjectTable objectTable, IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, Configuration configuration, ILogger logger) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, configuration, logger) { _fishingData = fishingData; _autoHookIpc = autoHookIpc; _vendorResolver = vendorResolver; _navmeshIpc = navmeshIpc; _routeEnqueuer = routeEnqueuer; _objectTable = objectTable; _condition = condition; } public bool Start(FishingRequest request) { if (State != EFishingState.Idle) { _logger.LogWarning("Cannot start fishing - already in state {State}", State); return false; } if (!_autoHookIpc.IsAvailable()) { _logger.LogError("AutoHook is not available, cannot fish"); return false; } _currentRequest = request; _actionThrottle = 0L; _baitReady = false; _taskQueue.Reset(); if (!TryEnqueueApproach(request)) { _currentRequest = null; return false; } State = EFishingState.Navigating; return true; } private unsafe bool TryEnqueueApproach(FishingRequest request) { if (!_fishingData.TryGetFishingSpot(request.ItemId, out var spot)) { _logger.LogError("No fishing spot found for item {ItemId}", request.ItemId); return false; } FishingSpotInfo value = spot.Value; Vector3 vector = new Vector3(value.X, 0f, value.Z); Vector3 vector2 = _navmeshIpc.GetPointOnFloor(vector, unlandable: false) ?? vector; _logger.LogInformation("Starting fishing: item {ItemId}, spot {SpotId}, territory {Territory}, position {Position}", request.ItemId, value.SpotId, value.TerritoryId, vector2); if (InventoryManager.Instance()->GetInventoryItemCount(29717u, isHq: false, checkEquipped: true, checkArmory: true, 0) <= 0 && !EnqueueLurePurchase()) { _logger.LogError("No Versatile Lure and no vendor found, cannot fish"); return false; } EnqueueNavigateTo(value.TerritoryId, vector2); _taskQueue.Enqueue(new Mount.UnmountTask()); return true; } protected override void OnRetryStep() { if (_currentRequest == null) { Stop("Retry without request"); return; } _logger.LogInformation("Retrying fishing approach"); _baitReady = false; if (!TryEnqueueApproach(_currentRequest)) { Stop("Retry failed"); } else { State = EFishingState.Navigating; } } public EFishingState Update() { if (State == EFishingState.Idle || State == EFishingState.Complete) { return State; } if (_currentRequest == null) { Stop("No request"); return EFishingState.Idle; } return State switch { EFishingState.Navigating => UpdateNavigating(), EFishingState.Orienting => UpdateOrienting(), EFishingState.Preparing => UpdatePreparing(), EFishingState.Fishing => UpdateFishing(), _ => State, }; } private EFishingState UpdateNavigating() { if (_taskQueue.AllTasksComplete) { _logger.LogDebug("Navigation complete, transitioning to orienting"); State = EFishingState.Orienting; return State; } UpdateCurrentTask(); return State; } private unsafe EFishingState UpdateOrienting() { IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer == null) { return State; } Vector3 position = localPlayer.Position; (float, Vector3)? tuple = WaterSurfaceDetector.FindFishableRotation(position); if (!tuple.HasValue) { _logger.LogWarning("No fishable rotation found at position {Position}", position); Stop("No fishable water nearby"); return State; } Vector3 item = tuple.Value.Item2; _logger.LogDebug("Found fishable rotation, facing hit point {HitPoint}", item); ActionManager.Instance()->AutoFaceTargetPosition(&item, 0uL); State = EFishingState.Preparing; return State; } private unsafe EFishingState UpdatePreparing() { if (!CanUseActionNow()) { return State; } if (!_baitReady) { if (_autoHookIpc.SwapBaitById(29717u)) { _logger.LogDebug("Bait set to Versatile Lure"); _baitReady = true; _actionThrottle = Environment.TickCount64 + 500; return State; } _logger.LogWarning("AutoHook failed to swap bait, stopping"); Stop("Bait swap failed"); return State; } if (_currentRequest.Collectability > 0) { IPlayerCharacter localPlayer = _objectTable.LocalPlayer; if (localPlayer != null) { BattleChara* address = (BattleChara*)localPlayer.Address; if (!address->GetStatusManager()->HasStatus(805u)) { if (ActionManager.Instance()->GetActionStatus(ActionType.Action, 4101u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Activating Collector's Glove"); ActionManager.Instance()->UseAction(ActionType.Action, 4101u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); _actionThrottle = Environment.TickCount64 + 1000; } return State; } } } _autoHookIpc.SetPluginState(enabled: true); _autoHookIpc.SetAutoStartFishing(enabled: true); _fishStartMs = Environment.TickCount64; _lastFishCount = GetItemCount(); State = EFishingState.Fishing; _logger.LogDebug("AutoHook enabled, transitioning to fishing state"); return State; } private unsafe EFishingState UpdateFishing() { int itemCount = GetItemCount(); if (itemCount > _lastFishCount) { _lastFishCount = itemCount; _fishStartMs = Environment.TickCount64; } if (Environment.TickCount64 - _fishStartMs > 180000) { _logger.LogWarning("Fishing made no progress for {Timeout}ms", 180000L); Stop("Fishing timeout"); return State; } if (HasEnoughItems()) { if (!_condition[ConditionFlag.Fishing]) { _logger.LogDebug("Have enough items, fishing complete"); State = EFishingState.Complete; DisableAutoHookAutoStart(); } return State; } if (!_condition[ConditionFlag.Fishing] && !_condition[ConditionFlag.Casting] && CanUseActionNow() && ActionManager.Instance()->GetActionStatus(ActionType.Action, 289u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Casting fishing rod"); ActionManager.Instance()->UseAction(ActionType.Action, 289u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); _actionThrottle = Environment.TickCount64 + 2000; } return State; } public override void Stop(string label) { base.Stop(label); if (State != EFishingState.Idle || _currentRequest != null) { _logger.LogInformation("Stopping fishing controller: {Label}", label); DisableAutoHookAutoStart(); _routeEnqueuer.ClearDestination(); _taskQueue.Reset(); _currentRequest = null; State = EFishingState.Idle; } } private void DisableAutoHookAutoStart() { try { _autoHookIpc.SetAutoStartFishing(enabled: false); } catch (Exception) { } } private bool HasEnoughItems() { if (_currentRequest == null) { return true; } return GetItemCount() >= _currentRequest.Quantity; } private unsafe int GetItemCount() { if (_currentRequest == null) { return 0; } return InventoryManager.Instance()->GetInventoryItemCount(_currentRequest.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)_currentRequest.Collectability); } private bool CanUseActionNow() { return Environment.TickCount64 >= _actionThrottle; } private void EnqueueNavigateTo(ushort territoryId, Vector3 position) { _routeEnqueuer.Enqueue(_taskQueue, territoryId, position); } private bool EnqueueLurePurchase() { VendorResolverService.VendorInfo? vendorInfo = _vendorResolver.ResolveVendor(29717u); if (!vendorInfo.HasValue) { _logger.LogWarning("No vendor found selling Versatile Lure"); return false; } _logger.LogInformation("No bait in inventory, purchasing {Quantity} Versatile Lure from vendor {VendorId}", 99, vendorInfo.Value.VendorNpcId); EnqueueNavigateTo(vendorInfo.Value.TerritoryId, vendorInfo.Value.Position); _taskQueue.Enqueue(new Mount.UnmountTask()); _taskQueue.Enqueue(new VendorPurchaseTask(vendorInfo.Value.VendorNpcId, new global::_003C_003Ez__ReadOnlySingleElementList(new VendorPurchaseTask.PurchaseItem(29717u, 99)), vendorInfo.Value.DialogOptionIndex)); return true; } }