Implement min/btn collectable logic

sb-p1
Liza 2024-08-03 17:26:49 +02:00
parent 82c20bf76d
commit f04233a325
Signed by: liza
GPG Key ID: 7199F8D727D55F67
27 changed files with 839 additions and 99 deletions

View File

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

View File

@ -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();
}
}
}

View File

@ -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
}
]
}
]
}
]
}

View File

@ -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
}
]
}
]
}
]
}

View File

@ -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
}
]
}
]
}
]
}

View File

@ -59,7 +59,8 @@
},
"StopDistance": 7,
"TerritoryId": 962,
"InteractionType": "CompleteQuest"
"InteractionType": "CompleteQuest",
"NextQuestId": 4154
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}
]
}

View File

@ -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
}
]
}
]
}

View File

@ -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"
}
]
}
]
}

View File

@ -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

View File

@ -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; }
}

View File

@ -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<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 readonly ICondition _condition;
private CurrentRequest? _currentRequest;
public GatheringController(MovementController movementController, GatheringData gatheringData,
GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui,
ILogger<GatheringController> logger, IServiceProvider serviceProvider)
public GatheringController(
MovementController movementController,
GameFunctions gameFunctions,
NavmeshIpc navmeshIpc,
IObjectTable objectTable,
IChatGui chatGui,
ILogger<GatheringController> 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<GatheringC
Data = gatheringRequest,
Root = gatheringRoot,
Nodes = gatheringRoot.Groups
.SelectMany(x => 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<GatheringC
if (_movementController.IsPathfinding || _movementController.IsPathfinding)
return EStatus.Moving;
if (HasRequestedItems())
if (HasRequestedItems() && !_condition[ConditionFlag.Gathering])
return EStatus.Complete;
if (_currentTask == null && _taskQueue.Count == 0)
@ -118,12 +125,13 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
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);
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<Move.MoveInternal>()
.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<GatheringC
.With(_currentRequest.Root.TerritoryId, currentNode));
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<Interact.DoInteract>()
.With(currentNode.DataId, true));
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<WaitGather>());
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<DoGather>()
.With(_currentRequest.Data, currentNode));
if (_currentRequest.Data.Collectability > 0)
{
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<DoGatherCollectable>()
.With(_currentRequest.Data, currentNode));
}
}
private bool HasRequestedItems()
@ -144,7 +158,13 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
return false;
return inventoryManager->GetInventoryItemCount(_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<string> GetRemainingTaskNames()
@ -168,7 +188,11 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public int CurrentIndex { get; set; }
}
public sealed record GatheringRequest(ushort GatheringPointId, uint ItemId, int Quantity, short Collectability = 0);
public sealed record GatheringRequest(
ushort GatheringPointId,
uint ItemId,
int Quantity,
ushort Collectability = 0);
public enum EStatus
{

View File

@ -260,7 +260,7 @@ internal sealed class MovementController : IDisposable
private bool IsOnFlightPath(Vector3 p)
{
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(p);
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(p, true);
return pointOnFloor != null && Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f;
}

View File

@ -461,35 +461,44 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_combatController.Stop("Execute next step");
_gatheringController.Stop("Execute next step");
var newTasks = _taskFactories
.SelectMany(x =>
{
IList<ITask> 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<ITask> 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()

View File

@ -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<SlotInfo>? _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<SlotInfo> ReadSlots(AtkUnitBase* atkUnitBase)
{
var atkValues = atkUnitBase->AtkValues;
List<SlotInfo> slots = new List<SlotInfo>();
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);
}

View File

@ -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<DoGatherCollectable> logger) : ITask
{
private GatheringController.GatheringRequest _currentRequest = null!;
private GatheringNode _currentNode = null!;
private Queue<EAction>? _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<EAction>();
_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<EAction>? GetNextActions(NodeCondition nodeCondition)
{
uint gp = clientState.LocalPlayer!.CurrentGp;
Queue<EAction> 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;
}
}
}

View File

@ -16,6 +16,7 @@ namespace Questionable.Controller.Steps.Gathering;
internal sealed class MoveToLandingLocation(
IServiceProvider serviceProvider,
GameFunctions gameFunctions,
IObjectTable objectTable,
NavmeshIpc navmeshIpc,
ILogger<MoveToLandingLocation> 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<Move.MoveInternal>()
.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();
}

View File

@ -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";
}

View File

@ -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})";
}
}
}

View File

@ -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)
{

View File

@ -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()

View File

@ -105,7 +105,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTransient<MountTask>();
serviceCollection.AddTransient<UnmountTask>();
serviceCollection.AddTransient<MoveToLandingLocation>();
serviceCollection.AddTransient<WaitGather>();
serviceCollection.AddTransient<DoGather>();
serviceCollection.AddTransient<DoGatherCollectable>();
// task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();