using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using LLib; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Movement; using Questionable.External; using Questionable.Functions; using Questionable.Model.Gathering; using Questionable.Model.Questing; namespace Questionable.Controller; internal sealed class GatheringController : MiniTaskController { internal sealed class CurrentRequest { public required GatheringRequest Data { get; init; } public required GatheringRoot Root { get; init; } public required List Nodes { get; init; } public int CurrentIndex { get; set; } } public sealed record GatheringRequest(GatheringPointId GatheringPointId, uint ItemId, uint AlternativeItemId, int Quantity, ushort Collectability = 0); public enum EStatus { Gathering, Moving, Complete } private readonly MovementController _movementController; private readonly GatheringPointRegistry _gatheringPointRegistry; private readonly GameFunctions _gameFunctions; private readonly NavmeshIpc _navmeshIpc; private readonly IObjectTable _objectTable; private readonly ICondition _condition; private readonly Configuration _configuration; private readonly Regex _revisitRegex; private CurrentRequest? _currentRequest; private int _movementFailureCount; public bool IsRunning => _currentRequest != null; public uint? CurrentItemId => _currentRequest?.Data.ItemId; public GatheringController(MovementController movementController, GatheringPointRegistry gatheringPointRegistry, GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui, ILogger logger, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, Configuration configuration, IPluginLog pluginLog) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, configuration, logger) { _movementController = movementController; _gatheringPointRegistry = gatheringPointRegistry; _gameFunctions = gameFunctions; _navmeshIpc = navmeshIpc; _objectTable = objectTable; _condition = condition; _configuration = configuration; _revisitRegex = dataManager.GetRegex(5574u, (LogMessage x) => x.Text, pluginLog) ?? throw new InvalidDataException("No regex found for revisit message"); } public bool Start(GatheringRequest gatheringRequest) { Stop("Start"); if (!_gatheringPointRegistry.TryGetGatheringPoint(gatheringRequest.GatheringPointId, out GatheringRoot gatheringRoot)) { _logger.LogError("Unable to resolve gathering point, no path found for {ItemId} / point {PointId}", gatheringRequest.ItemId, gatheringRequest.GatheringPointId); return false; } _currentRequest = new CurrentRequest { Data = gatheringRequest, Root = gatheringRoot, Nodes = gatheringRoot.Groups.SelectMany((GatheringNodeGroup x) => x.Nodes.OrderBy((GatheringNode y) => y.Locations.Count)).ToList() }; _movementFailureCount = 0; if (HasRequestedItems()) { _currentRequest = null; return false; } return true; } public EStatus Update() { if (_currentRequest == null) { Stop("No request"); return EStatus.Complete; } if (_movementController.IsPathfinding || _movementController.IsPathRunning) { return EStatus.Moving; } if (HasRequestedItems() && !_condition[ConditionFlag.Gathering]) { Stop("Has all items"); return EStatus.Complete; } if (_taskQueue.AllTasksComplete) { GoToNextNode(); } UpdateCurrentTask(); return EStatus.Gathering; } protected override void OnTaskComplete(ITask task) { if (task is DoGather.Task) { ResetStepRetryCounter(); _movementFailureCount = 0; } GoToNextNode(); } protected override void OnRetryStep() { _logger.LogInformation("Retrying gathering from the next targetable node"); } public void Replan() { if (_currentRequest != null) { _logger.LogInformation("Re-planning gathering for point {PointId}", _currentRequest.Data.GatheringPointId); _taskQueue.Reset(); } } public bool TryReplanAfterMovementFailure() { if (_currentRequest == null) { return false; } _movementFailureCount++; if (_movementFailureCount > _configuration.Advanced.InteractionRetryLimit) { _logger.LogError("Gathering movement retry limit exceeded after {Count} failures", _movementFailureCount); return false; } if (_taskQueue.CurrentTaskExecutor?.CurrentTask is MoveTask { InteractionType: EInteractionType.WalkTo, DataId: null, Fly: not false, DisableNavmesh: false } moveTask) { List list = _taskQueue.RemainingTasks.ToList(); MoveTask moveTask2 = moveTask._003CClone_003E_0024(); Vector3 destination = moveTask.Destination; destination.Y = moveTask.Destination.Y + 30f; moveTask2.Destination = destination; moveTask2.DisableNavmesh = true; MoveTask moveTask3 = moveTask2; _logger.LogInformation("Navmesh could not reach the gathering cluster; retrying the broad approach as a direct flight"); _taskQueue.Reset(); TaskQueue taskQueue = _taskQueue; ITask task = moveTask3; List list2 = list; int num = 0; ITask[] array = new ITask[1 + list2.Count]; array[num] = task; num++; Span span = CollectionsMarshal.AsSpan(list2); span.CopyTo(new Span(array).Slice(num, span.Length)); num += span.Length; taskQueue.EnqueueAll(new global::_003C_003Ez__ReadOnlyArray(array)); return true; } _logger.LogInformation("Gathering movement failed, selecting another active node (attempt {Count}/{Limit})", _movementFailureCount, _configuration.Advanced.InteractionRetryLimit); Replan(); return true; } public override void Stop(string label) { base.Stop(label); _currentRequest = null; _taskQueue.Reset(); } private void GoToNextNode() { if (_currentRequest == null || !_taskQueue.AllTasksComplete) { return; } GatheringNode gatheringNode = FindNextTargetableNodeAndUpdateIndex(_currentRequest); if (gatheringNode == null) { return; } if (_currentRequest.Root.Steps.Count == 0 || gatheringNode.Locations.Count == 0) { Stop("Empty gathering plan"); return; } List steps = _currentRequest.Root.Steps; ushort territoryId = steps[steps.Count - 1].TerritoryId; bool? fly = gatheringNode.Fly; bool? flyBetweenNodes = _currentRequest.Root.FlyBetweenNodes; bool flag = (fly ?? flyBetweenNodes ?? true) && _gameFunctions.IsFlyingUnlocked(territoryId); if (gatheringNode.Locations.Count > 1) { Vector3 vector = new Vector3 { X = gatheringNode.Locations.Sum((GatheringLocation x) => x.Position.X) / (float)gatheringNode.Locations.Count, Y = gatheringNode.Locations.Select((GatheringLocation x) => x.Position.Y).Max() + 5f, Z = gatheringNode.Locations.Sum((GatheringLocation x) => x.Position.Z) / (float)gatheringNode.Locations.Count }; Vector3? vector2 = _navmeshIpc.GetPointOnFloor(vector, unlandable: true); if (vector2.HasValue) { Vector3 value = vector2.Value; value.Y = vector2.Value.Y + (flag ? 3f : 0f); vector2 = value; } TaskQueue taskQueue = _taskQueue; Vector3 destination = vector2 ?? vector; float? stopDistance = 50f; bool fly2 = flag; taskQueue.Enqueue(new MoveTask(territoryId, destination, null, MountRequired: false, DismountRequired: false, stopDistance, null, DisableNavmesh: false, null, fly2, Land: false, IgnoreDistanceToObject: true, RestartNavigation: true, EInteractionType.WalkTo)); } _taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, flag, gatheringNode)); _taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask()); _taskQueue.Enqueue(new Interact.Task(gatheringNode.DataId, null, EInteractionType.Gather, SkipMarkerCheck: true)); QueueGatherNode(gatheringNode); } private void QueueGatherNode(GatheringNode currentNode) { bool[] array = new bool[2] { false, true }; foreach (bool revisitRequired in array) { _taskQueue.Enqueue(new DoGather.Task(_currentRequest.Data, currentNode, revisitRequired)); if (_currentRequest.Data.Collectability > 0) { _taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired)); } } } public unsafe bool HasRequestedItems() { if (_currentRequest == null) { return true; } InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } return ptr->GetInventoryItemCount(_currentRequest.Data.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)_currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity; } public bool HasNodeDisappeared(GatheringNode node) { return !_objectTable.Any((IGameObject x) => x.ObjectKind == ObjectKind.GatheringPoint && x.IsTargetable && x.BaseId == node.DataId); } private GatheringNode? FindNextTargetableNodeAndUpdateIndex(CurrentRequest currentRequest) { for (int i = 0; i < currentRequest.Nodes.Count; i++) { int num = (currentRequest.CurrentIndex + i) % currentRequest.Nodes.Count; GatheringNode currentNode = currentRequest.Nodes[num]; if (_objectTable.Any((IGameObject x) => x.ObjectKind == ObjectKind.GatheringPoint && x.BaseId == currentNode.DataId && x.IsTargetable)) { currentRequest.CurrentIndex = (num + 1) % currentRequest.Nodes.Count; return currentNode; } } for (int num2 = 0; num2 < currentRequest.Nodes.Count; num2++) { int num3 = (currentRequest.CurrentIndex + num2) % currentRequest.Nodes.Count; GatheringNode currentNode2 = currentRequest.Nodes[num3]; if (currentNode2.Locations.Select((GatheringLocation x) => _objectTable.FirstOrDefault((IGameObject y) => currentNode2.DataId == y.BaseId && Vector3.Distance(x.Position, y.Position) < 0.1f)).ToList().Any((IGameObject x) => x == null)) { currentRequest.CurrentIndex = (num3 + 1) % currentRequest.Nodes.Count; return currentNode2; } } return null; } public void OnNormalToast(SeString message) { if (!_revisitRegex.IsMatch(message.TextValue)) { return; } _logger.LogDebug("Revisit triggered for the current gathering node"); if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware revisitAware) { revisitAware.OnRevisit(); } foreach (ITask remainingTask in _taskQueue.RemainingTasks) { if (remainingTask is IRevisitAware revisitAware2) { revisitAware2.OnRevisit(); } } } }