using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ECommons;
using ECommons.Schedulers;
using ECommons.SplatoonAPI;
+using FFXIVClientStructs.FFXIV.Common.Math;
using GatheringPathRenderer.Windows;
using Questionable.Model;
using Questionable.Model.Gathering;
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
- IPluginLog pluginLog)
+ IObjectTable objectTable, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_clientState = clientState;
_pluginLog = pluginLog;
_editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui);
- _editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState) { IsOpen = true };
+ _editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable)
+ { IsOpen = true };
_windowSystem.AddWindow(_editorWindow);
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
bool isCone = false;
int minimumAngle = 0;
int maximumAngle = 0;
- if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && locationOverride != null)
+ if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) &&
+ locationOverride != null)
{
if (locationOverride.IsCone())
{
maximumAngle = x.MaximumAngle.GetValueOrDefault();
}
+ var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
+ var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
return new List<Element>
{
new Element(isCone
Enabled = true,
overlayText =
$"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}",
+ },
+ new Element(ElementType.CircleAtFixedCoordinates)
+ {
+ refX = a.X,
+ refY = a.Z,
+ refZ = a.Y,
+ color = _colors[0],
+ radius = 0.1f,
+ Enabled = true,
+ overlayText = "Min Angle"
+ },
+ new Element(ElementType.CircleAtFixedCoordinates)
+ {
+ refX = b.X,
+ refY = b.Z,
+ refZ = b.Y,
+ color = _colors[1],
+ radius = 0.1f,
+ Enabled = true,
+ overlayText = "Max Angle"
}
};
}))))
private readonly IDataManager _dataManager;
private readonly ITargetManager _targetManager;
private readonly IClientState _clientState;
+ private readonly IObjectTable _objectTable;
private readonly Dictionary<Guid, LocationOverride> _changes = [];
private IGameObject? _target;
- private (RendererPlugin.GatheringLocationContext, GatheringLocation)? _targetLocation;
+
+ private (RendererPlugin.GatheringLocationContext Context, GatheringNode Node, GatheringLocation Location)?
+ _targetLocation;
+
private string _newFileName = string.Empty;
public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager,
- ITargetManager targetManager, IClientState clientState)
+ ITargetManager targetManager, IClientState clientState, IObjectTable objectTable)
: base("Gathering Path Editor###QuestionableGatheringPathEditor")
{
_plugin = plugin;
_dataManager = dataManager;
_targetManager = targetManager;
_clientState = clientState;
+ _objectTable = objectTable;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(300, 300),
};
+ ShowCloseButton = false;
}
public override void Update()
{
_target = _targetManager.Target;
- if (_target == null || _target.ObjectKind != ObjectKind.GatheringPoint)
- {
- _targetLocation = null;
- return;
- }
-
var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType);
var location = gatheringLocations.SelectMany(context =>
context.Root.Groups.SelectMany(group =>
group.Nodes
- .Where(node => node.DataId == _target.DataId)
- .SelectMany(node => node.Locations)
- .Where(location => Vector3.Distance(location.Position, _target.Position) < 0.1f)
- .Select(location => new { Context = context, Location = location })))
+ .SelectMany(node => node.Locations
+ .Where(location =>
+ {
+ if (_target != null)
+ return Vector3.Distance(location.Position, _target.Position) < 0.1f;
+ else
+ return Vector3.Distance(location.Position, _clientState.LocalPlayer!.Position) < 3f;
+ })
+ .Select(location => new { Context = context, Node = node, Location = location }))))
.FirstOrDefault();
- if (location == null)
+ if (_target != null && _target.ObjectKind != ObjectKind.GatheringPoint || location == null)
{
+ _target = null;
_targetLocation = null;
return;
}
- _targetLocation = (location.Context, location.Location);
+ _target ??= _objectTable.FirstOrDefault(
+ x => x.ObjectKind == ObjectKind.GatheringPoint &&
+ x.DataId == location.Node.DataId &&
+ Vector3.Distance(location.Location.Position, _clientState.LocalPlayer!.Position) < 3f);
+ _targetLocation = (location.Context, location.Node, location.Location);
}
public override bool DrawConditions()
{
if (_target != null && _targetLocation != null)
{
- var context = _targetLocation.Value.Item1;
- var location = _targetLocation.Value.Item2;
+ var context = _targetLocation.Value.Context;
+ var node = _targetLocation.Value.Node;
+ var location = _targetLocation.Value.Location;
ImGui.Text(context.File.Directory?.Name ?? string.Empty);
ImGui.Indent();
ImGui.Text(context.File.Name);
}
int minAngle = locationOverride.MinimumAngle ?? location.MinimumAngle.GetValueOrDefault();
- if (ImGui.DragInt("Min Angle", ref minAngle, 5, -180, 360))
+ if (ImGui.DragInt("Min Angle", ref minAngle, 5, -360, 360))
{
locationOverride.MinimumAngle = minAngle;
locationOverride.MaximumAngle ??= location.MaximumAngle.GetValueOrDefault();
}
int maxAngle = locationOverride.MaximumAngle ?? location.MaximumAngle.GetValueOrDefault();
- if (ImGui.DragInt("Max Angle", ref maxAngle, 5, -180, 360))
+ if (ImGui.DragInt("Max Angle", ref maxAngle, 5, -360, 360))
{
locationOverride.MinimumAngle ??= location.MinimumAngle.GetValueOrDefault();
locationOverride.MaximumAngle = maxAngle;
location.MaximumAngle = locationOverride.MaximumAngle;
_plugin.Save(context.File, context.Root);
}
+
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
_changes[location.InternalId] = new LocationOverride();
_plugin.Redraw();
}
+
ImGui.EndDisabled();
+
+ List<IGameObject> nodesInObjectTable = _objectTable
+ .Where(x => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _target.DataId)
+ .ToList();
+ List<IGameObject> missingLocations = nodesInObjectTable
+ .Where(x => !node.Locations.Any(y => Vector3.Distance(x.Position, y.Position) < 0.1f))
+ .ToList();
+ if (missingLocations.Count > 0)
+ {
+ if (ImGui.Button("Add missing locations"))
+ {
+ foreach (var missing in missingLocations)
+ _editorCommands.AddToExistingGroup(context.Root, missing);
+
+ _plugin.Save(context.File, context.Root);
+ }
+ }
}
else if (_target != null)
{
_editorCommands.AddToNewGroup(root, _target);
_plugin.Save(targetFile, root);
}
+
ImGui.EndDisabled();
}
else
=> _changes.TryGetValue(internalId, out locationOverride);
}
-internal class LocationOverride
+internal sealed class LocationOverride
{
public int? MinimumAngle { get; set; }
public int? MaximumAngle { get; set; }
},
"MinimumAngle": 200,
"MaximumAngle": 360
+ },
+ {
+ "Position": {
+ "X": -606.7445,
+ "Y": 38.37634,
+ "Z": -425.5284
+ },
+ "MinimumAngle": -80,
+ "MaximumAngle": 70
}
]
}
"Y": 67.64153,
"Z": -477.6673
},
- "MinimumAngle": -90,
- "MaximumAngle": 60
+ "MinimumAngle": -105,
+ "MaximumAngle": 75
}
]
}
]
}
]
-}
+}
\ No newline at end of file
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
+ "AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
},
"MinimumAngle": -50,
"MaximumAngle": 210
+ },
+ {
+ "Position": {
+ "X": -394.2657,
+ "Y": -47.86026,
+ "Z": -394.9654
+ },
+ "MinimumAngle": -120,
+ "MaximumAngle": 120
}
]
}
},
"MinimumAngle": 225,
"MaximumAngle": 360
+ },
+ {
+ "Position": {
+ "X": -532.3487,
+ "Y": -22.79275,
+ "Z": -510.8069
+ },
+ "MinimumAngle": 135,
+ "MaximumAngle": 270
+ },
+ {
+ "Position": {
+ "X": -536.2922,
+ "Y": -23.79476,
+ "Z": -526.0406
+ },
+ "MinimumAngle": -110,
+ "MaximumAngle": 35
}
]
}
},
"MinimumAngle": 0,
"MaximumAngle": 150
+ },
+ {
+ "Position": {
+ "X": -431.5875,
+ "Y": -16.68724,
+ "Z": -656.528
+ },
+ "MinimumAngle": -35,
+ "MaximumAngle": 90
+ },
+ {
+ "Position": {
+ "X": -439.8079,
+ "Y": -16.67447,
+ "Z": -654.6749
+ },
+ "MinimumAngle": -45,
+ "MaximumAngle": 85
}
]
}
]
}
]
-}
+}
\ No newline at end of file
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
+ "AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
"Y": -129.3952,
"Z": -396.6573
}
+ },
+ {
+ "Position": {
+ "X": -16.08351,
+ "Y": -137.6674,
+ "Z": -464.35
+ },
+ "MinimumAngle": -65,
+ "MaximumAngle": 145
+ },
+ {
+ "Position": {
+ "X": -9.000858,
+ "Y": -134.9256,
+ "Z": -439.0332
+ },
+ "MinimumAngle": -125,
+ "MaximumAngle": 105
}
]
}
},
"MinimumAngle": -180,
"MaximumAngle": 45
+ },
+ {
+ "Position": {
+ "X": -249.7221,
+ "Y": -96.55618,
+ "Z": -386.2397
+ },
+ "MinimumAngle": 35,
+ "MaximumAngle": 280
+ },
+ {
+ "Position": {
+ "X": -241.8424,
+ "Y": -99.37369,
+ "Z": -386.2889
+ },
+ "MinimumAngle": -300,
+ "MaximumAngle": -45
}
]
}
"Y": -85.61841,
"Z": -240.1007
}
+ },
+ {
+ "Position": {
+ "X": -116.6446,
+ "Y": -93.99508,
+ "Z": -274.6102
+ },
+ "MinimumAngle": -140,
+ "MaximumAngle": 150
+ },
+ {
+ "Position": {
+ "X": -133.936,
+ "Y": -91.54122,
+ "Z": -273.3963
+ },
+ "MinimumAngle": -155,
+ "MaximumAngle": 85
}
]
},
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
+ "AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
"X": 242.7737,
"Y": -135.9734,
"Z": -431.2313
+ },
+ "MinimumAngle": -55,
+ "MaximumAngle": 100
+ },
+ {
+ "Position": {
+ "X": 302.1836,
+ "Y": -135.4149,
+ "Z": -359.7965
+ },
+ "MinimumAngle": 5,
+ "MaximumAngle": 155
+ },
+ {
+ "Position": {
+ "X": 256.1657,
+ "Y": -135.744,
+ "Z": -414.7577
}
}
]
"X": 269.7338,
"Y": -134.0488,
"Z": -381.6242
- }
+ },
+ "MinimumAngle": -85,
+ "MaximumAngle": 145
}
]
}
},
"MinimumAngle": 105,
"MaximumAngle": 345
+ },
+ {
+ "Position": {
+ "X": 401.9319,
+ "Y": -150.0004,
+ "Z": -408.114
+ },
+ "MinimumAngle": -70,
+ "MaximumAngle": 85
+ },
+ {
+ "Position": {
+ "X": 406.1098,
+ "Y": -152.2166,
+ "Z": -364.7227
+ },
+ "MinimumAngle": -210,
+ "MaximumAngle": 35
}
]
},
"Y": -161.1972,
"Z": -644.0471
}
+ },
+ {
+ "Position": {
+ "X": 307.4235,
+ "Y": -159.1669,
+ "Z": -622.6444
+ }
+ },
+ {
+ "Position": {
+ "X": 348.5925,
+ "Y": -165.3805,
+ "Z": -671.4193
+ }
}
]
},
"Z": -102.983154
},
"TerritoryId": 962,
- "InteractionType": "CompleteQuest"
+ "InteractionType": "CompleteQuest",
+ "RequiredGatheredItems": [
+ {
+ "ItemId": 35600,
+ "ItemCount": 6,
+ "Collectability": 600
+ }
+ ]
}
]
}
}
}
},
+ "RequiredGatheredItems": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "ItemId": {
+ "type": "number"
+ },
+ "ItemCount": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "Collectability": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1000
+ }
+ },
+ "required": [
+ "ItemId",
+ "ItemCount"
+ ]
+ }
+ },
"DelaySecondsAtStart": {
"description": "Time to wait before starting",
"type": [
--- /dev/null
+using System;
+using System.Numerics;
+using Questionable.Model.Gathering;
+
+namespace GatheringPathRenderer;
+
+public static class GatheringMath
+{
+ private static readonly Random RNG = new Random();
+
+ public static (Vector3, int, float) CalculateLandingLocation(GatheringLocation location)
+ {
+ int degrees;
+ if (location.IsCone())
+ degrees = RNG.Next(
+ location.MinimumAngle.GetValueOrDefault(),
+ location.MaximumAngle.GetValueOrDefault());
+ else
+ degrees = RNG.Next(0, 360);
+
+ float range = RNG.Next(
+ (int)(location.CalculateMinimumDistance() * 100),
+ (int)((location.CalculateMaximumDistance() - location.CalculateMinimumDistance()) * 100)) / 100f;
+ return (CalculateLandingLocation(location.Position, degrees, range), degrees, range);
+ }
+
+ public static Vector3 CalculateLandingLocation(GatheringLocation location, float angleScale, float rangeScale)
+ {
+ int degrees;
+ if (location.IsCone())
+ degrees = location.MinimumAngle.GetValueOrDefault()
+ + (int)(angleScale * (location.MaximumAngle.GetValueOrDefault() -
+ location.MinimumAngle.GetValueOrDefault()));
+ else
+ degrees = (int)(rangeScale * 360);
+
+ float range =
+ location.CalculateMinimumDistance() +
+ rangeScale * (location.CalculateMaximumDistance() - location.CalculateMinimumDistance());
+ return CalculateLandingLocation(location.Position, degrees, range);
+ }
+
+ private static Vector3 CalculateLandingLocation(Vector3 position, int degrees, float range)
+ {
+ float rad = -(float)(degrees * Math.PI / 180);
+ return new Vector3
+ {
+ X = position.X + range * (float)Math.Sin(rad),
+ Y = position.Y,
+ Z = position.Z + range * (float)Math.Cos(rad)
+ };
+ }
+}
--- /dev/null
+namespace Questionable.Model.Questing;
+
+public sealed class GatheredItem
+{
+ public uint ItemId { get; set; }
+ public int ItemCount { get; set; }
+ public short Collectability { get; set; }
+}
public SkipConditions? SkipConditions { get; set; }
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
+ public List<GatheredItem> RequiredGatheredItems { get; set; } = [];
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
<s:Boolean x:Key="/Default/UserDictionary/Words/=bestways/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=braax/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=brightploom/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/UserDictionary/Words/=collectability/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=earthenshire/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=electrope/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hanu/@EntryIndexedValue">True</s:Boolean>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Dalamud.Game.ClientState.Objects.Enums;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+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.Data;
+using Questionable.External;
+using Questionable.GatheringPaths;
+using Questionable.Model.Gathering;
+
+namespace Questionable.Controller;
+
+internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
+{
+ private readonly MovementController _movementController;
+ private readonly GatheringData _gatheringData;
+ private readonly GameFunctions _gameFunctions;
+ private readonly NavmeshIpc _navmeshIpc;
+ private readonly IObjectTable _objectTable;
+ private readonly IServiceProvider _serviceProvider;
+
+ private CurrentRequest? _currentRequest;
+
+ public GatheringController(MovementController movementController, GatheringData gatheringData,
+ GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui,
+ ILogger<GatheringController> logger, IServiceProvider serviceProvider)
+ : base(chatGui, logger)
+ {
+ _movementController = movementController;
+ _gatheringData = gatheringData;
+ _gameFunctions = gameFunctions;
+ _navmeshIpc = navmeshIpc;
+ _objectTable = objectTable;
+ _serviceProvider = serviceProvider;
+ }
+
+ public bool Start(GatheringRequest gatheringRequest)
+ {
+ if (!AssemblyGatheringLocationLoader.GetLocations()
+ .TryGetValue(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(x => x.Nodes)
+ .ToList(),
+ };
+
+ if (HasRequestedItems())
+ {
+ _currentRequest = null;
+ return false;
+ }
+
+ return true;
+ }
+
+ public EStatus Update()
+ {
+ if (_currentRequest == null)
+ return EStatus.Complete;
+
+ if (_movementController.IsPathfinding || _movementController.IsPathfinding)
+ return EStatus.Moving;
+
+ if (HasRequestedItems())
+ return EStatus.Complete;
+
+ if (_currentTask == null && _taskQueue.Count == 0)
+ GoToNextNode();
+
+ UpdateCurrentTask();
+ return EStatus.Gathering;
+ }
+
+ protected override void OnTaskComplete(ITask task) => GoToNextNode();
+
+ public override void Stop(string label)
+ {
+ _currentRequest = null;
+ _currentTask = null;
+ _taskQueue.Clear();
+ }
+
+ private void GoToNextNode()
+ {
+ if (_currentRequest == null)
+ return;
+
+ if (_taskQueue.Count > 0)
+ return;
+
+ var currentNode = _currentRequest.Nodes[_currentRequest.CurrentIndex++ % _currentRequest.Nodes.Count];
+
+ _taskQueue.Enqueue(_serviceProvider.GetRequiredService<MountTask>()
+ .With(_currentRequest.Root.TerritoryId, MountTask.EMountIf.Always));
+ 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);
+ if (pointOnFloor != null)
+ pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 3f };
+
+ _taskQueue.Enqueue(_serviceProvider.GetRequiredService<Move.MoveInternal>()
+ .With(_currentRequest.Root.TerritoryId, pointOnFloor ?? averagePosition, 50f, fly: true,
+ ignoreDistanceToObject: true));
+ }
+
+ _taskQueue.Enqueue(_serviceProvider.GetRequiredService<MoveToLandingLocation>()
+ .With(_currentRequest.Root.TerritoryId, currentNode));
+ _taskQueue.Enqueue(_serviceProvider.GetRequiredService<Interact.DoInteract>()
+ .With(currentNode.DataId, true));
+ _taskQueue.Enqueue(_serviceProvider.GetRequiredService<WaitGather>());
+ }
+
+ private bool HasRequestedItems()
+ {
+ if (_currentRequest == null)
+ return true;
+
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ if (inventoryManager == null)
+ return false;
+
+ return inventoryManager->GetInventoryItemCount(_currentRequest.Data.ItemId,
+ minCollectability: _currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity;
+ }
+
+ public override IList<string> GetRemainingTaskNames()
+ {
+ if (_currentTask != null)
+ return [_currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
+ else
+ return base.GetRemainingTaskNames();
+ }
+
+ private 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(ushort GatheringPointId, uint ItemId, int Quantity, short Collectability = 0);
+
+ public enum EStatus
+ {
+ Gathering,
+ Moving,
+ Complete,
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps;
+using Questionable.Controller.Steps.Shared;
+
+namespace Questionable.Controller;
+
+internal abstract class MiniTaskController<T>
+{
+ protected readonly IChatGui _chatGui;
+ protected readonly ILogger<T> _logger;
+
+ protected readonly Queue<ITask> _taskQueue = new();
+ protected ITask? _currentTask;
+
+ public MiniTaskController(IChatGui chatGui, ILogger<T> logger)
+ {
+ _chatGui = chatGui;
+ _logger = logger;
+ }
+
+ protected virtual void UpdateCurrentTask()
+ {
+ if (_currentTask == null)
+ {
+ if (_taskQueue.TryDequeue(out ITask? upcomingTask))
+ {
+ try
+ {
+ _logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
+ if (upcomingTask.Start())
+ {
+ _currentTask = upcomingTask;
+ return;
+ }
+ else
+ {
+ _logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
+ _chatGui.PrintError(
+ $"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details.");
+ Stop("Task failed to start");
+ return;
+ }
+ }
+ else
+ return;
+ }
+
+ ETaskResult result;
+ try
+ {
+ result = _currentTask.Update();
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
+ _chatGui.PrintError(
+ $"[Questionable] Failed to update task '{_currentTask}', please check /xllog for details.");
+ Stop("Task failed to update");
+ return;
+ }
+
+ switch (result)
+ {
+ case ETaskResult.StillRunning:
+ return;
+
+ case ETaskResult.SkipRemainingTasksForStep:
+ _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
+ _currentTask, result);
+ _currentTask = null;
+
+ while (_taskQueue.TryDequeue(out ITask? nextTask))
+ {
+ if (nextTask is ILastTask)
+ {
+ _currentTask = nextTask;
+ return;
+ }
+ }
+
+ return;
+
+ case ETaskResult.TaskComplete:
+ _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
+ _currentTask, result, _taskQueue.Count);
+
+ OnTaskComplete(_currentTask);
+
+ _currentTask = null;
+
+ // handled in next update
+ return;
+
+ case ETaskResult.NextStep:
+ _logger.LogInformation("{Task} → {Result}", _currentTask, result);
+
+ var lastTask = (ILastTask)_currentTask;
+ _currentTask = null;
+
+ OnNextStep(lastTask);
+ return;
+
+ case ETaskResult.End:
+ _logger.LogInformation("{Task} → {Result}", _currentTask, result);
+ _currentTask = null;
+ Stop("Task end");
+ return;
+ }
+ }
+
+ protected virtual void OnTaskComplete(ITask task)
+ {
+ }
+
+ protected virtual void OnNextStep(ILastTask task)
+ {
+
+ }
+
+ public abstract void Stop(string label);
+
+ public virtual IList<string> GetRemainingTaskNames() =>
+ _taskQueue.Select(x => x.ToString() ?? "?").ToList();
+}
namespace Questionable.Controller;
-internal sealed class QuestController
+internal sealed class QuestController : MiniTaskController<QuestController>
{
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController;
private readonly CombatController _combatController;
- private readonly ILogger<QuestController> _logger;
+ private readonly GatheringController _gatheringController;
private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState;
- private readonly IChatGui _chatGui;
private readonly ICondition _condition;
private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc;
private QuestProgress? _startedQuest;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
- private readonly Queue<ITask> _taskQueue = new();
- private ITask? _currentTask;
private bool _automatic;
/// <summary>
GameFunctions gameFunctions,
MovementController movementController,
CombatController combatController,
+ GatheringController gatheringController,
ILogger<QuestController> logger,
QuestRegistry questRegistry,
IKeyState keyState,
Configuration configuration,
YesAlreadyIpc yesAlreadyIpc,
IEnumerable<ITaskFactory> taskFactories)
+ : base(chatGui, logger)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_combatController = combatController;
- _logger = logger;
+ _gatheringController = gatheringController;
_questRegistry = questRegistry;
_keyState = keyState;
- _chatGui = chatGui;
_condition = condition;
_configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc;
Stop("HP = 0");
_movementController.Stop();
_combatController.Stop("HP = 0");
+ _gatheringController.Stop("HP = 0");
}
}
else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE])
Stop("ESC pressed");
_movementController.Stop();
_combatController.Stop("ESC pressed");
+ _gatheringController.Stop("ESC pressed");
}
}
_yesAlreadyIpc.RestoreYesAlready();
_combatController.Stop("ClearTasksInternal");
+ _gatheringController.Stop("ClearTasksInternal");
}
- public void Stop(string label, bool continueIfAutomatic = false)
+ public void Stop(string label, bool continueIfAutomatic)
{
using var scope = _logger.BeginScope(label);
}
}
+ public override void Stop(string label) => Stop(label, false);
+
public void SimulateQuest(Quest? quest)
{
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestId);
_nextQuest = null;
}
- private void UpdateCurrentTask()
+ protected override void UpdateCurrentTask()
{
if (_gameFunctions.IsOccupied())
return;
- if (_currentTask == null)
- {
- if (_taskQueue.TryDequeue(out ITask? upcomingTask))
- {
- try
- {
- _logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
- if (upcomingTask.Start())
- {
- _currentTask = upcomingTask;
- return;
- }
- else
- {
- _logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
- return;
- }
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
- _chatGui.PrintError(
- $"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details.");
- Stop("Task failed to start");
- return;
- }
- }
- else
- return;
- }
-
- ETaskResult result;
- try
- {
- result = _currentTask.Update();
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
- _chatGui.PrintError(
- $"[Questionable] Failed to update task '{_currentTask}', please check /xllog for details.");
- Stop("Task failed to update");
- return;
- }
-
- switch (result)
- {
- case ETaskResult.StillRunning:
- return;
-
- case ETaskResult.SkipRemainingTasksForStep:
- _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
- _currentTask, result);
- _currentTask = null;
-
- while (_taskQueue.TryDequeue(out ITask? nextTask))
- {
- if (nextTask is ILastTask)
- {
- _currentTask = nextTask;
- return;
- }
- }
-
- return;
-
- case ETaskResult.TaskComplete:
- _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
- _currentTask, result, _taskQueue.Count);
-
- if (_currentTask is WaitAtEnd.WaitQuestCompleted)
- _simulatedQuest = null;
-
- _currentTask = null;
-
- // handled in next update
- return;
-
- case ETaskResult.NextStep:
- _logger.LogInformation("{Task} → {Result}", _currentTask, result);
+ base.UpdateCurrentTask();
+ }
- var lastTask = (ILastTask)_currentTask;
- _currentTask = null;
- IncreaseStepCount(lastTask.QuestId, lastTask.Sequence, true);
- return;
+ protected override void OnTaskComplete(ITask task)
+ {
+ if (task is WaitAtEnd.WaitQuestCompleted)
+ _simulatedQuest = null;
+ }
- case ETaskResult.End:
- _logger.LogInformation("{Task} → {Result}", _currentTask, result);
- _currentTask = null;
- Stop("Task end");
- return;
- }
+ protected override void OnNextStep(ILastTask task)
+ {
+ IncreaseStepCount(task.QuestId, task.Sequence, true);
}
public void ExecuteNextStep(bool automatic)
_movementController.Stop();
_combatController.Stop("Execute next step");
+ _gatheringController.Stop("Execute next step");
var newTasks = _taskFactories
.SelectMany(x =>
_taskQueue.Enqueue(task);
}
- public IList<string> GetRemainingTaskNames() =>
- _taskQueue.Select(x => x.ToString() ?? "?").ToList();
-
public string ToStatString()
{
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
--- /dev/null
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+using System.Security.Cryptography;
+using Dalamud.Game.ClientState.Objects.Enums;
+using Dalamud.Plugin.Services;
+using GatheringPathRenderer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.Shared;
+using Questionable.External;
+using Questionable.Model.Gathering;
+
+namespace Questionable.Controller.Steps.Gathering;
+
+internal sealed class MoveToLandingLocation(
+ IServiceProvider serviceProvider,
+ IObjectTable objectTable,
+ NavmeshIpc navmeshIpc,
+ ILogger<MoveToLandingLocation> logger) : ITask
+{
+ private ushort _territoryId;
+ private GatheringNode _gatheringNode = null!;
+ private ITask _moveTask = null!;
+
+ public ITask With(ushort territoryId, GatheringNode gatheringNode)
+ {
+ _territoryId = territoryId;
+ _gatheringNode = gatheringNode;
+ return this;
+ }
+
+ public bool Start()
+ {
+ var location = _gatheringNode.Locations.First();
+ if (_gatheringNode.Locations.Count > 1)
+ {
+ var gameObject = objectTable.Single(x =>
+ x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _gatheringNode.DataId && x.IsTargetable);
+ location = _gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
+ }
+
+ var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
+ logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
+ target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
+
+ Vector3? pointOnFloor = navmeshIpc.GetPointOnFloor(target with { Y = target.Y + 5f });
+ if (pointOnFloor != null)
+ pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 0.5f };
+
+ logger.LogInformation("Final landing location: {Location}",
+ (pointOnFloor ?? target).ToString("G", CultureInfo.InvariantCulture));
+
+ _moveTask = serviceProvider.GetRequiredService<Move.MoveInternal>()
+ .With(_territoryId, pointOnFloor ?? target, 0.25f, dataId: _gatheringNode.DataId, fly: true,
+ ignoreDistanceToObject: true);
+ return _moveTask.Start();
+ }
+
+ public ETaskResult Update() => _moveTask.Update();
+
+ public override string ToString() => $"Land/{_moveTask}";
+}
--- /dev/null
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Plugin.Services;
+
+namespace Questionable.Controller.Steps.Gathering;
+
+internal sealed class WaitGather(ICondition condition) : ITask
+{
+ private bool _wasGathering;
+
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ if (condition[ConditionFlag.Gathering])
+ {
+ _wasGathering = true;
+ }
+
+ return _wasGathering && !condition[ConditionFlag.Gathering]
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => "WaitGather";
+}
public bool Start()
{
- IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
+ IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId, targetable: true);
if (gameObject == null)
{
logger.LogWarning("No game object with dataId {DataId}", DataId);
}
// this is only relevant for followers on quests
- if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted])
+ if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] &&
+ gameObject.ObjectKind != ObjectKind.GatheringPoint)
{
+ logger.LogInformation("Preparing interaction for {DataId} by unmounting", DataId);
_needsUnmount = true;
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
- if (gameObject.IsTargetable && HasAnyMarker(gameObject))
+ if (IsTargetable(gameObject) && HasAnyMarker(gameObject))
{
- _interacted = gameFunctions.InteractWith(DataId);
+ _interacted = gameFunctions.InteractWith(gameObject);
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
if (!_interacted)
{
- IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
- if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
+ IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId, targetable: true);
+ if (gameObject == null || !IsTargetable(gameObject) || !HasAnyMarker(gameObject))
return ETaskResult.StillRunning;
- _interacted = gameFunctions.InteractWith(DataId);
+ _interacted = gameFunctions.InteractWith(gameObject);
_continueAt = DateTime.Now.AddSeconds(0.5);
return ETaskResult.StillRunning;
}
return gameObjectStruct->NamePlateIconId != 0;
}
+ private static bool IsTargetable(IGameObject gameObject)
+ {
+ return gameObject.IsTargetable;
+ }
+
public override string ToString() => $"Interact({DataId})";
}
}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Data;
+using Questionable.GatheringPaths;
+using Questionable.Model;
+using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.Steps.Shared;
+
+internal static class GatheringRequiredItems
+{
+ internal sealed class Factory(
+ IServiceProvider serviceProvider,
+ IClientState clientState,
+ GatheringData gatheringData) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ foreach (var requiredGatheredItems in step.RequiredGatheredItems)
+ {
+ if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId,
+ clientState.LocalPlayer!.ClassJob.Id, out var gatheringPointId))
+ throw new TaskException($"No gathering point found for item {requiredGatheredItems.ItemId}");
+
+ if (!AssemblyGatheringLocationLoader.GetLocations()
+ .TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
+ throw new TaskException("No path found for gathering point");
+
+ if (gatheringRoot.AetheryteShortcut != null && clientState.TerritoryType != gatheringRoot.TerritoryId)
+ {
+ yield return serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>()
+ .With(null, gatheringRoot.AetheryteShortcut.Value, gatheringRoot.TerritoryId);
+ }
+
+ yield return serviceProvider.GetRequiredService<StartGathering>()
+ .With(gatheringPointId, requiredGatheredItems);
+ }
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new NotImplementedException();
+ }
+
+ internal sealed class StartGathering(GatheringController gatheringController) : ITask
+ {
+ private ushort _gatheringPointId;
+ private GatheredItem _gatheredItem = null!;
+
+ public ITask With(ushort gatheringPointId, GatheredItem gatheredItem)
+ {
+ _gatheringPointId = gatheringPointId;
+ _gatheredItem = gatheredItem;
+ return this;
+ }
+
+ public bool Start()
+ {
+ return gatheringController.Start(new GatheringController.GatheringRequest(_gatheringPointId,
+ _gatheredItem.ItemId, _gatheredItem.ItemCount, _gatheredItem.Collectability));
+ }
+
+ public ETaskResult Update()
+ {
+ if (gatheringController.Update() == GatheringController.EStatus.Complete)
+ return ETaskResult.TaskComplete;
+
+ return ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId})";
+ }
+}
--- /dev/null
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using Lumina.Excel.GeneratedSheets;
+
+namespace Questionable.Data;
+
+internal sealed class GatheringData
+{
+ private readonly Dictionary<uint, uint> _gatheringItemToItem;
+ private readonly Dictionary<uint, ushort> _minerGatheringPoints = [];
+ private readonly Dictionary<uint, ushort> _botanistGatheringPoints = [];
+
+ public GatheringData(IDataManager dataManager)
+ {
+ _gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
+ .Where(x => x.RowId != 0 && x.Item != 0)
+ .ToDictionary(x => x.RowId, x => (uint)x.Item);
+
+ foreach (var gatheringPointBase in dataManager.GetExcelSheet<GatheringPointBase>()!)
+ {
+ foreach (var gatheringItemId in gatheringPointBase.Item.Where(x => x != 0))
+ {
+ if (_gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
+ {
+ if (gatheringPointBase.GatheringType.Row is 0 or 1)
+ _minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
+ else if (gatheringPointBase.GatheringType.Row is 2 or 3)
+ _botanistGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
+ }
+ }
+ }
+ }
+
+
+ public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId)
+ {
+ if (classJobId == 16)
+ return _minerGatheringPoints.TryGetValue(itemId, out gatheringPointId);
+ else if (classJobId == 17)
+ return _botanistGatheringPoints.TryGetValue(itemId, out gatheringPointId);
+ else
+ {
+ gatheringPointId = 0;
+ return false;
+ }
+ }
+}
playerState->IsAetherCurrentUnlocked(aetherCurrentId);
}
- public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null)
+ public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null, bool targetable = false)
{
foreach (var gameObject in _objectTable)
{
+ if (targetable && !gameObject.IsTargetable)
+ continue;
+
if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
or ObjectKind.Retainer or ObjectKind.Housing)
continue;
{
IGameObject? gameObject = FindObjectByDataId(dataId, kind);
if (gameObject != null)
- {
- _logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.EntityId);
- _targetManager.Target = null;
- _targetManager.Target = gameObject;
+ return InteractWith(gameObject);
+
+ _logger.LogDebug("Game object is null");
+ return false;
+ }
+ public bool InteractWith(IGameObject gameObject)
+ {
+ _logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
+ _targetManager.Target = null;
+ _targetManager.Target = gameObject;
+
+ if (gameObject.ObjectKind == ObjectKind.GatheringPoint)
+ {
+ TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
+ _logger.LogInformation("Interact result: (none) for GatheringPoint");
+ return true;
+ }
+ else
+ {
long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
_logger.LogInformation("Interact result: {Result}", result);
return result != 7 && result > 0;
}
-
- _logger.LogDebug("Game object is null");
- return false;
}
public bool UseItem(uint itemId)
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj" />
<ProjectReference Include="..\LLib\LLib.csproj"/>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
using Questionable.Controller.NavigationOverrides;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Steps.Common;
+using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.External;
serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>();
+ serviceCollection.AddSingleton<GatheringData>();
serviceCollection.AddSingleton<JournalData>();
serviceCollection.AddSingleton<QuestData>();
serviceCollection.AddSingleton<TerritoryData>();
// individual tasks
serviceCollection.AddTransient<MountTask>();
serviceCollection.AddTransient<UnmountTask>();
+ serviceCollection.AddTransient<MoveToLandingLocation>();
+ serviceCollection.AddTransient<WaitGather>();
// task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
+ serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering>();
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
serviceCollection.AddSingleton<GameUiController>();
serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<CombatController>();
+ serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}
private readonly QuestController _questController;
private readonly MovementController _movementController;
private readonly CombatController _combatController;
+ private readonly GatheringController _gatheringController;
private readonly GameFunctions _gameFunctions;
private readonly ICommandManager _commandManager;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestRegistry _questRegistry;
private readonly IChatGui _chatGui;
- public ActiveQuestComponent(QuestController questController, MovementController movementController,
- CombatController combatController, GameFunctions gameFunctions, ICommandManager commandManager,
- IDalamudPluginInterface pluginInterface, Configuration configuration, QuestRegistry questRegistry,
+ public ActiveQuestComponent(
+ QuestController questController,
+ MovementController movementController,
+ CombatController combatController,
+ GatheringController gatheringController,
+ GameFunctions gameFunctions,
+ ICommandManager commandManager,
+ IDalamudPluginInterface pluginInterface,
+ Configuration configuration,
+ QuestRegistry questRegistry,
IChatGui chatGui)
{
_questController = questController;
_movementController = movementController;
_combatController = combatController;
+ _gatheringController = gatheringController;
_gameFunctions = gameFunctions;
_commandManager = commandManager;
_pluginInterface = pluginInterface;
{
_movementController.Stop();
_questController.Stop("Manual (no active quest)");
+ _gatheringController.Stop("Manual (no active quest)");
}
}
}
{
_movementController.Stop();
_questController.Stop("Manual");
+ _gatheringController.Stop("Manual");
}
bool lastStep = currentStep ==
-using ImGuiNET;
+using System.Collections.Generic;
+using ImGuiNET;
using Questionable.Controller;
namespace Questionable.Windows.QuestComponents;
internal sealed class RemainingTasksComponent
{
private readonly QuestController _questController;
+ private readonly GatheringController _gatheringController;
- public RemainingTasksComponent(QuestController questController)
+ public RemainingTasksComponent(QuestController questController, GatheringController gatheringController)
{
_questController = questController;
+ _gatheringController = gatheringController;
}
public void Draw()
{
- var remainingTasks = _questController.GetRemainingTaskNames();
- if (remainingTasks.Count > 0)
+ IList<string> gatheringTasks = _gatheringController.GetRemainingTaskNames();
+ if (gatheringTasks.Count > 0)
{
ImGui.Separator();
ImGui.BeginDisabled();
- foreach (var task in remainingTasks)
- ImGui.TextUnformatted(task);
+ foreach (var task in gatheringTasks)
+ ImGui.TextUnformatted($"G: {task}");
ImGui.EndDisabled();
}
+ else
+ {
+ var remainingTasks = _questController.GetRemainingTaskNames();
+ if (remainingTasks.Count > 0)
+ {
+ ImGui.Separator();
+ ImGui.BeginDisabled();
+ foreach (var task in remainingTasks)
+ ImGui.TextUnformatted(task);
+ ImGui.EndDisabled();
+ }
+ }
}
}
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
},
+ "gatheringpaths": {
+ "type": "Project",
+ "dependencies": {
+ "Questionable.Model": "[1.0.0, )"
+ }
+ },
"llib": {
"type": "Project",
"dependencies": {