Add basic support for gathering custom delivery items automatically

sb-p1
Liza 2024-08-04 16:03:23 +02:00
parent 2f4f4e24e2
commit 09f11d1914
Signed by: liza
GPG Key ID: 7199F8D727D55F67
64 changed files with 826 additions and 206 deletions

View File

@ -92,12 +92,6 @@
"ecommons": {
"type": "Project"
},
"gatheringpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[1.0.0, )"
}
},
"questionable.model": {
"type": "Project",
"dependencies": {

View File

@ -0,0 +1,115 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
"Author": "liza",
"TerritoryId": 614,
"AetheryteShortcut": "Yanxia - Namai",
"Groups": [
{
"Nodes": [
{
"DataId": 33334,
"Locations": [
{
"Position": {
"X": -222.386,
"Y": 23.28162,
"Z": 425.76
}
},
{
"Position": {
"X": -209.1725,
"Y": 22.35068,
"Z": 425.5524
}
}
]
},
{
"DataId": 33333,
"Locations": [
{
"Position": {
"X": -219.9592,
"Y": 22.78741,
"Z": 431.5036
}
}
]
}
]
},
{
"Nodes": [
{
"DataId": 33335,
"Locations": [
{
"Position": {
"X": -349.8553,
"Y": 33.90925,
"Z": 452.5893
},
"MinimumAngle": -90,
"MaximumAngle": 90
}
]
},
{
"DataId": 33336,
"Locations": [
{
"Position": {
"X": -361.5062,
"Y": 33.49068,
"Z": 453.4639
}
},
{
"Position": {
"X": -359.826,
"Y": 35.47207,
"Z": 442.164
}
}
]
}
]
},
{
"Nodes": [
{
"DataId": 33331,
"Locations": [
{
"Position": {
"X": -231.3864,
"Y": 17.74118,
"Z": 511.0694
}
}
]
},
{
"DataId": 33332,
"Locations": [
{
"Position": {
"X": -219.0789,
"Y": 18.05494,
"Z": 525.418
}
},
{
"Position": {
"X": -220.9139,
"Y": 17.97838,
"Z": 514.0063
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"Position": {
"X": -71.31451,
"Y": 206.56206,
"Z": 29.3684
},
"TerritoryId": 478,
"InteractionType": "WalkTo",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Idyllshire"
},
{
"DataId": 1019615,
"Position": {
"X": -71.763245,
"Y": 206.50021,
"Z": 32.638916
},
"StopDistance": 5,
"TerritoryId": 478,
"InteractionType": "Interact"
}
]
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1018393,
"Position": {
"X": -60.380005,
"Y": 206.50021,
"Z": 26.16919
},
"TerritoryId": 478,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Idyllshire"
}
]
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1025878,
"Position": {
"X": 343.984,
"Y": -120.32947,
"Z": -306.0197
},
"TerritoryId": 613,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Ruby Sea - Tamamizu"
}
]
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1020337,
"Position": {
"X": 171.31299,
"Y": 13.02367,
"Z": -89.951965
},
"TerritoryId": 635,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Rhalgr's Reach"
}
]
}
]
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1035211,
"Position": {
"X": -116.96039,
"Y": 0,
"Z": -133.95898
},
"TerritoryId": 886,
"InteractionType": "Interact",
"AetheryteShortcut": "Ishgard",
"AethernetShortcut": [
"[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament"
]
}
]
}
]
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1033543,
"Position": {
"X": 113.38977,
"Y": -20,
"Z": -0.96136475
},
"TerritoryId": 886,
"InteractionType": "Interact",
"AetheryteShortcut": "Ishgard",
"AethernetShortcut": [
"[Ishgard] Aetheryte Plaza",
"[Ishgard] Firmament"
]
}
]
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1031801,
"Position": {
"X": 52.8114,
"Y": 83.001076,
"Z": -65.38495
},
"TerritoryId": 820,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Eulmore"
}
]
}
]
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1042241,
"Position": {
"X": 222.85791,
"Y": 24.942732,
"Z": -197.77222
},
"TerritoryId": 962,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Old Sharlayan",
"AethernetShortcut": [
"[Old Sharlayan] Aetheryte Plaza",
"[Old Sharlayan] The Leveilleur Estate"
]
}
]
}
]
}

View File

@ -0,0 +1,24 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"DataId": 1044547,
"Position": {
"X": -241.68768,
"Y": 51.058994,
"Z": 620.8744
},
"TerritoryId": 816,
"InteractionType": "Interact",
"RequiredGatheredItems": [],
"AetheryteShortcut": "Il Mheg - Lydha Lran",
"Fly": true
}
]
}
]
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
"Author": "liza",
"QuestSequence": [
{
"Sequence": 0,
"Steps": [
{
"Position": {
"X": -44.066154,
"Y": -29.530005,
"Z": -55.85129
},
"TerritoryId": 956,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Labyrinthos - Sharlayan Hamlet",
"RequiredGatheredItems": [],
"Fly": true
},
{
"DataId": 1046073,
"Position": {
"X": -53.635498,
"Y": -29.497286,
"Z": -65.14081
},
"TerritoryId": 956,
"InteractionType": "Interact"
}
]
}
]
}

View File

@ -40,8 +40,4 @@
<AdditionalFiles Include="6.x - Endwalker\**\*.json" />
<AdditionalFiles Include="7.x - Dawntrail\**\*.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="7.x - Dawntrail\Custom Deliveries\" />
</ItemGroup>
</Project>

View File

@ -1041,7 +1041,8 @@
"PickUpQuestId": {
"type": [
"null",
"number"
"number",
"string"
],
"description": "Determines the quest which should be accepted. If empty/null, accepts the quest corresponding to the file name."
}
@ -1061,14 +1062,16 @@
"TurnInQuestId": {
"type": [
"null",
"number"
"number",
"string"
],
"description": "Determines the quest which should be turned in. If empty/null, turns in the quest corresponding to the file name."
},
"NextQuestId": {
"type": [
"null",
"number"
"number",
"string"
],
"description": "For quest chains (e.g. DT healer role quests) Which quest to do next, given that you meet the required level."
}

View File

@ -85,6 +85,7 @@ public enum EAetheryteLocation
IshgardTribunal = 86,
IshgardLastVigil = 87,
IshgardGatesOfJudgement = 88,
IshgardFirmament = 100001,
Idyllshire = 75,
IdyllshireWest = 90,

View File

@ -56,6 +56,7 @@ public sealed class AethernetShardConverter() : EnumConverter<EAetheryteLocation
{ EAetheryteLocation.IshgardTribunal, "[Ishgard] The Tribunal" },
{ EAetheryteLocation.IshgardLastVigil, "[Ishgard] The Last Vigil" },
{ EAetheryteLocation.IshgardGatesOfJudgement, "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)" },
{ EAetheryteLocation.IshgardFirmament, "[Ishgard] Firmament" },
{ EAetheryteLocation.Idyllshire, "[Idyllshire] Aetheryte Plaza" },
{ EAetheryteLocation.IdyllshireWest, "[Idyllshire] West Idyllshire" },

View File

@ -4,12 +4,14 @@ using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter;
public class ElementIdConverter : JsonConverter<ElementId>
public sealed class ElementIdConverter : JsonConverter<ElementId>
{
public override ElementId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
uint value = reader.GetUInt32();
return ElementId.From(value);
if (reader.TokenType == JsonTokenType.Number)
return new QuestId(reader.GetUInt16());
else
return ElementId.FromString(reader.GetString() ?? throw new JsonException());
}
public override void Write(Utf8JsonWriter writer, ElementId value, JsonSerializerOptions options)

View File

@ -50,37 +50,51 @@ public abstract class ElementId : IComparable<ElementId>, IEquatable<ElementId>
return !Equals(left, right);
}
public static ElementId From(uint value)
public static ElementId FromString(string value)
{
if (value >= 100_000 && value < 200_000)
return new LeveId((ushort)(value - 100_000));
if (value.StartsWith("L"))
return new LeveId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
else if (value.StartsWith("S"))
return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
else
return new QuestId((ushort)value);
return new QuestId(ushort.Parse(value, CultureInfo.InvariantCulture));
}
public static bool TryFromString(string value, out ElementId? elementId)
{
try
{
elementId = FromString(value);
return true;
}
catch (Exception)
{
elementId = null;
return false;
}
}
}
public sealed class QuestId : ElementId
public sealed class QuestId(ushort value) : ElementId(value)
{
public QuestId(ushort value)
: base(value)
{
}
public override string ToString()
{
return Value.ToString(CultureInfo.InvariantCulture);
}
}
public sealed class LeveId : ElementId
public sealed class LeveId(ushort value) : ElementId(value)
{
public LeveId(ushort value)
: base(value)
{
}
public override string ToString()
{
return "L" + Value.ToString(CultureInfo.InvariantCulture);
}
}
public sealed class SatisfactionSupplyNpcId(ushort value) : ElementId(value)
{
public override string ToString()
{
return "S" + Value.ToString(CultureInfo.InvariantCulture);
}
}

View File

@ -161,6 +161,7 @@
"[Ishgard] The Tribunal",
"[Ishgard] The Last Vigil",
"[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
"[Ishgard] Firmament",
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] West Idyllshire",
"[Idyllshire] Prologue Gate (Western Hinterlands)",

View File

@ -77,7 +77,7 @@ internal sealed class CommandHandler : IDisposable
case "start":
_questWindow.IsOpen = true;
_questController.ExecuteNextStep(true);
_questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
break;
case "stop":
@ -128,11 +128,11 @@ internal sealed class CommandHandler : IDisposable
return;
}
if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
_debugOverlay.HighlightedQuest = quest.QuestElementId;
_debugOverlay.HighlightedQuest = quest.Id;
_chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name}).");
}
else
@ -147,11 +147,11 @@ internal sealed class CommandHandler : IDisposable
private void SetNextQuest(string[] arguments)
{
if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
if (_gameFunctions.IsQuestLocked(ElementId.From(questId)))
if (_gameFunctions.IsQuestLocked(questId))
_chatGui.PrintError($"[Questionable] Quest {questId} is locked.");
else if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
else if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
_questController.SetNextQuest(quest);
_chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name}).");
@ -170,9 +170,9 @@ internal sealed class CommandHandler : IDisposable
private void SetSimulatedQuest(string[] arguments)
{
if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId))
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null)
{
if (_questRegistry.TryGetQuest(ElementId.From(questId), out Quest? quest))
if (_questRegistry.TryGetQuest(questId, out Quest? quest))
{
_questController.SimulateQuest(quest);
_chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name}).");

View File

@ -0,0 +1,111 @@
using System;
using System.Linq;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller;
internal sealed class ContextMenuController : IDisposable
{
private readonly IContextMenu _contextMenu;
private readonly QuestController _questController;
private readonly GatheringData _gatheringData;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly IGameGui _gameGui;
private readonly IChatGui _chatGui;
private readonly IClientState _clientState;
private readonly ILogger<ContextMenuController> _logger;
public ContextMenuController(
IContextMenu contextMenu,
QuestController questController,
GatheringData gatheringData,
QuestRegistry questRegistry,
QuestData questData,
IGameGui gameGui,
IChatGui chatGui,
IClientState clientState,
ILogger<ContextMenuController> logger)
{
_contextMenu = contextMenu;
_questController = questController;
_gatheringData = gatheringData;
_questRegistry = questRegistry;
_questData = questData;
_gameGui = gameGui;
_chatGui = chatGui;
_clientState = clientState;
_logger = logger;
_contextMenu.OnMenuOpened += MenuOpened;
}
private void MenuOpened(IMenuOpenedArgs args)
{
uint itemId = (uint) _gameGui.HoveredItem;
if (itemId == 0)
return;
if (itemId > 1_000_000)
itemId -= 1_000_000;
if (itemId >= 500_000)
itemId -= 500_000;
if (!_gatheringData.TryGetGatheringPointId(itemId, _clientState.LocalPlayer!.ClassJob.Id, out _))
{
_logger.LogInformation("No gathering point found for current job.");
return;
}
if (_gatheringData.TryGetCustomDeliveryNpc(itemId, out uint npcId))
{
ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
int quantityToGather = collectability > 0 ? 6 : int.MaxValue;
if (collectability == 0)
return;
args.AddMenuItem(new MenuItem
{
Prefix = SeIconChar.Hyadelyn,
PrefixColor = 52,
Name = "Gather with Questionable",
OnClicked = _ => StartGathering(npcId, itemId, quantityToGather, collectability),
});
}
}
private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability)
{
var info = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single(x => x is SatisfactionSupplyInfo);
if (_questRegistry.TryGetQuest(info.QuestId, out Quest? quest))
{
var step = quest.FindSequence(0)!.FindStep(0)!;
step.RequiredGatheredItems =
[
new GatheredItem
{
ItemId = itemId,
ItemCount = quantity,
Collectability = collectability
}
];
_questController.SetNextQuest(quest);
_questController.ExecuteNextStep(QuestController.EAutomationType.CurrentQuestOnly);
}
else
_chatGui.PrintError($"No associated quest ({info.QuestId}).", "Questionable");
}
public void Dispose()
{
_contextMenu.OnMenuOpened -= MenuOpened;
}
}

View File

@ -600,7 +600,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.StartedQuest?.Quest.QuestElementId.Value == 4526)
if (_questController.StartedQuest?.Quest.Id.Value == 4526)
{
_logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@ -610,7 +610,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245)
if (_questController.StartedQuest?.Quest.Id.Value == 245)
{
_logger.LogInformation("Closing ContentsTutorial");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@ -623,7 +623,7 @@ internal sealed class GameUiController : IDisposable
/// </summary>
private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.StartedQuest?.Quest.QuestElementId.Value == 245)
if (_questController.StartedQuest?.Quest.Id.Value == 245)
{
_logger.LogInformation("Closing MultipleHelpWindow");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;

View File

@ -33,7 +33,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private QuestProgress? _startedQuest;
private QuestProgress? _nextQuest;
private QuestProgress? _simulatedQuest;
private bool _automatic;
private EAutomationType _automationType;
/// <summary>
/// Some combat encounters finish relatively early (i.e. they're done as part of progressing the quest, but not
@ -71,16 +71,16 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_taskFactories = taskFactories.ToList().AsReadOnly();
}
public (QuestProgress Progress, CurrentQuestType Type)? CurrentQuestDetails
public (QuestProgress Progress, ECurrentQuestType Type)? CurrentQuestDetails
{
get
{
if (_simulatedQuest != null)
return (_simulatedQuest, CurrentQuestType.Simulated);
else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId))
return (_nextQuest, CurrentQuestType.Next);
return (_simulatedQuest, ECurrentQuestType.Simulated);
else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
return (_nextQuest, ECurrentQuestType.Next);
else if (_startedQuest != null)
return (_startedQuest, CurrentQuestType.Normal);
return (_startedQuest, ECurrentQuestType.Normal);
else
return null;
}
@ -153,10 +153,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (CurrentQuest != null && CurrentQuest.Quest.Root.TerritoryBlacklist.Contains(_clientState.TerritoryType))
return;
if (_automatic && ((_currentTask == null && _taskQueue.Count == 0) ||
_currentTask is WaitAtEnd.WaitQuestAccepted)
&& CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
&& DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
if (_automationType == EAutomationType.Automatic &&
((_currentTask == null && _taskQueue.Count == 0) ||
_currentTask is WaitAtEnd.WaitQuestAccepted)
&& CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
&& DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
{
lock (_progressLock)
{
@ -164,7 +165,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
CurrentQuest.SetStep(0);
}
ExecuteNextStep(true);
ExecuteNextStep(_automationType);
return;
}
@ -182,13 +183,14 @@ internal sealed class QuestController : MiniTaskController<QuestController>
// if the quest is accepted, we no longer track it
bool canUseNextQuest;
if (_nextQuest.Quest.Info.IsRepeatable)
canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.QuestElementId);
canUseNextQuest = !_gameFunctions.IsQuestAccepted(_nextQuest.Quest.Id);
else
canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.QuestElementId);
canUseNextQuest = !_gameFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id);
if (!canUseNextQuest)
{
_logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.QuestElementId);
_logger.LogInformation("Next quest {QuestId} accepted or completed",
_nextQuest.Quest.Id);
_nextQuest = null;
}
}
@ -200,12 +202,15 @@ internal sealed class QuestController : MiniTaskController<QuestController>
currentSequence = _simulatedQuest.Sequence;
questToRun = _simulatedQuest;
}
else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.QuestElementId))
else if (_nextQuest != null && _gameFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
{
questToRun = _nextQuest;
currentSequence = _nextQuest.Sequence; // by definition, this should always be 0
if (_nextQuest.Step == 0 && _currentTask == null && _taskQueue.Count == 0 && _automatic)
ExecuteNextStep(true);
if (_nextQuest.Step == 0 &&
_currentTask == null &&
_taskQueue.Count == 0 &&
_automationType == EAutomationType.Automatic)
ExecuteNextStep(_automationType);
}
else
{
@ -221,7 +226,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
questToRun = null;
}
else if (_startedQuest == null || _startedQuest.Quest.QuestElementId != currentQuestId)
else if (_startedQuest == null || _startedQuest.Quest.Id != currentQuestId)
{
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
{
@ -341,11 +346,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return;
}
if (questId != null && CurrentQuest.Quest.QuestElementId != questId)
if (questId != null && CurrentQuest.Quest.Id != questId)
{
_logger.LogWarning(
"Ignoring 'increase step count' for different quest (expected {ExpectedQuestId}, but we are at {CurrentQuestId}",
questId, CurrentQuest.Quest.QuestElementId);
questId, CurrentQuest.Quest.Id);
return;
}
@ -363,8 +368,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
CurrentQuest.SetStep(255);
}
if (shouldContinue && _automatic)
ExecuteNextStep(true);
if (shouldContinue && _automationType != EAutomationType.Manual)
ExecuteNextStep(_automationType);
}
private void ClearTasksInternal()
@ -387,17 +392,17 @@ internal sealed class QuestController : MiniTaskController<QuestController>
ClearTasksInternal();
// reset task queue
if (continueIfAutomatic && _automatic)
if (continueIfAutomatic && _automationType == EAutomationType.Automatic)
{
if (CurrentQuest?.Step is >= 0 and < 255)
ExecuteNextStep(true);
ExecuteNextStep(_automationType);
else
_logger.LogInformation("Couldn't execute next step during Stop() call");
}
else if (_automatic)
else if (_automationType != EAutomationType.Manual)
{
_logger.LogInformation("Stopping automatic questing");
_automatic = false;
_automationType = EAutomationType.Manual;
_nextQuest = null;
}
}
@ -406,7 +411,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
public void SimulateQuest(Quest? quest)
{
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.QuestElementId);
_logger.LogInformation("SimulateQuest: {QuestId}", quest?.Id);
if (quest != null)
_simulatedQuest = new QuestProgress(quest);
else
@ -415,7 +420,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
public void SetNextQuest(Quest? quest)
{
_logger.LogInformation("NextQuest: {QuestId}", quest?.QuestElementId);
_logger.LogInformation("NextQuest: {QuestId}", quest?.Id);
if (quest != null)
_nextQuest = new QuestProgress(quest);
else
@ -441,10 +446,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
IncreaseStepCount(task.QuestElementId, task.Sequence, true);
}
public void ExecuteNextStep(bool automatic)
public void ExecuteNextStep(EAutomationType automatic)
{
ClearTasksInternal();
_automatic = automatic;
_automationType = automatic;
if (TryPickPriorityQuest())
_logger.LogInformation("Using priority quest over current quest");
@ -452,8 +457,21 @@ internal sealed class QuestController : MiniTaskController<QuestController>
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
{
_logger.LogWarning("Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]",
CurrentQuest?.Quest.QuestElementId, CurrentQuest?.Sequence, CurrentQuest?.Step);
if (CurrentQuestDetails?.Progress.Quest.Id is SatisfactionSupplyNpcId &&
CurrentQuestDetails?.Progress.Sequence == 0 &&
CurrentQuestDetails?.Progress.Step == 255 &&
CurrentQuestDetails?.Type == ECurrentQuestType.Next)
{
_logger.LogInformation("Completed delivery quest");
SetNextQuest(null);
}
else
{
_logger.LogWarning(
"Could not retrieve next quest step, not doing anything [{QuestId}, {Sequence}, {Step}]",
CurrentQuest?.Quest.Id, CurrentQuest?.Sequence, CurrentQuest?.Step);
}
return;
}
@ -488,7 +506,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
}
_logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}",
CurrentQuest.Quest.QuestElementId, seq.Sequence, seq.Steps.IndexOf(step),
CurrentQuest.Quest.Id, seq.Sequence, seq.Steps.IndexOf(step),
string.Join(", ", newTasks.Select(x => x.ToString())));
foreach (var task in newTasks)
_taskQueue.Enqueue(task);
@ -587,7 +605,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return false;
var (currentQuest, type) = details.Value;
if (type != CurrentQuestType.Normal)
if (type != ECurrentQuestType.Normal)
return false;
QuestSequence? currentSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
@ -628,10 +646,17 @@ internal sealed class QuestController : MiniTaskController<QuestController>
DateTime StartedAt,
int PointMenuCounter = 0);
public enum CurrentQuestType
public enum ECurrentQuestType
{
Normal,
Next,
Simulated,
}
public enum EAutomationType
{
Manual,
Automatic,
CurrentQuestOnly,
}
}

View File

@ -91,12 +91,12 @@ internal sealed class QuestRegistry
{
Quest quest = new()
{
QuestElementId = new QuestId(questId),
Id = new QuestId(questId),
Root = questRoot,
Info = _questData.GetQuestInfo(new QuestId(questId)),
ReadOnly = true,
};
_quests[quest.QuestElementId] = quest;
_quests[quest.Id] = quest;
}
_logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count);
@ -145,12 +145,12 @@ internal sealed class QuestRegistry
Quest quest = new Quest
{
QuestElementId = questId,
Id = questId,
Root = questNode.Deserialize<QuestRoot>()!,
Info = _questData.GetQuestInfo(questId),
ReadOnly = false,
};
_quests[quest.QuestElementId] = quest;
_quests[quest.Id] = quest;
}
private void LoadFromDirectory(DirectoryInfo directory, LogLevel logLevel = LogLevel.Information)
@ -188,30 +188,11 @@ internal sealed class QuestRegistry
return null;
string[] parts = name.Split('_', 2);
return ElementId.From(uint.Parse(parts[0], CultureInfo.InvariantCulture));
return ElementId.FromString(parts[0]);
}
public bool IsKnownQuest(ElementId elementId)
{
if (elementId is QuestId questId)
return IsKnownQuest(questId);
else
return false;
}
public bool IsKnownQuest(ElementId questId) => _quests.ContainsKey(questId);
public bool IsKnownQuest(QuestId questId) => _quests.ContainsKey(questId);
public bool TryGetQuest(ElementId elementId, [NotNullWhen(true)] out Quest? quest)
{
if (elementId is QuestId questId)
return TryGetQuest(questId, out quest);
else
{
quest = null;
return false;
}
}
public bool TryGetQuest(QuestId questId, [NotNullWhen(true)] out Quest? quest)
public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest);
}

View File

@ -18,11 +18,11 @@ internal static class NextQuest
if (step.NextQuestId == null)
return null;
if (step.NextQuestId == quest.QuestElementId)
if (step.NextQuestId == quest.Id)
return null;
return serviceProvider.GetRequiredService<SetQuest>()
.With(step.NextQuestId, quest.QuestElementId);
.With(step.NextQuestId, quest.Id);
}
}

View File

@ -47,7 +47,7 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.ItemId);
yield return serviceProvider.GetRequiredService<UseItem.UseOnObject>()
.With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags,
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags,
true);
yield return CreateTask(quest, sequence, step);
break;
@ -73,7 +73,7 @@ internal static class Combat
bool isLastStep = sequence.Steps.Last() == step;
return serviceProvider.GetRequiredService<HandleCombat>()
.With(quest.QuestElementId, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
.With(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
step.CompletionQuestVariablesFlags, step.ComplexCombatData);
}
}

View File

@ -33,7 +33,8 @@ internal static class Interact
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>();
yield return serviceProvider.GetRequiredService<DoInteract>()
.With(step.DataId.Value, step.TargetTerritoryId != null);
.With(step.DataId.Value,
step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId);
}
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)

View File

@ -48,7 +48,7 @@ internal static class UseItem
}
var task = serviceProvider.GetRequiredService<Use>()
.With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return
[
unmount, task,
@ -65,12 +65,12 @@ internal static class UseItem
ITask task;
if (step.DataId != null)
task = serviceProvider.GetRequiredService<UseOnGround>()
.With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
else
{
ArgumentNullException.ThrowIfNull(step.Position);
task = serviceProvider.GetRequiredService<UseOnPosition>()
.With(quest.QuestElementId, step.Position.Value, step.ItemId.Value,
.With(quest.Id, step.Position.Value, step.ItemId.Value,
step.CompletionQuestVariablesFlags);
}
@ -79,13 +79,13 @@ internal static class UseItem
else if (step.DataId != null)
{
var task = serviceProvider.GetRequiredService<UseOnObject>()
.With(quest.QuestElementId, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task];
}
else
{
var task = serviceProvider.GetRequiredService<Use>()
.With(quest.QuestElementId, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task];
}
}

View File

@ -33,7 +33,7 @@ internal static class GatheringRequiredItems
if (!AssemblyGatheringLocationLoader.GetLocations()
.TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot))
throw new TaskException("No path found for gathering point");
throw new TaskException($"No path found for gathering point {gatheringPointId}");
if (HasRequiredItems(requiredGatheredItems))
continue;

View File

@ -34,7 +34,7 @@ internal static class SkipCondition
return null;
return serviceProvider.GetRequiredService<CheckSkip>()
.With(step, skipConditions ?? new(), quest.QuestElementId);
.With(step, skipConditions ?? new(), quest.Id);
}
}

View File

@ -30,7 +30,7 @@ internal static class WaitAtEnd
if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
.With((QuestId)quest.QuestElementId, step);
.With((QuestId)quest.Id, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, Next(quest, sequence)];
}
@ -110,7 +110,7 @@ internal static class WaitAtEnd
case EInteractionType.AcceptQuest:
{
var accept = serviceProvider.GetRequiredService<WaitQuestAccepted>()
.With(step.PickUpQuestId ?? quest.QuestElementId);
.With(step.PickUpQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.PickUpQuestId != null)
return [accept, delay, Next(quest, sequence)];
@ -121,7 +121,7 @@ internal static class WaitAtEnd
case EInteractionType.CompleteQuest:
{
var complete = serviceProvider.GetRequiredService<WaitQuestCompleted>()
.With(step.TurnInQuestId ?? quest.QuestElementId);
.With(step.TurnInQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.TurnInQuestId != null)
return [complete, delay, Next(quest, sequence)];
@ -140,7 +140,7 @@ internal static class WaitAtEnd
private static NextStep Next(Quest quest, QuestSequence sequence)
{
return new NextStep(quest.QuestElementId, sequence.Sequence);
return new NextStep(quest.Id, sequence.Sequence);
}
}

View File

@ -28,6 +28,10 @@ internal sealed class AetheryteData
aethernetGroups[(EAetheryteLocation)aetheryte.RowId] = aetheryte.AethernetGroup;
}
aethernetNames[EAetheryteLocation.IshgardFirmament] = "Firmament";
territoryIds[EAetheryteLocation.IshgardFirmament] = 886;
aethernetGroups[EAetheryteLocation.IshgardFirmament] = aethernetGroups[EAetheryteLocation.Ishgard];
AethernetNames = aethernetNames.AsReadOnly();
TerritoryIds = territoryIds.AsReadOnly();
AethernetGroups = aethernetGroups.AsReadOnly();
@ -267,6 +271,7 @@ internal sealed class AetheryteData
{ EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) },
{ EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) },
{ EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) },
{ EAetheryteLocation.IshgardFirmament, new(9.92315f, -15.2f, 173.5059f) },
}.AsReadOnly();
public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
@ -298,6 +303,9 @@ internal sealed class AetheryteData
public bool IsCityAetheryte(EAetheryteLocation aetheryte)
{
if (aetheryte == EAetheryteLocation.IshgardFirmament)
return true;
var territoryId = TerritoryIds[aetheryte];
return TownTerritoryIds.Contains(territoryId);
}

View File

@ -1,20 +1,21 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
namespace Questionable.Data;
internal sealed class GatheringData
{
private readonly Dictionary<uint, uint> _gatheringItemToItem;
private readonly Dictionary<uint, ushort> _minerGatheringPoints = [];
private readonly Dictionary<uint, ushort> _botanistGatheringPoints = [];
private readonly Dictionary<uint, ushort> _itemIdToCollectability;
private readonly Dictionary<uint, uint> _npcForCustomDeliveries;
public GatheringData(IDataManager dataManager)
{
_gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
Dictionary<uint, uint> gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
.Where(x => x.RowId != 0 && x.Item != 0)
.ToDictionary(x => x.RowId, x => (uint)x.Item);
@ -22,7 +23,7 @@ internal sealed class GatheringData
{
foreach (var gatheringItemId in gatheringPointBase.Item.Where(x => x != 0))
{
if (_gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
if (gatheringItemToItem.TryGetValue((uint)gatheringItemId, out uint itemId))
{
if (gatheringPointBase.GatheringType.Row is 0 or 1)
_minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId;
@ -31,8 +32,31 @@ internal sealed class GatheringData
}
}
}
}
_itemIdToCollectability = dataManager.GetExcelSheet<SatisfactionSupply>()!
.Where(x => x.RowId > 0)
.Where(x => x.Slot is 2)
.Select(x => new
{
ItemId = x.Item.Row,
Collectability = x.CollectabilityHigh,
})
.Distinct()
.ToDictionary(x => x.ItemId, x => x.Collectability);
_npcForCustomDeliveries = dataManager.GetExcelSheet<SatisfactionNpc>()!
.Where(x => x.RowId > 0)
.SelectMany(x => dataManager.GetExcelSheet<SatisfactionSupply>()!
.Where(y => y.RowId == x.SupplyIndex.Last())
.Select(y => new
{
ItemId = y.Item.Row,
NpcId = x.Npc.Row
}))
.Where(x => x.ItemId > 0)
.Distinct()
.ToDictionary(x => x.ItemId, x => x.NpcId);
}
public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId)
{
@ -46,4 +70,10 @@ internal sealed class GatheringData
return false;
}
}
public ushort GetRecommendedCollectability(uint itemId)
=> _itemIdToCollectability.GetValueOrDefault(itemId);
public bool TryGetCustomDeliveryNpc(uint itemId, out uint npcId)
=> _npcForCustomDeliveries.TryGetValue(itemId, out npcId);
}

View File

@ -22,17 +22,17 @@ internal sealed class JournalData
var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1,
new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1,
new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1,
new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row))
.Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
genres.Single(x => x.Id == 1)

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model;
using Questionable.Model.Questing;
using Quest = Lumina.Excel.GeneratedSheets.Quest;
@ -11,32 +12,30 @@ namespace Questionable.Data;
internal sealed class QuestData
{
private readonly ImmutableDictionary<QuestId, QuestInfo> _quests;
private readonly Dictionary<ElementId, IQuestInfo> _quests;
public QuestData(IDataManager dataManager)
{
_quests = dataManager.GetExcelSheet<Quest>()!
.Where(x => x.RowId > 0)
.Where(x => x.IssuerLocation.Row > 0)
.Where(x => x.Festival.Row == 0)
.Select(x => new QuestInfo(x))
.ToImmutableDictionary(x => x.QuestId, x => x);
List<IQuestInfo> quests =
[
..dataManager.GetExcelSheet<Quest>()!
.Where(x => x.RowId > 0)
.Where(x => x.IssuerLocation.Row > 0)
.Where(x => x.Festival.Row == 0)
.Select(x => new QuestInfo(x)),
..dataManager.GetExcelSheet<SatisfactionNpc>()!
.Where(x => x.RowId > 0)
.Select(x => new SatisfactionSupplyInfo(x))
];
_quests = quests.ToDictionary(x => x.QuestId, x => x);
}
public QuestInfo GetQuestInfo(ElementId elementId)
public IQuestInfo GetQuestInfo(ElementId elementId)
{
if (elementId is QuestId questId)
return GetQuestInfo(questId);
throw new ArgumentException("Invalid id", nameof(elementId));
return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId));
}
public QuestInfo GetQuestInfo(QuestId questId)
{
return _quests[questId] ?? throw new ArgumentOutOfRangeException(nameof(questId));
}
public List<QuestInfo> GetAllByIssuerDataId(uint targetId)
public List<IQuestInfo> GetAllByIssuerDataId(uint targetId)
{
return _quests.Values
.Where(x => x.IssuerDataId == targetId)
@ -48,6 +47,8 @@ internal sealed class QuestData
public List<QuestInfo> GetAllByJournalGenre(uint journalGenre)
{
return _quests.Values
.Where(x => x is QuestInfo)
.Cast<QuestInfo>()
.Where(x => x.JournalGenre == journalGenre)
.OrderBy(x => x.SortKey)
.ThenBy(x => x.QuestId)

View File

@ -18,6 +18,12 @@ internal sealed class LifestreamIpc
public bool Teleport(EAetheryteLocation aetheryteLocation)
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
{
// TODO does this even work on non-EN clients?
return _aethernetTeleport.InvokeFunc("Firmament");
}
if (!_aetheryteData.AethernetNames.TryGetValue(aetheryteLocation, out string? name))
return false;

View File

@ -246,7 +246,10 @@ internal sealed unsafe class GameFunctions
{
if (elementId is QuestId questId)
return IsReadyToAcceptQuest(questId);
return false;
else if (elementId is SatisfactionSupplyNpcId)
return true;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsReadyToAcceptQuest(QuestId questId)
@ -283,7 +286,10 @@ internal sealed unsafe class GameFunctions
{
if (elementId is QuestId questId)
return IsQuestAccepted(questId);
return false;
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestAccepted(QuestId questId)
@ -296,7 +302,10 @@ internal sealed unsafe class GameFunctions
{
if (elementId is QuestId questId)
return IsQuestComplete(questId);
return false;
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
[SuppressMessage("Performance", "CA1822")]
@ -309,12 +318,15 @@ internal sealed unsafe class GameFunctions
{
if (elementId is QuestId questId)
return IsQuestLocked(questId, extraCompletedQuest);
return false;
else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
{
var questInfo = _questData.GetQuestInfo(questId);
var questInfo = (QuestInfo) _questData.GetQuestInfo(questId);
if (questInfo.QuestLocks.Count > 0)
{
var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
@ -369,7 +381,11 @@ internal sealed unsafe class GameFunctions
}
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _);
{
if (aetheryteLocation == EAetheryteLocation.IshgardFirmament)
return IsQuestComplete(new QuestId(3672));
return IsAetheryteUnlocked((uint)aetheryteLocation, out _);
}
public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{
@ -707,15 +723,15 @@ internal sealed unsafe class GameFunctions
if (excelSheetName == null)
{
var questRow =
_dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestElementId.Value +
_dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.Id.Value +
0x10000);
if (questRow == null)
{
_logger.LogError("Could not find quest row for {QuestId}", currentQuest.QuestElementId);
_logger.LogError("Could not find quest row for {QuestId}", currentQuest.Id);
return null;
}
excelSheetName = $"quest/{(currentQuest.QuestElementId.Value / 100):000}/{questRow.Id}";
excelSheetName = $"quest/{(currentQuest.Id.Value / 100):000}/{questRow.Id}";
}
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);

View File

@ -0,0 +1,20 @@
using System;
using Dalamud.Game.Text;
using Questionable.Model.Questing;
namespace Questionable.Model;
public interface IQuestInfo
{
public ElementId QuestId { get; }
public string Name { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable { get; }
public ushort Level { get; }
public EBeastTribe BeastTribe { get; }
public bool IsMainScenarioQuest { get; }
public string SimplifiedName => Name
.Replace(".", "", StringComparison.Ordinal)
.TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' ');
}

View File

@ -6,9 +6,9 @@ namespace Questionable.Model;
internal sealed class Quest
{
public required ElementId QuestElementId { get; init; }
public required ElementId Id { get; init; }
public required QuestRoot Root { get; init; }
public required QuestInfo Info { get; init; }
public required IQuestInfo Info { get; init; }
public required bool ReadOnly { get; init; }
public QuestSequence? FindSequence(byte currentSequence)

View File

@ -10,7 +10,7 @@ using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest;
namespace Questionable.Model;
internal sealed class QuestInfo
internal sealed class QuestInfo : IQuestInfo
{
public QuestInfo(ExcelQuest quest)
{
@ -56,7 +56,7 @@ internal sealed class QuestInfo
}
public QuestId QuestId { get; }
public ElementId QuestId { get; }
public string Name { get; }
public ushort Level { get; }
public uint IssuerDataId { get; }
@ -74,10 +74,6 @@ internal sealed class QuestInfo
public GrandCompany GrandCompany { get; }
public EBeastTribe BeastTribe { get; }
public string SimplifiedName => Name
.Replace(".", "", StringComparison.Ordinal)
.TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' ');
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
public enum QuestJoin : byte
{

View File

@ -0,0 +1,23 @@
using Lumina.Excel.GeneratedSheets;
using Questionable.Model.Questing;
namespace Questionable.Model;
internal sealed class SatisfactionSupplyInfo : IQuestInfo
{
public SatisfactionSupplyInfo(SatisfactionNpc npc)
{
QuestId = new SatisfactionSupplyNpcId((ushort)npc.RowId);
Name = npc.Npc.Value!.Singular;
IssuerDataId = npc.Npc.Row;
Level = npc.LevelUnlock;
}
public ElementId QuestId { get; }
public string Name { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable => true;
public ushort Level { get; }
public EBeastTribe BeastTribe => EBeastTribe.None;
public bool IsMainScenarioQuest => false;
}

View File

@ -43,7 +43,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
IChatGui chatGui,
ICommandManager commandManager,
IAddonLifecycle addonLifecycle,
IKeyState keyState)
IKeyState keyState,
IContextMenu contextMenu)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
@ -66,6 +67,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(contextMenu);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
@ -81,6 +83,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
_serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload();
_serviceProvider.GetRequiredService<CommandHandler>();
_serviceProvider.GetRequiredService<ContextMenuController>();
_serviceProvider.GetRequiredService<DalamudInitializer>();
}
@ -156,6 +159,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ContextMenuController>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
}

View File

@ -18,7 +18,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
return quest.AllSteps()
.Select(x => Validate(quest.QuestElementId, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut))
.Select(x => Validate(quest.Id, x.Sequence.Sequence, x.StepId, x.Step.AethernetShortcut))
.Where(x => x != null)
.Cast<ValidationIssue>();
}

View File

@ -18,7 +18,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingSequence0,
@ -28,7 +28,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
yield break;
}
if (quest.Info.CompletesInstantly)
if (quest.Info is QuestInfo { CompletesInstantly: true })
{
foreach (var sequence in sequences)
{
@ -37,7 +37,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)sequence.Sequence,
Step = null,
Type = EIssueType.InstantQuestWithMultipleSteps,
@ -46,7 +46,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
};
}
}
else
else if (quest.Info is QuestInfo)
{
int maxSequence = sequences.Select(x => x.Sequence)
.Where(x => x != 255)
@ -73,7 +73,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{
return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.MissingSequence,
@ -85,7 +85,7 @@ internal sealed class BasicSequenceValidator : IQuestValidator
{
return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)sequenceNo,
Step = null,
Type = EIssueType.DuplicateSequence,

View File

@ -45,7 +45,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)sequence.Sequence,
Step = i,
Type = EIssueType.DuplicateCompletionFlags,

View File

@ -25,7 +25,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{
_questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
if (_questNodes.TryGetValue(quest.QuestElementId, out JsonNode? questNode))
if (_questNodes.TryGetValue(quest.Id, out JsonNode? questNode))
{
var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions
{
@ -36,7 +36,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSchema,

View File

@ -8,11 +8,11 @@ internal sealed class NextQuestValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.QuestElementId))
foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.Id))
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)invalidNextQuest.Sequence.Sequence,
Step = invalidNextQuest.StepId,
Type = EIssueType.InvalidNextQuestId,

View File

@ -11,7 +11,7 @@ internal sealed class QuestDisabledValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = null,
Step = null,
Type = EIssueType.QuestDisabled,

View File

@ -9,6 +9,9 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
public IEnumerable<ValidationIssue> Validate(Quest quest)
{
if (quest.Id is SatisfactionSupplyNpcId)
yield break;
var questAccepts = FindQuestStepsWithInteractionType(quest, EInteractionType.AcceptQuest)
.Where(x => x.Step.PickUpQuestId == null)
.ToList();
@ -18,7 +21,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)accept.Sequence.Sequence,
Step = accept.StepId,
Type = EIssueType.UnexpectedAcceptQuestStep,
@ -32,7 +35,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = 0,
Step = null,
Type = EIssueType.MissingQuestAccept,
@ -50,7 +53,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = (byte)complete.Sequence.Sequence,
Step = complete.StepId,
Type = EIssueType.UnexpectedCompleteQuestStep,
@ -64,7 +67,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator
{
yield return new ValidationIssue
{
QuestId = quest.QuestElementId,
QuestId = quest.Id,
Sequence = 255,
Step = null,
Type = EIssueType.MissingQuestComplete,

View File

@ -103,7 +103,7 @@ internal sealed class DebugOverlay : Window
QuestStep? step = sequence.FindStep(i);
if (step != null && TryGetPosition(step, out Vector3? position))
{
DrawStep($"{quest.QuestElementId} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF);
DrawStep($"{quest.Id} / {sequence.Sequence} / {i}", step, position.Value, 0xFFFFFFFF);
}
}
}

View File

@ -201,7 +201,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
if (ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
{
_commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString(), commandInfo);
_commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
}
if (ImGui.IsItemHovered())

View File

@ -58,7 +58,7 @@ internal sealed class ActiveQuestComponent
{
var currentQuestDetails = _questController.CurrentQuestDetails;
QuestController.QuestProgress? currentQuest = currentQuestDetails?.Progress;
QuestController.CurrentQuestType? currentQuestType = currentQuestDetails?.Type;
QuestController.ECurrentQuestType? currentQuestType = currentQuestDetails?.Type;
if (currentQuest != null)
{
DrawQuestNames(currentQuest, currentQuestType);
@ -108,9 +108,9 @@ internal sealed class ActiveQuestComponent
}
private void DrawQuestNames(QuestController.QuestProgress currentQuest,
QuestController.CurrentQuestType? currentQuestType)
QuestController.ECurrentQuestType? currentQuestType)
{
if (currentQuestType == QuestController.CurrentQuestType.Simulated)
if (currentQuestType == QuestController.ECurrentQuestType.Simulated)
{
using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextUnformatted(
@ -151,7 +151,7 @@ internal sealed class ActiveQuestComponent
private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest)
{
if (currentQuest.Quest.QuestElementId is not QuestId questId)
if (currentQuest.Quest.Id is not QuestId questId)
return null;
var questWork = _gameFunctions.GetQuestEx(questId);
@ -210,7 +210,7 @@ internal sealed class ActiveQuestComponent
{
using var disabled = ImRaii.Disabled();
if (currentQuest.Quest.QuestElementId == _questController.NextQuest?.Quest.QuestElementId)
if (currentQuest.Quest.Id == _questController.NextQuest?.Quest.Id)
ImGui.TextUnformatted("(Next quest in story line not accepted)");
else
ImGui.TextUnformatted("(Not accepted)");
@ -229,14 +229,14 @@ internal sealed class ActiveQuestComponent
if (questWork == null)
_questController.SetNextQuest(currentQuest.Quest);
_questController.ExecuteNextStep(true);
_questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
{
_questController.ExecuteNextStep(false);
_questController.ExecuteNextStep(QuestController.EAutomationType.Manual);
}
ImGui.EndDisabled();
@ -262,7 +262,7 @@ internal sealed class ActiveQuestComponent
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
{
_movementController.Stop();
_questController.Skip(currentQuest.Quest.QuestElementId, currentQuest.Sequence);
_questController.Skip(currentQuest.Quest.Id, currentQuest.Sequence);
}
if (colored)
@ -274,7 +274,7 @@ internal sealed class ActiveQuestComponent
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas))
_commandManager.DispatchCommand("/questinfo",
currentQuest.Quest.QuestElementId.ToString() ?? string.Empty, commandInfo);
currentQuest.Quest.Id.ToString() ?? string.Empty, commandInfo);
}
bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;

View File

@ -6,6 +6,7 @@ using ImGuiNET;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
@ -31,6 +32,12 @@ internal sealed class QuestTooltipComponent
_uiUtils = uiUtils;
}
public void Draw(IQuestInfo quest)
{
if (quest is QuestInfo questInfo)
Draw(questInfo);
}
public void Draw(QuestInfo quest)
{
using var tooltip = ImRaii.Tooltip();
@ -93,8 +100,8 @@ internal sealed class QuestTooltipComponent
_uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
if (counter <= 2 || icon != FontAwesomeIcon.Check)
DrawQuestUnlocks(qInfo, counter + 1);
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
DrawQuestUnlocks(qstInfo, counter + 1);
}
}
@ -162,7 +169,7 @@ internal sealed class QuestTooltipComponent
ImGui.Unindent();
}
private static string FormatQuestUnlockName(QuestInfo questInfo)
private static string FormatQuestUnlockName(IQuestInfo questInfo)
{
if (questInfo.IsMainScenarioQuest)
return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)";

View File

@ -39,8 +39,8 @@ internal sealed class QuestSelectionWindow : LWindow
private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent;
private List<QuestInfo> _quests = [];
private List<QuestInfo> _offeredQuests = [];
private List<IQuestInfo> _quests = [];
private List<IQuestInfo> _offeredQuests = [];
private bool _onlyAvailableQuests = true;
public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions,
@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow
_quests = _questRegistry.AllQuests
.Where(x => x.FindSequence(0)?.FindStep(0)?.TerritoryId == territoryId)
.Select(x => _questData.GetQuestInfo(x.QuestElementId))
.Select(x => _questData.GetQuestInfo(x.Id))
.ToList();
foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers)
@ -157,11 +157,11 @@ internal sealed class QuestSelectionWindow : LWindow
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, actionIconSize);
ImGui.TableHeadersRow();
foreach (QuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests)
foreach (IQuestInfo quest in (_offeredQuests.Count != 0 && _onlyAvailableQuests) ? _offeredQuests : _quests)
{
ImGui.TableNextRow();
string questId = quest.QuestId.ToString();
string questId = quest.QuestId.ToString() ?? string.Empty;
bool isKnownQuest = _questRegistry.TryGetQuest(quest.QuestId, out var knownQuest);
if (ImGui.TableNextColumn())
@ -228,7 +228,7 @@ internal sealed class QuestSelectionWindow : LWindow
if (startNextQuest)
{
_questController.SetNextQuest(knownQuest);
_questController.ExecuteNextStep(true);
_questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
}
ImGui.SameLine();
@ -245,7 +245,7 @@ internal sealed class QuestSelectionWindow : LWindow
}
}
private void CopyToClipboard(QuestInfo quest, bool suffix)
private void CopyToClipboard(IQuestInfo quest, bool suffix)
{
string fileName = $"{quest.QuestId}_{quest.SimplifiedName}{(suffix ? ".json" : "")}";
ImGui.SetClipboardText(fileName);

View File

@ -22,13 +22,13 @@ internal sealed class UiUtils
public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ElementId questElementId)
{
if (_gameFunctions.IsQuestAccepted(questElementId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active");
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active");
else if (_gameFunctions.IsQuestAcceptedOrComplete(questElementId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete");
else if (_gameFunctions.IsQuestLocked(questElementId))
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked");
else
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available");
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Available");
}
public static (Vector4 color, FontAwesomeIcon icon) GetInstanceStyle(ushort instanceId)
@ -36,7 +36,7 @@ internal sealed class UiUtils
if (UIState.IsInstanceContentCompleted(instanceId))
return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
else if (UIState.IsInstanceContentUnlocked(instanceId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight);
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
else
return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times);
}