1
0
forked from liza/Questionable

Auto-Moving to gathering locations

This commit is contained in:
Liza 2024-08-03 11:17:20 +02:00
parent ff7ee27fde
commit 82c20bf76d
Signed by: liza
GPG Key ID: 7199F8D727D55F67
26 changed files with 976 additions and 154 deletions

View File

@ -2,6 +2,7 @@
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;
@ -12,6 +13,7 @@ using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Schedulers;
using ECommons.SplatoonAPI;
using FFXIVClientStructs.FFXIV.Common.Math;
using GatheringPathRenderer.Windows;
using Questionable.Model;
using Questionable.Model.Gathering;
@ -36,14 +38,15 @@ public sealed class RendererPlugin : IDalamudPlugin
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")
@ -175,7 +178,8 @@ public sealed class RendererPlugin : IDalamudPlugin
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())
{
@ -192,6 +196,8 @@ public sealed class RendererPlugin : IDalamudPlugin
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
@ -219,6 +225,26 @@ public sealed class RendererPlugin : IDalamudPlugin
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"
}
};
}))))

View File

@ -22,15 +22,19 @@ internal sealed class EditorWindow : Window
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;
@ -38,38 +42,44 @@ internal sealed class EditorWindow : Window
_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 })))
.FirstOrDefault();
if (location == null)
.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 (_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()
@ -81,8 +91,9 @@ internal sealed class EditorWindow : Window
{
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);
@ -97,7 +108,7 @@ internal sealed class EditorWindow : Window
}
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();
@ -105,7 +116,7 @@ internal sealed class EditorWindow : Window
}
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;
@ -119,14 +130,33 @@ internal sealed class EditorWindow : Window
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)
{
@ -154,6 +184,7 @@ internal sealed class EditorWindow : Window
_editorCommands.AddToNewGroup(root, _target);
_plugin.Save(targetFile, root);
}
ImGui.EndDisabled();
}
else
@ -176,7 +207,7 @@ internal sealed class EditorWindow : Window
=> _changes.TryGetValue(internalId, out locationOverride);
}
internal class LocationOverride
internal sealed class LocationOverride
{
public int? MinimumAngle { get; set; }
public int? MaximumAngle { get; set; }

View File

@ -40,6 +40,15 @@
},
"MinimumAngle": 200,
"MaximumAngle": 360
},
{
"Position": {
"X": -606.7445,
"Y": 38.37634,
"Z": -425.5284
},
"MinimumAngle": -80,
"MaximumAngle": 70
}
]
}
@ -139,8 +148,8 @@
"Y": 67.64153,
"Z": -477.6673
},
"MinimumAngle": -90,
"MaximumAngle": 60
"MinimumAngle": -105,
"MaximumAngle": 75
}
]
}

View File

@ -2,6 +2,7 @@
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
"AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
@ -39,6 +40,15 @@
},
"MinimumAngle": -50,
"MaximumAngle": 210
},
{
"Position": {
"X": -394.2657,
"Y": -47.86026,
"Z": -394.9654
},
"MinimumAngle": -120,
"MaximumAngle": 120
}
]
}
@ -71,6 +81,24 @@
},
"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
}
]
}
@ -103,6 +131,24 @@
},
"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
}
]
}

View File

@ -2,6 +2,7 @@
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
"AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
@ -26,6 +27,24 @@
"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
}
]
}
@ -58,6 +77,24 @@
},
"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
}
]
}
@ -74,6 +111,24 @@
"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
}
]
},

View File

@ -2,6 +2,7 @@
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": [],
"TerritoryId": 1187,
"AetheryteShortcut": "Urqopacha - Wachunpelo",
"Groups": [
{
"Nodes": [
@ -13,6 +14,24 @@
"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
}
}
]
@ -25,7 +44,9 @@
"X": 269.7338,
"Y": -134.0488,
"Z": -381.6242
}
},
"MinimumAngle": -85,
"MaximumAngle": 145
}
]
}
@ -44,6 +65,24 @@
},
"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
}
]
},
@ -74,6 +113,20 @@
"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
}
}
]
},

View File

@ -29,7 +29,14 @@
"Z": -102.983154
},
"TerritoryId": 962,
"InteractionType": "CompleteQuest"
"InteractionType": "CompleteQuest",
"RequiredGatheredItems": [
{
"ItemId": 35600,
"ItemCount": 6,
"Collectability": 600
}
]
}
]
}

View File

@ -311,6 +311,30 @@
}
}
},
"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": [

View File

@ -0,0 +1,53 @@
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)
};
}
}

View File

@ -0,0 +1,8 @@
namespace Questionable.Model.Questing;
public sealed class GatheredItem
{
public uint ItemId { get; set; }
public int ItemCount { get; set; }
public short Collectability { get; set; }
}

View File

@ -66,6 +66,7 @@ public sealed class QuestStep
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>();

View File

@ -7,6 +7,7 @@
<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>

View File

@ -0,0 +1,179 @@
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,
}
}

View File

@ -0,0 +1,134 @@
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();
}

View File

@ -14,16 +14,15 @@ using Questionable.Model.Questing;
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;
@ -34,8 +33,6 @@ internal sealed class QuestController
private QuestProgress? _startedQuest;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
private readonly Queue<ITask> _taskQueue = new();
private ITask? _currentTask;
private bool _automatic;
/// <summary>
@ -50,6 +47,7 @@ internal sealed class QuestController
GameFunctions gameFunctions,
MovementController movementController,
CombatController combatController,
GatheringController gatheringController,
ILogger<QuestController> logger,
QuestRegistry questRegistry,
IKeyState keyState,
@ -58,15 +56,15 @@ internal sealed class QuestController
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;
@ -138,6 +136,7 @@ internal sealed class QuestController
Stop("HP = 0");
_movementController.Stop();
_combatController.Stop("HP = 0");
_gatheringController.Stop("HP = 0");
}
}
else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE])
@ -147,6 +146,7 @@ internal sealed class QuestController
Stop("ESC pressed");
_movementController.Stop();
_combatController.Stop("ESC pressed");
_gatheringController.Stop("ESC pressed");
}
}
@ -377,9 +377,10 @@ internal sealed class QuestController
_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);
@ -401,6 +402,8 @@ internal sealed class QuestController
}
}
public override void Stop(string label) => Stop(label, false);
public void SimulateQuest(Quest? quest)
{
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestId);
@ -419,103 +422,23 @@ internal sealed class QuestController
_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;
base.UpdateCurrentTask();
}
ETaskResult result;
try
protected override void OnTaskComplete(ITask task)
{
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)
if (task is WaitAtEnd.WaitQuestCompleted)
_simulatedQuest = null;
_currentTask = null;
// handled in next update
return;
case ETaskResult.NextStep:
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
var lastTask = (ILastTask)_currentTask;
_currentTask = null;
IncreaseStepCount(lastTask.QuestId, lastTask.Sequence, true);
return;
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)
@ -536,6 +459,7 @@ internal sealed class QuestController
_movementController.Stop();
_combatController.Stop("Execute next step");
_gatheringController.Stop("Execute next step");
var newTasks = _taskFactories
.SelectMany(x =>
@ -568,9 +492,6 @@ internal sealed class QuestController
_taskQueue.Enqueue(task);
}
public IList<string> GetRemainingTaskNames() =>
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
public string ToStatString()
{
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";

View File

@ -0,0 +1,64 @@
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}";
}

View File

@ -0,0 +1,25 @@
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";
}

View File

@ -59,7 +59,7 @@ internal static class Interact
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);
@ -67,17 +67,19 @@ internal static class Interact
}
// 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;
}
@ -104,11 +106,11 @@ internal static class Interact
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;
}
@ -125,6 +127,11 @@ internal static class Interact
return gameObjectStruct->NamePlateIconId != 0;
}
private static bool IsTargetable(IGameObject gameObject)
{
return gameObject.IsTargetable;
}
public override string ToString() => $"Interact({DataId})";
}
}

View File

@ -0,0 +1,75 @@
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})";
}
}

View File

@ -0,0 +1,49 @@
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;
}
}
}

View File

@ -407,10 +407,13 @@ internal sealed unsafe class GameFunctions
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;
@ -429,19 +432,31 @@ internal sealed unsafe class GameFunctions
{
IGameObject? gameObject = FindObjectByDataId(dataId, kind);
if (gameObject != null)
return InteractWith(gameObject);
_logger.LogDebug("Game object is null");
return false;
}
public bool InteractWith(IGameObject gameObject)
{
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.EntityId);
_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)

View File

@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj" />
<ProjectReference Include="..\LLib\LLib.csproj"/>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>

View File

@ -13,6 +13,7 @@ using Questionable.Controller.CombatModules;
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;
@ -89,6 +90,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<ChatFunctions>();
serviceCollection.AddSingleton<AetherCurrentData>();
serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<GatheringData>();
serviceCollection.AddSingleton<JournalData>();
serviceCollection.AddSingleton<QuestData>();
serviceCollection.AddSingleton<TerritoryData>();
@ -102,9 +104,12 @@ public sealed class QuestionablePlugin : IDalamudPlugin
// 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>();
@ -149,6 +154,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<GameUiController>();
serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}

View File

@ -22,6 +22,7 @@ internal sealed class ActiveQuestComponent
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;
@ -29,14 +30,22 @@ internal sealed class ActiveQuestComponent
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;
@ -93,6 +102,7 @@ internal sealed class ActiveQuestComponent
{
_movementController.Stop();
_questController.Stop("Manual (no active quest)");
_gatheringController.Stop("Manual (no active quest)");
}
}
}
@ -233,6 +243,7 @@ internal sealed class ActiveQuestComponent
{
_movementController.Stop();
_questController.Stop("Manual");
_gatheringController.Stop("Manual");
}
bool lastStep = currentStep ==

View File

@ -1,4 +1,5 @@
using ImGuiNET;
using System.Collections.Generic;
using ImGuiNET;
using Questionable.Controller;
namespace Questionable.Windows.QuestComponents;
@ -6,13 +7,26 @@ 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()
{
IList<string> gatheringTasks = _gatheringController.GetRemainingTaskNames();
if (gatheringTasks.Count > 0)
{
ImGui.Separator();
ImGui.BeginDisabled();
foreach (var task in gatheringTasks)
ImGui.TextUnformatted($"G: {task}");
ImGui.EndDisabled();
}
else
{
var remainingTasks = _questController.GetRemainingTaskNames();
if (remainingTasks.Count > 0)
@ -25,3 +39,4 @@ internal sealed class RemainingTasksComponent
}
}
}
}

View File

@ -179,6 +179,12 @@
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
},
"gatheringpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[1.0.0, )"
}
},
"llib": {
"type": "Project",
"dependencies": {