qstbak/Questionable/Questionable.Controller/GatheringController.cs
2025-10-09 08:41:52 +10:00

278 lines
8.8 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 ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest;
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, IPluginLog pluginLog)
: base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger)
{
_movementController = movementController;
_gatheringPointRegistry = gatheringPointRegistry;
_gameFunctions = gameFunctions;
_navmeshIpc = navmeshIpc;
_objectTable = objectTable;
_condition = condition;
_logger = logger;
_revisitRegex = dataManager.GetRegex(5574u, (LogMessage x) => x.Text, pluginLog) ?? throw new InvalidDataException("No regex found for revisit message");
}
public bool Start(GatheringRequest gatheringRequest)
{
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()
};
if (HasRequestedItems())
{
_currentRequest = null;
return false;
}
return true;
}
public EStatus Update()
{
if (_currentRequest == null)
{
Stop("No request");
return EStatus.Complete;
}
if (_movementController.IsPathfinding || _movementController.IsPathfinding)
{
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)
{
GoToNextNode();
}
public override void Stop(string label)
{
_currentRequest = null;
_taskQueue.Reset();
}
private void GoToNextNode()
{
if (_currentRequest == null || !_taskQueue.AllTasksComplete)
{
return;
}
GatheringNode gatheringNode = FindNextTargetableNodeAndUpdateIndex(_currentRequest);
if (gatheringNode == null)
{
return;
}
ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId;
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.MountTask(territoryId, Questionable.Controller.Steps.Common.Mount.EMountIf.Always));
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, 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];
List<IGameObject> source = currentNode.Locations.Select((GatheringLocation x) => _objectTable.FirstOrDefault((IGameObject y) => currentNode.DataId == y.BaseId && Vector3.Distance(x.Position, y.Position) < 0.1f)).ToList();
if (source.Any((IGameObject x) => x == null))
{
currentRequest.CurrentIndex = (num + 1) % currentRequest.Nodes.Count;
return currentNode;
}
if (source.Any((IGameObject x) => x?.IsTargetable ?? false))
{
currentRequest.CurrentIndex = (num + 1) % currentRequest.Nodes.Count;
return currentNode;
}
}
return null;
}
public override IList<string> GetRemainingTaskNames()
{
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
if (task != null)
{
string text = task.ToString() ?? "?";
IList<string> remainingTaskNames = base.GetRemainingTaskNames();
int num = 1 + remainingTaskNames.Count;
List<string> list = new List<string>(num);
CollectionsMarshal.SetCount(list, num);
Span<string> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = text;
num2++;
{
foreach (string item in remainingTaskNames)
{
span[num2] = item;
num2++;
}
return list;
}
}
return base.GetRemainingTaskNames();
}
public void OnNormalToast(SeString message)
{
if (!_revisitRegex.IsMatch(message.TextValue))
{
return;
}
if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware revisitAware)
{
revisitAware.OnRevisit();
}
foreach (ITask remainingTask in _taskQueue.RemainingTasks)
{
if (remainingTask is IRevisitAware revisitAware2)
{
revisitAware2.OnRevisit();
}
}
}
}