1
0
forked from liza/Questionable

Updates for everything

This commit is contained in:
Liza 2024-05-26 21:45:26 +02:00
parent ed109a0fca
commit 5040242dea
Signed by: liza
GPG Key ID: 7199F8D727D55F67
44 changed files with 1093 additions and 122 deletions

View File

@ -11,6 +11,7 @@ namespace Questionable.Controller;
internal sealed class MovementController : IDisposable
{
public const float DefaultStopDistance = 3f;
private readonly NavmeshIpc _navmeshIpc;
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
@ -30,6 +31,8 @@ internal sealed class MovementController : IDisposable
public bool IsNavmeshReady => _navmeshIpc.IsReady;
public bool IsPathRunning => _navmeshIpc.IsPathRunning;
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public Vector3? Destination { get; private set; }
public float StopDistance { get; private set; }
public void Update()
{
@ -48,14 +51,24 @@ internal sealed class MovementController : IDisposable
ResetPathfinding();
}
}
if (IsPathRunning && Destination != null)
{
Vector3 localPlayerPosition = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
if ((localPlayerPosition - Destination.Value).Length() < StopDistance)
Stop();
}
}
public void NavigateTo(EMovementType type, Vector3 to, bool fly)
public void NavigateTo(EMovementType type, Vector3 to, bool fly, float? stopDistance = null)
{
ResetPathfinding();
_gameFunctions.ExecuteCommand("/automove off");
Destination = to;
StopDistance = stopDistance ?? (DefaultStopDistance - 0.2f);
_cancellationTokenSource = new();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
_pathfindTask =

View File

@ -1,22 +1,61 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Numerics;
using System.Text.Json;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Questionable.Data;
using Questionable.Model.V1;
namespace Questionable.Controller;
internal sealed class QuestController
{
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 TerritoryData _territoryData;
private readonly Dictionary<ushort, Quest> _quests = new();
public QuestController(DalamudPluginInterface pluginInterface)
public QuestController(DalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState,
GameFunctions gameFunctions, MovementController movementController, IPluginLog pluginLog, ICondition condition,
IChatGui chatGui, ICommandManager commandManager)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_pluginLog = pluginLog;
_condition = condition;
_chatGui = chatGui;
_commandManager = commandManager;
_aetheryteData = new AetheryteData(dataManager);
_territoryData = new TerritoryData(dataManager);
#if false
LoadFromEmbeddedResources();
#endif
LoadFromDirectory(new DirectoryInfo(@"E:\ffxiv\Questionable\Questionable\QuestPaths"));
LoadFromDirectory(pluginInterface.ConfigDirectory);
foreach (var (questId, quest) in _quests)
{
var questData =
dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
if (questData == null)
continue;
quest.Name = questData.Name.ToString();
}
}
#if false
@ -40,20 +79,30 @@ internal sealed class QuestController
}
#endif
public QuestProgress? CurrentQuest { get; set; }
public string? DebugState { get; set; }
private void LoadFromDirectory(DirectoryInfo configDirectory)
{
foreach (FileInfo fileInfo in configDirectory.GetFiles("*.json"))
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
var (questId, name) = ExtractQuestDataFromName(fileInfo.Name);
Quest quest = new Quest
try
{
FilePath = fileInfo.FullName,
QuestId = questId,
Name = name,
Data = JsonSerializer.Deserialize<QuestData>(stream)!,
};
_quests[questId] = quest;
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
var (questId, name) = ExtractQuestDataFromName(fileInfo.Name);
Quest quest = new Quest
{
FilePath = fileInfo.FullName,
QuestId = questId,
Name = name,
Data = JsonSerializer.Deserialize<QuestData>(stream)!,
};
_quests[questId] = quest;
}
catch (Exception e)
{
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
}
}
foreach (DirectoryInfo childDirectory in configDirectory.GetDirectories())
@ -65,7 +114,253 @@ internal sealed class QuestController
string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
name = name.Substring(name.LastIndexOf('.') + 1);
ushort questId = ushort.Parse(name.Substring(0, name.IndexOf('_')));
return (questId, name);
string[] parts = name.Split('_', 2);
return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
}
public void Update()
{
(ushort currentQuestId, byte currentSequence) = _gameFunctions.GetCurrentQuest();
if (currentQuestId == 0)
{
if (CurrentQuest != null)
CurrentQuest = null;
}
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{
if (_quests.TryGetValue(currentQuestId, out var quest))
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
else if (CurrentQuest != null)
CurrentQuest = null;
}
if (CurrentQuest == null)
{
DebugState = "No quest active";
return;
}
if (_condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57])
{
DebugState = "Occupied";
return;
}
if (!_movementController.IsNavmeshReady)
{
DebugState = "Navmesh not ready";
return;
}
else if (_movementController.IsPathfinding || _movementController.IsPathRunning)
{
DebugState = "Path is running";
return;
}
if (CurrentQuest.Sequence != currentSequence)
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
var q = CurrentQuest.Quest;
var sequence = q.FindSequence(CurrentQuest.Sequence);
if (sequence == null)
{
DebugState = "Sequence not found";
return;
}
if (CurrentQuest.Step == 255)
{
DebugState = "Step completed";
return;
}
if (CurrentQuest.Step >= sequence.Steps.Count)
{
DebugState = "Step not found";
return;
}
var step = sequence.Steps[CurrentQuest.Step];
DebugState = step.Comment ?? sequence.Comment ?? q.Data.Comment;
}
public (QuestSequence? Sequence, QuestStep? Step) GetNextStep()
{
if (CurrentQuest == null)
return (null, null);
var q = CurrentQuest.Quest;
var seq = q.FindSequence(CurrentQuest.Sequence);
if (seq == null)
return (null, null);
if (CurrentQuest.Step >= seq.Steps.Count)
return (null, null);
return (seq, seq.Steps[CurrentQuest.Step]);
}
public void IncreaseStepCount()
{
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (seq == null || step == null)
return;
Debug.Assert(CurrentQuest != null, nameof(CurrentQuest) + " != null");
if (CurrentQuest.Step + 1 < seq.Steps.Count)
{
CurrentQuest = CurrentQuest with
{
Step = CurrentQuest.Step + 1,
AetheryteShortcutUsed = false,
AethernetShortcutUsed = false
};
}
else
{
CurrentQuest = CurrentQuest with
{
Step = 255,
AetheryteShortcutUsed = false,
AethernetShortcutUsed = false
};
}
}
public 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)
{
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 ||
(step.AethernetShortcut != null &&
(_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.From) < 11 ||
_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.To) < 11)))
{
skipTeleport = true;
}
}
if (skipTeleport)
{
CurrentQuest = CurrentQuest with { AetheryteShortcutUsed = true };
}
else
{
if (step.AetheryteShortcut != null)
{
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 };
else
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
}
else
_chatGui.Print("[Questionable] No aetheryte for teleport set.");
return;
}
}
if (!CurrentQuest.AethernetShortcutUsed)
{
if (step.AethernetShortcut != null)
{
EAetheryteLocation from = step.AethernetShortcut.From;
EAetheryteLocation to = step.AethernetShortcut.To;
ushort territoryType = _clientState.TerritoryType;
Vector3 playerPosition = _clientState.LocalPlayer!.Position;
// closer to the source
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
_aetheryteData.CalculateDistance(playerPosition, territoryType, to))
{
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 };
}
else
_movementController.NavigateTo(EMovementType.Quest, _aetheryteData.Locations[from], false,
6.9f);
return;
}
}
}
if (step.Position != null)
{
float distance;
if (step.InteractionType == EInteractionType.WalkTo)
distance = step.StopDistance ?? 0.25f;
else
distance = step.StopDistance ?? MovementController.DefaultStopDistance;
var position = _clientState.LocalPlayer?.Position ?? new Vector3();
float actualDistance = (position - step.Position.Value).Length();
if (actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
_territoryData.CanUseMount(_clientState.TerritoryType))
{
unsafe
{
ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
return;
}
else 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);
IncreaseStepCount();
}
break;
case EInteractionType.WalkTo:
IncreaseStepCount();
break;
default:
_pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
break;
}
}
public sealed record QuestProgress(
Quest Quest,
byte Sequence,
int Step,
bool AetheryteShortcutUsed = false,
bool AethernetShortcutUsed = false);
}

View File

@ -0,0 +1,130 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets2;
using Questionable.Model.V1;
namespace Questionable.Data;
internal sealed class AetheryteData
{
public ReadOnlyDictionary<EAetheryteLocation, Vector3> Locations { get; } =
new Dictionary<EAetheryteLocation, Vector3>
{
{ EAetheryteLocation.Gridania, new(32.913696f, 2.670288f, 30.014404f) },
{ EAetheryteLocation.GridaniaArcher, new(166.58276f, -1.7243042f, 86.13721f) },
{ EAetheryteLocation.GridaniaLeatherworker, new(101.27405f, 9.018005f, -111.31464f) },
{ EAetheryteLocation.GridaniaLancer, new(121.23291f, 12.649658f, -229.63306f) },
{ EAetheryteLocation.GridaniaConjurer, new(-145.15906f, 4.9591064f, -11.7647705f) },
{ EAetheryteLocation.GridaniaBotanist, new(-311.0857f, 7.94989f, -177.05048f) },
{ EAetheryteLocation.GridaniaAmphitheatre, new(-73.92999f, 7.9804688f, -140.15417f) },
{ EAetheryteLocation.Uldah, new(-144.51825f, -1.3580933f, -169.6651f) },
{ EAetheryteLocation.UldahAdventurers, new(64.22522f, 4.5318604f, -115.31244f) },
{ EAetheryteLocation.UldahThaumaturge, new(-154.83331f, 14.633362f, 73.07532f) },
{ EAetheryteLocation.UldahGladiator, new(-53.849182f, 10.696533f, 12.222412f) },
{ EAetheryteLocation.UldahMiner, new(33.49353f, 13.229492f, 113.206665f) },
{ EAetheryteLocation.UldahAlchemist, new(-98.25293f, 42.34375f, 88.45642f) },
{ EAetheryteLocation.UldahWeaver, new(89.64673f, 12.924377f, 58.27417f) },
{ EAetheryteLocation.UldahGoldsmith, new(-19.333252f, 14.602844f, 72.03784f) },
{ EAetheryteLocation.UldahSapphireAvenue, new(131.9447f, 4.714966f, -29.800903f) },
{ EAetheryteLocation.UldahChamberOfRule, new(6.6376343f, 30.655273f, -24.826477f) },
{ EAetheryteLocation.Limsa, new(-84.031494f, 20.767456f, 0.015197754f) },
{ EAetheryteLocation.LimsaAftcastle, new(16.067688f, 40.787354f, 68.80286f) },
{ EAetheryteLocation.LimsaCulinarian, new(-56.50421f, 44.47998f, -131.45648f) },
{ EAetheryteLocation.LimsaArcanist, new(-335.1645f, 12.619202f, 56.381958f) },
{ EAetheryteLocation.LimsaFisher, new(-179.40033f, 4.8065186f, 182.97095f) },
{ EAetheryteLocation.LimsaMarauder, new(-5.1728516f, 44.63257f, -218.06671f) },
{ EAetheryteLocation.LimsaHawkersAlley, new(-213.61108f, 16.739136f, 51.80432f) },
// ... missing a few
{ EAetheryteLocation.Crystarium, new(-65.0188f, 4.5318604f, 0.015197754f) },
{ EAetheryteLocation.CrystariumMarkets, new(-6.149414f, -7.736328f, 148.72961f) },
{ EAetheryteLocation.CrystariumThemenosRookery, new(-107.37775f, -0.015319824f, -58.762512f) },
{ EAetheryteLocation.CrystariumDossalGate, new(64.86609f, -0.015319824f, -18.173523f) },
{ EAetheryteLocation.CrystariumPendants, new(35.477173f, -0.015319824f, 222.58337f) },
{ EAetheryteLocation.CrystariumAmaroLaunch, new(66.60559f, 35.99597f, -131.09033f) },
{ EAetheryteLocation.CrystariumCrystallineMean, new(-52.506348f, 19.97406f, -173.35773f) },
{ EAetheryteLocation.CrystariumCabinetOfCuriosity, new(-54.398438f, -37.70508f, -241.07733f) },
{ EAetheryteLocation.Eulmore, new(0.015197754f, 81.986694f, 0.93078613f) },
{ EAetheryteLocation.EulmoreMainstay, new(10.940674f, 36.087524f, -4.196289f) },
{ EAetheryteLocation.EulmoreNightsoilPots, new(-54.093323f, -0.83929443f, 52.140015f) },
{ EAetheryteLocation.EulmoreGloryGate, new(6.9122925f, 6.240906f, -56.351562f) },
{ EAetheryteLocation.EulmoreSoutheastDerelict, new(71.82422f, -10.391418f, 65.32385f) },
// ... missing a few
{ EAetheryteLocation.OldSharlayan, new(0.07623291f, 4.8065186f, -0.10687256f) },
{ EAetheryteLocation.OldSharlayanStudium, new(-291.1574f, 20.004517f, -74.143616f) },
{ EAetheryteLocation.OldSharlayanBaldesionAnnex, new(-92.21033f, 2.304016f, 29.709229f) },
{ EAetheryteLocation.OldSharlayanRostra, new(-36.94214f, 41.367188f, -156.6034f) },
{ EAetheryteLocation.OldSharlayanLeveilleurEstate, new(204.79126f, 21.774597f, -118.73047f) },
{ EAetheryteLocation.OldSharlayanJourneysEnd, new(206.22559f, 1.8463135f, 13.77887f) },
{ EAetheryteLocation.OldSharlayanScholarsHarbor, new(16.494995f, -16.250854f, 127.73328f) },
{ EAetheryteLocation.RadzAtHan, new(25.986084f, 3.250122f, -27.023743f) },
{ EAetheryteLocation.RadzAtHanMeghaduta, new(-365.95715f, 44.99878f, -31.815125f) },
{ EAetheryteLocation.RadzAtHanRuveydahFibers, new(-156.14563f, 35.99597f, 27.725586f) },
{ EAetheryteLocation.RadzAtHanAirship, new(-144.33508f, 27.969727f, 202.2583f) },
{ EAetheryteLocation.RadzAtHanAlzadaalsPeace, new(6.6071167f, -2.02948f, 110.55151f) },
{ EAetheryteLocation.RadzAtHanHallOfTheRadiantHost, new(-141.37488f, 3.982544f, -98.435974f) },
{ EAetheryteLocation.RadzAtHanMehrydesMeyhane, new(-42.61847f, -0.015319824f, -197.61963f) },
{ EAetheryteLocation.RadzAtHanKama, new(129.59485f, 26.993164f, 13.473633f) },
{ EAetheryteLocation.RadzAtHanHighCrucible, new(57.90796f, -24.704407f, -210.6203f) },
{ EAetheryteLocation.LabyrinthosArcheion, new(443.5338f, 170.6416f, -476.18835f) },
{ EAetheryteLocation.LabyrinthosSharlayanHamlet, new(8.377136f, -27.542603f, -46.67737f) },
{ EAetheryteLocation.LabyrinthosAporia, new(-729.18286f, -27.634155f, 302.1438f) },
{ EAetheryteLocation.ThavnairYedlihmad, new(193.49963f, 6.9733276f, 629.2362f) },
{ EAetheryteLocation.ThavnairGreatWork, new(-527.48914f, 4.776001f, 36.75891f) },
{ EAetheryteLocation.ThavnairPalakasStand, new(405.1422f, 5.2643433f, -244.4953f) },
{ EAetheryteLocation.GarlemaldCampBrokenGlass, new(-408.10254f, 24.15503f, 479.9724f) },
{ EAetheryteLocation.GarlemaldTertium, new(518.9136f, -35.324707f, -178.36273f) },
{ EAetheryteLocation.MareLamentorumSinusLacrimarum, new(-566.2471f, 134.66089f, 650.6294f) },
{ EAetheryteLocation.MareLamentorumBestwaysBurrow, new(-0.015319824f, -128.83197f, -512.0165f) },
{ EAetheryteLocation.ElpisAnagnorisis, new(159.96033f, 11.703674f, 126.878784f) },
{ EAetheryteLocation.ElpisTwelveWonders, new(-633.7225f, -19.821533f, 542.56494f) },
{ EAetheryteLocation.ElpisPoietenOikos, new(-529.9001f, 161.24207f, -222.2782f) },
{ EAetheryteLocation.UltimaThuleReahTahra, new(-544.152f, 74.32666f, 269.6421f) },
{ EAetheryteLocation.UltimaThuleAbodeOfTheEa, new(64.286255f, 272.48022f, -657.49603f) },
{ EAetheryteLocation.UltimaThuleBaseOmicron, new(489.2804f, 437.5829f, 333.63843f) },
}
.AsReadOnly();
public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
public ReadOnlyDictionary<EAetheryteLocation, ushort> TerritoryIds { get; }
public AetheryteData(IDataManager dataManager)
{
Dictionary<EAetheryteLocation, string> aethernetNames = new();
Dictionary<EAetheryteLocation, ushort> territoryIds = new();
foreach (var aetheryte in dataManager.GetExcelSheet<Aetheryte>()!.Where(x => x.RowId > 0))
{
string? aethernetName = aetheryte.AethernetName?.Value?.Name.ToString();
if (!string.IsNullOrEmpty(aethernetName))
aethernetNames[(EAetheryteLocation)aetheryte.RowId] = aethernetName;
if (aetheryte.Territory != null && aetheryte.Territory.Row > 0)
territoryIds[(EAetheryteLocation)aetheryte.RowId] = (ushort)aetheryte.Territory.Row;
}
AethernetNames = aethernetNames.AsReadOnly();
TerritoryIds = territoryIds.AsReadOnly();
}
public float CalculateDistance(Vector3 fromPosition, ushort fromTerritoryType, EAetheryteLocation to)
{
if (!TerritoryIds.TryGetValue(to, out ushort toTerritoryType) || fromTerritoryType != toTerritoryType)
return float.MaxValue;
if (!Locations.TryGetValue(to, out Vector3 toPosition))
return float.MaxValue;
return (fromPosition - toPosition).Length();
}
}

View File

@ -0,0 +1,21 @@
using System.Collections.Immutable;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
namespace Questionable.Data;
internal sealed class TerritoryData
{
private readonly ImmutableHashSet<uint> _territoriesWithMount;
public TerritoryData(IDataManager dataManager)
{
_territoriesWithMount = dataManager.GetExcelSheet<TerritoryType>()!
.Where(x => x.RowId > 0 && x.Mount)
.Select(x => x.RowId)
.ToImmutableHashSet();
}
public bool CanUseMount(ushort territoryId) => _territoriesWithMount.Contains(territoryId);
}

View File

@ -53,6 +53,7 @@ internal sealed class NavmeshIpc
public Task<List<Vector3>> Pathfind(Vector3 localPlayerPosition, Vector3 targetPosition, bool fly,
CancellationToken cancellationToken)
{
_pathSetTolerance.InvokeAction(0.25f);
return _navPathfind.InvokeFunc(localPlayerPosition, targetPosition, fly, cancellationToken);
}

View File

@ -3,17 +3,21 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model.V1;
namespace Questionable;
@ -31,8 +35,15 @@ internal sealed unsafe class GameFunctions
private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
public GameFunctions(IDataManager dataManager, ISigScanner sigScanner)
private readonly IObjectTable _objectTable;
private readonly ITargetManager _targetManager;
private readonly IPluginLog _pluginLog;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, ITargetManager targetManager, IPluginLog pluginLog)
{
_objectTable = objectTable;
_targetManager = targetManager;
_pluginLog = pluginLog;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
_sanitiseString =
@ -45,7 +56,7 @@ internal sealed unsafe class GameFunctions
.AsReadOnly();
}
public (uint CurrentQuest, byte Sequence) GetCurrentQuest()
public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
{
var scenarioTree = AgentScenarioTree.Instance();
if (scenarioTree == null)
@ -69,9 +80,52 @@ internal sealed unsafe class GameFunctions
//ImGui.Text($"Current Quest: {currentQuest}");
//ImGui.Text($"Progress: {QuestManager.GetQuestSequence(currentQuest)}");
return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
return ((ushort)currentQuest, QuestManager.GetQuestSequence(currentQuest));
}
public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
{
var telepo = Telepo.Instance();
if (telepo == null || telepo->UpdateAetheryteList() == null)
{
subIndex = 0;
return false;
}
for (ulong i = 0; i < telepo->TeleportList.Size(); ++ i)
{
var data = telepo->TeleportList.Get(i);
if (data.AetheryteId == aetheryteId)
{
subIndex = data.SubIndex;
return true;
}
}
subIndex = 0;
return false;
}
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _);
public bool TeleportAetheryte(uint aetheryteId)
{
var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
if (status != 0)
return false;
if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
{
return Telepo.Instance()->Teleport(aetheryteId, subIndex);
}
return false;
}
public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
=> TeleportAetheryte((uint)aetheryteLocation);
public bool IsFlyingUnlocked(ushort territoryId)
{
var playerState = PlayerState.Instance();
@ -82,7 +136,7 @@ internal sealed unsafe class GameFunctions
public void ExecuteCommand(string command)
{
if (!command.StartsWith("/", StringComparison.Ordinal))
if (!command.StartsWith('/'))
return;
SendMessage(command);
@ -190,21 +244,37 @@ internal sealed unsafe class GameFunctions
internal ChatPayload(byte[] stringBytes)
{
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
Marshal.WriteByte(textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1);
textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
unk1 = 64;
unk2 = 0;
}
public void Dispose()
{
Marshal.FreeHGlobal(this.textPtr);
Marshal.FreeHGlobal(textPtr);
}
}
#endregion
public void InteractWith(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;
}
}
}
}

View File

@ -1,4 +1,5 @@
using Questionable.Model.V1;
using System.Linq;
using Questionable.Model.V1;
namespace Questionable;
@ -7,6 +8,9 @@ internal sealed class Quest
public required string FilePath { get; init; }
public required ushort QuestId { get; init; }
public required string Name { get; init; }
public required string Name { get; set; }
public required QuestData Data { get; init; }
public QuestSequence? FindSequence(byte currentSequence)
=> Data.QuestSequence.SingleOrDefault(seq => seq.Sequence == currentSequence);
}

View File

@ -61,14 +61,14 @@ public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut
{ EAetheryteLocation.KuganeSekiseigumiBarracks, "[Kugane] Sekiseigumi Barracks" },
{ EAetheryteLocation.KuganeRakuzaDistrict, "[Kugane] Rakuza District" },
{ EAetheryteLocation.KuganeAirship, "[Kugane] Airship Landing" },
{ EAetheryteLocation.Crystarium, "[The Crystarium] Aetheryte Plaza" },
{ EAetheryteLocation.CrystariumMarkets, "[The Crystarium] Musica Universalis Markets" },
{ EAetheryteLocation.CrystariumThemenosRookery, "[The Crystarium] Themenos Rookery" },
{ EAetheryteLocation.CrystariumDossalGate, "[The Crystarium] The Dossal Gate" },
{ EAetheryteLocation.CrystariumPendants, "[The Crystarium] The Pendants" },
{ EAetheryteLocation.CrystariumAmaroLaunch, "[The Crystarium] The Amaro Launch" },
{ EAetheryteLocation.CrystariumCrystallineMean, "[The Crystarium] The Crystalline Mean" },
{ EAetheryteLocation.CrystariumCabinetOfCuriosity, "[The Crystarium] The Cabinet of Curiosity" },
{ EAetheryteLocation.Crystarium, "[Crystarium] Aetheryte Plaza" },
{ EAetheryteLocation.CrystariumMarkets, "[Crystarium] Musica Universalis Markets" },
{ EAetheryteLocation.CrystariumThemenosRookery, "[Crystarium] Themenos Rookery" },
{ EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
{ EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
{ EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
{ EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
{ EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
{ EAetheryteLocation.Eulmore, "[Eulmore] Aetheryte Plaza" },
{ EAetheryteLocation.EulmoreSoutheastDerelict, "[Eulmore] Southeast Derelicts" },
{ EAetheryteLocation.EulmoreNightsoilPots, "[Eulmore] Nightsoil Pots" },
@ -95,7 +95,7 @@ public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut
private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
EnumToString.ToDictionary(x => x.Value, x => x.Key);
public override AethernetShortcut? Read(ref Utf8JsonReader reader, Type typeToConvert,
public override AethernetShortcut Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)

View File

@ -0,0 +1,96 @@
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 AetheryteConverter : JsonConverter<EAetheryteLocation>
{
private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
{
{ EAetheryteLocation.Limsa, "Limsa Lominsa" },
{ EAetheryteLocation.Gridania, "Gridania" },
{ EAetheryteLocation.Uldah, "Ul'dah" },
{ EAetheryteLocation.Ishgard, "Ishgard" },
{ EAetheryteLocation.RhalgrsReach, "Rhalgr's Reach" },
{ EAetheryteLocation.FringesCastrumOriens, "Fringes - Castrum Oriens" },
{ EAetheryteLocation.FringesPeeringStones, "Fringes - Peering Stones" },
{ EAetheryteLocation.PeaksAlaGannha, "Peaks - Ala Gannha" },
{ EAetheryteLocation.PeaksAlaGhiri, "Peaks - Ala Ghiri" },
{ EAetheryteLocation.LochsPortaPraetoria, "Lochs - Porta Praetoria" },
{ EAetheryteLocation.LochsAlaMhiganQuarter, "Lochs - Ala Mhigan Quarter" },
{ EAetheryteLocation.Kugane, "Kugane" },
{ EAetheryteLocation.RubySeaTamamizu, "Ruby Sea - Tamamizu" },
{ EAetheryteLocation.RubySeaOnokoro, "Ruby Sea - Onokoro" },
{ EAetheryteLocation.YanxiaNamai, "Yanxia - Namai" },
{ EAetheryteLocation.YanxiaHouseOfTheFierce, "Yanxia - House of the Fierce" },
{ EAetheryteLocation.AzimSteppeReunion, "Azim Steppe - Reunion" },
{ 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" },
{ EAetheryteLocation.LakelandFortJobb, "Lakeland - Fort Jobb" },
{ EAetheryteLocation.LakelandOstallImperative, "Lakeland - Ostall Imperative" },
{ EAetheryteLocation.KholusiaStilltide, "Kholusia - Stilltide" },
{ EAetheryteLocation.KholusiaWright, "Kholusia - Wright" },
{ EAetheryteLocation.KholusiaTomra, "Kholusia - Tomra" },
{ EAetheryteLocation.AmhAraengMordSouq, "Amh Araeng - Mord Souq" },
{ EAetheryteLocation.AmhAraengInnAtJourneysHead, "Amh Araeng - Inn at Journey's Head" },
{ EAetheryteLocation.AmhAraengTwine, "Amh Araeng - Twine" },
{ EAetheryteLocation.RaktikaSlitherbough, "Rak'tika - Slitherbough" },
{ EAetheryteLocation.RaktikaFanow, "Rak'tika - Fanow" },
{ EAetheryteLocation.IlMhegLydhaLran, "Il Mheg - Lydha Lran" },
{ EAetheryteLocation.IlMhegPiaEnni, "Il Mheg - Pia Enni" },
{ EAetheryteLocation.IlMhegWolekdorf, "Il Mheg - Wolekdorf" },
{ EAetheryteLocation.TempestOndoCups, "Tempest - Ondo Cups" },
{ EAetheryteLocation.TempestMacarensesAngle, "Tempest - Macarenses Angle" },
{ EAetheryteLocation.OldSharlayan, "Old Sharlayan" },
{ EAetheryteLocation.RadzAtHan, "Radz-at-Han" },
{ EAetheryteLocation.LabyrinthosArcheion, "Labyrinthos - Archeion" },
{ EAetheryteLocation.LabyrinthosSharlayanHamlet, "Labyrinthos - Sharlayan Hamlet" },
{ EAetheryteLocation.LabyrinthosAporia, "Labyrinthos - Aporia" },
{ EAetheryteLocation.ThavnairYedlihmad, "Thavnair - Yedlihmad" },
{ EAetheryteLocation.ThavnairGreatWork, "Thavnair - Great Work" },
{ EAetheryteLocation.ThavnairPalakasStand, "Thavnair - Palaka's Stand" },
{ EAetheryteLocation.GarlemaldCampBrokenGlass, "Garlemald - Camp Broken Glass" },
{ EAetheryteLocation.GarlemaldTertium, "Garlemald - Tertium" },
{ EAetheryteLocation.MareLamentorumSinusLacrimarum, "Mare Lamentorum - Sinus Lacrimarum" },
{ EAetheryteLocation.MareLamentorumBestwaysBurrow, "Mare Lamentorum - Bestways Burrow" },
{ EAetheryteLocation.ElpisAnagnorisis, "Elpis - Anagnorisis" },
{ EAetheryteLocation.ElpisTwelveWonders, "Elpis - Twelve Wonders" },
{ EAetheryteLocation.ElpisPoietenOikos, "Elpis - Poieten Oikos" },
{ EAetheryteLocation.UltimaThuleReahTahra, "Ultima Thule - Reah Tahra" },
{ EAetheryteLocation.UltimaThuleAbodeOfTheEa, "Ultima Thula - Abode of the Ea" },
{ EAetheryteLocation.UltimaThuleBaseOmicron, "Ultima Thule - Base Omicron" }
};
private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
EnumToString.ToDictionary(x => x.Value, x => x.Key);
public override EAetheryteLocation 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 EAetheryteLocation value) ? value : throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, EAetheryteLocation value, JsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(writer);
writer.WriteStringValue(EnumToString[value]);
}
}

View File

@ -12,7 +12,7 @@ public sealed class InteractionTypeConverter : JsonConverter<EInteractionType>
{
{ EInteractionType.Interact, "Interact" },
{ EInteractionType.WalkTo, "WalkTo" },
{ EInteractionType.AttuneAethenetShard, "AttuneAethenetShard" },
{ EInteractionType.AttuneAethernetShard, "AttuneAethenetShard" },
{ EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
{ EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
{ EInteractionType.Combat, "Combat" },

View File

@ -0,0 +1,66 @@
using System;
using System.Numerics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Questionable.Model.V1.Converter;
public class VectorConverter : JsonConverter<Vector3>
{
public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
Vector3 vec = new Vector3();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
string? propertyName = reader.GetString();
if (propertyName == null || !reader.Read())
throw new JsonException();
switch (propertyName)
{
case nameof(Vector3.X):
vec.X = reader.GetSingle();
break;
case nameof(Vector3.Y):
vec.Y = reader.GetSingle();
break;
case nameof(Vector3.Z):
vec.Z = reader.GetSingle();
break;
default:
throw new JsonException();
}
break;
case JsonTokenType.EndObject:
return vec;
default:
throw new JsonException();
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(writer);
writer.WriteStartObject();
writer.WriteNumber(nameof(Vector3.X), value.X);
writer.WriteNumber(nameof(Vector3.Y), value.X);
writer.WriteNumber(nameof(Vector3.Z), value.X);
writer.WriteEndObject();
}
}

View File

@ -2,6 +2,8 @@
public enum EAetheryteLocation
{
None = 0,
Gridania = 2,
GridaniaArcher = 25,
GridaniaLeatherworker = 26,
@ -60,6 +62,24 @@ public enum EAetheryteLocation
KuganeRakuzaDistrict = 119,
KuganeAirship = 120,
FringesCastrumOriens = 98,
FringesPeeringStones = 99,
PeaksAlaGannha = 100,
PeaksAlaGhiri = 101,
LochsPortaPraetoria = 102,
LochsAlaMhiganQuarter = 103,
RubySeaTamamizu = 105,
RubySeaOnokoro = 106,
YanxiaNamai = 107,
YanxiaHouseOfTheFierce = 108,
AzimSteppeReunion = 109,
AzimSteppeDawnThrone = 110,
AzimSteppeDhoroIloh = 128,
DomanEnclave = 127,
DomamEnclaveNorthern = 129,
DomamEnclaveSouthern = 130,
Crystarium = 133,
CrystariumMarkets = 149,
CrystariumThemenosRookery = 150,
@ -75,6 +95,22 @@ public enum EAetheryteLocation
EulmoreGloryGate = 159,
EulmoreSoutheastDerelict = 135,
LakelandFortJobb = 132,
LakelandOstallImperative = 136,
KholusiaStilltide = 137,
KholusiaWright = 138,
KholusiaTomra = 139,
AmhAraengMordSouq = 140,
AmhAraengInnAtJourneysHead = 141,
AmhAraengTwine = 142,
RaktikaSlitherbough = 143,
RaktikaFanow = 144,
IlMhegLydhaLran = 145,
IlMhegPiaEnni = 146,
IlMhegWolekdorf = 147,
TempestOndoCups = 148,
TempestMacarensesAngle = 156,
OldSharlayan = 182,
OldSharlayanStudium = 184,
OldSharlayanBaldesionAnnex = 185,
@ -92,4 +128,21 @@ public enum EAetheryteLocation
RadzAtHanMehrydesMeyhane = 196,
RadzAtHanKama = 198,
RadzAtHanHighCrucible = 199,
LabyrinthosArcheion = 166,
LabyrinthosSharlayanHamlet = 167,
LabyrinthosAporia = 168,
ThavnairYedlihmad = 169,
ThavnairGreatWork = 170,
ThavnairPalakasStand = 171,
GarlemaldCampBrokenGlass = 172,
GarlemaldTertium = 173,
MareLamentorumSinusLacrimarum = 174,
MareLamentorumBestwaysBurrow = 175,
ElpisAnagnorisis = 176,
ElpisTwelveWonders = 177,
ElpisPoietenOikos = 178,
UltimaThuleReahTahra = 179,
UltimaThuleAbodeOfTheEa = 180,
UltimaThuleBaseOmicron = 181
}

View File

@ -4,7 +4,7 @@ public enum EInteractionType
{
Interact,
WalkTo,
AttuneAethenetShard,
AttuneAethernetShard,
AttuneAetheryte,
AttuneAetherCurrent,
Combat,

View File

@ -7,7 +7,7 @@ public class QuestData
public required int Version { get; set; }
public required string Author { get; set; }
public List<string> Contributors { get; set; } = new();
public string Comment { get; set; }
public string? Comment { get; set; }
public List<ushort> TerritoryBlacklist { get; set; } = new();
public required List<QuestSequence> QuestSequence { get; set; } = new();
}

View File

@ -5,6 +5,6 @@ namespace Questionable.Model.V1;
public class QuestSequence
{
public required int Sequence { get; set; }
public string Comment { get; set; }
public string? Comment { get; set; }
public List<QuestStep> Steps { get; set; } = new();
}

View File

@ -10,11 +10,19 @@ public class QuestStep
[JsonConverter(typeof(InteractionTypeConverter))]
public EInteractionType InteractionType { get; set; }
public ulong? DataId { get; set; }
public Vector3 Position { get; set; }
public uint? DataId { get; set; }
[JsonConverter(typeof(VectorConverter))]
public Vector3? Position { get; set; }
public float? StopDistance { get; set; }
public ushort TerritoryId { get; set; }
public bool Disabled { get; set; }
public string? Comment { get; set; }
[JsonConverter(typeof(AetheryteConverter))]
public EAetheryteLocation? AetheryteShortcut { get; set; }
[JsonConverter(typeof(AethernetShortcutConverter))]
public AethernetShortcut AethernetShortcut { get; set; }
public AethernetShortcut? AethernetShortcut { get; set; }
}

View File

@ -15,6 +15,7 @@
},
"TerritoryId": 129,
"InteractionType": "Interact",
"AetheryteShortcut": "Limsa Lominsa",
"AethernetShortcut": [
"[Limsa Lominsa] Aetheryte Plaza",
"[Limsa Lominsa] Arcanist's Guild"

View File

@ -63,10 +63,11 @@
{
"DataId": 1038578,
"Position": {
"X": -53.576973,
"X": -56.737106,
"Y": -15.127001,
"Z": 131.09679
"Z": 130.76611
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact"
}
@ -78,10 +79,11 @@
{
"DataId": 1038578,
"Position": {
"X": 0.86839586,
"Y": 3.2250001,
"Z": 9.54673
"X": -0.03130532,
"Y": 3.2249997,
"Z": 8.909777
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact"
}
@ -103,10 +105,11 @@
{
"DataId": 1038578,
"Position": {
"X": 65.84385,
"X": 66.10567,
"Y": 5.0999994,
"Z": -63.417946
"Z": -63.37148
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact"
}
@ -115,6 +118,15 @@
{
"Sequence": 6,
"Steps": [
{
"Position": {
"X": 93.30914,
"Y": 8.920153,
"Z": -89.12467
},
"TerritoryId": 962,
"InteractionType": "WalkTo"
},
{
"DataId": 1038578,
"Position": {
@ -122,6 +134,7 @@
"Y": 41.37599,
"Z": -142.5033
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact"
}
@ -137,6 +150,7 @@
"Y": 41.367188,
"Z": -156.6034
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "AttuneAethenetShard"
},
@ -182,6 +196,7 @@
"Y": 1.0594833,
"Z": 30.052902
},
"StopDistance": 0.25,
"TerritoryId": 962,
"InteractionType": "Interact",
"AethernetShortcut": [

View File

@ -156,6 +156,21 @@
"InteractionType": "Interact"
}
]
},
{
"Sequence": 255,
"Steps": [
{
"DataId": 1038679,
"Position": {
"X": -275.5932,
"Y": 19.003881,
"Z": 13.321045
},
"TerritoryId": 962,
"InteractionType": "Interact"
}
]
}
]
}

View File

@ -67,9 +67,9 @@
{
"DataId": 1037077,
"Position": {
"X": -38.07129,
"X": -38.066784,
"Y": -14.169313,
"Z": 105.30249
"Z": 107.68768
},
"TerritoryId": 962,
"InteractionType": "Interact"
@ -141,9 +141,9 @@
"Steps": [
{
"Position": {
"X": 15.242085,
"X": 19.79008,
"Y": -16.247002,
"Z": 109.177666
"Z": 108.36692
},
"TerritoryId": 962,
"InteractionType": "WalkTo"

View File

@ -44,6 +44,7 @@
},
"TerritoryId": 956,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
14024
]
@ -77,6 +78,7 @@
},
"TerritoryId": 956,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
14023,
14022

View File

@ -109,7 +109,7 @@
},
"TerritoryId": 956,
"InteractionType": "ManualAction",
"Comment": "Duty - Shoot some bird"
"Comment": "Duty - Shoot Large Green Bird"
}
]
},

View File

@ -38,6 +38,7 @@
"Z": -72.22095
},
"TerritoryId": 956,
"EnemySpawnType": "AfterInteraction",
"InteractionType": "Combat",
"KillEnemyDataIds": [
14020
@ -57,6 +58,7 @@
},
"TerritoryId": 956,
"InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [
14021
]
@ -97,21 +99,11 @@
"Sequence": 5,
"Steps": [
{
"DataId": 2011984,
"DataId": 1037985,
"Position": {
"X": 497.09314,
"Y": 73.41101,
"Z": -267.23126
},
"TerritoryId": 956,
"InteractionType": "AttuneAetherCurrent"
},
{
"DataId": 1038708,
"Position": {
"X": 408.31604,
"Y": 65.3329,
"Z": -130.11371
"X": 481.8036,
"Y": 66.16195,
"Z": -108.537415
},
"TerritoryId": 956,
"InteractionType": "Interact"
@ -127,11 +119,11 @@
"InteractionType": "Interact"
},
{
"DataId": 1037985,
"DataId": 1038708,
"Position": {
"X": 481.8036,
"Y": 66.16195,
"Z": -108.537415
"X": 408.31604,
"Y": 65.3329,
"Z": -130.11371
},
"TerritoryId": 956,
"InteractionType": "Interact"
@ -141,6 +133,16 @@
{
"Sequence": 6,
"Steps": [
{
"DataId": 2011984,
"Position": {
"X": 497.09314,
"Y": 73.41101,
"Z": -267.23126
},
"TerritoryId": 956,
"InteractionType": "AttuneAetherCurrent"
},
{
"DataId": 2011843,
"Position": {

View File

@ -56,6 +56,7 @@
"Y": 81.17488,
"Z": -534.99316
},
"StopDistance": 0.25,
"TerritoryId": 956,
"InteractionType": "WalkTo"
}

View File

@ -20,6 +20,24 @@
{
"Sequence": 1,
"Steps": [
{
"Position": {
"X": -329.64972,
"Y": 77.91884,
"Z": -448.5044
},
"TerritoryId": 956,
"InteractionType": "WalkTo"
},
{
"Position": {
"X": -327.6718,
"Y": 79.535736,
"Z": -400.00397
},
"TerritoryId": 956,
"InteractionType": "WalkTo"
},
{
"DataId": 2011982,
"Position": {
@ -30,6 +48,15 @@
"TerritoryId": 956,
"InteractionType": "AttuneAetherCurrent"
},
{
"Position": {
"X": -327.6718,
"Y": 79.535736,
"Z": -400.00397
},
"TerritoryId": 956,
"InteractionType": "WalkTo"
},
{
"DataId": 2011983,
"Position": {

View File

@ -27,28 +27,10 @@
"Y": 6.991216,
"Z": 629.2394
},
"StopDistance": 5,
"TerritoryId": 957,
"InteractionType": "AttuneAetheryte"
},
{
"Position": {
"X": 190.45029,
"Y": 0.9965663,
"Z": 703.01746
},
"TerritoryId": 957,
"InteractionType": "WalkTo"
},
{
"DataId": 1037622,
"Position": {
"X": 187.9148,
"Y": 0.26447815,
"Z": 700.12964
},
"TerritoryId": 957,
"InteractionType": "Interact"
},
{
"DataId": 2011948,
"Position": {
@ -59,6 +41,17 @@
"TerritoryId": 957,
"InteractionType": "Interact"
},
{
"DataId": 1037622,
"Position": {
"X": 189.94562,
"Y": 0.8560083,
"Z": 702.7896
},
"StopDistance": 0.25,
"TerritoryId": 957,
"InteractionType": "Interact"
},
{
"DataId": 2011949,
"Position": {

View File

@ -109,6 +109,7 @@
"Y": 1.769943,
"Z": 738.9843
},
"StopDistance": 0.25,
"TerritoryId": 957,
"InteractionType": "Interact"
}

View File

@ -59,6 +59,7 @@
},
"TerritoryId": 957,
"InteractionType": "Combat",
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
14006
]

View File

@ -42,6 +42,7 @@
"Y": 4.785123,
"Z": 36.76496
},
"StopDistance": 5,
"TerritoryId": 957,
"InteractionType": "AttuneAetheryte"
},

View File

@ -29,6 +29,7 @@
},
"TerritoryId": 957,
"InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [
14004
]

View File

@ -121,6 +121,7 @@
"Y": 88.84356,
"Z": -608.6588
},
"StopDistance": 0.25,
"TerritoryId": 957,
"InteractionType": "WalkTo"
}

View File

@ -20,6 +20,24 @@
{
"Sequence": 1,
"Steps": [
{
"Position": {
"X": -80.636894,
"Y": 99.974266,
"Z": -708.7214
},
"TerritoryId": 957,
"InteractionType": "WalkTo"
},
{
"Position": {
"X": -67.775665,
"Y": 97.140656,
"Z": -710.1025
},
"TerritoryId": 957,
"InteractionType": "WalkTo"
},
{
"DataId": 2011991,
"Position": {

View File

@ -69,6 +69,7 @@
},
"TerritoryId": 962,
"InteractionType": "Interact",
"AetheryteShortcut": "Old Sharlayan",
"AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Baldesion Annex"

View File

@ -28,6 +28,7 @@
"Z": -569.8787
},
"TerritoryId": 957,
"AetheryteShortcut": "Thavnair - Great Work",
"InteractionType": "Interact"
}
]

View File

@ -31,7 +31,8 @@
"Z": 539.6046
},
"TerritoryId": 621,
"InteractionType": "Interact"
"InteractionType": "ManualAction",
"Comment": "Duty - A Frosty Reception"
}
]
},

View File

@ -46,8 +46,8 @@
"InteractionType": "ManualAction",
"Comment": "Cutscene Interaction needed",
"AethernetShortcut": [
"[The Crystarium] Aetheryte Plaza",
"[The Crystarium] The Cabinet of Curiosity"
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Cabinet of Curiosity"
]
}
]

View File

@ -30,8 +30,8 @@
"TerritoryId": 819,
"InteractionType": "Interact",
"AethernetShortcut": [
"[The Crystarium] The Cabinet of Curiosity",
"[The Crystarium] The Dossal Gate"
"[Crystarium] The Cabinet of Curiosity",
"[Crystarium] The Dossal Gate"
]
}
]

View File

@ -45,8 +45,8 @@
"TerritoryId": 819,
"InteractionType": "Interact",
"AethernetShortcut": [
"[The Crystarium] Aetheryte Plaza",
"[The Crystarium] The Cabinet of Curiosity"
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Cabinet of Curiosity"
]
}
]

View File

@ -1,5 +1,5 @@
{
"Version": 1
"Version": 1,
"Author": "liza",
"QuestSequence": [
{
@ -30,8 +30,8 @@
"TerritoryId": 819,
"InteractionType": "Interact",
"AethernetShortcut": [
"[The Crystarium] Aetheryte Plaza",
"[The Crystarium] The Dossal Gate"
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
}
]

View File

@ -30,8 +30,8 @@
"TerritoryId": 819,
"InteractionType": "Interact",
"AethernetShortcut": [
"[The Crystarium] Aetheryte Plaza",
"[The Crystarium] The Dossal Gate"
"[Crystarium] Aetheryte Plaza",
"[Crystarium] The Dossal Gate"
]
}
]

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/quest/1.0",
"$id": "/quest/1.0",
"title": "Questionable V1",
"description": "A series of quest sequences",
"type": "object",
@ -68,6 +68,10 @@
"Z"
]
},
"StopDistance": {
"type": "number",
"exclusiveMinimum": 0
},
"TerritoryId": {
"type": "integer",
"exclusiveMinimum": 0
@ -89,6 +93,72 @@
"Disabled": {
"type": "boolean"
},
"AetheryteShortcut": {
"type": "string",
"$comment": "TODO add remaining aetherytes for 2.x/3.x",
"enum": [
"Limsa Lominsa",
"Gridania",
"Ul'dah",
"Ishgard",
"Rhalgr's Reach",
"Fringes - Castrum Oriens",
"Fringes - Peering Stones",
"Peaks - Ala Gannha",
"Peaks - Ala Ghiri",
"Lochs - Porta Praetoria",
"Lochs - Ala Mhigan Quarter",
"Kugane",
"Ruby Sea - Tamamizu",
"Ruby Sea - Onokoro",
"Yanxia - Namai",
"Yanxia - House of the Fierce",
"Azim Steppe - Reunion",
"Azim Steppe - Dawn Throne",
"Azim Steppe - Dhoro Iloh",
"Doman Enclave",
"Doman Enclave - Northern Enclave",
"Doman Enclave - Southern Enclave",
"Crystarium",
"Eulmore",
"Lakeland - Fort Jobb",
"Lakeland - Ostall Imperative",
"Kholusia - Stilltide",
"Kholusia - Wright",
"Kholusia - Tomra",
"Amh Araeng - Mord Souq",
"Amh Araeng - Inn at Journey's Head",
"Amh Araeng - Twine",
"Rak'tika - Slitherbough",
"Rak'tika - Fanow",
"Il Mheg - Lydha Lran",
"Il Mheg - Pia Enni",
"Il Mheg - Wolekdorf",
"Tempest - Ondo Cups",
"Tempest - Macarenses Angle",
"Old Sharlayan",
"Radz-at-Han",
"Labyrinthos - Archeion",
"Labyrinthos - Sharlayan Hamlet",
"Labyrinthos - Aporia",
"Thavnair - Yedlihmad",
"Thavnair - Great Work",
"Thavnair - Palaka's Stand",
"Garlemald - Camp Broken Glass",
"Garlemald - Tertium",
"Mare Lamentorum - Sinus Lacrimarum",
"Mare Lamentorum - Bestways Burrow",
"Elpis - Anagnorisis",
"Elpis - Twelve Wonders",
"Elpis - Poieten Oikos",
"Ultima Thule - Reah Tahra",
"Ultima Thula - Abode of the Ea",
"Ultima Thule - Base Omicron"
]
},
"AethernetShortcut": {
"type": "array",
"minItems": 2,
@ -147,14 +217,14 @@
"[Kugane] Sekiseigumi Barracks",
"[Kugane] Rakuza District",
"[Kugane] Airship Landing",
"[The Crystarium] Aetheryte Plaza",
"[The Crystarium] Musica Universalis Markets",
"[The Crystarium] Themenos Rookery",
"[The Crystarium] The Dossal Gate",
"[The Crystarium] The Pendants",
"[The Crystarium] The Amaro Launch",
"[The Crystarium] The Crystalline Mean",
"[The Crystarium] The Cabinet of Curiosity",
"[Crystarium] Aetheryte Plaza",
"[Crystarium] Musica Universalis Markets",
"[Crystarium] Themenos Rookery",
"[Crystarium] The Dossal Gate",
"[Crystarium] The Pendants",
"[Crystarium] The Amaro Launch",
"[Crystarium] The Crystalline Mean",
"[Crystarium] The Cabinet of Curiosity",
"[Eulmore] Aetheryte Plaza",
"[Eulmore] Southeast Derelicts",
"[Eulmore] Nightsoil Pots",
@ -179,6 +249,13 @@
]
}
},
"EnemySpawnType": {
"type": "string",
"enum": [
"AutoOnEnterArea",
"AfterInteraction"
]
},
"KillEnemyDataIds": {
"type": "array",
"items": {

View File

@ -1,4 +1,5 @@
using System.Numerics;
using System;
using System.Numerics;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Windowing;
@ -20,20 +21,32 @@ public sealed class Questionable : IDalamudPlugin
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,
IPluginLog pluginLog)
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, sigScanner);
_gameFunctions = new GameFunctions(dataManager, objectTable, sigScanner, targetManager, pluginLog);
NavmeshIpc navmeshIpc = new NavmeshIpc(pluginInterface);
_movementController =
new MovementController(new NavmeshIpc(pluginInterface), clientState, _gameFunctions, pluginLog);
_windowSystem.AddWindow(new DebugWindow(_movementController, _gameFunctions, clientState, targetManager));
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;
@ -41,8 +54,9 @@ public sealed class Questionable : IDalamudPlugin
private void FrameworkUpdate(IFramework framework)
{
HandleNavigationShortcut();
_questController.Update();
HandleNavigationShortcut();
_movementController.Update();
}

View File

@ -1,27 +1,33 @@
using System.Globalization;
using System.Numerics;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
using Questionable.Controller;
using Questionable.Model.V1;
namespace Questionable.Windows;
internal sealed class DebugWindow : Window
{
private readonly MovementController _movementController;
private readonly QuestController _questController;
private readonly GameFunctions _gameFunctions;
private readonly IClientState _clientState;
private readonly ITargetManager _targetManager;
public DebugWindow(MovementController movementController, GameFunctions gameFunctions, IClientState clientState,
public DebugWindow(MovementController movementController, QuestController questController,
GameFunctions gameFunctions, IClientState clientState,
ITargetManager targetManager)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{
_movementController = movementController;
_questController = questController;
_gameFunctions = gameFunctions;
_clientState = clientState;
_targetManager = targetManager;
@ -39,6 +45,33 @@ internal sealed class DebugWindow : Window
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
return;
var currentQuest = _questController.CurrentQuest;
if (currentQuest != null)
{
ImGui.TextUnformatted($"Quest: {currentQuest.Quest.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
ImGui.TextUnformatted(_questController.DebugState ?? "--");
ImGui.BeginDisabled(_questController.GetNextStep().Step == null);
ImGui.Text($"{_questController.GetNextStep().Step?.Position}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
_questController.ExecuteNextStep();
}
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{
_questController.IncreaseStepCount();
}
ImGui.EndDisabled();
}
else
ImGui.Text("No active quest");
ImGui.Separator();
ImGui.Text(
$"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "Yes" : "No")}");
@ -48,7 +81,8 @@ internal sealed class DebugWindow : Window
if (_targetManager.Target != null)
{
ImGui.Separator();
ImGui.Text($"Target: {_targetManager.Target.Name}");
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
$"Target: {_targetManager.Target.Name} ({(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2})"));
ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
if (!_movementController.IsPathfinding)
@ -76,7 +110,8 @@ internal sealed class DebugWindow : Window
ImGui.SameLine();
if (ImGui.Button($"Copy"))
ImGui.Button("Copy");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText($$"""
"DataId": {{_targetManager.Target.DataId}},
@ -89,6 +124,12 @@ internal sealed class DebugWindow : Window
"InteractionType": "Interact"
""");
}
else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
EAetheryteLocation location = (EAetheryteLocation)_targetManager.Target.DataId;
ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
$"{{EAetheryteLocation.{location}, new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)}},"));
}
}
else
{