forked from aly/qstbak
365 lines
10 KiB
C#
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;
|
|
}
|
|
}
|