diff --git a/GatheringPathRenderer/EditorCommands.cs b/GatheringPathRenderer/EditorCommands.cs index d5008b27..d15e6207 100644 --- a/GatheringPathRenderer/EditorCommands.cs +++ b/GatheringPathRenderer/EditorCommands.cs @@ -93,7 +93,7 @@ internal sealed class EditorCommands : IDisposable } else { - (targetFile, root) = CreateNewFile(gatheringPoint, target, string.Join(" ", arguments)); + (targetFile, root) = CreateNewFile(gatheringPoint, target); _chatGui.Print($"Creating new file under {targetFile.FullName}", "qG"); } @@ -164,12 +164,8 @@ internal sealed class EditorCommands : IDisposable } } - public (FileInfo targetFile, GatheringRoot root) CreateNewFile(GatheringPoint gatheringPoint, IGameObject target, - string fileName) + public (FileInfo targetFile, GatheringRoot root) CreateNewFile(GatheringPoint gatheringPoint, IGameObject target) { - if (string.IsNullOrEmpty(fileName)) - throw new ArgumentException(nameof(fileName)); - // determine target folder DirectoryInfo? targetFolder = _plugin.GetLocationsInTerritory(_clientState.TerritoryType).FirstOrDefault() ?.File.Directory; @@ -183,7 +179,8 @@ internal sealed class EditorCommands : IDisposable FileInfo targetFile = new FileInfo( - Path.Combine(targetFolder.FullName, $"{gatheringPoint.GatheringPointBase.Row}_{fileName}.json")); + Path.Combine(targetFolder.FullName, + $"{gatheringPoint.GatheringPointBase.Row}_{gatheringPoint.PlaceName.Value!.Name}_{(_clientState.LocalPlayer!.ClassJob.Id == 16 ? "MIN" : "BTN")}.json")); var root = new GatheringRoot { TerritoryId = _clientState.TerritoryType, diff --git a/GatheringPathRenderer/Windows/EditorWindow.cs b/GatheringPathRenderer/Windows/EditorWindow.cs index ec7293f6..d2edf65e 100644 --- a/GatheringPathRenderer/Windows/EditorWindow.cs +++ b/GatheringPathRenderer/Windows/EditorWindow.cs @@ -7,6 +7,7 @@ using System.Numerics; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; @@ -31,8 +32,6 @@ internal sealed class EditorWindow : Window 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, IObjectTable objectTable) : base("Gathering Path Editor###QuestionableGatheringPathEditor") @@ -68,13 +67,19 @@ internal sealed class EditorWindow : Window }) .Select(location => new { Context = context, Node = node, Location = location })))) .FirstOrDefault(); - if (_target != null && _target.ObjectKind != ObjectKind.GatheringPoint || location == null) + if (_target != null && _target.ObjectKind != ObjectKind.GatheringPoint) { _target = null; _targetLocation = null; return; } + if (location == null) + { + _targetLocation = null; + return; + } + _target ??= _objectTable.FirstOrDefault( x => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == location.Node.DataId && @@ -123,13 +128,18 @@ internal sealed class EditorWindow : Window _plugin.Redraw(); } - ImGui.BeginDisabled(locationOverride.MinimumAngle == null && locationOverride.MaximumAngle == null); + bool unsaved = locationOverride is { MinimumAngle: not null, MaximumAngle: not null }; + ImGui.BeginDisabled(!unsaved); + if (unsaved) + ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DalamudRed); if (ImGui.Button("Save")) { location.MinimumAngle = locationOverride.MinimumAngle; location.MaximumAngle = locationOverride.MaximumAngle; _plugin.Save(context.File, context.Root); } + if (unsaved) + ImGui.PopStyleColor(); ImGui.SameLine(); if (ImGui.Button("Reset")) @@ -189,16 +199,11 @@ internal sealed class EditorWindow : Window } else { - ImGui.InputText("File Name", ref _newFileName, 128); - ImGui.BeginDisabled(string.IsNullOrEmpty(_newFileName)); if (ImGui.Button("Create location")) { - var (targetFile, root) = _editorCommands.CreateNewFile(gatheringPoint, _target, _newFileName); + var (targetFile, root) = _editorCommands.CreateNewFile(gatheringPoint, _target); _plugin.Save(targetFile, root); - _newFileName = string.Empty; } - - ImGui.EndDisabled(); } } } diff --git a/GatheringPaths/6.x - Endwalker/Garlemald/822_Monitoring Station G_MIN.json b/GatheringPaths/6.x - Endwalker/Garlemald/822_Monitoring Station G_MIN.json new file mode 100644 index 00000000..2ba70d89 --- /dev/null +++ b/GatheringPaths/6.x - Endwalker/Garlemald/822_Monitoring Station G_MIN.json @@ -0,0 +1,158 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": [], + "TerritoryId": 958, + "AetheryteShortcut": "Garlemald - Camp Broken Glass", + "Groups": [ + { + "Nodes": [ + { + "DataId": 33932, + "Locations": [ + { + "Position": { + "X": -80.95969, + "Y": -9.810837, + "Z": 462.2579 + }, + "MinimumAngle": 130, + "MaximumAngle": 260 + } + ] + }, + { + "DataId": 33933, + "Locations": [ + { + "Position": { + "X": -72.11935, + "Y": -10.90324, + "Z": 471.2258 + }, + "MinimumAngle": 105, + "MaximumAngle": 250 + }, + { + "Position": { + "X": -98.97565, + "Y": -5.664787, + "Z": 463.9966 + }, + "MinimumAngle": 60, + "MaximumAngle": 230 + }, + { + "Position": { + "X": -63.49503, + "Y": -11.21235, + "Z": 469.3839 + }, + "MinimumAngle": 80, + "MaximumAngle": 255 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33931, + "Locations": [ + { + "Position": { + "X": -61.34306, + "Y": 6.11244, + "Z": 318.3409 + }, + "MinimumAngle": -120, + "MaximumAngle": 70 + }, + { + "Position": { + "X": -61.47854, + "Y": 6.076105, + "Z": 281.4938 + }, + "MinimumAngle": 65, + "MaximumAngle": 240 + }, + { + "Position": { + "X": -73.25829, + "Y": 6.108262, + "Z": 302.9926 + }, + "MinimumAngle": 50, + "MaximumAngle": 220 + } + ] + }, + { + "DataId": 33930, + "Locations": [ + { + "Position": { + "X": -51.28564, + "Y": 6.088318, + "Z": 318.0529 + }, + "MinimumAngle": -65, + "MaximumAngle": 110 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33935, + "Locations": [ + { + "Position": { + "X": 72.58704, + "Y": -11.59895, + "Z": 354.757 + }, + "MinimumAngle": 75, + "MaximumAngle": 235 + }, + { + "Position": { + "X": 65.33016, + "Y": -11.61111, + "Z": 358.7321 + }, + "MinimumAngle": 65, + "MaximumAngle": 235 + }, + { + "Position": { + "X": 68.21196, + "Y": -11.81954, + "Z": 366.5172 + }, + "MinimumAngle": 5, + "MaximumAngle": 85 + } + ] + }, + { + "DataId": 33934, + "Locations": [ + { + "Position": { + "X": 81.30492, + "Y": -11.53227, + "Z": 347.9922 + }, + "MinimumAngle": 50, + "MaximumAngle": 215 + } + ] + } + ] + } + ] +} diff --git a/GatheringPaths/6.x - Endwalker/Mare Lamentorum/821_The Crushing Brand_MIN.json b/GatheringPaths/6.x - Endwalker/Mare Lamentorum/821_The Crushing Brand_MIN.json new file mode 100644 index 00000000..0b667246 --- /dev/null +++ b/GatheringPaths/6.x - Endwalker/Mare Lamentorum/821_The Crushing Brand_MIN.json @@ -0,0 +1,157 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": [], + "TerritoryId": 959, + "Groups": [ + { + "Nodes": [ + { + "DataId": 33929, + "Locations": [ + { + "Position": { + "X": 304.4121, + "Y": 118.8077, + "Z": 673.4494 + }, + "MinimumAngle": 50, + "MaximumAngle": 230 + }, + { + "Position": { + "X": 297.7666, + "Y": 119.4976, + "Z": 679.5604 + }, + "MinimumAngle": 50, + "MaximumAngle": 220 + }, + { + "Position": { + "X": 322.163, + "Y": 119.0883, + "Z": 657.4384 + }, + "MinimumAngle": 55, + "MaximumAngle": 235 + } + ] + }, + { + "DataId": 33928, + "Locations": [ + { + "Position": { + "X": 313.72, + "Y": 118.3442, + "Z": 664.8668 + }, + "MinimumAngle": 60, + "MaximumAngle": 230 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33927, + "Locations": [ + { + "Position": { + "X": 394.3838, + "Y": 144.7951, + "Z": 820.7851 + }, + "MinimumAngle": 75, + "MaximumAngle": 250 + }, + { + "Position": { + "X": 421.0549, + "Y": 143.6111, + "Z": 805.9457 + }, + "MinimumAngle": 60, + "MaximumAngle": 225 + }, + { + "Position": { + "X": 414.2961, + "Y": 143.2405, + "Z": 811.3884 + }, + "MinimumAngle": 65, + "MaximumAngle": 230 + } + ] + }, + { + "DataId": 33926, + "Locations": [ + { + "Position": { + "X": 405.2481, + "Y": 143.6621, + "Z": 816.6496 + }, + "MinimumAngle": 75, + "MaximumAngle": 230 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 33925, + "Locations": [ + { + "Position": { + "X": 474.679, + "Y": 143.4776, + "Z": 698.5961 + }, + "MinimumAngle": 20, + "MaximumAngle": 170 + }, + { + "Position": { + "X": 474.8585, + "Y": 144.2588, + "Z": 685.7468 + }, + "MinimumAngle": 0, + "MaximumAngle": 155 + }, + { + "Position": { + "X": 467.506, + "Y": 144.9235, + "Z": 654.2 + }, + "MinimumAngle": 0, + "MaximumAngle": 150 + } + ] + }, + { + "DataId": 33924, + "Locations": [ + { + "Position": { + "X": 470.7754, + "Y": 144.8793, + "Z": 672.114 + }, + "MinimumAngle": -5, + "MaximumAngle": 165 + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json b/GatheringPaths/6.x - Endwalker/Thavnair/820_The Hamsa Hatchery_MIN.json similarity index 100% rename from GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json rename to GatheringPaths/6.x - Endwalker/Thavnair/820_The Hamsa Hatchery_MIN.json diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Chabameki_MIN.json similarity index 100% rename from GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json rename to GatheringPaths/7.x - Dawntrail/Urqopacha/974_Chabameki_MIN.json diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Chabameki_BTN.json similarity index 100% rename from GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json rename to GatheringPaths/7.x - Dawntrail/Urqopacha/992_Chabameki_BTN.json diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Chabayuqeq_MIN.json similarity index 100% rename from GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json rename to GatheringPaths/7.x - Dawntrail/Urqopacha/993_Chabayuqeq_MIN.json diff --git a/QuestPaths/6.x - Endwalker/4807_DebugGathering.json b/QuestPaths/6.x - Endwalker/4807_DebugGathering.json new file mode 100644 index 00000000..3d411f43 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/4807_DebugGathering.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 255, + "Steps": [ + { + "Position": { + "X": -435.39066, + "Y": -9.809827, + "Z": -594.5472 + }, + "TerritoryId": 1187, + "InteractionType": "WalkTo", + "Fly": true, + "RequiredGatheredItems": [ + { + "ItemId": 43992, + "ItemCount": 1234 + } + ] + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json index 9e198c81..d9f54e22 100644 --- a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json +++ b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4153_Cultured Pursuits.json @@ -59,7 +59,8 @@ }, "StopDistance": 7, "TerritoryId": 962, - "InteractionType": "CompleteQuest" + "InteractionType": "CompleteQuest", + "NextQuestId": 4154 } ] } 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 4be51dda..30720303 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 @@ -1,7 +1,6 @@ { "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", "Author": "liza", - "Disabled": true, "QuestSequence": [ { "Sequence": 0, @@ -30,13 +29,19 @@ }, "TerritoryId": 962, "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Old Sharlayan", + "AethernetShortcut": [ + "[Old Sharlayan] Aetheryte Plaza", + "[Old Sharlayan] The Studium" + ], "RequiredGatheredItems": [ { "ItemId": 35600, "ItemCount": 6, "Collectability": 600 } - ] + ], + "NextQuestId": 4155 } ] } diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json new file mode 100644 index 00000000..ca598552 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4155_The Culture of Ceruleum.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1038501, + "Position": { + "X": -367.3305, + "Y": 21.846018, + "Z": -102.983154 + }, + "TerritoryId": 962, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1038501, + "Position": { + "X": -367.3305, + "Y": 21.846018, + "Z": -102.983154 + }, + "TerritoryId": 962, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Old Sharlayan", + "AethernetShortcut": [ + "[Old Sharlayan] Aetheryte Plaza", + "[Old Sharlayan] The Studium" + ], + "RequiredGatheredItems": [ + { + "ItemId": 35601, + "ItemCount": 6, + "Collectability": 600 + } + ], + "NextQuestId": 4156 + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json new file mode 100644 index 00000000..d25034d3 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4156_The Culture of Carrots.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1038501, + "Position": { + "X": -367.3305, + "Y": 21.846018, + "Z": -102.983154 + }, + "TerritoryId": 962, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1038501, + "Position": { + "X": -367.3305, + "Y": 21.846018, + "Z": -102.983154 + }, + "TerritoryId": 962, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Old Sharlayan", + "AethernetShortcut": [ + "[Old Sharlayan] Aetheryte Plaza", + "[Old Sharlayan] The Studium" + ], + "RequiredGatheredItems": [ + { + "ItemId": 35602, + "ItemCount": 6, + "Collectability": 600 + } + ], + "NextQuestId": 4157 + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json new file mode 100644 index 00000000..a59c4db7 --- /dev/null +++ b/QuestPaths/6.x - Endwalker/Studium Deliveries/MIN, BTN/4157_Hinageshi in Hingashi.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1038501, + "Position": { + "X": -367.3305, + "Y": 21.846018, + "Z": -102.983154 + }, + "TerritoryId": 962, + "InteractionType": "AcceptQuest" + } + ] + } + ] +} diff --git a/Questionable.Model/Questing/EAction.cs b/Questionable.Model/Questing/EAction.cs index 4b24dee1..c078e4a3 100644 --- a/Questionable.Model/Questing/EAction.cs +++ b/Questionable.Model/Questing/EAction.cs @@ -15,6 +15,18 @@ public enum EAction RedGulal = 29382, YellowGulal = 29383, BlueGulal = 29384, + + CollectMiner = 240, + ScourMiner = 22182, + MeticulousMiner = 22184, + ScrutinyMiner = 22185, + + CollectBotanist = 815, + ScourBotanist = 22186, + MeticulousBotanist = 22188, + ScrutinyBotanist = 22189, + + } public static class EActionExtensions diff --git a/Questionable.Model/Questing/GatheredItem.cs b/Questionable.Model/Questing/GatheredItem.cs index 41bd0624..bfc6fd1a 100644 --- a/Questionable.Model/Questing/GatheredItem.cs +++ b/Questionable.Model/Questing/GatheredItem.cs @@ -4,5 +4,5 @@ public sealed class GatheredItem { public uint ItemId { get; set; } public int ItemCount { get; set; } - public short Collectability { get; set; } + public ushort Collectability { get; set; } } diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 45fccf64..ff19d3f4 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; @@ -12,7 +13,6 @@ 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; @@ -22,25 +22,31 @@ 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 readonly ICondition _condition; private CurrentRequest? _currentRequest; - public GatheringController(MovementController movementController, GatheringData gatheringData, - GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui, - ILogger logger, IServiceProvider serviceProvider) + public GatheringController( + MovementController movementController, + GameFunctions gameFunctions, + NavmeshIpc navmeshIpc, + IObjectTable objectTable, + IChatGui chatGui, + ILogger logger, + IServiceProvider serviceProvider, + ICondition condition) : base(chatGui, logger) { _movementController = movementController; - _gatheringData = gatheringData; _gameFunctions = gameFunctions; _navmeshIpc = navmeshIpc; _objectTable = objectTable; _serviceProvider = serviceProvider; + _condition = condition; } public bool Start(GatheringRequest gatheringRequest) @@ -58,7 +64,8 @@ internal sealed unsafe class GatheringController : MiniTaskController x.Nodes) + // at least in EW-ish, there's one node with 1 fixed location and one node with 3 random locations + .SelectMany(x => x.Nodes.OrderBy(y => y.Locations.Count)) .ToList(), }; @@ -79,7 +86,7 @@ internal sealed unsafe class GatheringController : MiniTaskController x.Position.Y).Max() + 5f, Z = currentNode.Locations.Sum(x => x.Position.Z) / currentNode.Locations.Count, }; - Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(averagePosition); + bool fly = _gameFunctions.IsFlyingUnlocked(_currentRequest.Root.TerritoryId); + Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(averagePosition, true); if (pointOnFloor != null) - pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 3f }; + pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) }; _taskQueue.Enqueue(_serviceProvider.GetRequiredService() - .With(_currentRequest.Root.TerritoryId, pointOnFloor ?? averagePosition, 50f, fly: true, + .With(_currentRequest.Root.TerritoryId, pointOnFloor ?? averagePosition, 50f, fly: fly, ignoreDistanceToObject: true)); } @@ -131,7 +139,13 @@ internal sealed unsafe class GatheringController : MiniTaskController() .With(currentNode.DataId, true)); - _taskQueue.Enqueue(_serviceProvider.GetRequiredService()); + _taskQueue.Enqueue(_serviceProvider.GetRequiredService() + .With(_currentRequest.Data, currentNode)); + if (_currentRequest.Data.Collectability > 0) + { + _taskQueue.Enqueue(_serviceProvider.GetRequiredService() + .With(_currentRequest.Data, currentNode)); + } } private bool HasRequestedItems() @@ -144,7 +158,13 @@ internal sealed unsafe class GatheringController : MiniTaskControllerGetInventoryItemCount(_currentRequest.Data.ItemId, - minCollectability: _currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity; + minCollectability: (short)_currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity; + } + + public bool HasNodeDisappeared(GatheringNode node) + { + return !_objectTable.Any(x => + x.ObjectKind == ObjectKind.GatheringPoint && x.IsTargetable && x.DataId == node.DataId); } public override IList GetRemainingTaskNames() @@ -168,7 +188,11 @@ internal sealed unsafe class GatheringController : MiniTaskController 0.5f; } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index a7b77ac2..9b7fe9a9 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -461,35 +461,44 @@ internal sealed class QuestController : MiniTaskController _combatController.Stop("Execute next step"); _gatheringController.Stop("Execute next step"); - var newTasks = _taskFactories - .SelectMany(x => - { - IList tasks = x.CreateAllTasks(CurrentQuest.Quest, seq, step).ToList(); - - if (tasks.Count > 0 && _logger.IsEnabled(LogLevel.Trace)) - { - string factoryName = x.GetType().FullName ?? x.GetType().Name; - if (factoryName.Contains('.', StringComparison.Ordinal)) - factoryName = factoryName[(factoryName.LastIndexOf('.') + 1)..]; - - _logger.LogTrace("Factory {FactoryName} created Task {TaskNames}", - factoryName, string.Join(", ", tasks.Select(y => y.ToString()))); - } - - return tasks; - }) - .ToList(); - if (newTasks.Count == 0) + try { - _logger.LogInformation("Nothing to execute for step?"); - return; - } + var newTasks = _taskFactories + .SelectMany(x => + { + IList tasks = x.CreateAllTasks(CurrentQuest.Quest, seq, step).ToList(); - _logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}", - CurrentQuest.Quest.QuestId, seq.Sequence, seq.Steps.IndexOf(step), - string.Join(", ", newTasks.Select(x => x.ToString()))); - foreach (var task in newTasks) - _taskQueue.Enqueue(task); + if (tasks.Count > 0 && _logger.IsEnabled(LogLevel.Trace)) + { + string factoryName = x.GetType().FullName ?? x.GetType().Name; + if (factoryName.Contains('.', StringComparison.Ordinal)) + factoryName = factoryName[(factoryName.LastIndexOf('.') + 1)..]; + + _logger.LogTrace("Factory {FactoryName} created Task {TaskNames}", + factoryName, string.Join(", ", tasks.Select(y => y.ToString()))); + } + + return tasks; + }) + .ToList(); + if (newTasks.Count == 0) + { + _logger.LogInformation("Nothing to execute for step?"); + return; + } + + _logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}", + CurrentQuest.Quest.QuestId, seq.Sequence, seq.Steps.IndexOf(step), + string.Join(", ", newTasks.Select(x => x.ToString()))); + foreach (var task in newTasks) + _taskQueue.Enqueue(task); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to create tasks"); + _chatGui.PrintError("[Questionable] Failed to start next task sequence, please check /xllog for details."); + Stop("Tasks failed to create"); + } } public string ToStatString() diff --git a/Questionable/Controller/Steps/Gathering/DoGather.cs b/Questionable/Controller/Steps/Gathering/DoGather.cs new file mode 100644 index 00000000..914d91b1 --- /dev/null +++ b/Questionable/Controller/Steps/Gathering/DoGather.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LLib.GameUI; +using Questionable.Model.Gathering; + +namespace Questionable.Controller.Steps.Gathering; + +internal sealed class DoGather( + GatheringController gatheringController, + IGameGui gameGui, + ICondition condition) : ITask +{ + private GatheringController.GatheringRequest _currentRequest = null!; + private GatheringNode _currentNode = null!; + private bool _wasGathering; + private List? _slots; + + + public ITask With(GatheringController.GatheringRequest currentRequest, GatheringNode currentNode) + { + _currentRequest = currentRequest; + _currentNode = currentNode; + return this; + } + + public bool Start() => true; + + public unsafe ETaskResult Update() + { + if (gatheringController.HasNodeDisappeared(_currentNode)) + return ETaskResult.TaskComplete; + + if (condition[ConditionFlag.Gathering]) + { + if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _)) + return ETaskResult.TaskComplete; + + _wasGathering = true; + + if (gameGui.TryGetAddonByName("Gathering", out AtkUnitBase* atkUnitBase)) + { + _slots ??= ReadSlots(atkUnitBase); + var slot = _slots.Single(x => x.ItemId == _currentRequest.ItemId); + atkUnitBase->FireCallbackInt(slot.Index); + } + } + + return _wasGathering && !condition[ConditionFlag.Gathering] + ? ETaskResult.TaskComplete + : ETaskResult.StillRunning; + } + + private unsafe List ReadSlots(AtkUnitBase* atkUnitBase) + { + var atkValues = atkUnitBase->AtkValues; + List slots = new List(); + for (int i = 0; i < 8; ++i) + { + // +8 = new item? + uint itemId = atkValues[i * 11 + 7].UInt; + if (itemId == 0) + continue; + + var slot = new SlotInfo(i, itemId); + slots.Add(slot); + } + + return slots; + } + + public override string ToString() => "DoGather"; + + private sealed record SlotInfo(int Index, uint ItemId); +} diff --git a/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs new file mode 100644 index 00000000..6a331ac0 --- /dev/null +++ b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using Dalamud.Game.Text; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +using LLib.GameUI; +using Microsoft.Extensions.Logging; +using Questionable.Model.Gathering; +using Questionable.Model.Questing; + +namespace Questionable.Controller.Steps.Gathering; + +internal sealed class DoGatherCollectable( + GatheringController gatheringController, + GameFunctions gameFunctions, + IClientState clientState, + IGameGui gameGui, + ILogger logger) : ITask +{ + private GatheringController.GatheringRequest _currentRequest = null!; + private GatheringNode _currentNode = null!; + private Queue? _actionQueue; + + public ITask With(GatheringController.GatheringRequest currentRequest, GatheringNode currentNode) + { + _currentRequest = currentRequest; + _currentNode = currentNode; + return this; + } + + public bool Start() => true; + + public ETaskResult Update() + { + if (gatheringController.HasNodeDisappeared(_currentNode)) + return ETaskResult.TaskComplete; + + NodeCondition? nodeCondition = GetNodeCondition(); + if (nodeCondition == null) + return ETaskResult.TaskComplete; + + if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction)) + { + if (gameFunctions.UseAction(nextAction)) + { + logger.LogInformation("Used action {Action} on node", nextAction); + _actionQueue.Dequeue(); + } + + return ETaskResult.StillRunning; + } + + if (nodeCondition.CollectabilityToGoal(_currentRequest.Collectability) > 0) + { + _actionQueue = GetNextActions(nodeCondition); + if (_actionQueue != null) + { + foreach (var action in _actionQueue) + logger.LogInformation("Next Actions {Action}", action); + return ETaskResult.StillRunning; + } + } + + _actionQueue = new Queue(); + _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist)); + return ETaskResult.StillRunning; + } + + private unsafe NodeCondition? GetNodeCondition() + { + if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) + { + var atkValues = atkUnitBase->AtkValues; + return new NodeCondition( + CurrentCollectability: atkValues[13].UInt, + MaxCollectability: atkValues[14].UInt, + CurrentIntegrity: atkValues[62].UInt, + MaxIntegrity: atkValues[63].UInt, + ScrutinyActive: atkValues[80].Bool, + CollectabilityFromScour: atkValues[48].UInt, + CollectabilityFromMeticulous: atkValues[51].UInt + ); + } + + return null; + } + + private Queue? GetNextActions(NodeCondition nodeCondition) + { + uint gp = clientState.LocalPlayer!.CurrentGp; + Queue actions = new(); + + uint neededCollectability = nodeCondition.CollectabilityToGoal(_currentRequest.Collectability); + if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous) + { + actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist)); + return actions; + } + + if (neededCollectability <= nodeCondition.CollectabilityFromScour) + { + actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist)); + return actions; + } + + // neither action directly solves our problem + if (!nodeCondition.ScrutinyActive && gp >= 200) + { + actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist)); + return actions; + } + + if (nodeCondition.ScrutinyActive) + { + actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist)); + return actions; + } + else + { + actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist)); + return actions; + } + } + + private EAction PickAction(EAction minerAction, EAction botanistAction) + { + if (clientState.LocalPlayer?.ClassJob.Id == 16) + return minerAction; + else + return botanistAction; + } + + public override string ToString() => + $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()} {_currentRequest.Collectability})"; + + private sealed record NodeCondition( + uint CurrentCollectability, + uint MaxCollectability, + uint CurrentIntegrity, + uint MaxIntegrity, + bool ScrutinyActive, + uint CollectabilityFromScour, + uint CollectabilityFromMeticulous) + { + public uint CollectabilityToGoal(uint goal) + { + if (goal >= CurrentCollectability) + return goal - CurrentCollectability; + return CurrentCollectability == 0 ? 1u : 0u; + } + } +} diff --git a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs index 3336500f..5f6a2844 100644 --- a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs +++ b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs @@ -16,6 +16,7 @@ namespace Questionable.Controller.Steps.Gathering; internal sealed class MoveToLandingLocation( IServiceProvider serviceProvider, + GameFunctions gameFunctions, IObjectTable objectTable, NavmeshIpc navmeshIpc, ILogger logger) : ITask @@ -36,8 +37,11 @@ internal sealed class MoveToLandingLocation( var location = _gatheringNode.Locations.First(); if (_gatheringNode.Locations.Count > 1) { - var gameObject = objectTable.Single(x => + var gameObject = objectTable.SingleOrDefault(x => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _gatheringNode.DataId && x.IsTargetable); + if (gameObject == null) + return false; + location = _gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f); } @@ -45,15 +49,26 @@ internal sealed class MoveToLandingLocation( 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 }); + bool fly = gameFunctions.IsFlyingUnlocked(_territoryId); + Vector3? pointOnFloor = navmeshIpc.GetPointOnFloor(target with { Y = target.Y + 5f }, false); if (pointOnFloor != null) - pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 0.5f }; + pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 0.5f : 0f) }; + + // since we only allow points that can be landed on, the distance is important but the angle shouldn't matter + if (pointOnFloor != null && Vector3.Distance(pointOnFloor.Value, location.Position) > + location.CalculateMaximumDistance()) + { + pointOnFloor = location.Position + Vector3.Normalize(pointOnFloor.Value - location.Position) * location.CalculateMaximumDistance(); + logger.LogInformation("Adjusted landing location: {Location}", pointOnFloor.Value.ToString("G", CultureInfo.InvariantCulture)); } + else + { + logger.LogInformation("Final landing location: {Location}", + (pointOnFloor ?? target).ToString("G", CultureInfo.InvariantCulture)); + } - 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, + .With(_territoryId, pointOnFloor ?? target, 0.25f, dataId: _gatheringNode.DataId, fly: fly, ignoreDistanceToObject: true); return _moveTask.Start(); } diff --git a/Questionable/Controller/Steps/Gathering/WaitGather.cs b/Questionable/Controller/Steps/Gathering/WaitGather.cs deleted file mode 100644 index e2b3a889..00000000 --- a/Questionable/Controller/Steps/Gathering/WaitGather.cs +++ /dev/null @@ -1,25 +0,0 @@ -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/Shared/GatheringRequiredItems.cs b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs index ea602723..5d8022e6 100644 --- a/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs +++ b/Questionable/Controller/Steps/Shared/GatheringRequiredItems.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Dalamud.Game.Text; using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using Questionable.Data; @@ -70,6 +71,12 @@ internal static class GatheringRequiredItems return ETaskResult.StillRunning; } - public override string ToString() => $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId})"; + public override string ToString() + { + if (_gatheredItem.Collectability == 0) + return $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId})"; + else + return $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {_gatheredItem.Collectability})"; + } } } diff --git a/Questionable/External/NavmeshIpc.cs b/Questionable/External/NavmeshIpc.cs index 2f6c5e36..0c8ce0a7 100644 --- a/Questionable/External/NavmeshIpc.cs +++ b/Questionable/External/NavmeshIpc.cs @@ -108,11 +108,11 @@ internal sealed class NavmeshIpc } } - public Vector3? GetPointOnFloor(Vector3 position) + public Vector3? GetPointOnFloor(Vector3 position, bool unlandable) { try { - return _queryPointOnFloor.InvokeFunc(position, true, 1); + return _queryPointOnFloor.InvokeFunc(position, unlandable, 0.2f); } catch (IpcError) { diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index 1cd229d1..00ff3c8a 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -743,7 +743,7 @@ internal sealed unsafe class GameFunctions _condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] || _condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] || _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || - _condition[ConditionFlag.Jumping61]; + _condition[ConditionFlag.Jumping61] || _condition[ConditionFlag.Gathering42]; } public bool IsLoadingScreenVisible() diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 0febd3bd..944e6beb 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -105,7 +105,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); // task factories serviceCollection.AddTaskWithFactory();