From 82c20bf76d46f21b743f9b26e77c6aa5a8bb864a Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 3 Aug 2024 11:17:20 +0200 Subject: [PATCH] Auto-Moving to gathering locations --- GatheringPathRenderer/RendererPlugin.cs | 32 +++- GatheringPathRenderer/Windows/EditorWindow.cs | 69 +++++-- .../Thavnair/820_Pewter Ore.json | 15 +- .../Urqopacha/974_Mountain Chromite Ore.json | 48 ++++- .../Urqopacha/992_Snow Cotton.json | 55 ++++++ .../Urqopacha/993_Turali Aloe.json | 55 +++++- .../MIN, BTN/4154_Cooking Up a Culture.json | 9 +- QuestPaths/quest-v1.json | 24 +++ Questionable.Model/GatheringMath.cs | 53 ++++++ Questionable.Model/Questing/GatheredItem.cs | 8 + Questionable.Model/Questing/QuestStep.cs | 1 + Questionable.sln.DotSettings | 1 + .../Controller/GatheringController.cs | 179 ++++++++++++++++++ Questionable/Controller/MiniTaskController.cs | 134 +++++++++++++ Questionable/Controller/QuestController.cs | 125 +++--------- .../Steps/Gathering/MoveToLandingLocation.cs | 64 +++++++ .../Controller/Steps/Gathering/WaitGather.cs | 25 +++ .../Controller/Steps/Interactions/Interact.cs | 21 +- .../Steps/Shared/GatheringRequiredItems.cs | 75 ++++++++ Questionable/Data/GatheringData.cs | 49 +++++ Questionable/GameFunctions.cs | 31 ++- Questionable/Questionable.csproj | 1 + Questionable/QuestionablePlugin.cs | 6 + .../QuestComponents/ActiveQuestComponent.cs | 17 +- .../RemainingTasksComponent.cs | 27 ++- Questionable/packages.lock.json | 6 + 26 files changed, 976 insertions(+), 154 deletions(-) create mode 100644 Questionable.Model/GatheringMath.cs create mode 100644 Questionable.Model/Questing/GatheredItem.cs create mode 100644 Questionable/Controller/GatheringController.cs create mode 100644 Questionable/Controller/MiniTaskController.cs create mode 100644 Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs create mode 100644 Questionable/Controller/Steps/Gathering/WaitGather.cs create mode 100644 Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs create mode 100644 Questionable/Data/GatheringData.cs diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs index 8e1c864b..ef2b352c 100644 --- a/GatheringPathRenderer/RendererPlugin.cs +++ b/GatheringPathRenderer/RendererPlugin.cs @@ -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("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 { 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" } }; })))) diff --git a/GatheringPathRenderer/Windows/EditorWindow.cs b/GatheringPathRenderer/Windows/EditorWindow.cs index ae8a77d3..ec7293f6 100644 --- a/GatheringPathRenderer/Windows/EditorWindow.cs +++ b/GatheringPathRenderer/Windows/EditorWindow.cs @@ -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 _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 }))) + .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() @@ -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 nodesInObjectTable = _objectTable + .Where(x => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _target.DataId) + .ToList(); + List 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; } diff --git a/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json b/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json index de175725..d5a75e9c 100644 --- a/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json +++ b/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json @@ -40,6 +40,15 @@ }, "MinimumAngle": 200, "MaximumAngle": 360 + }, + { + "Position": { + "X": -606.7445, + "Y": 38.37634, + "Z": -425.5284 + }, + "MinimumAngle": -80, + "MaximumAngle": 70 } ] } @@ -139,12 +148,12 @@ "Y": 67.64153, "Z": -477.6673 }, - "MinimumAngle": -90, - "MaximumAngle": 60 + "MinimumAngle": -105, + "MaximumAngle": 75 } ] } ] } ] -} +} \ No newline at end of file diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json index 83bd6138..8b3ade12 100644 --- a/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json @@ -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,10 +131,28 @@ }, "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 diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json index 7cf1be27..494b74d6 100644 --- a/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json @@ -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 } ] }, diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json index e17f0f0d..b0117970 100644 --- a/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json @@ -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 + } } ] }, diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json index ea7b708c..4be51dda 100644 --- a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json +++ b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4154_Cooking Up a Culture.json @@ -29,7 +29,14 @@ "Z": -102.983154 }, "TerritoryId": 962, - "InteractionType": "CompleteQuest" + "InteractionType": "CompleteQuest", + "RequiredGatheredItems": [ + { + "ItemId": 35600, + "ItemCount": 6, + "Collectability": 600 + } + ] } ] } diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index a11a610c..f18d33a7 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -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": [ diff --git a/Questionable.Model/GatheringMath.cs b/Questionable.Model/GatheringMath.cs new file mode 100644 index 00000000..46105e1a --- /dev/null +++ b/Questionable.Model/GatheringMath.cs @@ -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) + }; + } +} diff --git a/Questionable.Model/Questing/GatheredItem.cs b/Questionable.Model/Questing/GatheredItem.cs new file mode 100644 index 00000000..41bd0624 --- /dev/null +++ b/Questionable.Model/Questing/GatheredItem.cs @@ -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; } +} diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 53ed7ced..4ec5f81d 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -66,6 +66,7 @@ public sealed class QuestStep public SkipConditions? SkipConditions { get; set; } public List?> RequiredQuestVariables { get; set; } = new(); + public List RequiredGatheredItems { get; set; } = []; public IList CompletionQuestVariablesFlags { get; set; } = new List(); public IList DialogueChoices { get; set; } = new List(); public IList PointMenuChoices { get; set; } = new List(); diff --git a/Questionable.sln.DotSettings b/Questionable.sln.DotSettings index 3588f8f8..a8a650e8 100644 --- a/Questionable.sln.DotSettings +++ b/Questionable.sln.DotSettings @@ -7,6 +7,7 @@ True True True + True True True True diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs new file mode 100644 index 00000000..45fccf64 --- /dev/null +++ b/Questionable/Controller/GatheringController.cs @@ -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 +{ + 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 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() + .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() + .With(_currentRequest.Root.TerritoryId, pointOnFloor ?? averagePosition, 50f, fly: true, + ignoreDistanceToObject: true)); + } + + _taskQueue.Enqueue(_serviceProvider.GetRequiredService() + .With(_currentRequest.Root.TerritoryId, currentNode)); + _taskQueue.Enqueue(_serviceProvider.GetRequiredService() + .With(currentNode.DataId, true)); + _taskQueue.Enqueue(_serviceProvider.GetRequiredService()); + } + + 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 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; } + + /// + /// To make indexing easy with , we flatten the list of gathering locations. + /// + public required List 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, + } +} diff --git a/Questionable/Controller/MiniTaskController.cs b/Questionable/Controller/MiniTaskController.cs new file mode 100644 index 00000000..4d19d73b --- /dev/null +++ b/Questionable/Controller/MiniTaskController.cs @@ -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 +{ + protected readonly IChatGui _chatGui; + protected readonly ILogger _logger; + + protected readonly Queue _taskQueue = new(); + protected ITask? _currentTask; + + public MiniTaskController(IChatGui chatGui, ILogger 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 GetRemainingTaskNames() => + _taskQueue.Select(x => x.ToString() ?? "?").ToList(); +} diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index e50891d2..a7b77ac2 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -14,16 +14,15 @@ using Questionable.Model.Questing; namespace Questionable.Controller; -internal sealed class QuestController +internal sealed class QuestController : MiniTaskController { private readonly IClientState _clientState; private readonly GameFunctions _gameFunctions; private readonly MovementController _movementController; private readonly CombatController _combatController; - private readonly ILogger _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 _taskQueue = new(); - private ITask? _currentTask; private bool _automatic; /// @@ -50,6 +47,7 @@ internal sealed class QuestController GameFunctions gameFunctions, MovementController movementController, CombatController combatController, + GatheringController gatheringController, ILogger logger, QuestRegistry questRegistry, IKeyState keyState, @@ -58,15 +56,15 @@ internal sealed class QuestController Configuration configuration, YesAlreadyIpc yesAlreadyIpc, IEnumerable 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 - { - 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; - } + protected override void OnTaskComplete(ITask task) + { + if (task is WaitAtEnd.WaitQuestCompleted) + _simulatedQuest = null; + } - 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); - - 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 GetRemainingTaskNames() => - _taskQueue.Select(x => x.ToString() ?? "?").ToList(); - public string ToStatString() { return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})"; diff --git a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs new file mode 100644 index 00000000..3336500f --- /dev/null +++ b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs @@ -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 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() + .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}"; +} diff --git a/Questionable/Controller/Steps/Gathering/WaitGather.cs b/Questionable/Controller/Steps/Gathering/WaitGather.cs new file mode 100644 index 00000000..e2b3a889 --- /dev/null +++ b/Questionable/Controller/Steps/Gathering/WaitGather.cs @@ -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"; +} diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index 7dba0d44..df27d3c2 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -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})"; } } diff --git a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs new file mode 100644 index 00000000..ea602723 --- /dev/null +++ b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs @@ -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 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() + .With(null, gatheringRoot.AetheryteShortcut.Value, gatheringRoot.TerritoryId); + } + + yield return serviceProvider.GetRequiredService() + .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})"; + } +} diff --git a/Questionable/Data/GatheringData.cs b/Questionable/Data/GatheringData.cs new file mode 100644 index 00000000..ed44fdc2 --- /dev/null +++ b/Questionable/Data/GatheringData.cs @@ -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 _gatheringItemToItem; + private readonly Dictionary _minerGatheringPoints = []; + private readonly Dictionary _botanistGatheringPoints = []; + + public GatheringData(IDataManager dataManager) + { + _gatheringItemToItem = dataManager.GetExcelSheet()! + .Where(x => x.RowId != 0 && x.Item != 0) + .ToDictionary(x => x.RowId, x => (uint)x.Item); + + foreach (var gatheringPointBase in dataManager.GetExcelSheet()!) + { + 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; + } + } +} diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index 724f2f11..1cd229d1 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -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) - { - _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) diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index ae232ac1..ceb37e8c 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -18,6 +18,7 @@ + diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index df100963..0febd3bd 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -102,9 +104,12 @@ public sealed class QuestionablePlugin : IDalamudPlugin // individual tasks serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); // task factories serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); serviceCollection.AddTaskWithFactory(); @@ -149,6 +154,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index 14b628d3..17ab37f4 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -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 == diff --git a/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs index bd35f699..84d44c90 100644 --- a/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs +++ b/Questionable/Windows/QuestComponents/RemainingTasksComponent.cs @@ -1,4 +1,5 @@ -using ImGuiNET; +using System.Collections.Generic; +using ImGuiNET; using Questionable.Controller; namespace Questionable.Windows.QuestComponents; @@ -6,22 +7,36 @@ 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 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(); + } + } } } diff --git a/Questionable/packages.lock.json b/Questionable/packages.lock.json index 53ca99c0..1fd12865 100644 --- a/Questionable/packages.lock.json +++ b/Questionable/packages.lock.json @@ -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": {