1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.Controller/FishingController.cs
2026-08-17 20:29:32 +10:00

365 lines
10 KiB
C#

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<FishingController>
{
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<FishingController> 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<VendorPurchaseTask.PurchaseItem>(new VendorPurchaseTask.PurchaseItem(29717u, 99)), vendorInfo.Value.DialogOptionIndex));
return true;
}
}