using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
+using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
using Questionable.External;
namespace Questionable.Controller;
private readonly NavmeshIpc _navmeshIpc;
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
+ private readonly ICondition _condition;
private readonly IPluginLog _pluginLog;
private CancellationTokenSource? _cancellationTokenSource;
private Task<List<Vector3>>? _pathfindTask;
public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
- IPluginLog pluginLog)
+ ICondition condition, IPluginLog pluginLog)
{
_navmeshIpc = navmeshIpc;
_clientState = clientState;
_gameFunctions = gameFunctions;
+ _condition = condition;
_pluginLog = pluginLog;
}
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public Vector3? Destination { get; private set; }
public float StopDistance { get; private set; }
+ public bool IsFlying { get; private set; }
public void Update()
{
if (_pathfindTask.IsCompletedSuccessfully)
{
_pluginLog.Information(
- $"Pathfinding complete, route: [{string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString()))}]");
- _navmeshIpc.MoveTo(_pathfindTask.Result.Skip(1).ToList());
+ string.Create(CultureInfo.InvariantCulture,
+ $"Pathfinding complete, route: [{string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString()))}]"));
+
+ var navPoints = _pathfindTask.Result.Skip(1).ToList();
+ if (!IsFlying && !_condition[ConditionFlag.Mounted] && navPoints.Count > 0 &&
+ !_gameFunctions.HasStatusPreventingSprintOrMount())
+ {
+ Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
+ float actualDistance = 0;
+ foreach (Vector3 end in navPoints)
+ {
+ actualDistance += (start - end).Length();
+ start = end;
+ }
+
+ _pluginLog.Information($"Distance: {actualDistance}");
+ unsafe
+ {
+ // 70 is ~10 seconds of sprint
+ if (actualDistance > 100f &&
+ ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4) == 0)
+ {
+ _pluginLog.Information("Triggering Sprint");
+ ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4);
+ }
+ }
+ }
+
+ _navmeshIpc.MoveTo(navPoints);
ResetPathfinding();
}
else if (_pathfindTask.IsCompleted)
}
}
- public void NavigateTo(EMovementType type, Vector3 to, bool fly, float? stopDistance = null)
+ private void PrepareNavigation(EMovementType type, Vector3 to, bool fly, float? stopDistance)
{
ResetPathfinding();
-
_gameFunctions.ExecuteCommand("/automove off");
Destination = to;
StopDistance = stopDistance ?? (DefaultStopDistance - 0.2f);
+ IsFlying = fly;
+ }
+
+ public void NavigateTo(EMovementType type, Vector3 to, bool fly, float? stopDistance = null)
+ {
+ PrepareNavigation(type, to, fly, stopDistance);
_cancellationTokenSource = new();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
_pathfindTask =
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
}
+ public void NavigateTo(EMovementType type, List<Vector3> to, bool fly, float? stopDistance)
+ {
+ PrepareNavigation(type, to.Last(), fly, stopDistance);
+ _navmeshIpc.MoveTo(to);
+ }
+
public void ResetPathfinding()
{
if (_cancellationTokenSource != null)
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Questionable.Data;
+using Questionable.External;
using Questionable.Model.V1;
namespace Questionable.Controller;
internal sealed class QuestController
{
+ private readonly DalamudPluginInterface _pluginInterface;
+ private readonly IDataManager _dataManager;
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController;
private readonly IPluginLog _pluginLog;
private readonly ICondition _condition;
private readonly IChatGui _chatGui;
- private readonly ICommandManager _commandManager;
private readonly AetheryteData _aetheryteData;
+ private readonly LifestreamIpc _lifestreamIpc;
private readonly TerritoryData _territoryData;
private readonly Dictionary<ushort, Quest> _quests = new();
public QuestController(DalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState,
GameFunctions gameFunctions, MovementController movementController, IPluginLog pluginLog, ICondition condition,
- IChatGui chatGui, ICommandManager commandManager)
+ IChatGui chatGui, AetheryteData aetheryteData, LifestreamIpc lifestreamIpc)
{
+ _pluginInterface = pluginInterface;
+ _dataManager = dataManager;
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_pluginLog = pluginLog;
_condition = condition;
_chatGui = chatGui;
- _commandManager = commandManager;
- _aetheryteData = new AetheryteData(dataManager);
+ _aetheryteData = aetheryteData;
+ _lifestreamIpc = lifestreamIpc;
_territoryData = new TerritoryData(dataManager);
+
+ Reload();
+ }
+
+
+ public QuestProgress? CurrentQuest { get; set; }
+ public string? DebugState { get; private set; }
+ public string? Comment { get; private set; }
+
+ public void Reload()
+ {
+ _quests.Clear();
+
+ CurrentQuest = null;
+ DebugState = null;
+
#if false
LoadFromEmbeddedResources();
#endif
LoadFromDirectory(new DirectoryInfo(@"E:\ffxiv\Questionable\Questionable\QuestPaths"));
- LoadFromDirectory(pluginInterface.ConfigDirectory);
+ LoadFromDirectory(_pluginInterface.ConfigDirectory);
foreach (var (questId, quest) in _quests)
{
var questData =
- dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
+ _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
if (questData == null)
continue;
}
#endif
- public QuestProgress? CurrentQuest { get; set; }
- public string? DebugState { get; set; }
-
private void LoadFromDirectory(DirectoryInfo configDirectory)
{
foreach (FileInfo fileInfo in configDirectory.GetFiles("*.json"))
public void Update()
{
+ Comment = null;
+ DebugState = null;
+
(ushort currentQuestId, byte currentSequence) = _gameFunctions.GetCurrentQuest();
if (currentQuestId == 0)
{
}
var step = sequence.Steps[CurrentQuest.Step];
- DebugState = step.Comment ?? sequence.Comment ?? q.Data.Comment;
+ DebugState = null;
+ Comment = step.Comment ?? sequence.Comment ?? q.Data.Comment;
}
public (QuestSequence? Sequence, QuestStep? Step) GetNextStep()
CurrentQuest = CurrentQuest with
{
Step = CurrentQuest.Step + 1,
- AetheryteShortcutUsed = false,
- AethernetShortcutUsed = false
+ StepProgress = new()
};
}
else
CurrentQuest = CurrentQuest with
{
Step = 255,
- AetheryteShortcutUsed = false,
- AethernetShortcutUsed = false
+ StepProgress = new()
};
}
}
- public void ExecuteNextStep()
+ public unsafe void ExecuteNextStep()
{
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (seq == null || step == null)
return;
Debug.Assert(CurrentQuest != null, nameof(CurrentQuest) + " != null");
- if (!CurrentQuest.AetheryteShortcutUsed && step.AetheryteShortcut != null)
+ if (!CurrentQuest.StepProgress.AetheryteShortcutUsed && step.AetheryteShortcut != null)
{
bool skipTeleport = false;
ushort territoryType = _clientState.TerritoryType;
if (step.TerritoryId == territoryType)
{
- Vector3 playerPosition = _clientState.LocalPlayer!.Position;
- if (_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AetheryteShortcut.Value) < 11 ||
+ Vector3 pos = _clientState.LocalPlayer!.Position;
+ if (_aetheryteData.CalculateDistance(pos, territoryType, step.AetheryteShortcut.Value) < 11 ||
(step.AethernetShortcut != null &&
- (_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.From) < 11 ||
- _aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.To) < 11)))
+ (_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
+ _aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
{
skipTeleport = true;
}
if (skipTeleport)
{
- CurrentQuest = CurrentQuest with { AetheryteShortcutUsed = true };
+ CurrentQuest = CurrentQuest with
+ {
+ StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
+ };
}
else
{
if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
_chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
- CurrentQuest = CurrentQuest with { AetheryteShortcutUsed = true };
+ CurrentQuest = CurrentQuest with
+ {
+ StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
+ };
else
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
}
}
}
- if (!CurrentQuest.AethernetShortcutUsed)
+ if (!CurrentQuest.StepProgress.AethernetShortcutUsed)
{
if (step.AethernetShortcut != null)
{
{
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
{
- // unsure if this works across languages
- _commandManager.ProcessCommand(
- $"/li {_aetheryteData.AethernetNames[step.AethernetShortcut.To].Replace("The ", "", StringComparison.Ordinal)}");
- CurrentQuest = CurrentQuest with { AethernetShortcutUsed = true };
+ _lifestreamIpc.Teleport(to);
+ CurrentQuest = CurrentQuest with
+ {
+ StepProgress = CurrentQuest.StepProgress with { AethernetShortcutUsed = true }
+ };
}
else
_movementController.NavigateTo(EMovementType.Quest, _aetheryteData.Locations[from], false,
var position = _clientState.LocalPlayer?.Position ?? new Vector3();
float actualDistance = (position - step.Position.Value).Length();
- if (actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
- _territoryData.CanUseMount(_clientState.TerritoryType))
+
+ if (step.Mount == true && !_gameFunctions.HasStatusPreventingSprintOrMount())
{
- unsafe
+ if (!_condition[ConditionFlag.Mounted] && _territoryData.CanUseMount(_clientState.TerritoryType))
{
- ActionManager.Instance()->UseAction(ActionType.Mount, 71);
+ if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
+ ActionManager.Instance()->UseAction(ActionType.Mount, 71);
+ return;
}
+ }
+ else if (step.Mount == false)
+ {
+ if (_condition[ConditionFlag.Mounted])
+ {
+ _gameFunctions.Unmount();
+ return;
+ }
+ }
- return;
+ if (!step.DisableNavmesh)
+ {
+ if (step.Mount != false && actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
+ _territoryData.CanUseMount(_clientState.TerritoryType))
+ {
+ if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
+ ActionManager.Instance()->UseAction(ActionType.Mount, 71);
+ return;
+ }
+
+ if (actualDistance > distance)
+ {
+ _movementController.NavigateTo(EMovementType.Quest, step.Position.Value,
+ _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType), distance);
+ return;
+ }
}
- else if (actualDistance > distance)
+ else
{
- _movementController.NavigateTo(EMovementType.Quest, step.Position.Value,
- _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType), distance);
- return;
+ if (actualDistance > distance)
+ {
+ _movementController.NavigateTo(EMovementType.Quest, [step.Position.Value],
+ _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType), distance);
+ return;
+ }
}
}
switch (step.InteractionType)
{
case EInteractionType.Interact:
- case EInteractionType.AttuneAetheryte:
case EInteractionType.AttuneAethernetShard:
- case EInteractionType.AttuneAetherCurrent:
if (step.DataId != null)
{
_gameFunctions.InteractWith(step.DataId.Value);
break;
+ case EInteractionType.AttuneAetheryte:
+ if (step.DataId != null)
+ {
+ if (!_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
+ _gameFunctions.InteractWith(step.DataId.Value);
+
+ IncreaseStepCount();
+ }
+
+ break;
+
+ case EInteractionType.AttuneAetherCurrent:
+ if (step.DataId != null)
+ {
+ _pluginLog.Information(
+ $"{step.AetherCurrentId} → {_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault())}");
+ if (step.AetherCurrentId == null ||
+ !_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.Value))
+ _gameFunctions.InteractWith(step.DataId.Value);
+
+ IncreaseStepCount();
+ }
+
+ break;
+
case EInteractionType.WalkTo:
IncreaseStepCount();
break;
+ case EInteractionType.UseItem:
+ if (step is { DataId: not null, ItemId: not null })
+ {
+ if (_gameFunctions.Unmount())
+ return;
+
+ _gameFunctions.UseItem(step.DataId.Value, step.ItemId.Value);
+ IncreaseStepCount();
+ }
+
+ break;
+
+ case EInteractionType.Combat:
+ if (step.EnemySpawnType != null)
+ {
+ if (_gameFunctions.Unmount())
+ return;
+
+ if (step.DataId != null && step.EnemySpawnType == EEnemySpawnType.AfterInteraction)
+ _gameFunctions.InteractWith(step.DataId.Value);
+
+ // next sequence should trigger automatically
+ IncreaseStepCount();
+ }
+
+ break;
+
+ case EInteractionType.Emote:
+ if (step is { DataId: not null, Emote: not null })
+ {
+ _gameFunctions.UseEmote(step.DataId.Value, step.Emote.Value);
+ IncreaseStepCount();
+ }
+
+ break;
+
+ case EInteractionType.WaitForObjectAtPosition:
+ if (step is { DataId: not null, Position: not null } &&
+ !_gameFunctions.IsObbjectAtPosition(step.DataId.Value, step.Position.Value))
+ {
+ return;
+ }
+
+ IncreaseStepCount();
+ break;
+
default:
_pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
break;
Quest Quest,
byte Sequence,
int Step,
+ StepProgress StepProgress)
+ {
+ public QuestProgress(Quest quest, byte sequence, int step)
+ : this(quest, sequence, step, new StepProgress())
+ {
+ }
+ }
+
+ public sealed record StepProgress(
bool AetheryteShortcutUsed = false,
bool AethernetShortcutUsed = false);
}
{ EAetheryteLocation.LimsaMarauder, new(-5.1728516f, 44.63257f, -218.06671f) },
{ EAetheryteLocation.LimsaHawkersAlley, new(-213.61108f, 16.739136f, 51.80432f) },
- // ... missing a few
+ { EAetheryteLocation.Ishgard, new(-63.98114f, 11.154297f, 43.9917f) },
+ { EAetheryteLocation.IshgardForgottenKnight, new(45.792236f, 24.551636f, 0.99176025f) },
+ { EAetheryteLocation.IshgardSkysteelManufactory, new(-111.436646f, 16.128723f, -27.054321f) },
+ { EAetheryteLocation.IshgardBrume, new(49.42395f, -11.154419f, 66.69714f) },
+ { EAetheryteLocation.IshgardAthenaeumAstrologicum, new(133.37903f, -8.86554f, -64.77466f) },
+ { EAetheryteLocation.IshgardJeweledCrozier, new(-134.6914f, -11.795227f, -15.396423f) },
+ { EAetheryteLocation.IshgardSaintReymanaudsCathedral, new(-77.958374f, 10.60498f, -126.54315f) },
+ { EAetheryteLocation.IshgardTribunal, new(78.01941f, 11.001709f, -126.51257f) },
+ { EAetheryteLocation.IshgardLastVigil, new(0.015197754f, 16.525452f, -32.51703f) },
+
+ { EAetheryteLocation.Idyllshire, new(71.94617f, 211.26111f, -18.905945f) },
+ { EAetheryteLocation.IdyllshireWest, new(-75.48645f, 210.22351f, -21.347473f) },
+
+ { EAetheryteLocation.RhalgrsReach, new(78.23291f, 1.9683228f, 97.45935f) },
+ { EAetheryteLocation.RhalgrsReachWest, new(-84.275635f, 0.503479f, 9.323181f) },
+ { EAetheryteLocation.RhalgrsReachNorthEast, new(101.24353f, 3.463745f, -115.46509f) },
+
+ { EAetheryteLocation.Kugane, new(47.501343f, 8.438171f, -37.30841f) },
+ { EAetheryteLocation.KuganeShiokazeHostelry, new(-73.16705f, -6.088379f, -77.77527f) },
+ { EAetheryteLocation.KuganePier1, new(-113.57294f, -3.8911133f, 155.41309f) },
+ { EAetheryteLocation.KuganeThavnairianConsulate, new(27.17627f, 9.048584f, 141.58838f) },
+ { EAetheryteLocation.KuganeMarkets, new(26.687988f, 4.92865f, 73.3501f) },
+ { EAetheryteLocation.KuganeBokairoInn, new(-76.00525f, 19.058472f, -161.18109f) },
+ { EAetheryteLocation.KuganeRubyBazaar, new(132.40247f, 12.954895f, 83.02429f) },
+ { EAetheryteLocation.KuganeSekiseigumiBarracks, new(119.09656f, 13.01593f, -92.881714f) },
+ { EAetheryteLocation.KuganeRakuzaDistrict, new(24.64331f, 7.003784f, -152.97174f) },
+
+ { EAetheryteLocation.FringesCastrumOriens, new(-629.11426f, 132.89075f, -509.14783f) },
+ { EAetheryteLocation.FringesPeeringStones, new(415.3047f, 117.357056f, 246.75354f) },
+ { EAetheryteLocation.PeaksAlaGannha, new(114.579956f, 120.10376f, -747.06647f) },
+ { EAetheryteLocation.PeaksAlaGhiri, new(-271.3817f, 259.87634f, 748.86694f) },
+ { EAetheryteLocation.LochsPortaPraetoria, new(-652.0333f, 53.391357f, -16.006714f) },
+ { EAetheryteLocation.LochsAlaMhiganQuarter, new(612.4512f, 84.45862f, 656.82446f) },
+ { EAetheryteLocation.RubySeaTamamizu, new(358.72437f, -118.05908f, -263.4165f) },
+ { EAetheryteLocation.RubySeaOnokoro, new(88.181885f, 4.135132f, -583.3677f) },
+ { EAetheryteLocation.YanxiaNamai, new(432.66956f, 73.07532f, -90.74542f) },
+ { EAetheryteLocation.YanxiaHouseOfTheFierce, new(246.02112f, 9.079041f, -401.3581f) },
+ { EAetheryteLocation.AzimSteppeReunion, new(556.1454f, -16.800232f, 340.10828f) },
+ { EAetheryteLocation.AzimSteppeDawnThrone, new(78.26355f, 119.37134f, 36.301147f) },
+ { EAetheryteLocation.AzimSteppeDhoroIloh, new(-754.63495f, 131.2428f, 116.5636f) },
+
+ { EAetheryteLocation.DomanEnclave, new(42.648926f, 1.4190674f, -14.8776245f) },
+ { EAetheryteLocation.DomanEnclaveNorthern, new(8.987488f, 0.8086548f, -105.85187f) },
+ { EAetheryteLocation.DomanEnclaveSouthern, new(-61.57019f, 0.77819824f, 90.684326f) },
+ { EAetheryteLocation.DomanEnclaveDocks, new(96.269165f, -3.4332886f, 81.01013f) },
{ EAetheryteLocation.Crystarium, new(-65.0188f, 4.5318604f, 0.015197754f) },
{ EAetheryteLocation.CrystariumMarkets, new(-6.149414f, -7.736328f, 148.72961f) },
{ EAetheryteLocation.EulmoreGloryGate, new(6.9122925f, 6.240906f, -56.351562f) },
{ EAetheryteLocation.EulmoreSoutheastDerelict, new(71.82422f, -10.391418f, 65.32385f) },
- // ... missing a few
+ { EAetheryteLocation.LakelandFortJobb, new(753.7803f, 24.338135f, -28.82434f) },
+ { EAetheryteLocation.LakelandOstallImperative, new(-735.01184f, 53.391357f, -230.02979f) },
+ { EAetheryteLocation.KholusiaStilltide, new(668.32983f, 29.465088f, 289.17358f) },
+ { EAetheryteLocation.KholusiaWright, new(-244.00702f, 20.736938f, 385.45813f) },
+ { EAetheryteLocation.KholusiaTomra, new(-426.38287f, 419.27222f, -623.5294f) },
+ { EAetheryteLocation.AmhAraengMordSouq, new(246.38745f, 12.985352f, -220.29456f) },
+ { EAetheryteLocation.AmhAraengInnAtJourneysHead, new(399.0996f, -24.521301f, 307.97278f) },
+ { EAetheryteLocation.AmhAraengTwine, new(-511.3451f, 47.989624f, -212.604f) },
+ { EAetheryteLocation.RaktikaSlitherbough, new(-103.4104f, -19.333252f, 297.23047f) },
+ { EAetheryteLocation.RaktikaFanow, new(382.77246f, 21.042175f, -194.11005f) },
+ { EAetheryteLocation.IlMhegLydhaLran, new(-344.71655f, 48.722046f, 512.2606f) },
+ { EAetheryteLocation.IlMhegPiaEnni, new(-72.55664f, 103.95972f, -857.35864f) },
+ { EAetheryteLocation.IlMhegWolekdorf, new(380.51416f, 87.20532f, -687.2511f) },
+ { EAetheryteLocation.TempestOndoCups, new(561.76074f, 352.62073f, -199.17603f) },
+ { EAetheryteLocation.TempestMacarensesAngle, new(-141.74109f, -280.5371f, 218.00562f) },
{ EAetheryteLocation.OldSharlayan, new(0.07623291f, 4.8065186f, -0.10687256f) },
{ EAetheryteLocation.OldSharlayanStudium, new(-291.1574f, 20.004517f, -74.143616f) },
--- /dev/null
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+using Questionable.Data;
+using Questionable.Model.V1;
+
+namespace Questionable.External;
+
+internal sealed class LifestreamIpc
+{
+ private readonly AetheryteData _aetheryteData;
+ private readonly ICallGateSubscriber<string, bool> _aethernetTeleport;
+
+ public LifestreamIpc(DalamudPluginInterface pluginInterface, AetheryteData aetheryteData)
+ {
+ _aetheryteData = aetheryteData;
+ _aethernetTeleport = pluginInterface.GetIpcSubscriber<string, bool>("Lifestream.AethernetTeleport");
+ }
+
+ public bool Teleport(EAetheryteLocation aetheryteLocation)
+ {
+ if (!_aetheryteData.AethernetNames.TryGetValue(aetheryteLocation, out string? name))
+ return false;
+
+ return _aethernetTeleport.InvokeFunc(name);
+ }
+}
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game;
+using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
+using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model.V1;
+using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
+using GameObject = Dalamud.Game.ClientState.Objects.Types.GameObject;
namespace Questionable;
private readonly ProcessChatBoxDelegate _processChatBox;
private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
+ private readonly ReadOnlyDictionary<EEmote, string> _emoteCommands;
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
+ private readonly ICondition _condition;
private readonly IPluginLog _pluginLog;
- public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, ITargetManager targetManager, IPluginLog pluginLog)
+ public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
+ ITargetManager targetManager, ICondition condition, IPluginLog pluginLog)
{
_objectTable = objectTable;
_targetManager = targetManager;
+ _condition = condition;
_pluginLog = pluginLog;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
.Where(x => x.Unknown32 > 0)
.ToDictionary(x => (ushort)x.RowId, x => x.Unknown32)
.AsReadOnly();
+ _emoteCommands = dataManager.GetExcelSheet<Emote>()!
+ .Where(x => x.RowId > 0)
+ .Where(x => x.TextCommand != null && x.TextCommand.Value != null)
+ .Select(x => (x.RowId, Command: x.TextCommand.Value!.Command?.ToString()))
+ .Where(x => x.Command != null && x.Command.StartsWith('/'))
+ .ToDictionary(x => (EEmote)x.RowId, x => x.Command!)
+ .AsReadOnly();
}
public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
return false;
}
- for (ulong i = 0; i < telepo->TeleportList.Size(); ++ i)
+ for (ulong i = 0; i < telepo->TeleportList.Size(); ++i)
{
var data = telepo->TeleportList.Get(i);
if (data.AetheryteId == aetheryteId)
playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
}
+ public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
+ {
+ var playerState = PlayerState.Instance();
+ return playerState != null &&
+ playerState->IsAetherCurrentUnlocked(aetherCurrentId);
+ }
+
public void ExecuteCommand(string command)
{
if (!command.StartsWith('/'))
#endregion
- public void InteractWith(uint dataId)
+ private GameObject? FindObjectByDataId(uint dataId)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.DataId == dataId)
{
- _targetManager.Target = null;
- _targetManager.Target = gameObject;
-
- TargetSystem.Instance()->InteractWithObject(
- (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
- return;
+ return gameObject;
}
}
+
+ return null;
+ }
+
+ public void InteractWith(uint dataId)
+ {
+ GameObject? gameObject = FindObjectByDataId(dataId);
+ if (gameObject != null)
+ {
+ _targetManager.Target = null;
+ _targetManager.Target = gameObject;
+
+ TargetSystem.Instance()->InteractWithObject(
+ (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
+ }
+ }
+
+ public void UseItem(uint dataId, uint itemId)
+ {
+ GameObject? gameObject = FindObjectByDataId(dataId);
+ if (gameObject != null)
+ {
+ _targetManager.Target = gameObject;
+ AgentInventoryContext.Instance()->UseItem(itemId);
+ }
+ }
+
+ public void UseEmote(uint dataId, EEmote emote)
+ {
+ GameObject? gameObject = FindObjectByDataId(dataId);
+ if (gameObject != null)
+ {
+ _targetManager.Target = gameObject;
+ ExecuteCommand($"{_emoteCommands[emote]} motion");
+ }
+ }
+
+ public bool IsObbjectAtPosition(uint dataId, Vector3 position)
+ {
+ GameObject? gameObject = FindObjectByDataId(dataId);
+ return gameObject != null && (gameObject.Position - position).Length() < 0.05f;
+ }
+
+ public bool HasStatusPreventingSprintOrMount()
+ {
+ var gameObject = GameObjectManager.GetGameObjectByIndex(0);
+ if (gameObject != null && gameObject->ObjectKind == 1)
+ {
+ var battleChara = (BattleChara*)gameObject;
+ StatusManager* statusManager = battleChara->GetStatusManager;
+ return statusManager->HasStatus(565);
+ }
+
+ return false;
+ }
+
+ public bool Unmount()
+ {
+ if (_condition[ConditionFlag.Mounted])
+ {
+ if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
+ ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
+
+ return true;
+ }
+
+ return false;
}
}
{ EAetheryteLocation.LimsaAirship, "[Limsa Lominsa] Airship Landing" },
{ EAetheryteLocation.Gridania, "[Gridania] Aetheryte Plaza" },
{ EAetheryteLocation.GridaniaArcher, "[Gridania] Archer's Guild" },
- { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworker's Guld & Shaded Bower" },
+ { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworker's Guild & Shaded Bower" },
{ EAetheryteLocation.GridaniaLancer, "[Gridania] Lancer's Guild" },
{ EAetheryteLocation.GridaniaConjurer, "[Gridania] Conjurer's Guild" },
{ EAetheryteLocation.GridaniaBotanist, "[Gridania] Botanist's Guild" },
namespace Questionable.Model.V1.Converter;
-public class AetheryteConverter : JsonConverter<EAetheryteLocation>
+public sealed class AetheryteConverter : JsonConverter<EAetheryteLocation>
{
private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
{
{ EAetheryteLocation.Gridania, "Gridania" },
{ EAetheryteLocation.Uldah, "Ul'dah" },
{ EAetheryteLocation.Ishgard, "Ishgard" },
+ { EAetheryteLocation.Idyllshire, "Idyllshire" },
{ EAetheryteLocation.RhalgrsReach, "Rhalgr's Reach" },
{ EAetheryteLocation.FringesCastrumOriens, "Fringes - Castrum Oriens" },
{ EAetheryteLocation.AzimSteppeDawnThrone, "Azim Steppe - Dawn Throne" },
{ EAetheryteLocation.AzimSteppeDhoroIloh, "Azim Steppe - Dhoro Iloh" },
{ EAetheryteLocation.DomanEnclave, "Doman Enclave" },
- { EAetheryteLocation.DomamEnclaveNorthern, "Doman Enclave - Northern Enclave" },
- { EAetheryteLocation.DomamEnclaveSouthern, "Doman Enclave - Southern Enclave" },
{ EAetheryteLocation.Crystarium, "Crystarium" },
{ EAetheryteLocation.Eulmore, "Eulmore" },
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.V1.Converter;
+
+public class EmoteConverter : JsonConverter<EEmote>
+{
+ private static readonly Dictionary<EEmote, string> EnumToString = new()
+ {
+ { EEmote.Stretch, "stretch" },
+ { EEmote.Wave, "wave" },
+ { EEmote.Rally, "rally" },
+ { EEmote.Deny, "deny" },
+ };
+
+ private static readonly Dictionary<string, EEmote> StringToEnum =
+ EnumToString.ToDictionary(x => x.Value, x => x.Key);
+
+ public override EEmote Read(ref Utf8JsonReader reader, Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException();
+
+ string? str = reader.GetString();
+ if (str == null)
+ throw new JsonException();
+
+ return StringToEnum.TryGetValue(str, out EEmote value) ? value : throw new JsonException();
+ }
+
+ public override void Write(Utf8JsonWriter writer, EEmote value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue(EnumToString[value]);
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.V1.Converter;
+
+public class EnemySpawnTypeConverter : JsonConverter<EEnemySpawnType>
+{
+ private static readonly Dictionary<EEnemySpawnType, string> EnumToString = new()
+ {
+ { EEnemySpawnType.AfterInteraction, "AfterInteraction" },
+ { EEnemySpawnType.AutoOnEnterArea, "AutoOnEnterArea" },
+ };
+
+ private static readonly Dictionary<string, EEnemySpawnType> StringToEnum =
+ EnumToString.ToDictionary(x => x.Value, x => x.Key);
+
+ public override EEnemySpawnType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException();
+
+ string? str = reader.GetString();
+ if (str == null)
+ throw new JsonException();
+
+ return StringToEnum.TryGetValue(str, out EEnemySpawnType value) ? value : throw new JsonException();
+ }
+
+ public override void Write(Utf8JsonWriter writer, EEnemySpawnType value, JsonSerializerOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.WriteStringValue(EnumToString[value]);
+ }
+
+}
{
{ EInteractionType.Interact, "Interact" },
{ EInteractionType.WalkTo, "WalkTo" },
- { EInteractionType.AttuneAethernetShard, "AttuneAethenetShard" },
+ { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
{ EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
{ EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
{ EInteractionType.Combat, "Combat" },
{ EInteractionType.UseItem, "UseItem" },
{ EInteractionType.Emote, "Emote" },
+ { EInteractionType.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
{ EInteractionType.ManualAction, "ManualAction" }
};
IshgardLastVigil = 87,
Idyllshire = 75,
- IdyllshireWest = 76,
+ IdyllshireWest = 90,
RhalgrsReach = 104,
RhalgrsReachWest = 121,
AzimSteppeDhoroIloh = 128,
DomanEnclave = 127,
- DomamEnclaveNorthern = 129,
- DomamEnclaveSouthern = 130,
+ DomanEnclaveNorthern = 129,
+ DomanEnclaveSouthern = 130,
+ DomanEnclaveDocks = 162,
Crystarium = 133,
CrystariumMarkets = 149,
KholusiaWright = 138,
KholusiaTomra = 139,
AmhAraengMordSouq = 140,
- AmhAraengInnAtJourneysHead = 141,
- AmhAraengTwine = 142,
- RaktikaSlitherbough = 143,
- RaktikaFanow = 144,
- IlMhegLydhaLran = 145,
- IlMhegPiaEnni = 146,
- IlMhegWolekdorf = 147,
- TempestOndoCups = 148,
- TempestMacarensesAngle = 156,
+ AmhAraengInnAtJourneysHead = 161,
+ AmhAraengTwine = 141,
+ RaktikaSlitherbough = 142,
+ RaktikaFanow = 143,
+ IlMhegLydhaLran = 144,
+ IlMhegPiaEnni = 145,
+ IlMhegWolekdorf = 146,
+ TempestOndoCups = 147,
+ TempestMacarensesAngle = 148,
OldSharlayan = 182,
OldSharlayanStudium = 184,
--- /dev/null
+namespace Questionable.Model.V1;
+
+public enum EEmote
+{
+ None = 0,
+
+ Stretch = 37,
+ Wave = 16,
+ Rally = 34,
+ Deny = 25,
+}
--- /dev/null
+namespace Questionable.Model.V1;
+
+public enum EEnemySpawnType
+{
+ None = 0,
+ AfterInteraction,
+ AutoOnEnterArea,
+}
Combat,
UseItem,
Emote,
+ WaitForObjectAtPosition,
ManualAction
}
public float? StopDistance { get; set; }
public ushort TerritoryId { get; set; }
public bool Disabled { get; set; }
+ public bool DisableNavmesh { get; set; }
+ public bool? Mount { get; set; }
public string? Comment { get; set; }
[JsonConverter(typeof(AetheryteConverter))]
[JsonConverter(typeof(AethernetShortcutConverter))]
public AethernetShortcut? AethernetShortcut { get; set; }
+ public uint? AetherCurrentId { get; set; }
+
+ public uint? ItemId { get; set; }
+
+ [JsonConverter(typeof(EmoteConverter))]
+ public EEmote? Emote { get; set; }
+
+ [JsonConverter(typeof(EnemySpawnTypeConverter))]
+ public EEnemySpawnType? EnemySpawnType { get; set; }
+
+ public IList<uint>? KillEnemyDataIds { get; set; }
}
"Z": 127.753
},
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1038578,
{
"Sequence": 4,
"Steps": [
+ {
+ "Position": {
+ "X": -8.38828,
+ "Y": 3.2249968,
+ "Z": 9.224017
+ },
+ "TerritoryId": 962,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 1038578,
"Position": {
"TerritoryId": 962,
"InteractionType": "WalkTo"
},
+ {
+ "Position": {
+ "X": 96.67595,
+ "Y": 15.025446,
+ "Z": -134.08261
+ },
+ "TerritoryId": 962,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 1038578,
"Position": {
"Y": 41.367188,
"Z": -156.6034
},
- "StopDistance": 0.25,
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1038578,
"Y": 18.800978,
"Z": -142.65858
},
+ "StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Z": -118.73047
},
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 188,
"Z": 13.77887
},
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1038578,
"Z": 29.709229
},
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 184,
"Z": -74.143616
},
"TerritoryId": 962,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1038675,
"Y": 19.003881,
"Z": 13.321045
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": 19.003874,
"Z": 18.966919
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": 186.03699,
"Z": -740.9324
},
+ "StopDistance": 1,
"TerritoryId": 956,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"Z": -595.69696
},
"TerritoryId": 956,
+ "StopDistance": 1,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
"Z": -595.69696
},
"TerritoryId": 956,
- "InteractionType": "UseItem"
+ "InteractionType": "UseItem",
+ "ItemId": 2003129
}
]
},
{
"Sequence": 6,
"Steps": [
+ {
+ "Position": {
+ "X": 222.61905,
+ "Y": 182.78828,
+ "Z": -704.0299
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 2011980,
"Position": {
"Z": -767.7577
},
"TerritoryId": 956,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818314
},
{
"DataId": 1038701,
"Z": -823.3921
},
"TerritoryId": 956,
- "InteractionType": "UseItem"
+ "InteractionType": "UseItem",
+ "ItemId": 2003129
}
]
},
{
"Sequence": 255,
"Steps": [
+ {
+ "Position": {
+ "X": 254.80028,
+ "Y": 163.44171,
+ "Z": -626.4951
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 1038702,
"Position": {
"Z": 66.75818
},
"TerritoryId": 956,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818315
+ },
+ {
+ "Position": {
+ "X": 760.1999,
+ "Y": 145.74788,
+ "Z": -52.025288
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo"
},
{
"DataId": 2011840,
{
"Sequence": 4,
"Steps": [
+ {
+ "Position": {
+ "X": 483.16574,
+ "Y": 83.132675,
+ "Z": 74.693695
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo",
+ "Comment": "Avoids aggroing some enemies on the hill"
+ },
{
"DataId": 2011842,
"Position": {
"Z": -267.23126
},
"TerritoryId": 956,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818320
},
{
"DataId": 2011843,
{
"Sequence": 1,
"Steps": [
+ {
+ "Position": {
+ "X": 304.306,
+ "Y": 84.01365,
+ "Z": -292.01114
+ },
+ "TerritoryId": 956,
+ "InteractionType": "WalkTo",
+ "DisableNavmesh": true,
+ "Mount": true
+ },
{
"DataId": 1038736,
"Position": {
"Z": -395.31555
},
"TerritoryId": 956,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818318
},
{
"Position": {
- "X": -327.6718,
- "Y": 79.535736,
- "Z": -400.00397
+ "X": -300.80545,
+ "Y": 59.384476,
+ "Z": -409.0928
},
"TerritoryId": 956,
- "InteractionType": "WalkTo"
+ "InteractionType": "WalkTo",
+ "DisableNavmesh": true,
+ "Mount": true
},
{
"DataId": 2011983,
"Z": -286.27454
},
"TerritoryId": 956,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818319
},
{
"DataId": 1038736,
"Y": 41.37599,
"Z": -141.16125
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": 41.37599,
"Z": -142.1684
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": 41.37599,
"Z": -141.00867
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Y": 41.37599,
"Z": -142.1684
},
+ "StopDistance": 5,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"Z": 4.5318604
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AethernetShortcut": [
+ "[Old Sharlayan] The Leveilleur Estate",
+ "[Old Sharlayan] The Baldesion Annex"
+ ]
},
{
"DataId": 1040291,
"Z": 633.93604
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "DisableNavmesh": true
}
]
},
{
"Sequence": 5,
"Steps": [
+ {
+ "Position": {
+ "X": 232.93636,
+ "Y": 15.136732,
+ "Z": 526.6279
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo",
+ "Mount": true
+ },
{
"DataId": 2011992,
"Position": {
"Z": 473.65527
},
"TerritoryId": 957,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818333,
+ "DisableNavmesh": true
},
{
"Position": {
"TerritoryId": 957,
"InteractionType": "WalkTo"
},
+ {
+ "Position": {
+ "X": 199.50157,
+ "Y": 1.769943,
+ "Z": 738.831
+ },
+ "StopDistance": 0.25,
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 1038608,
"Position": {
"Y": 1.769943,
"Z": 738.9843
},
- "StopDistance": 0.25,
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "Mount": false
}
]
},
"Y": 5.880045,
"Z": 612.02405
},
+ "StopDistance": 5,
"TerritoryId": 957,
"InteractionType": "Interact"
}
"Z": 537.8346
},
"TerritoryId": 957,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818329
},
{
"DataId": 1038616,
{
"Version": 1,
"Author": "liza",
+ "Comment": "This is possibly the least polished quest so far, as the follow steps are awkward (need to move >10 units away to trigger the follow-up)",
"QuestSequence": [
{
"Sequence": 0,
"Y": 49.103825,
"Z": 152.91064
},
+ "StopDistance": 5,
+ "TerritoryId": 957,
+ "InteractionType": "Interact"
+ },
+ {
+ "DataId": 1038636,
+ "Position": {
+ "X": -425.414,
+ "Y": 38.415024,
+ "Z": 160.1206
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WaitForNpcAtPosition"
+ },
+ {
+ "Position": {
+ "X": -425.43683,
+ "Y": 38.413155,
+ "Z": 160.11292
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 1038636,
+ "Position": {
+ "X": -425.414,
+ "Y": 38.415024,
+ "Z": 160.1206
+ },
+ "StopDistance": 4,
"TerritoryId": 957,
- "InteractionType": "ManualAction",
- "Comment": "Follow the Lantern"
+ "InteractionType": "WalkTo"
}
]
},
{
"Sequence": 5,
- "Comment": "Follow the Lantern"
+ "Steps": [
+ {
+ "DataId": 1041219,
+ "Position": {
+ "X": -425.43683,
+ "Y": 38.413155,
+ "Z": 160.11292
+ },
+ "StopDistance": 5,
+ "TerritoryId": 957,
+ "InteractionType": "Interact"
+ },
+ {
+ "DataId": 1041219,
+ "Position": {
+ "X": -419.6078,
+ "Y": 43.950115,
+ "Z": 87.3025
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WaitForNpcAtPosition"
+ },
+ {
+ "Position": {
+ "X": -430.35034,
+ "Y": 46.160213,
+ "Z": 93.2392
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 1041220,
+ "Position": {
+ "X": -419.60785,
+ "Y": 43.9499,
+ "Z": 87.296875
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ }
+ ]
},
{
"Sequence": 6,
- "Comment": "Follow the Lantern"
+ "Steps": [
+ {
+ "DataId": 1041220,
+ "Position": {
+ "X": -419.60785,
+ "Y": 43.9499,
+ "Z": 87.296875
+ },
+ "StopDistance": 5,
+ "TerritoryId": 957,
+ "InteractionType": "Interact"
+ },
+ {
+ "DataId": 1041220,
+ "Position": {
+ "X": -444.7032,
+ "Y": 49.551,
+ "Z": 97.1969
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WaitForNpcAtPosition"
+ },
+ {
+ "Position": {
+ "X": -433.19608,
+ "Y": 46.94587,
+ "Z": 93.295135
+ },
+ "StopDistance": 0.35,
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 1041221,
+ "Position": {
+ "X": -444.72424,
+ "Y": 49.55681,
+ "Z": 97.18469
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ }
+ ]
},
{
"Sequence": 7,
- "Comment": "Follow the Lantern"
+ "Steps": [
+ {
+ "DataId": 1041221,
+ "Position": {
+ "X": -444.72424,
+ "Y": 49.55681,
+ "Z": 97.18469
+ },
+ "StopDistance": 5,
+ "TerritoryId": 957,
+ "InteractionType": "Interact"
+ },
+ {
+ "DataId": 1041221,
+ "Position": {
+ "X": -484.5909,
+ "Y": 54.18516,
+ "Z": 128.3601
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WaitForNpcAtPosition"
+ },
+ {
+ "Position": {
+ "X": -472.65085,
+ "Y": 53.779083,
+ "Z": 124.34451
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 1038637,
+ "Position": {
+ "X": -484.61133,
+ "Y": 54.187614,
+ "Z": 128.34363
+ },
+ "StopDistance": 4,
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ }
+ ]
},
{
"Sequence": 8,
"Y": 54.187614,
"Z": 128.34363
},
+ "StopDistance": 5,
"TerritoryId": 957,
"InteractionType": "Interact"
}
"Z": 117.69275
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Thavnair - Great Work"
}
]
}
"Steps": [
{
"Position": {
- "X": -80.636894,
- "Y": 99.974266,
- "Z": -708.7214
+ "X": -82.02301,
+ "Y": 95.24942,
+ "Z": -697.3925
},
"TerritoryId": 957,
"InteractionType": "WalkTo"
},
{
"Position": {
- "X": -67.775665,
- "Y": 97.140656,
- "Z": -710.1025
+ "X": -78.47051,
+ "Y": 99.96379,
+ "Z": -711.17303
},
"TerritoryId": 957,
"InteractionType": "WalkTo"
"Z": -710.7805
},
"TerritoryId": 957,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818330,
+ "DisableNavmesh": true
},
{
"DataId": 1038631,
"Z": 117.69275
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Thavnair - Great Work"
}
]
},
"Z": -561.82196
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818334
+ },
+ {
+ "Position": {
+ "X": -489.27457,
+ "Y": 72.74904,
+ "Z": -546.8438
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo",
+ "Mount": true
+ },
+ {
+ "Position": {
+ "X": -523.7225,
+ "Y": 9.401685,
+ "Z": -554.4276
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo",
+ "DisableNavmesh": true
},
{
"DataId": 1038651,
"Y": -0.090369135,
"Z": -562.4323
},
+ "StopDistance": 6,
"TerritoryId": 957,
"InteractionType": "Interact"
}
"Z": 17.532532
},
"TerritoryId": 957,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Thavnair - Great Work"
}
]
}
{
"Sequence": 1,
"Steps": [
+ {
+ "Position": {
+ "X": -130.78743,
+ "Y": 86.83725,
+ "Z": -252.08578
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
{
"DataId": 2011994,
"Position": {
"Z": -288.3192
},
"TerritoryId": 957,
- "InteractionType": "AttuneAetherCurrent"
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818335,
+ "DisableNavmesh": true
+ },
+ {
+ "Position": {
+ "X": -156.25183,
+ "Y": 90.34184,
+ "Z": -399.8714
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "Position": {
+ "X": -126.149765,
+ "Y": 73.745605,
+ "Z": -427.64508
+ },
+ "TerritoryId": 957,
+ "InteractionType": "WalkTo",
+ "DisableNavmesh": true
},
{
"DataId": 1038656,
"Y": 1.9073486E-06,
"Z": 1.5715942
},
+ "StopDistance": 5,
"TerritoryId": 987,
"InteractionType": "Interact"
}
"Y": -27.000013,
"Z": 177.5387
},
+ "StopDistance": 5,
"TerritoryId": 963,
"InteractionType": "Interact"
}
"Z": 110.55151
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1040256,
"Z": 202.2583
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 191,
"Z": -31.815125
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1040259,
"Z": 27.725586
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1040264,
"Z": 1.3884888
},
"TerritoryId": 962,
- "InteractionType": "Interact"
+ "InteractionType": "Interact",
+ "AetheryteShortcut": "Old Sharlayan",
+ "AethernetShortcut": [
+ "[Old Sharlayan] Aetheryte Plaza",
+ "[Old Sharlayan] The Baldesion Annex"
+ ]
}
]
},
"Y": 4.357494,
"Z": 0.7476196
},
+ "StopDistance": 6,
"TerritoryId": 962,
"InteractionType": "Interact"
}
"InteractionType": "Interact"
},
{
- "ItemId": -1,
+ "ItemId": null,
"TerritoryId": 959,
"InteractionType": "UseItem"
}
"Z": -197.61963
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1039001,
"Z": 13.473633
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1039540,
"Z": -98.43509
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard"
+ "InteractionType": "AttuneAethernetShard"
},
{
"DataId": 1040374,
"Z": -210.6151
},
"TerritoryId": 963,
- "InteractionType": "AttuneAethenetShard",
+ "InteractionType": "AttuneAethernetShard",
"Comment": "This is pretty late here, maybe move it to some other quest"
},
{
"properties": {
"DataId": {
"type": "integer",
+ "description": "The data id of the NPC/Object/Aetheryte/Aether Current",
"exclusiveMinimum": 0
},
"Position": {
"type": "object",
+ "description": "Position to (typically) walk to",
"properties": {
"X": {
"type": "number"
]
},
"StopDistance": {
- "type": "number",
+ "type": ["number", "null"],
+ "description": "Set if pathfinding should stop closer or further away from the default stop distance",
"exclusiveMinimum": 0
},
"TerritoryId": {
"type": "integer",
+ "description": "The territory id associated with the location",
"exclusiveMinimum": 0
},
"InteractionType": {
"type": "string",
+ "description": "What to do at the position",
"enum": [
"Interact",
"WalkTo",
- "AttuneAethenetShard",
+ "AttuneAethernetShard",
"AttuneAetheryte",
"AttuneAetherCurrent",
"Combat",
"UseItem",
"Emote",
+ "WaitForNpcAtPosition",
"ManualAction"
]
},
"Disabled": {
+ "description": "Unused (TODO)",
+ "type": "boolean"
+ },
+ "DisableNavmesh": {
+ "description": "If true, will go to the position in a straight line instead of using pathfinding",
"type": "boolean"
},
+ "Mount": {
+ "type": ["boolean", "null"],
+ "description": "If true, will mount regardless of distance to position. If false, will unmount."
+ },
"AetheryteShortcut": {
"type": "string",
+ "description": "The Aetheryte to teleport to (before moving)",
"$comment": "TODO add remaining aetherytes for 2.x/3.x",
"enum": [
"Limsa Lominsa",
"Gridania",
"Ul'dah",
"Ishgard",
+ "Idyllshire",
"Rhalgr's Reach",
"Fringes - Castrum Oriens",
"Azim Steppe - Dawn Throne",
"Azim Steppe - Dhoro Iloh",
"Doman Enclave",
- "Doman Enclave - Northern Enclave",
- "Doman Enclave - Southern Enclave",
"Crystarium",
"Eulmore",
"Elpis - Twelve Wonders",
"Elpis - Poieten Oikos",
"Ultima Thule - Reah Tahra",
- "Ultima Thula - Abode of the Ea",
+ "Ultima Thule - Abode of the Ea",
"Ultima Thule - Base Omicron"
]
},
"AethernetShortcut": {
"type": "array",
+ "description": "A pair of aethernet locations (from + to) to use as a shortcut",
"minItems": 2,
"maxItems": 2,
"items": {
"[Limsa Lominsa] Airship Landing",
"[Gridania] Aetheryte Plaza",
"[Gridania] Archer's Guild",
- "[Gridania] Leatherworker's Guld & Shaded Bower",
+ "[Gridania] Leatherworker's Guild & Shaded Bower",
"[Gridania] Lancer's Guild",
"[Gridania] Conjurer's Guild",
"[Gridania] Botanist's Guild",
]
}
},
+ "AetherCurrentId": {
+ "type": "number",
+ "description": "The aether current id, used to check if a given aetheryte is unlocked"
+ },
"EnemySpawnType": {
"type": "string",
+ "description": "Determines how enemy spawning is handled in combat locations",
"enum": [
"AutoOnEnterArea",
"AfterInteraction"
]
},
"KillEnemyDataIds": {
+ "description": "The enemy data ids which are supposed to be killed",
"type": "array",
"items": {
"type": "integer"
},
"Emote": {
"type": "string",
+ "description": "The emote to use",
"enum": [
"stretch",
"wave",
"deny"
]
},
+ "ItemId": {
+ "type": ["number", "null"],
+ "description": "The Item to use",
+ "exclusiveMinimum": 0
+ },
"SkipIf": {
"type": "array",
+ "description": "TODO Not implemented",
"items": {
"type": "string",
"enum": [
+++ /dev/null
-using System;
-using System.Numerics;
-using Dalamud.Game;
-using Dalamud.Game.ClientState.Objects;
-using Dalamud.Interface.Windowing;
-using Dalamud.Plugin;
-using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
-using Questionable.Controller;
-using Questionable.External;
-using Questionable.Windows;
-
-namespace Questionable;
-
-public sealed class Questionable : IDalamudPlugin
-{
- private readonly WindowSystem _windowSystem = new(nameof(Questionable));
-
- private readonly DalamudPluginInterface _pluginInterface;
- private readonly IClientState _clientState;
- private readonly IFramework _framework;
- private readonly IGameGui _gameGui;
- private readonly GameFunctions _gameFunctions;
- private readonly QuestController _questController;
-
- private readonly MovementController _movementController;
-
- public Questionable(DalamudPluginInterface pluginInterface, IClientState clientState, ITargetManager targetManager,
- IFramework framework, IGameGui gameGui, IDataManager dataManager, ISigScanner sigScanner,
- IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui, ICommandManager commandManager)
- {
- ArgumentNullException.ThrowIfNull(pluginInterface);
- ArgumentNullException.ThrowIfNull(sigScanner);
- ArgumentNullException.ThrowIfNull(dataManager);
- ArgumentNullException.ThrowIfNull(objectTable);
-
- _pluginInterface = pluginInterface;
- _clientState = clientState;
- _framework = framework;
- _gameGui = gameGui;
- _gameFunctions = new GameFunctions(dataManager, objectTable, sigScanner, targetManager, pluginLog);
-
- NavmeshIpc navmeshIpc = new NavmeshIpc(pluginInterface);
- _movementController =
- new MovementController(navmeshIpc, clientState, _gameFunctions, pluginLog);
- _questController = new QuestController(pluginInterface, dataManager, _clientState, _gameFunctions,
- _movementController, pluginLog, condition, chatGui, commandManager);
- _windowSystem.AddWindow(new DebugWindow(_movementController, _questController, _gameFunctions, clientState,
- targetManager));
-
- _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
- _framework.Update += FrameworkUpdate;
- }
-
- private void FrameworkUpdate(IFramework framework)
- {
- _questController.Update();
-
- HandleNavigationShortcut();
- _movementController.Update();
- }
-
- private unsafe void HandleNavigationShortcut()
- {
- var inputData = UIInputData.Instance();
- if (inputData == null)
- return;
-
- if (inputData->IsGameWindowFocused &&
- inputData->UIFilteredMouseButtonReleasedFlags.HasFlag(MouseButtonFlags.LBUTTON) &&
- inputData->GetKeyState(SeVirtualKey.MENU).HasFlag(KeyStateFlags.Down) &&
- _gameGui.ScreenToWorld(new Vector2(inputData->CursorXPosition, inputData->CursorYPosition),
- out Vector3 worldPos))
- {
- _movementController.NavigateTo(EMovementType.Shortcut, worldPos,
- _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType));
- }
- }
-
-
- public void Dispose()
- {
- _framework.Update -= FrameworkUpdate;
- _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
-
- _movementController.Dispose();
- }
-}
--- /dev/null
+using System;
+using System.Numerics;
+using Dalamud.Game;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.External;
+using Questionable.Windows;
+
+namespace Questionable;
+
+public sealed class QuestionablePlugin : IDalamudPlugin
+{
+ private readonly WindowSystem _windowSystem = new(nameof(Questionable));
+
+ private readonly DalamudPluginInterface _pluginInterface;
+ private readonly IClientState _clientState;
+ private readonly IFramework _framework;
+ private readonly IGameGui _gameGui;
+ private readonly GameFunctions _gameFunctions;
+ private readonly QuestController _questController;
+
+ private readonly MovementController _movementController;
+
+ public QuestionablePlugin(DalamudPluginInterface pluginInterface, IClientState clientState,
+ ITargetManager targetManager, IFramework framework, IGameGui gameGui, IDataManager dataManager,
+ ISigScanner sigScanner, IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui,
+ ICommandManager commandManager)
+ {
+ ArgumentNullException.ThrowIfNull(pluginInterface);
+ ArgumentNullException.ThrowIfNull(sigScanner);
+ ArgumentNullException.ThrowIfNull(dataManager);
+ ArgumentNullException.ThrowIfNull(objectTable);
+
+ _pluginInterface = pluginInterface;
+ _clientState = clientState;
+ _framework = framework;
+ _gameGui = gameGui;
+ _gameFunctions = new GameFunctions(dataManager, objectTable, sigScanner, targetManager, condition, pluginLog);
+
+ AetheryteData aetheryteData = new AetheryteData(dataManager);
+ NavmeshIpc navmeshIpc = new NavmeshIpc(pluginInterface);
+ LifestreamIpc lifestreamIpc = new LifestreamIpc(pluginInterface, aetheryteData);
+ _movementController =
+ new MovementController(navmeshIpc, clientState, _gameFunctions, condition, pluginLog);
+ _questController = new QuestController(pluginInterface, dataManager, _clientState, _gameFunctions,
+ _movementController, pluginLog, condition, chatGui, aetheryteData, lifestreamIpc);
+ _windowSystem.AddWindow(new DebugWindow(_movementController, _questController, _gameFunctions, clientState,
+ targetManager));
+
+ _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
+ _framework.Update += FrameworkUpdate;
+ }
+
+ private void FrameworkUpdate(IFramework framework)
+ {
+ _questController.Update();
+
+ HandleNavigationShortcut();
+ _movementController.Update();
+ }
+
+ private unsafe void HandleNavigationShortcut()
+ {
+ var inputData = UIInputData.Instance();
+ if (inputData == null)
+ return;
+
+ if (inputData->IsGameWindowFocused &&
+ inputData->UIFilteredMouseButtonReleasedFlags.HasFlag(MouseButtonFlags.LBUTTON) &&
+ inputData->GetKeyState(SeVirtualKey.MENU).HasFlag(KeyStateFlags.Down) &&
+ _gameGui.ScreenToWorld(new Vector2(inputData->CursorXPosition, inputData->CursorYPosition),
+ out Vector3 worldPos))
+ {
+ _movementController.NavigateTo(EMovementType.Shortcut, worldPos,
+ _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType));
+ }
+ }
+
+
+ public void Dispose()
+ {
+ _framework.Update -= FrameworkUpdate;
+ _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
+
+ _movementController.Dispose();
+ }
+}
IsOpen = true;
SizeConstraints = new WindowSizeConstraints
{
- MinimumSize = new Vector2(100, 0),
+ MinimumSize = new Vector2(200, 30),
MaximumSize = default
};
}
public override unsafe void Draw()
{
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
+ {
+ ImGui.Text("Not logged in.");
return;
+ }
var currentQuest = _questController.CurrentQuest;
if (currentQuest != null)
{
ImGui.TextUnformatted($"Quest: {currentQuest.Quest.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
ImGui.TextUnformatted(_questController.DebugState ?? "--");
+ ImGui.TextUnformatted(_questController.Comment ?? "--");
- ImGui.BeginDisabled(_questController.GetNextStep().Step == null);
- ImGui.Text($"{_questController.GetNextStep().Step?.Position}");
+ var nextStep = _questController.GetNextStep();
+ ImGui.BeginDisabled(nextStep.Step == null);
+ ImGui.Text(string.Create(CultureInfo.InvariantCulture,
+ $"{nextStep.Step?.InteractionType} @ {nextStep.Step?.Position}"));
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
_questController.ExecuteNextStep();
if (ImGui.Button("Stop Nav"))
_movementController.Stop();
ImGui.EndDisabled();
+
+ if (ImGui.Button("Reload Data"))
+ _questController.Reload();
}
}