forked from aly/qstbak
331 lines
11 KiB
C#
331 lines
11 KiB
C#
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<GatheringController>
|
|
{
|
|
internal sealed class CurrentRequest
|
|
{
|
|
public required GatheringRequest Data { get; init; }
|
|
|
|
public required GatheringRoot Root { get; init; }
|
|
|
|
public required List<GatheringNode> 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<GatheringController> 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<ITask> 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<ITask> list2 = list;
|
|
int num = 0;
|
|
ITask[] array = new ITask[1 + list2.Count];
|
|
array[num] = task;
|
|
num++;
|
|
Span<ITask> span = CollectionsMarshal.AsSpan(list2);
|
|
span.CopyTo(new Span<ITask>(array).Slice(num, span.Length));
|
|
num += span.Length;
|
|
taskQueue.EnqueueAll(new global::_003C_003Ez__ReadOnlyArray<ITask>(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<QuestStep> 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();
|
|
}
|
|
}
|
|
}
|
|
}
|