Questionable/Questionable/Controller/GatheringController.cs

320 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
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.Shared;
using Questionable.External;
using Questionable.Functions;
using Questionable.GatheringPaths;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
using Mount = Questionable.Controller.Steps.Common.Mount;
namespace Questionable.Controller;
internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
{
private readonly MovementController _movementController;
private readonly MoveTo.Factory _moveFactory;
private readonly Mount.Factory _mountFactory;
private readonly Interact.Factory _interactFactory;
private readonly GatheringPointRegistry _gatheringPointRegistry;
private readonly GameFunctions _gameFunctions;
private readonly NavmeshIpc _navmeshIpc;
private readonly IObjectTable _objectTable;
private readonly ICondition _condition;
private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui;
private readonly IClientState _clientState;
private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest;
public GatheringController(
MovementController movementController,
MoveTo.Factory moveFactory,
Mount.Factory mountFactory,
Combat.Factory combatFactory,
Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions,
NavmeshIpc navmeshIpc,
IObjectTable objectTable,
IChatGui chatGui,
ILogger<GatheringController> logger,
ICondition condition,
IDataManager dataManager,
ILoggerFactory loggerFactory,
IGameGui gameGui,
IClientState clientState,
IPluginLog pluginLog)
: base(chatGui, mountFactory, combatFactory, condition, logger)
{
_movementController = movementController;
_moveFactory = moveFactory;
_mountFactory = mountFactory;
_interactFactory = interactFactory;
_gatheringPointRegistry = gatheringPointRegistry;
_gameFunctions = gameFunctions;
_navmeshIpc = navmeshIpc;
_objectTable = objectTable;
_condition = condition;
_loggerFactory = loggerFactory;
_gameGui = gameGui;
_clientState = clientState;
_logger = logger;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, 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
// at least in EW-ish, there's one node with 1 fixed location and one node with 3 random locations
.SelectMany(x => x.Nodes.OrderBy(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)
return;
if (!_taskQueue.AllTasksComplete)
return;
var director = UIState.Instance()->DirectorTodo.Director;
if (director != null && director->EventHandlerInfo != null &&
director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector)
{
if (director->Sequence == 254)
return;
_taskQueue.Enqueue(new WaitAtEnd.WaitDelay());
}
GatheringNode? currentNode = FindNextTargetableNodeAndUpdateIndex(_currentRequest);
if (currentNode == null)
return;
ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId;
_taskQueue.Enqueue(_mountFactory.Mount(territoryId, Mount.EMountIf.Always));
bool fly = currentNode.Fly.GetValueOrDefault(_currentRequest.Root.FlyBetweenNodes.GetValueOrDefault(true)) &&
_gameFunctions.IsFlyingUnlocked(territoryId);
if (currentNode.Locations.Count > 1)
{
Vector3 averagePosition = new Vector3
{
X = currentNode.Locations.Sum(x => x.Position.X) / currentNode.Locations.Count,
Y = currentNode.Locations.Select(x => x.Position.Y).Max() + 5f,
Z = currentNode.Locations.Sum(x => x.Position.Z) / currentNode.Locations.Count,
};
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(averagePosition, true);
if (pointOnFloor != null)
pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) };
_taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition,
null, 50f, Fly: fly, IgnoreDistanceToObject: true)));
}
_taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions,
_objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>()));
_taskQueue.Enqueue(_mountFactory.Unmount());
_taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.Gather, true));
QueueGatherNode(currentNode);
}
private void QueueGatherNode(GatheringNode currentNode)
{
foreach (bool revisitRequired in new[] { false, true })
{
_taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions,
_gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
if (_currentRequest.Data.Collectability > 0)
{
_taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this,
_gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
}
}
}
public bool HasRequestedItems()
{
if (_currentRequest == null)
return true;
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
return false;
return inventoryManager->GetInventoryItemCount(_currentRequest.Data.ItemId,
minCollectability: (short)_currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity;
}
public bool HasNodeDisappeared(GatheringNode node)
{
return !_objectTable.Any(x =>
x.ObjectKind == ObjectKind.GatheringPoint && x.IsTargetable && x.DataId == node.DataId);
}
/// <summary>
/// For leves in particular, there's a good chance you're close enough to all nodes in the next group
/// but none are targetable (if they're not part of the randomly-picked route).
/// </summary>
private GatheringNode? FindNextTargetableNodeAndUpdateIndex(CurrentRequest currentRequest)
{
for (int i = 0; i < currentRequest.Nodes.Count; ++i)
{
int currentIndex = (currentRequest.CurrentIndex + i) % currentRequest.Nodes.Count;
var currentNode = currentRequest.Nodes[currentIndex];
var locationsAsObjects = currentNode.Locations.Select(x =>
_objectTable.FirstOrDefault(y =>
currentNode.DataId == y.DataId && Vector3.Distance(x.Position, y.Position) < 0.1f))
.ToList();
// Are any of the nodes too far away to be found? This is likely around ~100 yalms. All closer gathering
// points are always in the object table, even if they're not targetable.
if (locationsAsObjects.Any(x => x == null))
{
currentRequest.CurrentIndex = (currentIndex + 1) % currentRequest.Nodes.Count;
return currentNode;
}
// If any are targetable, this group should be targeted as part of the route.
if (locationsAsObjects.Any(x => x is { IsTargetable: true }))
{
currentRequest.CurrentIndex = (currentIndex + 1) % currentRequest.Nodes.Count;
return currentNode;
}
}
// unsure what to even do here
return null;
}
public override IList<string> GetRemainingTaskNames()
{
if (_taskQueue.CurrentTask is {} currentTask)
return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
else
return base.GetRemainingTaskNames();
}
public void OnNormalToast(SeString message)
{
if (_revisitRegex.IsMatch(message.TextValue))
{
if (_taskQueue.CurrentTask is IRevisitAware currentTaskRevisitAware)
currentTaskRevisitAware.OnRevisit();
foreach (ITask task in _taskQueue.RemainingTasks)
{
if (task is IRevisitAware taskRevisitAware)
taskRevisitAware.OnRevisit();
}
}
}
internal sealed class CurrentRequest
{
public required GatheringRequest Data { get; init; }
public required GatheringRoot Root { get; init; }
/// <summary>
/// To make indexing easy with <see cref="CurrentIndex"/>, we flatten the list of gathering locations.
/// </summary>
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,
}
}