Auto-Moving to gathering locations
This commit is contained in:
parent
ff7ee27fde
commit
82c20bf76d
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
@ -12,6 +13,7 @@ using Dalamud.Plugin.Services;
|
|||||||
using ECommons;
|
using ECommons;
|
||||||
using ECommons.Schedulers;
|
using ECommons.Schedulers;
|
||||||
using ECommons.SplatoonAPI;
|
using ECommons.SplatoonAPI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||||
using GatheringPathRenderer.Windows;
|
using GatheringPathRenderer.Windows;
|
||||||
using Questionable.Model;
|
using Questionable.Model;
|
||||||
using Questionable.Model.Gathering;
|
using Questionable.Model.Gathering;
|
||||||
@ -36,14 +38,15 @@ public sealed class RendererPlugin : IDalamudPlugin
|
|||||||
|
|
||||||
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
|
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
|
||||||
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
|
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
|
||||||
IPluginLog pluginLog)
|
IObjectTable objectTable, IPluginLog pluginLog)
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_pluginLog = pluginLog;
|
_pluginLog = pluginLog;
|
||||||
|
|
||||||
_editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui);
|
_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);
|
_windowSystem.AddWindow(_editorWindow);
|
||||||
|
|
||||||
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
|
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
|
||||||
@ -175,7 +178,8 @@ public sealed class RendererPlugin : IDalamudPlugin
|
|||||||
bool isCone = false;
|
bool isCone = false;
|
||||||
int minimumAngle = 0;
|
int minimumAngle = 0;
|
||||||
int maximumAngle = 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())
|
if (locationOverride.IsCone())
|
||||||
{
|
{
|
||||||
@ -192,6 +196,8 @@ public sealed class RendererPlugin : IDalamudPlugin
|
|||||||
maximumAngle = x.MaximumAngle.GetValueOrDefault();
|
maximumAngle = x.MaximumAngle.GetValueOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
|
||||||
|
var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
|
||||||
return new List<Element>
|
return new List<Element>
|
||||||
{
|
{
|
||||||
new Element(isCone
|
new Element(isCone
|
||||||
@ -219,6 +225,26 @@ public sealed class RendererPlugin : IDalamudPlugin
|
|||||||
Enabled = true,
|
Enabled = true,
|
||||||
overlayText =
|
overlayText =
|
||||||
$"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}",
|
$"{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 IDataManager _dataManager;
|
||||||
private readonly ITargetManager _targetManager;
|
private readonly ITargetManager _targetManager;
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
|
||||||
private readonly Dictionary<Guid, LocationOverride> _changes = [];
|
private readonly Dictionary<Guid, LocationOverride> _changes = [];
|
||||||
|
|
||||||
private IGameObject? _target;
|
private IGameObject? _target;
|
||||||
private (RendererPlugin.GatheringLocationContext, GatheringLocation)? _targetLocation;
|
|
||||||
|
private (RendererPlugin.GatheringLocationContext Context, GatheringNode Node, GatheringLocation Location)?
|
||||||
|
_targetLocation;
|
||||||
|
|
||||||
private string _newFileName = string.Empty;
|
private string _newFileName = string.Empty;
|
||||||
|
|
||||||
public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager,
|
public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager,
|
||||||
ITargetManager targetManager, IClientState clientState)
|
ITargetManager targetManager, IClientState clientState, IObjectTable objectTable)
|
||||||
: base("Gathering Path Editor###QuestionableGatheringPathEditor")
|
: base("Gathering Path Editor###QuestionableGatheringPathEditor")
|
||||||
{
|
{
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
@ -38,38 +42,44 @@ internal sealed class EditorWindow : Window
|
|||||||
_dataManager = dataManager;
|
_dataManager = dataManager;
|
||||||
_targetManager = targetManager;
|
_targetManager = targetManager;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(300, 300),
|
MinimumSize = new Vector2(300, 300),
|
||||||
};
|
};
|
||||||
|
ShowCloseButton = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update()
|
public override void Update()
|
||||||
{
|
{
|
||||||
_target = _targetManager.Target;
|
_target = _targetManager.Target;
|
||||||
if (_target == null || _target.ObjectKind != ObjectKind.GatheringPoint)
|
|
||||||
{
|
|
||||||
_targetLocation = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType);
|
var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType);
|
||||||
var location = gatheringLocations.SelectMany(context =>
|
var location = gatheringLocations.SelectMany(context =>
|
||||||
context.Root.Groups.SelectMany(group =>
|
context.Root.Groups.SelectMany(group =>
|
||||||
group.Nodes
|
group.Nodes
|
||||||
.Where(node => node.DataId == _target.DataId)
|
.SelectMany(node => node.Locations
|
||||||
.SelectMany(node => node.Locations)
|
.Where(location =>
|
||||||
.Where(location => Vector3.Distance(location.Position, _target.Position) < 0.1f)
|
{
|
||||||
.Select(location => new { Context = context, Location = 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();
|
.FirstOrDefault();
|
||||||
if (location == null)
|
if (_target != null && _target.ObjectKind != ObjectKind.GatheringPoint || location == null)
|
||||||
{
|
{
|
||||||
|
_target = null;
|
||||||
_targetLocation = null;
|
_targetLocation = null;
|
||||||
return;
|
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()
|
public override bool DrawConditions()
|
||||||
@ -81,8 +91,9 @@ internal sealed class EditorWindow : Window
|
|||||||
{
|
{
|
||||||
if (_target != null && _targetLocation != null)
|
if (_target != null && _targetLocation != null)
|
||||||
{
|
{
|
||||||
var context = _targetLocation.Value.Item1;
|
var context = _targetLocation.Value.Context;
|
||||||
var location = _targetLocation.Value.Item2;
|
var node = _targetLocation.Value.Node;
|
||||||
|
var location = _targetLocation.Value.Location;
|
||||||
ImGui.Text(context.File.Directory?.Name ?? string.Empty);
|
ImGui.Text(context.File.Directory?.Name ?? string.Empty);
|
||||||
ImGui.Indent();
|
ImGui.Indent();
|
||||||
ImGui.Text(context.File.Name);
|
ImGui.Text(context.File.Name);
|
||||||
@ -97,7 +108,7 @@ internal sealed class EditorWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
int minAngle = locationOverride.MinimumAngle ?? location.MinimumAngle.GetValueOrDefault();
|
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.MinimumAngle = minAngle;
|
||||||
locationOverride.MaximumAngle ??= location.MaximumAngle.GetValueOrDefault();
|
locationOverride.MaximumAngle ??= location.MaximumAngle.GetValueOrDefault();
|
||||||
@ -105,7 +116,7 @@ internal sealed class EditorWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
int maxAngle = locationOverride.MaximumAngle ?? location.MaximumAngle.GetValueOrDefault();
|
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.MinimumAngle ??= location.MinimumAngle.GetValueOrDefault();
|
||||||
locationOverride.MaximumAngle = maxAngle;
|
locationOverride.MaximumAngle = maxAngle;
|
||||||
@ -119,14 +130,33 @@ internal sealed class EditorWindow : Window
|
|||||||
location.MaximumAngle = locationOverride.MaximumAngle;
|
location.MaximumAngle = locationOverride.MaximumAngle;
|
||||||
_plugin.Save(context.File, context.Root);
|
_plugin.Save(context.File, context.Root);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button("Reset"))
|
if (ImGui.Button("Reset"))
|
||||||
{
|
{
|
||||||
_changes[location.InternalId] = new LocationOverride();
|
_changes[location.InternalId] = new LocationOverride();
|
||||||
_plugin.Redraw();
|
_plugin.Redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndDisabled();
|
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)
|
else if (_target != null)
|
||||||
{
|
{
|
||||||
@ -154,6 +184,7 @@ internal sealed class EditorWindow : Window
|
|||||||
_editorCommands.AddToNewGroup(root, _target);
|
_editorCommands.AddToNewGroup(root, _target);
|
||||||
_plugin.Save(targetFile, root);
|
_plugin.Save(targetFile, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -176,7 +207,7 @@ internal sealed class EditorWindow : Window
|
|||||||
=> _changes.TryGetValue(internalId, out locationOverride);
|
=> _changes.TryGetValue(internalId, out locationOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class LocationOverride
|
internal sealed class LocationOverride
|
||||||
{
|
{
|
||||||
public int? MinimumAngle { get; set; }
|
public int? MinimumAngle { get; set; }
|
||||||
public int? MaximumAngle { get; set; }
|
public int? MaximumAngle { get; set; }
|
||||||
|
@ -40,6 +40,15 @@
|
|||||||
},
|
},
|
||||||
"MinimumAngle": 200,
|
"MinimumAngle": 200,
|
||||||
"MaximumAngle": 360
|
"MaximumAngle": 360
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Position": {
|
||||||
|
"X": -606.7445,
|
||||||
|
"Y": 38.37634,
|
||||||
|
"Z": -425.5284
|
||||||
|
},
|
||||||
|
"MinimumAngle": -80,
|
||||||
|
"MaximumAngle": 70
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -139,8 +148,8 @@
|
|||||||
"Y": 67.64153,
|
"Y": 67.64153,
|
||||||
"Z": -477.6673
|
"Z": -477.6673
|
||||||
},
|
},
|
||||||
"MinimumAngle": -90,
|
"MinimumAngle": -105,
|
||||||
"MaximumAngle": 60
|
"MaximumAngle": 75
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
|
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
|
||||||
"Author": [],
|
"Author": [],
|
||||||
"TerritoryId": 1187,
|
"TerritoryId": 1187,
|
||||||
|
"AetheryteShortcut": "Urqopacha - Wachunpelo",
|
||||||
"Groups": [
|
"Groups": [
|
||||||
{
|
{
|
||||||
"Nodes": [
|
"Nodes": [
|
||||||
@ -39,6 +40,15 @@
|
|||||||
},
|
},
|
||||||
"MinimumAngle": -50,
|
"MinimumAngle": -50,
|
||||||
"MaximumAngle": 210
|
"MaximumAngle": 210
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Position": {
|
||||||
|
"X": -394.2657,
|
||||||
|
"Y": -47.86026,
|
||||||
|
"Z": -394.9654
|
||||||
|
},
|
||||||
|
"MinimumAngle": -120,
|
||||||
|
"MaximumAngle": 120
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -71,6 +81,24 @@
|
|||||||
},
|
},
|
||||||
"MinimumAngle": 225,
|
"MinimumAngle": 225,
|
||||||
"MaximumAngle": 360
|
"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,
|
"MinimumAngle": 0,
|
||||||
"MaximumAngle": 150
|
"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",
|
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
|
||||||
"Author": [],
|
"Author": [],
|
||||||
"TerritoryId": 1187,
|
"TerritoryId": 1187,
|
||||||
|
"AetheryteShortcut": "Urqopacha - Wachunpelo",
|
||||||
"Groups": [
|
"Groups": [
|
||||||
{
|
{
|
||||||
"Nodes": [
|
"Nodes": [
|
||||||
@ -26,6 +27,24 @@
|
|||||||
"Y": -129.3952,
|
"Y": -129.3952,
|
||||||
"Z": -396.6573
|
"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,
|
"MinimumAngle": -180,
|
||||||
"MaximumAngle": 45
|
"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,
|
"Y": -85.61841,
|
||||||
"Z": -240.1007
|
"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",
|
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
|
||||||
"Author": [],
|
"Author": [],
|
||||||
"TerritoryId": 1187,
|
"TerritoryId": 1187,
|
||||||
|
"AetheryteShortcut": "Urqopacha - Wachunpelo",
|
||||||
"Groups": [
|
"Groups": [
|
||||||
{
|
{
|
||||||
"Nodes": [
|
"Nodes": [
|
||||||
@ -13,6 +14,24 @@
|
|||||||
"X": 242.7737,
|
"X": 242.7737,
|
||||||
"Y": -135.9734,
|
"Y": -135.9734,
|
||||||
"Z": -431.2313
|
"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,
|
"X": 269.7338,
|
||||||
"Y": -134.0488,
|
"Y": -134.0488,
|
||||||
"Z": -381.6242
|
"Z": -381.6242
|
||||||
}
|
},
|
||||||
|
"MinimumAngle": -85,
|
||||||
|
"MaximumAngle": 145
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -44,6 +65,24 @@
|
|||||||
},
|
},
|
||||||
"MinimumAngle": 105,
|
"MinimumAngle": 105,
|
||||||
"MaximumAngle": 345
|
"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,
|
"Y": -161.1972,
|
||||||
"Z": -644.0471
|
"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
|
"Z": -102.983154
|
||||||
},
|
},
|
||||||
"TerritoryId": 962,
|
"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": {
|
"DelaySecondsAtStart": {
|
||||||
"description": "Time to wait before starting",
|
"description": "Time to wait before starting",
|
||||||
"type": [
|
"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 SkipConditions? SkipConditions { get; set; }
|
||||||
|
|
||||||
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
|
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<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
|
||||||
public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
|
public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
|
||||||
public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
|
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/=bestways/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=braax/@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/=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/=earthenshire/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=electrope/@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>
|
<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;
|
namespace Questionable.Controller;
|
||||||
|
|
||||||
internal sealed class QuestController
|
internal sealed class QuestController : MiniTaskController<QuestController>
|
||||||
{
|
{
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly GameFunctions _gameFunctions;
|
private readonly GameFunctions _gameFunctions;
|
||||||
private readonly MovementController _movementController;
|
private readonly MovementController _movementController;
|
||||||
private readonly CombatController _combatController;
|
private readonly CombatController _combatController;
|
||||||
private readonly ILogger<QuestController> _logger;
|
private readonly GatheringController _gatheringController;
|
||||||
private readonly QuestRegistry _questRegistry;
|
private readonly QuestRegistry _questRegistry;
|
||||||
private readonly IKeyState _keyState;
|
private readonly IKeyState _keyState;
|
||||||
private readonly IChatGui _chatGui;
|
|
||||||
private readonly ICondition _condition;
|
private readonly ICondition _condition;
|
||||||
private readonly Configuration _configuration;
|
private readonly Configuration _configuration;
|
||||||
private readonly YesAlreadyIpc _yesAlreadyIpc;
|
private readonly YesAlreadyIpc _yesAlreadyIpc;
|
||||||
@ -34,8 +33,6 @@ internal sealed class QuestController
|
|||||||
private QuestProgress? _startedQuest;
|
private QuestProgress? _startedQuest;
|
||||||
private QuestProgress? _nextQuest;
|
private QuestProgress? _nextQuest;
|
||||||
private QuestProgress? _simulatedQuest;
|
private QuestProgress? _simulatedQuest;
|
||||||
private readonly Queue<ITask> _taskQueue = new();
|
|
||||||
private ITask? _currentTask;
|
|
||||||
private bool _automatic;
|
private bool _automatic;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -50,6 +47,7 @@ internal sealed class QuestController
|
|||||||
GameFunctions gameFunctions,
|
GameFunctions gameFunctions,
|
||||||
MovementController movementController,
|
MovementController movementController,
|
||||||
CombatController combatController,
|
CombatController combatController,
|
||||||
|
GatheringController gatheringController,
|
||||||
ILogger<QuestController> logger,
|
ILogger<QuestController> logger,
|
||||||
QuestRegistry questRegistry,
|
QuestRegistry questRegistry,
|
||||||
IKeyState keyState,
|
IKeyState keyState,
|
||||||
@ -58,15 +56,15 @@ internal sealed class QuestController
|
|||||||
Configuration configuration,
|
Configuration configuration,
|
||||||
YesAlreadyIpc yesAlreadyIpc,
|
YesAlreadyIpc yesAlreadyIpc,
|
||||||
IEnumerable<ITaskFactory> taskFactories)
|
IEnumerable<ITaskFactory> taskFactories)
|
||||||
|
: base(chatGui, logger)
|
||||||
{
|
{
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_gameFunctions = gameFunctions;
|
_gameFunctions = gameFunctions;
|
||||||
_movementController = movementController;
|
_movementController = movementController;
|
||||||
_combatController = combatController;
|
_combatController = combatController;
|
||||||
_logger = logger;
|
_gatheringController = gatheringController;
|
||||||
_questRegistry = questRegistry;
|
_questRegistry = questRegistry;
|
||||||
_keyState = keyState;
|
_keyState = keyState;
|
||||||
_chatGui = chatGui;
|
|
||||||
_condition = condition;
|
_condition = condition;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_yesAlreadyIpc = yesAlreadyIpc;
|
_yesAlreadyIpc = yesAlreadyIpc;
|
||||||
@ -138,6 +136,7 @@ internal sealed class QuestController
|
|||||||
Stop("HP = 0");
|
Stop("HP = 0");
|
||||||
_movementController.Stop();
|
_movementController.Stop();
|
||||||
_combatController.Stop("HP = 0");
|
_combatController.Stop("HP = 0");
|
||||||
|
_gatheringController.Stop("HP = 0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE])
|
else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE])
|
||||||
@ -147,6 +146,7 @@ internal sealed class QuestController
|
|||||||
Stop("ESC pressed");
|
Stop("ESC pressed");
|
||||||
_movementController.Stop();
|
_movementController.Stop();
|
||||||
_combatController.Stop("ESC pressed");
|
_combatController.Stop("ESC pressed");
|
||||||
|
_gatheringController.Stop("ESC pressed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,9 +377,10 @@ internal sealed class QuestController
|
|||||||
|
|
||||||
_yesAlreadyIpc.RestoreYesAlready();
|
_yesAlreadyIpc.RestoreYesAlready();
|
||||||
_combatController.Stop("ClearTasksInternal");
|
_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);
|
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)
|
public void SimulateQuest(Quest? quest)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestId);
|
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestId);
|
||||||
@ -419,103 +422,23 @@ internal sealed class QuestController
|
|||||||
_nextQuest = null;
|
_nextQuest = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCurrentTask()
|
protected override void UpdateCurrentTask()
|
||||||
{
|
{
|
||||||
if (_gameFunctions.IsOccupied())
|
if (_gameFunctions.IsOccupied())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_currentTask == null)
|
base.UpdateCurrentTask();
|
||||||
{
|
}
|
||||||
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;
|
protected override void OnTaskComplete(ITask task)
|
||||||
try
|
{
|
||||||
{
|
if (task is WaitAtEnd.WaitQuestCompleted)
|
||||||
result = _currentTask.Update();
|
_simulatedQuest = null;
|
||||||
}
|
}
|
||||||
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)
|
protected override void OnNextStep(ILastTask task)
|
||||||
{
|
{
|
||||||
case ETaskResult.StillRunning:
|
IncreaseStepCount(task.QuestId, task.Sequence, true);
|
||||||
return;
|
|
||||||
|
|
||||||
case ETaskResult.SkipRemainingTasksForStep:
|
|
||||||
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
|
|
||||||
_currentTask, result);
|
|
||||||
_currentTask = null;
|
|
||||||
|
|
||||||
while (_taskQueue.TryDequeue(out ITask? nextTask))
|
|
||||||
{
|
|
||||||
if (nextTask is ILastTask)
|
|
||||||
{
|
|
||||||
_currentTask = nextTask;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
case ETaskResult.TaskComplete:
|
|
||||||
_logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
|
|
||||||
_currentTask, result, _taskQueue.Count);
|
|
||||||
|
|
||||||
if (_currentTask is WaitAtEnd.WaitQuestCompleted)
|
|
||||||
_simulatedQuest = null;
|
|
||||||
|
|
||||||
_currentTask = null;
|
|
||||||
|
|
||||||
// handled in next update
|
|
||||||
return;
|
|
||||||
|
|
||||||
case ETaskResult.NextStep:
|
|
||||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
|
||||||
|
|
||||||
var lastTask = (ILastTask)_currentTask;
|
|
||||||
_currentTask = null;
|
|
||||||
IncreaseStepCount(lastTask.QuestId, lastTask.Sequence, true);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case ETaskResult.End:
|
|
||||||
_logger.LogInformation("{Task} → {Result}", _currentTask, result);
|
|
||||||
_currentTask = null;
|
|
||||||
Stop("Task end");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExecuteNextStep(bool automatic)
|
public void ExecuteNextStep(bool automatic)
|
||||||
@ -536,6 +459,7 @@ internal sealed class QuestController
|
|||||||
|
|
||||||
_movementController.Stop();
|
_movementController.Stop();
|
||||||
_combatController.Stop("Execute next step");
|
_combatController.Stop("Execute next step");
|
||||||
|
_gatheringController.Stop("Execute next step");
|
||||||
|
|
||||||
var newTasks = _taskFactories
|
var newTasks = _taskFactories
|
||||||
.SelectMany(x =>
|
.SelectMany(x =>
|
||||||
@ -568,9 +492,6 @@ internal sealed class QuestController
|
|||||||
_taskQueue.Enqueue(task);
|
_taskQueue.Enqueue(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<string> GetRemainingTaskNames() =>
|
|
||||||
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
|
|
||||||
|
|
||||||
public string ToStatString()
|
public string ToStatString()
|
||||||
{
|
{
|
||||||
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
|
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()
|
public bool Start()
|
||||||
{
|
{
|
||||||
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
|
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId, targetable: true);
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("No game object with dataId {DataId}", DataId);
|
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
|
// 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;
|
_needsUnmount = true;
|
||||||
gameFunctions.Unmount();
|
gameFunctions.Unmount();
|
||||||
_continueAt = DateTime.Now.AddSeconds(1);
|
_continueAt = DateTime.Now.AddSeconds(1);
|
||||||
return true;
|
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);
|
_continueAt = DateTime.Now.AddSeconds(0.5);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -104,11 +106,11 @@ internal static class Interact
|
|||||||
|
|
||||||
if (!_interacted)
|
if (!_interacted)
|
||||||
{
|
{
|
||||||
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
|
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId, targetable: true);
|
||||||
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
|
if (gameObject == null || !IsTargetable(gameObject) || !HasAnyMarker(gameObject))
|
||||||
return ETaskResult.StillRunning;
|
return ETaskResult.StillRunning;
|
||||||
|
|
||||||
_interacted = gameFunctions.InteractWith(DataId);
|
_interacted = gameFunctions.InteractWith(gameObject);
|
||||||
_continueAt = DateTime.Now.AddSeconds(0.5);
|
_continueAt = DateTime.Now.AddSeconds(0.5);
|
||||||
return ETaskResult.StillRunning;
|
return ETaskResult.StillRunning;
|
||||||
}
|
}
|
||||||
@ -125,6 +127,11 @@ internal static class Interact
|
|||||||
return gameObjectStruct->NamePlateIconId != 0;
|
return gameObjectStruct->NamePlateIconId != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTargetable(IGameObject gameObject)
|
||||||
|
{
|
||||||
|
return gameObject.IsTargetable;
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString() => $"Interact({DataId})";
|
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);
|
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)
|
foreach (var gameObject in _objectTable)
|
||||||
{
|
{
|
||||||
|
if (targetable && !gameObject.IsTargetable)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
|
if (gameObject.ObjectKind is ObjectKind.Player or ObjectKind.Companion or ObjectKind.MountType
|
||||||
or ObjectKind.Retainer or ObjectKind.Housing)
|
or ObjectKind.Retainer or ObjectKind.Housing)
|
||||||
continue;
|
continue;
|
||||||
@ -429,19 +432,31 @@ internal sealed unsafe class GameFunctions
|
|||||||
{
|
{
|
||||||
IGameObject? gameObject = FindObjectByDataId(dataId, kind);
|
IGameObject? gameObject = FindObjectByDataId(dataId, kind);
|
||||||
if (gameObject != null)
|
if (gameObject != null)
|
||||||
{
|
return InteractWith(gameObject);
|
||||||
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.EntityId);
|
|
||||||
_targetManager.Target = null;
|
|
||||||
_targetManager.Target = gameObject;
|
|
||||||
|
|
||||||
|
_logger.LogDebug("Game object is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool InteractWith(IGameObject gameObject)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId);
|
||||||
|
_targetManager.Target = null;
|
||||||
|
_targetManager.Target = gameObject;
|
||||||
|
|
||||||
|
if (gameObject.ObjectKind == ObjectKind.GatheringPoint)
|
||||||
|
{
|
||||||
|
TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address);
|
||||||
|
_logger.LogInformation("Interact result: (none) for GatheringPoint");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
|
long result = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, false);
|
||||||
|
|
||||||
_logger.LogInformation("Interact result: {Result}", result);
|
_logger.LogInformation("Interact result: {Result}", result);
|
||||||
return result != 7 && result > 0;
|
return result != 7 && result > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Game object is null");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UseItem(uint itemId)
|
public bool UseItem(uint itemId)
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GatheringPaths\GatheringPaths.csproj" />
|
||||||
<ProjectReference Include="..\LLib\LLib.csproj"/>
|
<ProjectReference Include="..\LLib\LLib.csproj"/>
|
||||||
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
|
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
|
||||||
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
|
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
|
||||||
|
@ -13,6 +13,7 @@ using Questionable.Controller.CombatModules;
|
|||||||
using Questionable.Controller.NavigationOverrides;
|
using Questionable.Controller.NavigationOverrides;
|
||||||
using Questionable.Controller.Steps.Shared;
|
using Questionable.Controller.Steps.Shared;
|
||||||
using Questionable.Controller.Steps.Common;
|
using Questionable.Controller.Steps.Common;
|
||||||
|
using Questionable.Controller.Steps.Gathering;
|
||||||
using Questionable.Controller.Steps.Interactions;
|
using Questionable.Controller.Steps.Interactions;
|
||||||
using Questionable.Data;
|
using Questionable.Data;
|
||||||
using Questionable.External;
|
using Questionable.External;
|
||||||
@ -89,6 +90,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
|||||||
serviceCollection.AddSingleton<ChatFunctions>();
|
serviceCollection.AddSingleton<ChatFunctions>();
|
||||||
serviceCollection.AddSingleton<AetherCurrentData>();
|
serviceCollection.AddSingleton<AetherCurrentData>();
|
||||||
serviceCollection.AddSingleton<AetheryteData>();
|
serviceCollection.AddSingleton<AetheryteData>();
|
||||||
|
serviceCollection.AddSingleton<GatheringData>();
|
||||||
serviceCollection.AddSingleton<JournalData>();
|
serviceCollection.AddSingleton<JournalData>();
|
||||||
serviceCollection.AddSingleton<QuestData>();
|
serviceCollection.AddSingleton<QuestData>();
|
||||||
serviceCollection.AddSingleton<TerritoryData>();
|
serviceCollection.AddSingleton<TerritoryData>();
|
||||||
@ -102,9 +104,12 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
|||||||
// individual tasks
|
// individual tasks
|
||||||
serviceCollection.AddTransient<MountTask>();
|
serviceCollection.AddTransient<MountTask>();
|
||||||
serviceCollection.AddTransient<UnmountTask>();
|
serviceCollection.AddTransient<UnmountTask>();
|
||||||
|
serviceCollection.AddTransient<MoveToLandingLocation>();
|
||||||
|
serviceCollection.AddTransient<WaitGather>();
|
||||||
|
|
||||||
// task factories
|
// task factories
|
||||||
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
|
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
|
||||||
|
serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering>();
|
||||||
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
|
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
|
||||||
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
|
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>();
|
||||||
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
|
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
|
||||||
@ -149,6 +154,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
|||||||
serviceCollection.AddSingleton<GameUiController>();
|
serviceCollection.AddSingleton<GameUiController>();
|
||||||
serviceCollection.AddSingleton<NavigationShortcutController>();
|
serviceCollection.AddSingleton<NavigationShortcutController>();
|
||||||
serviceCollection.AddSingleton<CombatController>();
|
serviceCollection.AddSingleton<CombatController>();
|
||||||
|
serviceCollection.AddSingleton<GatheringController>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
|
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ internal sealed class ActiveQuestComponent
|
|||||||
private readonly QuestController _questController;
|
private readonly QuestController _questController;
|
||||||
private readonly MovementController _movementController;
|
private readonly MovementController _movementController;
|
||||||
private readonly CombatController _combatController;
|
private readonly CombatController _combatController;
|
||||||
|
private readonly GatheringController _gatheringController;
|
||||||
private readonly GameFunctions _gameFunctions;
|
private readonly GameFunctions _gameFunctions;
|
||||||
private readonly ICommandManager _commandManager;
|
private readonly ICommandManager _commandManager;
|
||||||
private readonly IDalamudPluginInterface _pluginInterface;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
@ -29,14 +30,22 @@ internal sealed class ActiveQuestComponent
|
|||||||
private readonly QuestRegistry _questRegistry;
|
private readonly QuestRegistry _questRegistry;
|
||||||
private readonly IChatGui _chatGui;
|
private readonly IChatGui _chatGui;
|
||||||
|
|
||||||
public ActiveQuestComponent(QuestController questController, MovementController movementController,
|
public ActiveQuestComponent(
|
||||||
CombatController combatController, GameFunctions gameFunctions, ICommandManager commandManager,
|
QuestController questController,
|
||||||
IDalamudPluginInterface pluginInterface, Configuration configuration, QuestRegistry questRegistry,
|
MovementController movementController,
|
||||||
|
CombatController combatController,
|
||||||
|
GatheringController gatheringController,
|
||||||
|
GameFunctions gameFunctions,
|
||||||
|
ICommandManager commandManager,
|
||||||
|
IDalamudPluginInterface pluginInterface,
|
||||||
|
Configuration configuration,
|
||||||
|
QuestRegistry questRegistry,
|
||||||
IChatGui chatGui)
|
IChatGui chatGui)
|
||||||
{
|
{
|
||||||
_questController = questController;
|
_questController = questController;
|
||||||
_movementController = movementController;
|
_movementController = movementController;
|
||||||
_combatController = combatController;
|
_combatController = combatController;
|
||||||
|
_gatheringController = gatheringController;
|
||||||
_gameFunctions = gameFunctions;
|
_gameFunctions = gameFunctions;
|
||||||
_commandManager = commandManager;
|
_commandManager = commandManager;
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
@ -93,6 +102,7 @@ internal sealed class ActiveQuestComponent
|
|||||||
{
|
{
|
||||||
_movementController.Stop();
|
_movementController.Stop();
|
||||||
_questController.Stop("Manual (no active quest)");
|
_questController.Stop("Manual (no active quest)");
|
||||||
|
_gatheringController.Stop("Manual (no active quest)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,6 +243,7 @@ internal sealed class ActiveQuestComponent
|
|||||||
{
|
{
|
||||||
_movementController.Stop();
|
_movementController.Stop();
|
||||||
_questController.Stop("Manual");
|
_questController.Stop("Manual");
|
||||||
|
_gatheringController.Stop("Manual");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool lastStep = currentStep ==
|
bool lastStep = currentStep ==
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using ImGuiNET;
|
using System.Collections.Generic;
|
||||||
|
using ImGuiNET;
|
||||||
using Questionable.Controller;
|
using Questionable.Controller;
|
||||||
|
|
||||||
namespace Questionable.Windows.QuestComponents;
|
namespace Questionable.Windows.QuestComponents;
|
||||||
@ -6,22 +7,36 @@ namespace Questionable.Windows.QuestComponents;
|
|||||||
internal sealed class RemainingTasksComponent
|
internal sealed class RemainingTasksComponent
|
||||||
{
|
{
|
||||||
private readonly QuestController _questController;
|
private readonly QuestController _questController;
|
||||||
|
private readonly GatheringController _gatheringController;
|
||||||
|
|
||||||
public RemainingTasksComponent(QuestController questController)
|
public RemainingTasksComponent(QuestController questController, GatheringController gatheringController)
|
||||||
{
|
{
|
||||||
_questController = questController;
|
_questController = questController;
|
||||||
|
_gatheringController = gatheringController;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
var remainingTasks = _questController.GetRemainingTaskNames();
|
IList<string> gatheringTasks = _gatheringController.GetRemainingTaskNames();
|
||||||
if (remainingTasks.Count > 0)
|
if (gatheringTasks.Count > 0)
|
||||||
{
|
{
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
foreach (var task in remainingTasks)
|
foreach (var task in gatheringTasks)
|
||||||
ImGui.TextUnformatted(task);
|
ImGui.TextUnformatted($"G: {task}");
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remainingTasks = _questController.GetRemainingTaskNames();
|
||||||
|
if (remainingTasks.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
foreach (var task in remainingTasks)
|
||||||
|
ImGui.TextUnformatted(task);
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,12 @@
|
|||||||
"resolved": "8.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
||||||
},
|
},
|
||||||
|
"gatheringpaths": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"Questionable.Model": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
"llib": {
|
"llib": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
Loading…
Reference in New Issue
Block a user