Auto-Moving to gathering locations
This commit is contained in:
parent
ff7ee27fde
commit
82c20bf76d
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
@ -12,6 +13,7 @@ using Dalamud.Plugin.Services;
|
||||
using ECommons;
|
||||
using ECommons.Schedulers;
|
||||
using ECommons.SplatoonAPI;
|
||||
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||
using GatheringPathRenderer.Windows;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Gathering;
|
||||
@ -36,14 +38,15 @@ public sealed class RendererPlugin : IDalamudPlugin
|
||||
|
||||
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
|
||||
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
|
||||
IPluginLog pluginLog)
|
||||
IObjectTable objectTable, IPluginLog pluginLog)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_clientState = clientState;
|
||||
_pluginLog = pluginLog;
|
||||
|
||||
_editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui);
|
||||
_editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState) { IsOpen = true };
|
||||
_editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable)
|
||||
{ IsOpen = true };
|
||||
_windowSystem.AddWindow(_editorWindow);
|
||||
|
||||
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
|
||||
@ -175,7 +178,8 @@ public sealed class RendererPlugin : IDalamudPlugin
|
||||
bool isCone = false;
|
||||
int minimumAngle = 0;
|
||||
int maximumAngle = 0;
|
||||
if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && locationOverride != null)
|
||||
if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) &&
|
||||
locationOverride != null)
|
||||
{
|
||||
if (locationOverride.IsCone())
|
||||
{
|
||||
@ -192,6 +196,8 @@ public sealed class RendererPlugin : IDalamudPlugin
|
||||
maximumAngle = x.MaximumAngle.GetValueOrDefault();
|
||||
}
|
||||
|
||||
var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
|
||||
var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
|
||||
return new List<Element>
|
||||
{
|
||||
new Element(isCone
|
||||
@ -219,6 +225,26 @@ public sealed class RendererPlugin : IDalamudPlugin
|
||||
Enabled = true,
|
||||
overlayText =
|
||||
$"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}",
|
||||
},
|
||||
new Element(ElementType.CircleAtFixedCoordinates)
|
||||
{
|
||||
refX = a.X,
|
||||
refY = a.Z,
|
||||
refZ = a.Y,
|
||||
color = _colors[0],
|
||||
radius = 0.1f,
|
||||
Enabled = true,
|
||||
overlayText = "Min Angle"
|
||||
},
|
||||
new Element(ElementType.CircleAtFixedCoordinates)
|
||||
{
|
||||
refX = b.X,
|
||||
refY = b.Z,
|
||||
refZ = b.Y,
|
||||
color = _colors[1],
|
||||
radius = 0.1f,
|
||||
Enabled = true,
|
||||
overlayText = "Max Angle"
|
||||
}
|
||||
};
|
||||
}))))
|
||||
|
@ -22,15 +22,19 @@ internal sealed class EditorWindow : Window
|
||||
private readonly IDataManager _dataManager;
|
||||
private readonly ITargetManager _targetManager;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private readonly Dictionary<Guid, LocationOverride> _changes = [];
|
||||
|
||||
private IGameObject? _target;
|
||||
private (RendererPlugin.GatheringLocationContext, GatheringLocation)? _targetLocation;
|
||||
|
||||
private (RendererPlugin.GatheringLocationContext Context, GatheringNode Node, GatheringLocation Location)?
|
||||
_targetLocation;
|
||||
|
||||
private string _newFileName = string.Empty;
|
||||
|
||||
public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager,
|
||||
ITargetManager targetManager, IClientState clientState)
|
||||
ITargetManager targetManager, IClientState clientState, IObjectTable objectTable)
|
||||
: base("Gathering Path Editor###QuestionableGatheringPathEditor")
|
||||
{
|
||||
_plugin = plugin;
|
||||
@ -38,38 +42,44 @@ internal sealed class EditorWindow : Window
|
||||
_dataManager = dataManager;
|
||||
_targetManager = targetManager;
|
||||
_clientState = clientState;
|
||||
_objectTable = objectTable;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(300, 300),
|
||||
};
|
||||
ShowCloseButton = false;
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
_target = _targetManager.Target;
|
||||
if (_target == null || _target.ObjectKind != ObjectKind.GatheringPoint)
|
||||
{
|
||||
_targetLocation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType);
|
||||
var location = gatheringLocations.SelectMany(context =>
|
||||
context.Root.Groups.SelectMany(group =>
|
||||
group.Nodes
|
||||
.Where(node => node.DataId == _target.DataId)
|
||||
.SelectMany(node => node.Locations)
|
||||
.Where(location => Vector3.Distance(location.Position, _target.Position) < 0.1f)
|
||||
.Select(location => new { Context = context, Location = location })))
|
||||
.FirstOrDefault();
|
||||
if (location == null)
|
||||
.SelectMany(node => node.Locations
|
||||
.Where(location =>
|
||||
{
|
||||
if (_target != null)
|
||||
return Vector3.Distance(location.Position, _target.Position) < 0.1f;
|
||||
else
|
||||
return Vector3.Distance(location.Position, _clientState.LocalPlayer!.Position) < 3f;
|
||||
})
|
||||
.Select(location => new { Context = context, Node = node, Location = location }))))
|
||||
.FirstOrDefault();
|
||||
if (_target != null && _target.ObjectKind != ObjectKind.GatheringPoint || location == null)
|
||||
{
|
||||
_target = null;
|
||||
_targetLocation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_targetLocation = (location.Context, location.Location);
|
||||
_target ??= _objectTable.FirstOrDefault(
|
||||
x => x.ObjectKind == ObjectKind.GatheringPoint &&
|
||||
x.DataId == location.Node.DataId &&
|
||||
Vector3.Distance(location.Location.Position, _clientState.LocalPlayer!.Position) < 3f);
|
||||
_targetLocation = (location.Context, location.Node, location.Location);
|
||||
}
|
||||
|
||||
public override bool DrawConditions()
|
||||
@ -81,8 +91,9 @@ internal sealed class EditorWindow : Window
|
||||
{
|
||||
if (_target != null && _targetLocation != null)
|
||||
{
|
||||
var context = _targetLocation.Value.Item1;
|
||||
var location = _targetLocation.Value.Item2;
|
||||
var context = _targetLocation.Value.Context;
|
||||
var node = _targetLocation.Value.Node;
|
||||
var location = _targetLocation.Value.Location;
|
||||
ImGui.Text(context.File.Directory?.Name ?? string.Empty);
|
||||
ImGui.Indent();
|
||||
ImGui.Text(context.File.Name);
|
||||
@ -97,7 +108,7 @@ internal sealed class EditorWindow : Window
|
||||
}
|
||||
|
||||
int minAngle = locationOverride.MinimumAngle ?? location.MinimumAngle.GetValueOrDefault();
|
||||
if (ImGui.DragInt("Min Angle", ref minAngle, 5, -180, 360))
|
||||
if (ImGui.DragInt("Min Angle", ref minAngle, 5, -360, 360))
|
||||
{
|
||||
locationOverride.MinimumAngle = minAngle;
|
||||
locationOverride.MaximumAngle ??= location.MaximumAngle.GetValueOrDefault();
|
||||
@ -105,7 +116,7 @@ internal sealed class EditorWindow : Window
|
||||
}
|
||||
|
||||
int maxAngle = locationOverride.MaximumAngle ?? location.MaximumAngle.GetValueOrDefault();
|
||||
if (ImGui.DragInt("Max Angle", ref maxAngle, 5, -180, 360))
|
||||
if (ImGui.DragInt("Max Angle", ref maxAngle, 5, -360, 360))
|
||||
{
|
||||
locationOverride.MinimumAngle ??= location.MinimumAngle.GetValueOrDefault();
|
||||
locationOverride.MaximumAngle = maxAngle;
|
||||
@ -119,14 +130,33 @@ internal sealed class EditorWindow : Window
|
||||
location.MaximumAngle = locationOverride.MaximumAngle;
|
||||
_plugin.Save(context.File, context.Root);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset"))
|
||||
{
|
||||
_changes[location.InternalId] = new LocationOverride();
|
||||
_plugin.Redraw();
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
|
||||
|
||||
List<IGameObject> nodesInObjectTable = _objectTable
|
||||
.Where(x => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _target.DataId)
|
||||
.ToList();
|
||||
List<IGameObject> missingLocations = nodesInObjectTable
|
||||
.Where(x => !node.Locations.Any(y => Vector3.Distance(x.Position, y.Position) < 0.1f))
|
||||
.ToList();
|
||||
if (missingLocations.Count > 0)
|
||||
{
|
||||
if (ImGui.Button("Add missing locations"))
|
||||
{
|
||||
foreach (var missing in missingLocations)
|
||||
_editorCommands.AddToExistingGroup(context.Root, missing);
|
||||
|
||||
_plugin.Save(context.File, context.Root);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_target != null)
|
||||
{
|
||||
@ -154,6 +184,7 @@ internal sealed class EditorWindow : Window
|
||||
_editorCommands.AddToNewGroup(root, _target);
|
||||
_plugin.Save(targetFile, root);
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
else
|
||||
@ -176,7 +207,7 @@ internal sealed class EditorWindow : Window
|
||||
=> _changes.TryGetValue(internalId, out locationOverride);
|
||||
}
|
||||
|
||||
internal class LocationOverride
|
||||
internal sealed class LocationOverride
|
||||
{
|
||||
public int? MinimumAngle { get; set; }
|
||||
public int? MaximumAngle { get; set; }
|
||||
|
@ -40,6 +40,15 @@
|
||||
},
|
||||
"MinimumAngle": 200,
|
||||
"MaximumAngle": 360
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -606.7445,
|
||||
"Y": 38.37634,
|
||||
"Z": -425.5284
|
||||
},
|
||||
"MinimumAngle": -80,
|
||||
"MaximumAngle": 70
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -139,8 +148,8 @@
|
||||
"Y": 67.64153,
|
||||
"Z": -477.6673
|
||||
},
|
||||
"MinimumAngle": -90,
|
||||
"MaximumAngle": 60
|
||||
"MinimumAngle": -105,
|
||||
"MaximumAngle": 75
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
|
||||
"Author": [],
|
||||
"TerritoryId": 1187,
|
||||
"AetheryteShortcut": "Urqopacha - Wachunpelo",
|
||||
"Groups": [
|
||||
{
|
||||
"Nodes": [
|
||||
@ -39,6 +40,15 @@
|
||||
},
|
||||
"MinimumAngle": -50,
|
||||
"MaximumAngle": 210
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -394.2657,
|
||||
"Y": -47.86026,
|
||||
"Z": -394.9654
|
||||
},
|
||||
"MinimumAngle": -120,
|
||||
"MaximumAngle": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -71,6 +81,24 @@
|
||||
},
|
||||
"MinimumAngle": 225,
|
||||
"MaximumAngle": 360
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -532.3487,
|
||||
"Y": -22.79275,
|
||||
"Z": -510.8069
|
||||
},
|
||||
"MinimumAngle": 135,
|
||||
"MaximumAngle": 270
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -536.2922,
|
||||
"Y": -23.79476,
|
||||
"Z": -526.0406
|
||||
},
|
||||
"MinimumAngle": -110,
|
||||
"MaximumAngle": 35
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -103,6 +131,24 @@
|
||||
},
|
||||
"MinimumAngle": 0,
|
||||
"MaximumAngle": 150
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -431.5875,
|
||||
"Y": -16.68724,
|
||||
"Z": -656.528
|
||||
},
|
||||
"MinimumAngle": -35,
|
||||
"MaximumAngle": 90
|
||||
},
|
||||
{
|
||||
"Position": {
|
||||
"X": -439.8079,
|
||||
"Y": -16.67447,
|
||||
"Z": -654.6749
|
||||
},
|
||||
"MinimumAngle": -45,
|
||||
"MaximumAngle": 85
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -29,7 +29,14 @@
|
||||
"Z": -102.983154
|
||||
},
|
||||
"TerritoryId": 962,
|
||||
"InteractionType": "CompleteQuest"
|
||||
"InteractionType": "CompleteQuest",
|
||||
"RequiredGatheredItems": [
|
||||
{
|
||||
"ItemId": 35600,
|
||||
"ItemCount": 6,
|
||||
"Collectability": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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": [
|
||||
|
53
Questionable.Model/GatheringMath.cs
Normal file
53
Questionable.Model/GatheringMath.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Questionable.Model.Gathering;
|
||||
|
||||
namespace GatheringPathRenderer;
|
||||
|
||||
public static class GatheringMath
|
||||
{
|
||||
private static readonly Random RNG = new Random();
|
||||
|
||||
public static (Vector3, int, float) CalculateLandingLocation(GatheringLocation location)
|
||||
{
|
||||
int degrees;
|
||||
if (location.IsCone())
|
||||
degrees = RNG.Next(
|
||||
location.MinimumAngle.GetValueOrDefault(),
|
||||
location.MaximumAngle.GetValueOrDefault());
|
||||
else
|
||||
degrees = RNG.Next(0, 360);
|
||||
|
||||
float range = RNG.Next(
|
||||
(int)(location.CalculateMinimumDistance() * 100),
|
||||
(int)((location.CalculateMaximumDistance() - location.CalculateMinimumDistance()) * 100)) / 100f;
|
||||
return (CalculateLandingLocation(location.Position, degrees, range), degrees, range);
|
||||
}
|
||||
|
||||
public static Vector3 CalculateLandingLocation(GatheringLocation location, float angleScale, float rangeScale)
|
||||
{
|
||||
int degrees;
|
||||
if (location.IsCone())
|
||||
degrees = location.MinimumAngle.GetValueOrDefault()
|
||||
+ (int)(angleScale * (location.MaximumAngle.GetValueOrDefault() -
|
||||
location.MinimumAngle.GetValueOrDefault()));
|
||||
else
|
||||
degrees = (int)(rangeScale * 360);
|
||||
|
||||
float range =
|
||||
location.CalculateMinimumDistance() +
|
||||
rangeScale * (location.CalculateMaximumDistance() - location.CalculateMinimumDistance());
|
||||
return CalculateLandingLocation(location.Position, degrees, range);
|
||||
}
|
||||
|
||||
private static Vector3 CalculateLandingLocation(Vector3 position, int degrees, float range)
|
||||
{
|
||||
float rad = -(float)(degrees * Math.PI / 180);
|
||||
return new Vector3
|
||||
{
|
||||
X = position.X + range * (float)Math.Sin(rad),
|
||||
Y = position.Y,
|
||||
Z = position.Z + range * (float)Math.Cos(rad)
|
||||
};
|
||||
}
|
||||
}
|
8
Questionable.Model/Questing/GatheredItem.cs
Normal file
8
Questionable.Model/Questing/GatheredItem.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Questionable.Model.Questing;
|
||||
|
||||
public sealed class GatheredItem
|
||||
{
|
||||
public uint ItemId { get; set; }
|
||||
public int ItemCount { get; set; }
|
||||
public short Collectability { get; set; }
|
||||
}
|
@ -66,6 +66,7 @@ public sealed class QuestStep
|
||||
public SkipConditions? SkipConditions { get; set; }
|
||||
|
||||
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
|
||||
public List<GatheredItem> RequiredGatheredItems { get; set; } = [];
|
||||
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
|
||||
public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
|
||||
public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
|
||||
|
@ -7,6 +7,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=bestways/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=braax/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=brightploom/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=collectability/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=earthenshire/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=electrope/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=hanu/@EntryIndexedValue">True</s:Boolean>
|
||||
|
179
Questionable/Controller/GatheringController.cs
Normal file
179
Questionable/Controller/GatheringController.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Gathering;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
using Questionable.GatheringPaths;
|
||||
using Questionable.Model.Gathering;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
|
||||
{
|
||||
private readonly MovementController _movementController;
|
||||
private readonly GatheringData _gatheringData;
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
private readonly NavmeshIpc _navmeshIpc;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private CurrentRequest? _currentRequest;
|
||||
|
||||
public GatheringController(MovementController movementController, GatheringData gatheringData,
|
||||
GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui,
|
||||
ILogger<GatheringController> logger, IServiceProvider serviceProvider)
|
||||
: base(chatGui, logger)
|
||||
{
|
||||
_movementController = movementController;
|
||||
_gatheringData = gatheringData;
|
||||
_gameFunctions = gameFunctions;
|
||||
_navmeshIpc = navmeshIpc;
|
||||
_objectTable = objectTable;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public bool Start(GatheringRequest gatheringRequest)
|
||||
{
|
||||
if (!AssemblyGatheringLocationLoader.GetLocations()
|
||||
.TryGetValue(gatheringRequest.GatheringPointId, out GatheringRoot? gatheringRoot))
|
||||
{
|
||||
_logger.LogError("Unable to resolve gathering point, no path found for {ItemId} / point {PointId}",
|
||||
gatheringRequest.ItemId, gatheringRequest.GatheringPointId);
|
||||
return false;
|
||||
}
|
||||
|
||||
_currentRequest = new CurrentRequest
|
||||
{
|
||||
Data = gatheringRequest,
|
||||
Root = gatheringRoot,
|
||||
Nodes = gatheringRoot.Groups
|
||||
.SelectMany(x => x.Nodes)
|
||||
.ToList(),
|
||||
};
|
||||
|
||||
if (HasRequestedItems())
|
||||
{
|
||||
_currentRequest = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public EStatus Update()
|
||||
{
|
||||
if (_currentRequest == null)
|
||||
return EStatus.Complete;
|
||||
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathfinding)
|
||||
return EStatus.Moving;
|
||||
|
||||
if (HasRequestedItems())
|
||||
return EStatus.Complete;
|
||||
|
||||
if (_currentTask == null && _taskQueue.Count == 0)
|
||||
GoToNextNode();
|
||||
|
||||
UpdateCurrentTask();
|
||||
return EStatus.Gathering;
|
||||
}
|
||||
|
||||
protected override void OnTaskComplete(ITask task) => GoToNextNode();
|
||||
|
||||
public override void Stop(string label)
|
||||
{
|
||||
_currentRequest = null;
|
||||
_currentTask = null;
|
||||
_taskQueue.Clear();
|
||||
}
|
||||
|
||||
private void GoToNextNode()
|
||||
{
|
||||
if (_currentRequest == null)
|
||||
return;
|
||||
|
||||
if (_taskQueue.Count > 0)
|
||||
return;
|
||||
|
||||
var currentNode = _currentRequest.Nodes[_currentRequest.CurrentIndex++ % _currentRequest.Nodes.Count];
|
||||
|
||||
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<MountTask>()
|
||||
.With(_currentRequest.Root.TerritoryId, MountTask.EMountIf.Always));
|
||||
if (currentNode.Locations.Count > 1)
|
||||
{
|
||||
Vector3 averagePosition = new Vector3
|
||||
{
|
||||
X = currentNode.Locations.Sum(x => x.Position.X) / currentNode.Locations.Count,
|
||||
Y = currentNode.Locations.Select(x => x.Position.Y).Max() + 5f,
|
||||
Z = currentNode.Locations.Sum(x => x.Position.Z) / currentNode.Locations.Count,
|
||||
};
|
||||
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(averagePosition);
|
||||
if (pointOnFloor != null)
|
||||
pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 3f };
|
||||
|
||||
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<Move.MoveInternal>()
|
||||
.With(_currentRequest.Root.TerritoryId, pointOnFloor ?? averagePosition, 50f, fly: true,
|
||||
ignoreDistanceToObject: true));
|
||||
}
|
||||
|
||||
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<MoveToLandingLocation>()
|
||||
.With(_currentRequest.Root.TerritoryId, currentNode));
|
||||
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<Interact.DoInteract>()
|
||||
.With(currentNode.DataId, true));
|
||||
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<WaitGather>());
|
||||
}
|
||||
|
||||
private bool HasRequestedItems()
|
||||
{
|
||||
if (_currentRequest == null)
|
||||
return true;
|
||||
|
||||
InventoryManager* inventoryManager = InventoryManager.Instance();
|
||||
if (inventoryManager == null)
|
||||
return false;
|
||||
|
||||
return inventoryManager->GetInventoryItemCount(_currentRequest.Data.ItemId,
|
||||
minCollectability: _currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity;
|
||||
}
|
||||
|
||||
public override IList<string> GetRemainingTaskNames()
|
||||
{
|
||||
if (_currentTask != null)
|
||||
return [_currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
|
||||
else
|
||||
return base.GetRemainingTaskNames();
|
||||
}
|
||||
|
||||
private sealed class CurrentRequest
|
||||
{
|
||||
public required GatheringRequest Data { get; init; }
|
||||
public required GatheringRoot Root { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// To make indexing easy with <see cref="CurrentIndex"/>, we flatten the list of gathering locations.
|
||||
/// </summary>
|
||||
public required List<GatheringNode> Nodes { get; init; }
|
||||
|
||||
public int CurrentIndex { get; set; }
|
||||
}
|
||||
|
||||
public sealed record GatheringRequest(ushort GatheringPointId, uint ItemId, int Quantity, short Collectability = 0);
|
||||
|
||||
public enum EStatus
|
||||
{
|
||||
Gathering,
|
||||
Moving,
|
||||
Complete,
|
||||
}
|
||||
}
|
134
Questionable/Controller/MiniTaskController.cs
Normal file
134
Questionable/Controller/MiniTaskController.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal abstract class MiniTaskController<T>
|
||||
{
|
||||
protected readonly IChatGui _chatGui;
|
||||
protected readonly ILogger<T> _logger;
|
||||
|
||||
protected readonly Queue<ITask> _taskQueue = new();
|
||||
protected ITask? _currentTask;
|
||||
|
||||
public MiniTaskController(IChatGui chatGui, ILogger<T> logger)
|
||||
{
|
||||
_chatGui = chatGui;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected virtual void UpdateCurrentTask()
|
||||
{
|
||||
if (_currentTask == null)
|
||||
{
|
||||
if (_taskQueue.TryDequeue(out ITask? upcomingTask))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
|
||||
if (upcomingTask.Start())
|
||||
{
|
||||
_currentTask = upcomingTask;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
|
||||
_chatGui.PrintError(
|
||||
$"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details.");
|
||||
Stop("Task failed to start");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
ETaskResult result;
|
||||
try
|
||||
{
|
||||
result = _currentTask.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
|
||||
_chatGui.PrintError(
|
||||
$"[Questionable] Failed to update task '{_currentTask}', please check /xllog for details.");
|
||||
Stop("Task failed to update");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ETaskResult.StillRunning:
|
||||
return;
|
||||
|
||||
case ETaskResult.SkipRemainingTasksForStep:
|
||||
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
|
||||
_currentTask, result);
|
||||
_currentTask = null;
|
||||
|
||||
while (_taskQueue.TryDequeue(out ITask? nextTask))
|
||||
{
|
||||
if (nextTask is ILastTask)
|
||||
{
|
||||
_currentTask = nextTask;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case ETaskResult.TaskComplete:
|
||||
_logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
|
||||
_currentTask, result, _taskQueue.Count);
|
||||
|
||||
OnTaskComplete(_currentTask);
|
||||
|
||||
_currentTask = null;
|
||||
|
||||
// handled in next update
|
||||
return;
|
||||
|
||||
case ETaskResult.NextStep:
|
||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
||||
|
||||
var lastTask = (ILastTask)_currentTask;
|
||||
_currentTask = null;
|
||||
|
||||
OnNextStep(lastTask);
|
||||
return;
|
||||
|
||||
case ETaskResult.End:
|
||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
||||
_currentTask = null;
|
||||
Stop("Task end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnTaskComplete(ITask task)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnNextStep(ILastTask task)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public abstract void Stop(string label);
|
||||
|
||||
public virtual IList<string> GetRemainingTaskNames() =>
|
||||
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
|
||||
}
|
@ -14,16 +14,15 @@ using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class QuestController
|
||||
internal sealed class QuestController : MiniTaskController<QuestController>
|
||||
{
|
||||
private readonly IClientState _clientState;
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
private readonly MovementController _movementController;
|
||||
private readonly CombatController _combatController;
|
||||
private readonly ILogger<QuestController> _logger;
|
||||
private readonly GatheringController _gatheringController;
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
private readonly IKeyState _keyState;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly ICondition _condition;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly YesAlreadyIpc _yesAlreadyIpc;
|
||||
@ -34,8 +33,6 @@ internal sealed class QuestController
|
||||
private QuestProgress? _startedQuest;
|
||||
private QuestProgress? _nextQuest;
|
||||
private QuestProgress? _simulatedQuest;
|
||||
private readonly Queue<ITask> _taskQueue = new();
|
||||
private ITask? _currentTask;
|
||||
private bool _automatic;
|
||||
|
||||
/// <summary>
|
||||
@ -50,6 +47,7 @@ internal sealed class QuestController
|
||||
GameFunctions gameFunctions,
|
||||
MovementController movementController,
|
||||
CombatController combatController,
|
||||
GatheringController gatheringController,
|
||||
ILogger<QuestController> logger,
|
||||
QuestRegistry questRegistry,
|
||||
IKeyState keyState,
|
||||
@ -58,15 +56,15 @@ internal sealed class QuestController
|
||||
Configuration configuration,
|
||||
YesAlreadyIpc yesAlreadyIpc,
|
||||
IEnumerable<ITaskFactory> taskFactories)
|
||||
: base(chatGui, logger)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_gameFunctions = gameFunctions;
|
||||
_movementController = movementController;
|
||||
_combatController = combatController;
|
||||
_logger = logger;
|
||||
_gatheringController = gatheringController;
|
||||
_questRegistry = questRegistry;
|
||||
_keyState = keyState;
|
||||
_chatGui = chatGui;
|
||||
_condition = condition;
|
||||
_configuration = configuration;
|
||||
_yesAlreadyIpc = yesAlreadyIpc;
|
||||
@ -138,6 +136,7 @@ internal sealed class QuestController
|
||||
Stop("HP = 0");
|
||||
_movementController.Stop();
|
||||
_combatController.Stop("HP = 0");
|
||||
_gatheringController.Stop("HP = 0");
|
||||
}
|
||||
}
|
||||
else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE])
|
||||
@ -147,6 +146,7 @@ internal sealed class QuestController
|
||||
Stop("ESC pressed");
|
||||
_movementController.Stop();
|
||||
_combatController.Stop("ESC pressed");
|
||||
_gatheringController.Stop("ESC pressed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,9 +377,10 @@ internal sealed class QuestController
|
||||
|
||||
_yesAlreadyIpc.RestoreYesAlready();
|
||||
_combatController.Stop("ClearTasksInternal");
|
||||
_gatheringController.Stop("ClearTasksInternal");
|
||||
}
|
||||
|
||||
public void Stop(string label, bool continueIfAutomatic = false)
|
||||
public void Stop(string label, bool continueIfAutomatic)
|
||||
{
|
||||
using var scope = _logger.BeginScope(label);
|
||||
|
||||
@ -401,6 +402,8 @@ internal sealed class QuestController
|
||||
}
|
||||
}
|
||||
|
||||
public override void Stop(string label) => Stop(label, false);
|
||||
|
||||
public void SimulateQuest(Quest? quest)
|
||||
{
|
||||
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestId);
|
||||
@ -419,103 +422,23 @@ internal sealed class QuestController
|
||||
_nextQuest = null;
|
||||
}
|
||||
|
||||
private void UpdateCurrentTask()
|
||||
protected override void UpdateCurrentTask()
|
||||
{
|
||||
if (_gameFunctions.IsOccupied())
|
||||
return;
|
||||
|
||||
if (_currentTask == null)
|
||||
{
|
||||
if (_taskQueue.TryDequeue(out ITask? upcomingTask))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
|
||||
if (upcomingTask.Start())
|
||||
{
|
||||
_currentTask = upcomingTask;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
|
||||
_chatGui.PrintError(
|
||||
$"[Questionable] Failed to start task '{upcomingTask}', please check /xllog for details.");
|
||||
Stop("Task failed to start");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
base.UpdateCurrentTask();
|
||||
}
|
||||
|
||||
ETaskResult result;
|
||||
try
|
||||
protected override void OnTaskComplete(ITask task)
|
||||
{
|
||||
result = _currentTask.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
|
||||
_chatGui.PrintError(
|
||||
$"[Questionable] Failed to update task '{_currentTask}', please check /xllog for details.");
|
||||
Stop("Task failed to update");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ETaskResult.StillRunning:
|
||||
return;
|
||||
|
||||
case ETaskResult.SkipRemainingTasksForStep:
|
||||
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
|
||||
_currentTask, result);
|
||||
_currentTask = null;
|
||||
|
||||
while (_taskQueue.TryDequeue(out ITask? nextTask))
|
||||
{
|
||||
if (nextTask is ILastTask)
|
||||
{
|
||||
_currentTask = nextTask;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case ETaskResult.TaskComplete:
|
||||
_logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
|
||||
_currentTask, result, _taskQueue.Count);
|
||||
|
||||
if (_currentTask is WaitAtEnd.WaitQuestCompleted)
|
||||
if (task is WaitAtEnd.WaitQuestCompleted)
|
||||
_simulatedQuest = null;
|
||||
|
||||
_currentTask = null;
|
||||
|
||||
// handled in next update
|
||||
return;
|
||||
|
||||
case ETaskResult.NextStep:
|
||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
||||
|
||||
var lastTask = (ILastTask)_currentTask;
|
||||
_currentTask = null;
|
||||
IncreaseStepCount(lastTask.QuestId, lastTask.Sequence, true);
|
||||
return;
|
||||
|
||||
case ETaskResult.End:
|
||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
||||
_currentTask = null;
|
||||
Stop("Task end");
|
||||
return;
|
||||
}
|
||||
|
||||
protected override void OnNextStep(ILastTask task)
|
||||
{
|
||||
IncreaseStepCount(task.QuestId, task.Sequence, true);
|
||||
}
|
||||
|
||||
public void ExecuteNextStep(bool automatic)
|
||||
@ -536,6 +459,7 @@ internal sealed class QuestController
|
||||
|
||||
_movementController.Stop();
|
||||
_combatController.Stop("Execute next step");
|
||||
_gatheringController.Stop("Execute next step");
|
||||
|
||||
var newTasks = _taskFactories
|
||||
.SelectMany(x =>
|
||||
@ -568,9 +492,6 @@ internal sealed class QuestController
|
||||
_taskQueue.Enqueue(task);
|
||||
}
|
||||
|
||||
public IList<string> GetRemainingTaskNames() =>
|
||||
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
|
||||
|
||||
public string ToStatString()
|
||||
{
|
||||
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
|
||||
|
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Plugin.Services;
|
||||
using GatheringPathRenderer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.External;
|
||||
using Questionable.Model.Gathering;
|
||||
|
||||
namespace Questionable.Controller.Steps.Gathering;
|
||||
|
||||
internal sealed class MoveToLandingLocation(
|
||||
IServiceProvider serviceProvider,
|
||||
IObjectTable objectTable,
|
||||
NavmeshIpc navmeshIpc,
|
||||
ILogger<MoveToLandingLocation> logger) : ITask
|
||||
{
|
||||
private ushort _territoryId;
|
||||
private GatheringNode _gatheringNode = null!;
|
||||
private ITask _moveTask = null!;
|
||||
|
||||
public ITask With(ushort territoryId, GatheringNode gatheringNode)
|
||||
{
|
||||
_territoryId = territoryId;
|
||||
_gatheringNode = gatheringNode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
var location = _gatheringNode.Locations.First();
|
||||
if (_gatheringNode.Locations.Count > 1)
|
||||
{
|
||||
var gameObject = objectTable.Single(x =>
|
||||
x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _gatheringNode.DataId && x.IsTargetable);
|
||||
location = _gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
|
||||
}
|
||||
|
||||
var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
|
||||
logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
|
||||
target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
|
||||
|
||||
Vector3? pointOnFloor = navmeshIpc.GetPointOnFloor(target with { Y = target.Y + 5f });
|
||||
if (pointOnFloor != null)
|
||||
pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + 0.5f };
|
||||
|
||||
logger.LogInformation("Final landing location: {Location}",
|
||||
(pointOnFloor ?? target).ToString("G", CultureInfo.InvariantCulture));
|
||||
|
||||
_moveTask = serviceProvider.GetRequiredService<Move.MoveInternal>()
|
||||
.With(_territoryId, pointOnFloor ?? target, 0.25f, dataId: _gatheringNode.DataId, fly: true,
|
||||
ignoreDistanceToObject: true);
|
||||
return _moveTask.Start();
|
||||
}
|
||||
|
||||
public ETaskResult Update() => _moveTask.Update();
|
||||
|
||||
public override string ToString() => $"Land/{_moveTask}";
|
||||
}
|
25
Questionable/Controller/Steps/Gathering/WaitGather.cs
Normal file
25
Questionable/Controller/Steps/Gathering/WaitGather.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace Questionable.Controller.Steps.Gathering;
|
||||
|
||||
internal sealed class WaitGather(ICondition condition) : ITask
|
||||
{
|
||||
private bool _wasGathering;
|
||||
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (condition[ConditionFlag.Gathering])
|
||||
{
|
||||
_wasGathering = true;
|
||||
}
|
||||
|
||||
return _wasGathering && !condition[ConditionFlag.Gathering]
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
public override string ToString() => "WaitGather";
|
||||
}
|
@ -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})";
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Data;
|
||||
using Questionable.GatheringPaths;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Gathering;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller.Steps.Shared;
|
||||
|
||||
internal static class GatheringRequiredItems
|
||||
{
|
||||
internal sealed class Factory(
|
||||
IServiceProvider serviceProvider,
|
||||
IClientState clientState,
|
||||
GatheringData gatheringData) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
foreach (var requiredGatheredItems in step.RequiredGatheredItems)
|
||||
{
|
||||
if (!gatheringData.TryGetGatheringPointId(requiredGatheredItems.ItemId,
|
||||
clientState.LocalPlayer!.ClassJob.Id, out var gatheringPointId))
|
||||
throw new TaskException($"No gathering point found for item {requiredGatheredItems.ItemId}");
|
||||
|
||||
if (!AssemblyGatheringLocationLoader.GetLocations()
|
||||
.TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
|
||||
throw new TaskException("No path found for gathering point");
|
||||
|
||||
if (gatheringRoot.AetheryteShortcut != null && clientState.TerritoryType != gatheringRoot.TerritoryId)
|
||||
{
|
||||
yield return serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>()
|
||||
.With(null, gatheringRoot.AetheryteShortcut.Value, gatheringRoot.TerritoryId);
|
||||
}
|
||||
|
||||
yield return serviceProvider.GetRequiredService<StartGathering>()
|
||||
.With(gatheringPointId, requiredGatheredItems);
|
||||
}
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal sealed class StartGathering(GatheringController gatheringController) : ITask
|
||||
{
|
||||
private ushort _gatheringPointId;
|
||||
private GatheredItem _gatheredItem = null!;
|
||||
|
||||
public ITask With(ushort gatheringPointId, GatheredItem gatheredItem)
|
||||
{
|
||||
_gatheringPointId = gatheringPointId;
|
||||
_gatheredItem = gatheredItem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
return gatheringController.Start(new GatheringController.GatheringRequest(_gatheringPointId,
|
||||
_gatheredItem.ItemId, _gatheredItem.ItemCount, _gatheredItem.Collectability));
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (gatheringController.Update() == GatheringController.EStatus.Complete)
|
||||
return ETaskResult.TaskComplete;
|
||||
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId})";
|
||||
}
|
||||
}
|
49
Questionable/Data/GatheringData.cs
Normal file
49
Questionable/Data/GatheringData.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Questionable.Data;
|
||||
|
||||
internal sealed class GatheringData
|
||||
{
|
||||
private readonly Dictionary<uint, uint> _gatheringItemToItem;
|
||||
private readonly Dictionary<uint, ushort> _minerGatheringPoints = [];
|
||||
private readonly Dictionary<uint, ushort> _botanistGatheringPoints = [];
|
||||
|
||||
public GatheringData(IDataManager dataManager)
|
||||
{
|
||||
_gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
|
||||
.Where(x => x.RowId != 0 && x.Item != 0)
|
||||
.ToDictionary(x => x.RowId, x => (uint)x.Item);
|
||||
|
||||
foreach (var gatheringPointBase in dataManager.GetExcelSheet<GatheringPointBase>()!)
|
||||
{
|
||||
foreach (var gatheringItemId in gatheringPointBase.Item.Where(x => x != 0))
|
||||
{
|
||||
if (_gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
|
||||
{
|
||||
if (gatheringPointBase.GatheringType.Row is 0 or 1)
|
||||
_minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
|
||||
else if (gatheringPointBase.GatheringType.Row is 2 or 3)
|
||||
_botanistGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId)
|
||||
{
|
||||
if (classJobId == 16)
|
||||
return _minerGatheringPoints.TryGetValue(itemId, out gatheringPointId);
|
||||
else if (classJobId == 17)
|
||||
return _botanistGatheringPoints.TryGetValue(itemId, out gatheringPointId);
|
||||
else
|
||||
{
|
||||
gatheringPointId = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -407,10 +407,13 @@ internal sealed unsafe class GameFunctions
|
||||
playerState->IsAetherCurrentUnlocked(aetherCurrentId);
|
||||
}
|
||||
|
||||
public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null)
|
||||
public IGameObject? FindObjectByDataId(uint dataId, ObjectKind? kind = null, bool targetable = false)
|
||||
{
|
||||
foreach (var gameObject in _objectTable)
|
||||
{
|
||||
if (targetable && !gameObject.IsTargetable)
|
||||
continue;
|
||||
|
||||
if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
|
||||
or ObjectKind.Retainer or ObjectKind.Housing)
|
||||
continue;
|
||||
@ -429,19 +432,31 @@ internal sealed unsafe class GameFunctions
|
||||
{
|
||||
IGameObject? gameObject = FindObjectByDataId(dataId, kind);
|
||||
if (gameObject != null)
|
||||
return InteractWith(gameObject);
|
||||
|
||||
_logger.LogDebug("Game object is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool InteractWith(IGameObject gameObject)
|
||||
{
|
||||
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.EntityId);
|
||||
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
|
||||
_targetManager.Target = null;
|
||||
_targetManager.Target = gameObject;
|
||||
|
||||
if (gameObject.ObjectKind == ObjectKind.GatheringPoint)
|
||||
{
|
||||
TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
|
||||
_logger.LogInformation("Interact result: (none) for GatheringPoint");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
|
||||
|
||||
_logger.LogInformation("Interact result: {Result}", result);
|
||||
return result != 7 && result > 0;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Game object is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool UseItem(uint itemId)
|
||||
|
@ -18,6 +18,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj" />
|
||||
<ProjectReference Include="..\LLib\LLib.csproj"/>
|
||||
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
|
||||
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
|
||||
|
@ -13,6 +13,7 @@ using Questionable.Controller.CombatModules;
|
||||
using Questionable.Controller.NavigationOverrides;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Gathering;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
@ -89,6 +90,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton<ChatFunctions>();
|
||||
serviceCollection.AddSingleton<AetherCurrentData>();
|
||||
serviceCollection.AddSingleton<AetheryteData>();
|
||||
serviceCollection.AddSingleton<GatheringData>();
|
||||
serviceCollection.AddSingleton<JournalData>();
|
||||
serviceCollection.AddSingleton<QuestData>();
|
||||
serviceCollection.AddSingleton<TerritoryData>();
|
||||
@ -102,9 +104,12 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
// individual tasks
|
||||
serviceCollection.AddTransient<MountTask>();
|
||||
serviceCollection.AddTransient<UnmountTask>();
|
||||
serviceCollection.AddTransient<MoveToLandingLocation>();
|
||||
serviceCollection.AddTransient<WaitGather>();
|
||||
|
||||
// task factories
|
||||
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
|
||||
serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering>();
|
||||
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
|
||||
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
|
||||
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
|
||||
@ -149,6 +154,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton<GameUiController>();
|
||||
serviceCollection.AddSingleton<NavigationShortcutController>();
|
||||
serviceCollection.AddSingleton<CombatController>();
|
||||
serviceCollection.AddSingleton<GatheringController>();
|
||||
|
||||
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
|
||||
}
|
||||
|
@ -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 ==
|
||||
|
@ -1,4 +1,5 @@
|
||||
using ImGuiNET;
|
||||
using System.Collections.Generic;
|
||||
using ImGuiNET;
|
||||
using Questionable.Controller;
|
||||
|
||||
namespace Questionable.Windows.QuestComponents;
|
||||
@ -6,13 +7,26 @@ namespace Questionable.Windows.QuestComponents;
|
||||
internal sealed class RemainingTasksComponent
|
||||
{
|
||||
private readonly QuestController _questController;
|
||||
private readonly GatheringController _gatheringController;
|
||||
|
||||
public RemainingTasksComponent(QuestController questController)
|
||||
public RemainingTasksComponent(QuestController questController, GatheringController gatheringController)
|
||||
{
|
||||
_questController = questController;
|
||||
_gatheringController = gatheringController;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
IList<string> gatheringTasks = _gatheringController.GetRemainingTaskNames();
|
||||
if (gatheringTasks.Count > 0)
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.BeginDisabled();
|
||||
foreach (var task in gatheringTasks)
|
||||
ImGui.TextUnformatted($"G: {task}");
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
var remainingTasks = _questController.GetRemainingTaskNames();
|
||||
if (remainingTasks.Count > 0)
|
||||
@ -24,4 +38,5 @@ internal sealed class RemainingTasksComponent
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user