using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using LLib.Fishing; using LLib.GameData; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Shared; internal static class Fish { internal sealed class Factory(AutoHookIpc autoHookIpc) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.Fish || !autoHookIpc.IsAvailable()) { yield break; } yield return new Mount.UnmountTask(); yield return new SwitchClassJob.Task(EClassJob.Fisher); yield return new FishTask(quest, step.ItemsToGather.FirstOrDefault(), step.CompletionQuestVariablesFlags); foreach (GatheredItem item in step.ItemsToGather.Skip(1)) { yield return new FishTask(quest, item, step.CompletionQuestVariablesFlags); } } } internal sealed record FishTask(Quest Quest, GatheredItem? GatheredItem, IList CompletionQuestVariablesFlags) : ITask { public bool HasCompletionQuestVariablesFlags { get; } = QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags); public override string ToString() { return "Fish" + (HasCompletionQuestVariablesFlags ? "*" : "") + ((GatheredItem != null) ? $"({GatheredItem.ItemCount}x {GatheredItem.ItemId})" : ""); } } internal sealed class DoFish(AutoHookIpc autoHookIpc, QuestFunctions questFunctions, IObjectTable objectTable, IChatGui chatGui, SendNotification.Executor sendNotificationExecutor, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private readonly bool _wasAutoHookEnabled = autoHookIpc.GetPluginState(); private bool _started; private bool _cleanupDone; protected override bool Start() { if (HasRequestedItem(base.Task.GatheredItem)) { logger.LogInformation("Already have the requested fish in inventory, skipping fish task."); return false; } if (HasMatchingCompletionQuestWork()) { logger.LogInformation("Quest variables already match, skipping fish task."); return false; } if (!_wasAutoHookEnabled) { autoHookIpc.SetPluginState(enabled: true); if (!autoHookIpc.GetPluginState()) { logger.LogWarning("{ErrorText}", "AutoHook is required for fishing but could not be enabled. Please install or enable AutoHook."); if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Fish, "AutoHook is required for fishing but could not be enabled. Please install or enable AutoHook."))) { chatGui.PrintError("AutoHook is required for fishing but could not be enabled. Please install or enable AutoHook.", "Questionable", 576); } throw new TaskException("AutoHook is required for fishing but could not be enabled. Please install or enable AutoHook."); } } if (!AutoHookPresetData.FishingPresets.TryGetValue((QuestId)base.Task.Quest.Id, out string value)) { logger.LogWarning("No fishing preset found for quest {QuestId}", base.Task.Quest.Id); throw new TaskException($"No fishing preset found for quest {base.Task.Quest.Id}"); } logger.LogInformation("Creating anonymous AutoHook preset for quest {QuestId}", base.Task.Quest.Id); autoHookIpc.CreateAndSelectAnonymousPreset(value); OrientToWater(); autoHookIpc.SetAutoStartFishing(enabled: true); _started = true; return true; } public override ETaskResult Update() { if (HasRequestedItem(base.Task.GatheredItem)) { logger.LogDebug("Requested fish collected, completing task."); Cleanup(); return ETaskResult.TaskComplete; } if (HasMatchingCompletionQuestWork()) { logger.LogDebug("Quest variables match, completing task."); Cleanup(); return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; } public void StopNow() { if (_started) { Cleanup(); } } public override bool ShouldInterruptOnDamage() { return false; } private bool HasMatchingCompletionQuestWork() { if (!base.Task.HasCompletionQuestVariablesFlags) { return false; } QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.Quest.Id); if (questProgressInfo != null) { return QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo); } return false; } private void Cleanup() { if (!_cleanupDone) { logger.LogDebug("Cleaning up fish task."); autoHookIpc.SetAutoStartFishing(enabled: false); StopFishing(); autoHookIpc.DeleteAllAnonymousPresets(); autoHookIpc.SetPluginState(_wasAutoHookEnabled); _cleanupDone = true; } } private unsafe void OrientToWater() { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null) { (float, Vector3)? tuple = WaterSurfaceDetector.FindFishableRotation(localPlayer.Position); if (!tuple.HasValue) { logger.LogWarning("No fishable water found near the player; AutoHook may fail to cast."); return; } Vector3 item = tuple.Value.Item2; ActionManager.Instance()->AutoFaceTargetPosition(&item, 0uL); } } private unsafe static void StopFishing() { ActionManager* ptr = ActionManager.Instance(); if (ptr != null) { ptr->UseAction(ActionType.Action, 299u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); } } } private const uint QuitActionId = 299u; private unsafe static bool HasRequestedItem(GatheredItem? item) { if (item == null) { return false; } InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } return ptr->GetInventoryItemCount(item.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)item.Collectability) >= item.ItemCount; } }