Add basic support for gathering custom delivery items automatically

pull/14/head
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": { "ecommons": {
"type": "Project" "type": "Project"
}, },
"gatheringpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[1.0.0, )"
}
},
"questionable.model": { "questionable.model": {
"type": "Project", "type": "Project",
"dependencies": { "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="6.x - Endwalker\**\*.json" />
<AdditionalFiles Include="7.x - Dawntrail\**\*.json" /> <AdditionalFiles Include="7.x - Dawntrail\**\*.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="7.x - Dawntrail\Custom Deliveries\" />
</ItemGroup>
</Project> </Project>

View File

@ -1041,7 +1041,8 @@
"PickUpQuestId": { "PickUpQuestId": {
"type": [ "type": [
"null", "null",
"number" "number",
"string"
], ],
"description": "Determines the quest which should be accepted. If empty/null, accepts the quest corresponding to the file name." "description": "Determines the quest which should be accepted. If empty/null, accepts the quest corresponding to the file name."
} }
@ -1061,14 +1062,16 @@
"TurnInQuestId": { "TurnInQuestId": {
"type": [ "type": [
"null", "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." "description": "Determines the quest which should be turned in. If empty/null, turns in the quest corresponding to the file name."
}, },
"NextQuestId": { "NextQuestId": {
"type": [ "type": [
"null", "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." "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, IshgardTribunal = 86,
IshgardLastVigil = 87, IshgardLastVigil = 87,
IshgardGatesOfJudgement = 88, IshgardGatesOfJudgement = 88,
IshgardFirmament = 100001,
Idyllshire = 75, Idyllshire = 75,
IdyllshireWest = 90, IdyllshireWest = 90,

View File

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

View File

@ -4,12 +4,14 @@ using System.Text.Json.Serialization;
namespace Questionable.Model.Questing.Converter; 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) public override ElementId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
uint value = reader.GetUInt32(); if (reader.TokenType == JsonTokenType.Number)
return ElementId.From(value); return new QuestId(reader.GetUInt16());
else
return ElementId.FromString(reader.GetString() ?? throw new JsonException());
} }
public override void Write(Utf8JsonWriter writer, ElementId value, JsonSerializerOptions options) 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); return !Equals(left, right);
} }
public static ElementId From(uint value) public static ElementId FromString(string value)
{ {
if (value >= 100_000 && value < 200_000) if (value.StartsWith("L"))
return new LeveId((ushort)(value - 100_000)); 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 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() public override string ToString()
{ {
return Value.ToString(CultureInfo.InvariantCulture); 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() public override string ToString()
{ {
return "L" + Value.ToString(CultureInfo.InvariantCulture); 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 Tribunal",
"[Ishgard] The Last Vigil", "[Ishgard] The Last Vigil",
"[Ishgard] The Gates of Judgement (Coerthas Central Highlands)", "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
"[Ishgard] Firmament",
"[Idyllshire] Aetheryte Plaza", "[Idyllshire] Aetheryte Plaza",
"[Idyllshire] West Idyllshire", "[Idyllshire] West Idyllshire",
"[Idyllshire] Prologue Gate (Western Hinterlands)", "[Idyllshire] Prologue Gate (Western Hinterlands)",

View File

@ -77,7 +77,7 @@ internal sealed class CommandHandler : IDisposable
case "start": case "start":
_questWindow.IsOpen = true; _questWindow.IsOpen = true;
_questController.ExecuteNextStep(true); _questController.ExecuteNextStep(QuestController.EAutomationType.Automatic);
break; break;
case "stop": case "stop":
@ -128,11 +128,11 @@ internal sealed class CommandHandler : IDisposable
return; 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})."); _chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name}).");
} }
else else
@ -147,11 +147,11 @@ internal sealed class CommandHandler : IDisposable
private void SetNextQuest(string[] arguments) 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."); _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); _questController.SetNextQuest(quest);
_chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name})."); _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) 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); _questController.SimulateQuest(quest);
_chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name})."); _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) 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"); _logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon; AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@ -610,7 +610,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args) 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"); _logger.LogInformation("Closing ContentsTutorial");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon; AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
@ -623,7 +623,7 @@ internal sealed class GameUiController : IDisposable
/// </summary> /// </summary>
private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args) 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"); _logger.LogInformation("Closing MultipleHelpWindow");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon; AtkUnitBase* addon = (AtkUnitBase*)args.Addon;

View File

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

View File

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

View File

@ -18,11 +18,11 @@ internal static class NextQuest
if (step.NextQuestId == null) if (step.NextQuestId == null)
return null; return null;
if (step.NextQuestId == quest.QuestElementId) if (step.NextQuestId == quest.Id)
return null; return null;
return serviceProvider.GetRequiredService<SetQuest>() 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); ArgumentNullException.ThrowIfNull(step.ItemId);
yield return serviceProvider.GetRequiredService<UseItem.UseOnObject>() 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); true);
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
break; break;
@ -73,7 +73,7 @@ internal static class Combat
bool isLastStep = sequence.Steps.Last() == step; bool isLastStep = sequence.Steps.Last() == step;
return serviceProvider.GetRequiredService<HandleCombat>() 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); step.CompletionQuestVariablesFlags, step.ComplexCombatData);
} }
} }

View File

@ -33,7 +33,8 @@ internal static class Interact
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>(); yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>();
yield return serviceProvider.GetRequiredService<DoInteract>() 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) public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)

View File

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

View File

@ -33,7 +33,7 @@ internal static class GatheringRequiredItems
if (!AssemblyGatheringLocationLoader.GetLocations() if (!AssemblyGatheringLocationLoader.GetLocations()
.TryGetValue(gatheringPointId, out GatheringRoot? gatheringRoot)) .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)) if (HasRequiredItems(requiredGatheredItems))
continue; continue;

View File

@ -34,7 +34,7 @@ internal static class SkipCondition
return null; return null;
return serviceProvider.GetRequiredService<CheckSkip>() 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)) if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{ {
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>() var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
.With((QuestId)quest.QuestElementId, step); .With((QuestId)quest.Id, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>(); var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, Next(quest, sequence)]; return [task, delay, Next(quest, sequence)];
} }
@ -110,7 +110,7 @@ internal static class WaitAtEnd
case EInteractionType.AcceptQuest: case EInteractionType.AcceptQuest:
{ {
var accept = serviceProvider.GetRequiredService<WaitQuestAccepted>() var accept = serviceProvider.GetRequiredService<WaitQuestAccepted>()
.With(step.PickUpQuestId ?? quest.QuestElementId); .With(step.PickUpQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>(); var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.PickUpQuestId != null) if (step.PickUpQuestId != null)
return [accept, delay, Next(quest, sequence)]; return [accept, delay, Next(quest, sequence)];
@ -121,7 +121,7 @@ internal static class WaitAtEnd
case EInteractionType.CompleteQuest: case EInteractionType.CompleteQuest:
{ {
var complete = serviceProvider.GetRequiredService<WaitQuestCompleted>() var complete = serviceProvider.GetRequiredService<WaitQuestCompleted>()
.With(step.TurnInQuestId ?? quest.QuestElementId); .With(step.TurnInQuestId ?? quest.Id);
var delay = serviceProvider.GetRequiredService<WaitDelay>(); var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.TurnInQuestId != null) if (step.TurnInQuestId != null)
return [complete, delay, Next(quest, sequence)]; return [complete, delay, Next(quest, sequence)];
@ -140,7 +140,7 @@ internal static class WaitAtEnd
private static NextStep Next(Quest quest, QuestSequence sequence) 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; aethernetGroups[(EAetheryteLocation)aetheryte.RowId] = aetheryte.AethernetGroup;
} }
aethernetNames[EAetheryteLocation.IshgardFirmament] = "Firmament";
territoryIds[EAetheryteLocation.IshgardFirmament] = 886;
aethernetGroups[EAetheryteLocation.IshgardFirmament] = aethernetGroups[EAetheryteLocation.Ishgard];
AethernetNames = aethernetNames.AsReadOnly(); AethernetNames = aethernetNames.AsReadOnly();
TerritoryIds = territoryIds.AsReadOnly(); TerritoryIds = territoryIds.AsReadOnly();
AethernetGroups = aethernetGroups.AsReadOnly(); AethernetGroups = aethernetGroups.AsReadOnly();
@ -267,6 +271,7 @@ internal sealed class AetheryteData
{ EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) }, { EAetheryteLocation.GridaniaAirship, new(24.86354f, -19.000002f, 96f) },
{ EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) }, { EAetheryteLocation.UldahAirship, new(-16.954851f, 82.999985f, -9.421141f) },
{ EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) }, { EAetheryteLocation.KuganeAirship, new(-55.72525f, 79.10602f, 46.23109f) },
{ EAetheryteLocation.IshgardFirmament, new(9.92315f, -15.2f, 173.5059f) },
}.AsReadOnly(); }.AsReadOnly();
public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; } public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
@ -298,6 +303,9 @@ internal sealed class AetheryteData
public bool IsCityAetheryte(EAetheryteLocation aetheryte) public bool IsCityAetheryte(EAetheryteLocation aetheryte)
{ {
if (aetheryte == EAetheryteLocation.IshgardFirmament)
return true;
var territoryId = TerritoryIds[aetheryte]; var territoryId = TerritoryIds[aetheryte];
return TownTerritoryIds.Contains(territoryId); return TownTerritoryIds.Contains(territoryId);
} }

View File

@ -1,20 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
namespace Questionable.Data; namespace Questionable.Data;
internal sealed class GatheringData internal sealed class GatheringData
{ {
private readonly Dictionary<uint, uint> _gatheringItemToItem;
private readonly Dictionary<uint, ushort> _minerGatheringPoints = []; private readonly Dictionary<uint, ushort> _minerGatheringPoints = [];
private readonly Dictionary<uint, ushort> _botanistGatheringPoints = []; private readonly Dictionary<uint, ushort> _botanistGatheringPoints = [];
private readonly Dictionary<uint, ushort> _itemIdToCollectability;
private readonly Dictionary<uint, uint> _npcForCustomDeliveries;
public GatheringData(IDataManager dataManager) public GatheringData(IDataManager dataManager)
{ {
_gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()! Dictionary<uint, uint> gatheringItemToItem = dataManager.GetExcelSheet<GatheringItem>()!
.Where(x => x.RowId != 0 && x.Item != 0) .Where(x => x.RowId != 0 && x.Item != 0)
.ToDictionary(x => x.RowId, x => (uint)x.Item); .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)) 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) if (gatheringPointBase.GatheringType.Row is 0 or 1)
_minerGatheringPoints[itemId] = (ushort)gatheringPointBase.RowId; _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) public bool TryGetGatheringPointId(uint itemId, uint classJobId, out ushort gatheringPointId)
{ {
@ -46,4 +70,10 @@ internal sealed class GatheringData
return false; 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, var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1,
new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row)) new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row))
.Where(x => x != 0) .Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList()); .ToList());
var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1, var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1,
new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row)) new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row))
.Where(x => x != 0) .Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList()); .ToList());
var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1, var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1,
new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row)) new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row))
.Where(x => x != 0) .Where(x => x != 0)
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) .Select(x => (QuestInfo)questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList()); .ToList());
genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]); genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
genres.Single(x => x.Id == 1) genres.Single(x => x.Id == 1)

View File

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

View File

@ -18,6 +18,12 @@ internal sealed class LifestreamIpc
public bool Teleport(EAetheryteLocation aetheryteLocation) 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)) if (!_aetheryteData.AethernetNames.TryGetValue(aetheryteLocation, out string? name))
return false; return false;

View File

@ -246,7 +246,10 @@ internal sealed unsafe class GameFunctions
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)
return IsReadyToAcceptQuest(questId); return IsReadyToAcceptQuest(questId);
return false; else if (elementId is SatisfactionSupplyNpcId)
return true;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
} }
public bool IsReadyToAcceptQuest(QuestId questId) public bool IsReadyToAcceptQuest(QuestId questId)
@ -283,7 +286,10 @@ internal sealed unsafe class GameFunctions
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)
return IsQuestAccepted(questId); return IsQuestAccepted(questId);
return false; else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
} }
public bool IsQuestAccepted(QuestId questId) public bool IsQuestAccepted(QuestId questId)
@ -296,7 +302,10 @@ internal sealed unsafe class GameFunctions
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)
return IsQuestComplete(questId); return IsQuestComplete(questId);
return false; else if (elementId is SatisfactionSupplyNpcId)
return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
} }
[SuppressMessage("Performance", "CA1822")] [SuppressMessage("Performance", "CA1822")]
@ -309,12 +318,15 @@ internal sealed unsafe class GameFunctions
{ {
if (elementId is QuestId questId) if (elementId is QuestId questId)
return IsQuestLocked(questId, extraCompletedQuest); 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) public bool IsQuestLocked(QuestId questId, ElementId? extraCompletedQuest = null)
{ {
var questInfo = _questData.GetQuestInfo(questId); var questInfo = (QuestInfo) _questData.GetQuestInfo(questId);
if (questInfo.QuestLocks.Count > 0) if (questInfo.QuestLocks.Count > 0)
{ {
var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest)); 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) 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) public bool CanTeleport(EAetheryteLocation aetheryteLocation)
{ {
@ -707,15 +723,15 @@ internal sealed unsafe class GameFunctions
if (excelSheetName == null) if (excelSheetName == null)
{ {
var questRow = 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); 0x10000);
if (questRow == null) 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; 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); 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 internal sealed class Quest
{ {
public required ElementId QuestElementId { get; init; } public required ElementId Id { get; init; }
public required QuestRoot Root { 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 required bool ReadOnly { get; init; }
public QuestSequence? FindSequence(byte currentSequence) public QuestSequence? FindSequence(byte currentSequence)

View File

@ -10,7 +10,7 @@ using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest;
namespace Questionable.Model; namespace Questionable.Model;
internal sealed class QuestInfo internal sealed class QuestInfo : IQuestInfo
{ {
public QuestInfo(ExcelQuest quest) 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 string Name { get; }
public ushort Level { get; } public ushort Level { get; }
public uint IssuerDataId { get; } public uint IssuerDataId { get; }
@ -74,10 +74,6 @@ internal sealed class QuestInfo
public GrandCompany GrandCompany { get; } public GrandCompany GrandCompany { get; }
public EBeastTribe BeastTribe { get; } public EBeastTribe BeastTribe { get; }
public string SimplifiedName => Name
.Replace(".", "", StringComparison.Ordinal)
.TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' ');
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)]
public enum QuestJoin : byte 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, IChatGui chatGui,
ICommandManager commandManager, ICommandManager commandManager,
IAddonLifecycle addonLifecycle, IAddonLifecycle addonLifecycle,
IKeyState keyState) IKeyState keyState,
IContextMenu contextMenu)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface); ArgumentNullException.ThrowIfNull(pluginInterface);
@ -66,6 +67,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton(commandManager); serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle); serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState); serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(contextMenu);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable))); serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
@ -81,6 +83,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload(); _serviceProvider.GetRequiredService<QuestRegistry>().Reload();
_serviceProvider.GetRequiredService<CommandHandler>(); _serviceProvider.GetRequiredService<CommandHandler>();
_serviceProvider.GetRequiredService<ContextMenuController>();
_serviceProvider.GetRequiredService<DalamudInitializer>(); _serviceProvider.GetRequiredService<DalamudInitializer>();
} }
@ -156,6 +159,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NavigationShortcutController>(); serviceCollection.AddSingleton<NavigationShortcutController>();
serviceCollection.AddSingleton<CombatController>(); serviceCollection.AddSingleton<CombatController>();
serviceCollection.AddSingleton<GatheringController>(); serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ContextMenuController>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>(); serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
} }

View File

@ -18,7 +18,7 @@ internal sealed class AethernetShortcutValidator : IQuestValidator
public IEnumerable<ValidationIssue> Validate(Quest quest) public IEnumerable<ValidationIssue> Validate(Quest quest)
{ {
return quest.AllSteps() 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) .Where(x => x != null)
.Cast<ValidationIssue>(); .Cast<ValidationIssue>();
} }

View File

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

View File

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

View File

@ -25,7 +25,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{ {
_questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result; _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 var evaluationResult = _questSchema.Evaluate(questNode, new EvaluationOptions
{ {
@ -36,7 +36,7 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{ {
yield return new ValidationIssue yield return new ValidationIssue
{ {
QuestId = quest.QuestElementId, QuestId = quest.Id,
Sequence = null, Sequence = null,
Step = null, Step = null,
Type = EIssueType.InvalidJsonSchema, Type = EIssueType.InvalidJsonSchema,

View File

@ -8,11 +8,11 @@ internal sealed class NextQuestValidator : IQuestValidator
{ {
public IEnumerable<ValidationIssue> Validate(Quest quest) 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 yield return new ValidationIssue
{ {
QuestId = quest.QuestElementId, QuestId = quest.Id,
Sequence = (byte)invalidNextQuest.Sequence.Sequence, Sequence = (byte)invalidNextQuest.Sequence.Sequence,
Step = invalidNextQuest.StepId, Step = invalidNextQuest.StepId,
Type = EIssueType.InvalidNextQuestId, Type = EIssueType.InvalidNextQuestId,

View File

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

View File

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

View File

@ -103,7 +103,7 @@ internal sealed class DebugOverlay : Window
QuestStep? step = sequence.FindStep(i); QuestStep? step = sequence.FindStep(i);
if (step != null && TryGetPosition(step, out Vector3? position)) 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)) 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()) if (ImGui.IsItemHovered())

View File

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

View File

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

View File

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

View File

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