SyntaxNodeList(
AssignmentList(nameof(QuestRoot.Author), quest.Author)
.AsSyntaxNodeOrToken(),
+ Assignment(nameof(QuestRoot.Disabled), quest.Disabled, false).AsSyntaxNodeOrToken(),
Assignment(nameof(QuestRoot.Comment), quest.Comment, null)
.AsSyntaxNodeOrToken(),
AssignmentList(nameof(QuestRoot.TerritoryBlacklist),
Assignment(nameof(QuestStep.ContentFinderConditionId),
step.ContentFinderConditionId, emptyStep.ContentFinderConditionId)
.AsSyntaxNodeOrToken(),
- Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions, emptyStep.SkipConditions)
+ Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
+ emptyStep.SkipConditions)
.AsSyntaxNodeOrToken(),
AssignmentList(nameof(QuestStep.RequiredQuestVariables),
step.RequiredQuestVariables)
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Microsoft.CodeAnalysis;
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(value.GetType().Name),
IdentifierName(value.GetType().GetEnumName(value)!));
+ else if (value is QuestId questId)
+ {
+ return ObjectCreationExpression(
+ IdentifierName(nameof(QuestId)))
+ .WithArgumentList(
+ ArgumentList(
+ SingletonSeparatedList(
+ Argument(LiteralValue(questId.Value)))));
+ }
+ else if (value is LeveId leveId)
+ {
+ return ObjectCreationExpression(
+ IdentifierName(nameof(LeveId)))
+ .WithArgumentList(
+ ArgumentList(
+ SingletonSeparatedList(
+ Argument(LiteralValue(leveId.Value)))));
+ }
else if (value is Vector3 vector)
{
return ObjectCreationExpression(
+++ /dev/null
-{
- "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 255,
- "Steps": [
- {
- "Position": {
- "X": -435.39066,
- "Y": -9.809827,
- "Z": -594.5472
- },
- "TerritoryId": 1187,
- "InteractionType": "WalkTo",
- "Fly": true,
- "RequiredGatheredItems": [
- {
- "ItemId": 43992,
- "ItemCount": 1234
- }
- ]
- }
- ]
- }
- ]
-}
"ItemId": 35848,
"ItemCount": 1
}
- ],
- "NextQuestId": 4159
+ ]
}
]
},
--- /dev/null
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Questing.Converter;
+
+public class IdConverter : JsonConverter<IId>
+{
+ public override IId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ uint value = reader.GetUInt32();
+ return Id.From(value);
+ }
+
+ public override void Write(Utf8JsonWriter writer, IId value, JsonSerializerOptions options)
+ {
+ throw new NotImplementedException();
+ }
+}
--- /dev/null
+using System;
+using System.Globalization;
+
+namespace Questionable.Model.Questing
+{
+ public interface IId : IComparable<IId>
+ {
+ public ushort Value { get; }
+ }
+
+ public static class Id
+ {
+ public static IId From(uint value)
+ {
+ if (value >= 100_000 && value < 200_000)
+ return new LeveId((ushort)(value - 100_000));
+ else
+ return new QuestId((ushort)value);
+ }
+ }
+
+ public sealed record QuestId(ushort Value) : IId
+ {
+ public override string ToString()
+ {
+ return "Q" + Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public int CompareTo(IId? other)
+ {
+ if (ReferenceEquals(this, other)) return 0;
+ if (ReferenceEquals(null, other)) return 1;
+ return Value.CompareTo(other.Value);
+ }
+ }
+
+ public sealed record LeveId(ushort Value) : IId
+ {
+ public override string ToString()
+ {
+ return "L" + Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public int CompareTo(IId? other)
+ {
+ if (ReferenceEquals(this, other)) return 0;
+ if (ReferenceEquals(null, other)) return 1;
+ return Value.CompareTo(other.Value);
+ }
+ }
+}
+
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit
+ {
+ }
+}
public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
// TODO: Not implemented
- public ushort? PickUpQuestId { get; set; }
+ [JsonConverter(typeof(IdConverter))]
+ public IId? PickUpQuestId { get; set; }
- public ushort? TurnInQuestId { get; set; }
- public ushort? NextQuestId { get; set; }
+ [JsonConverter(typeof(IdConverter))]
+ public IId? TurnInQuestId { get; set; }
+
+ [JsonConverter(typeof(IdConverter))]
+ public IId? NextQuestId { get; set; }
[JsonConstructor]
public QuestStep()
public List<ushort> InTerritory { get; set; } = new();
public List<ushort> NotInTerritory { get; set; } = new();
public SkipItemConditions? Item { get; set; }
- public List<ushort> QuestsAccepted { get; set; } = new();
- public List<ushort> QuestsCompleted { get; set; } = new();
+ public List<IId> QuestsAccepted { get; set; } = new();
+ public List<IId> QuestsCompleted { get; set; } = new();
public EExtraSkipCondition? ExtraCondition { get; set; }
public bool HasSkipConditions()
}
}
- if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags))
+ if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) && _currentFight.Data.QuestId is QuestId questId)
{
- var questWork = _gameFunctions.GetQuestEx(_currentFight.Data.QuestId);
+ var questWork = _gameFunctions.GetQuestEx(questId);
if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags,
questWork.Value))
{
public sealed class CombatData
{
- public required ushort QuestId { get; init; }
+ public required IId QuestId { get; init; }
public required EEnemySpawnType SpawnType { get; init; }
public required List<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
using Dalamud.Game.Command;
using Dalamud.Plugin.Services;
using Questionable.Model;
+using Questionable.Model.Questing;
using Questionable.Windows;
using Questionable.Windows.QuestComponents;
return;
}
- if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId))
+ if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
{
- if (_questRegistry.TryGetQuest(questId, out Quest? quest))
+ if (_questRegistry.TryGetQuest(Id.From(questId), out Quest? quest))
{
- _debugOverlay.HighlightedQuest = questId;
+ _debugOverlay.HighlightedQuest = quest.QuestId;
_chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name}).");
}
else
private void SetNextQuest(string[] arguments)
{
- if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId))
+ if (arguments.Length >= 1 && uint.TryParse(arguments[0], out uint questId))
{
- if (_gameFunctions.IsQuestLocked(questId, 0))
+ if (_gameFunctions.IsQuestLocked(Id.From(questId)))
_chatGui.PrintError($"[Questionable] Quest {questId} is locked.");
- else if (_questRegistry.TryGetQuest(questId, out Quest? quest))
+ else if (_questRegistry.TryGetQuest(Id.From(questId), out Quest? quest))
{
_questController.SetNextQuest(quest);
_chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name}).");
{
if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId))
{
- if (_questRegistry.TryGetQuest(questId, out Quest? quest))
+ if (_questRegistry.TryGetQuest(Id.From(questId), out Quest? quest))
{
_questController.SimulateQuest(quest);
_chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name}).");
private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestId == 4526)
+ if (_questController.StartedQuest?.Quest.QuestId.Value == 4526)
{
_logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestId == 245)
+ if (_questController.StartedQuest?.Quest.QuestId.Value == 245)
{
_logger.LogInformation("Closing ContentsTutorial");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
/// </summary>
private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
{
- if (_questController.StartedQuest?.Quest.QuestId == 245)
+ if (_questController.StartedQuest?.Quest.QuestId.Value == 245)
{
_logger.LogInformation("Closing MultipleHelpWindow");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
}
else
{
- (ushort currentQuestId, currentSequence) = _gameFunctions.GetCurrentQuest();
- if (currentQuestId == 0)
+ (IId? currentQuestId, currentSequence) = _gameFunctions.GetCurrentQuest();
+ if (currentQuestId == null || currentQuestId.Value == 0)
{
if (_startedQuest != null)
{
return (seq, seq.Steps[CurrentQuest.Step]);
}
- public void IncreaseStepCount(ushort? questId, int? sequence, bool shouldContinue = false)
+ public void IncreaseStepCount(IId? questId, int? sequence, bool shouldContinue = false)
{
lock (_progressLock)
{
}
}
- public void Skip(ushort questQuestId, byte currentQuestSequence)
+ public void Skip(IId questQuestId, byte currentQuestSequence)
{
lock (_progressLock)
{
1158, // Titan (Hard)
];
- foreach (var questId in priorityQuests)
+ foreach (var id in priorityQuests)
{
+ var questId = new QuestId(id);
if (_gameFunctions.IsReadyToAcceptQuest(questId) && _questRegistry.TryGetQuest(questId, out var quest))
{
SetNextQuest(quest);
private readonly ILogger<QuestRegistry> _logger;
private readonly ICallGateProvider<object> _reloadDataIpc;
- private readonly Dictionary<ushort, Quest> _quests = new();
+ private readonly Dictionary<IId, Quest> _quests = new();
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
{
Quest quest = new()
{
- QuestId = questId,
+ QuestId = new QuestId(questId),
Root = questRoot,
- Info = _questData.GetQuestInfo(questId),
+ Info = _questData.GetQuestInfo(new QuestId(questId)),
ReadOnly = true,
};
- _quests[questId] = quest;
+ _quests[quest.QuestId] = quest;
}
_logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count);
private void LoadQuestFromStream(string fileName, Stream stream)
{
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
- ushort? questId = ExtractQuestIdFromName(fileName);
+ IId? questId = ExtractQuestIdFromName(fileName);
if (questId == null)
return;
var questNode = JsonNode.Parse(stream)!;
- _jsonSchemaValidator.Enqueue(questId.Value, questNode);
+ _jsonSchemaValidator.Enqueue(questId, questNode);
Quest quest = new Quest
{
- QuestId = questId.Value,
+ QuestId = questId,
Root = questNode.Deserialize<QuestRoot>()!,
- Info = _questData.GetQuestInfo(questId.Value),
+ Info = _questData.GetQuestInfo(questId),
ReadOnly = false,
};
- _quests[questId.Value] = quest;
+ _quests[quest.QuestId] = quest;
}
private void LoadFromDirectory(DirectoryInfo directory, LogLevel logLevel = LogLevel.Information)
LoadFromDirectory(childDirectory, logLevel);
}
- private static ushort? ExtractQuestIdFromName(string resourceName)
+ private static IId? ExtractQuestIdFromName(string resourceName)
{
string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
name = name.Substring(name.LastIndexOf('.') + 1);
return null;
string[] parts = name.Split('_', 2);
- return ushort.Parse(parts[0], CultureInfo.InvariantCulture);
+ return Id.From(uint.Parse(parts[0], CultureInfo.InvariantCulture));
}
- public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId);
+ public bool IsKnownQuest(IId id)
+ {
+ if (id is QuestId questId)
+ return IsKnownQuest(questId);
+ else
+ return false;
+ }
+
+ public bool IsKnownQuest(QuestId questId) => _quests.ContainsKey(questId);
+
+ public bool TryGetQuest(IId id, [NotNullWhen(true)] out Quest? quest)
+ {
+ if (id is QuestId questId)
+ return TryGetQuest(questId, out quest);
+ else
+ {
+ quest = null;
+ return false;
+ }
+ }
- public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest)
+ public bool TryGetQuest(QuestId questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest);
}
if (step.NextQuestId == null)
return null;
- if (step.NextQuestId.Value == quest.QuestId)
+ if (step.NextQuestId == quest.QuestId)
return null;
return serviceProvider.GetRequiredService<SetQuest>()
- .With(step.NextQuestId.Value, quest.QuestId);
+ .With(step.NextQuestId, quest.QuestId);
}
}
internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, GameFunctions gameFunctions, ILogger<SetQuest> logger) : ITask
{
- public ushort NextQuestId { get; set; }
- public ushort CurrentQuestId { get; set; }
+ public IId NextQuestId { get; set; } = null!;
+ public IId CurrentQuestId { get; set; } = null!;
- public ITask With(ushort nextQuestId, ushort currentQuestId)
+ public ITask With(IId nextQuestId, IId currentQuestId)
{
NextQuestId = nextQuestId;
CurrentQuestId = currentQuestId;
-namespace Questionable.Controller.Steps;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.Steps;
internal interface ILastTask : ITask
{
- public ushort QuestId { get; }
+ public IId QuestId { get; }
public int Sequence { get; }
}
private CombatController.CombatData _combatData = null!;
private IList<QuestWorkValue?> _completionQuestVariableFlags = null!;
- public ITask With(ushort questId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
+ public ITask With(IId questId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData)
{
_isLastStep = isLastStep;
return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set
- if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags))
+ if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.QuestId is QuestId questId)
{
- var questWork = gameFunctions.GetQuestEx(_combatData.QuestId);
+ var questWork = gameFunctions.GetQuestEx(questId);
if (questWork == null)
return ETaskResult.StillRunning;
private DateTime _continueAt;
private int _itemCount;
- public ushort? QuestId { get; set; }
+ public IId? QuestId { get; set; }
public uint ItemId { get; set; }
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
public bool StartingCombat { get; set; }
public unsafe ETaskResult Update()
{
- if (QuestId.HasValue && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
+ if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{
- QuestWork? questWork = gameFunctions.GetQuestEx(QuestId.Value);
+ QuestWork? questWork = gameFunctions.GetQuestEx(questId);
if (questWork != null &&
QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork.Value))
return ETaskResult.TaskComplete;
public uint DataId { get; set; }
- public ITask With(ushort? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
+ public ITask With(IId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
DataId = dataId;
public Vector3 Position { get; set; }
- public ITask With(ushort? questId, Vector3 position, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
+ public ITask With(IId? questId, Vector3 position, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
Position = position;
public uint DataId { get; set; }
- public ITask With(ushort? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags,
+ public ITask With(IId? questId, uint dataId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags,
bool startingCombat = false)
{
QuestId = questId;
{
private readonly GameFunctions _gameFunctions = gameFunctions;
- public ITask With(ushort? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
+ public ITask With(IId? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
ItemId = itemId;
{
public QuestStep Step { get; set; } = null!;
public SkipStepConditions SkipConditions { get; set; } = null!;
- public ushort QuestId { get; set; }
+ public IId QuestId { get; set; } = null!;
- public ITask With(QuestStep step, SkipStepConditions skipConditions, ushort questId)
+ public ITask With(QuestStep step, SkipStepConditions skipConditions, IId questId)
{
Step = step;
SkipConditions = skipConditions;
return true;
}
- QuestWork? questWork = gameFunctions.GetQuestEx(QuestId);
- if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null)
+ if (QuestId is QuestId questId)
{
- if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value))
+ QuestWork? questWork = gameFunctions.GetQuestEx(questId);
+ if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && questWork != null)
{
- logger.LogInformation("Skipping step, as quest variables match (step is complete)");
- return true;
+ if (QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value))
+ {
+ logger.LogInformation("Skipping step, as quest variables match (step is complete)");
+ return true;
+ }
}
- }
- if (Step is { SkipConditions.StepIf: { } conditions } && questWork != null)
- {
- if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork.Value))
+ if (Step is { SkipConditions.StepIf: { } conditions } && questWork != null)
{
- logger.LogInformation("Skipping step, as quest variables match (step can be skipped)");
- return true;
+ if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork.Value))
+ {
+ logger.LogInformation("Skipping step, as quest variables match (step can be skipped)");
+ return true;
+ }
}
- }
- if (Step is { RequiredQuestVariables: { } requiredQuestVariables } && questWork != null)
- {
- if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork.Value,
- logger))
+ if (Step is { RequiredQuestVariables: { } requiredQuestVariables } && questWork != null)
{
- logger.LogInformation("Skipping step, as required variables do not match");
- return true;
+ if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork.Value,
+ logger))
+ {
+ logger.LogInformation("Skipping step, as required variables do not match");
+ return true;
+ }
}
}
}
}
- if (Step.PickUpQuestId != null && gameFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId.Value))
+ if (Step.PickUpQuestId != null && gameFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId))
{
logger.LogInformation("Skipping step, as we have already picked up the relevant quest");
return true;
}
- if (Step.TurnInQuestId != null && gameFunctions.IsQuestComplete(Step.TurnInQuestId.Value))
+ if (Step.TurnInQuestId != null && gameFunctions.IsQuestComplete(Step.TurnInQuestId))
{
logger.LogInformation("Skipping step, as we have already completed the relevant quest");
return true;
if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
- .With(quest, step);
+ .With((QuestId)quest.QuestId, step);
var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, Next(quest, sequence)];
}
internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask
{
- public Quest Quest { get; set; } = null!;
+ public QuestId Quest { get; set; } = null!;
public QuestStep Step { get; set; } = null!;
public IList<QuestWorkValue?> Flags { get; set; } = null!;
- public ITask With(Quest quest, QuestStep step)
+ public ITask With(QuestId quest, QuestStep step)
{
Quest = quest;
Step = step;
public ETaskResult Update()
{
- QuestWork? questWork = gameFunctions.GetQuestEx(Quest.QuestId);
+ QuestWork? questWork = gameFunctions.GetQuestEx(Quest);
return questWork != null &&
QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork.Value)
? ETaskResult.TaskComplete
$"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})";
}
- internal sealed class WaitQuestAccepted : ITask
+ internal sealed class WaitQuestAccepted(GameFunctions gameFunctions) : ITask
{
- public ushort QuestId { get; set; }
+ public IId QuestId { get; set; } = null!;
- public ITask With(ushort questId)
+ public ITask With(IId questId)
{
QuestId = questId;
return this;
public ETaskResult Update()
{
- unsafe
- {
- var questManager = QuestManager.Instance();
- return questManager != null && questManager->IsQuestAccepted(QuestId)
- ? ETaskResult.TaskComplete
- : ETaskResult.StillRunning;
- }
+ return gameFunctions.IsQuestAccepted(QuestId)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
}
public override string ToString() => $"WaitQuestAccepted({QuestId})";
}
- internal sealed class WaitQuestCompleted : ITask
+ internal sealed class WaitQuestCompleted(GameFunctions gameFunctions) : ITask
{
- public ushort QuestId { get; set; }
+ public IId QuestId { get; set; } = null!;
- public ITask With(ushort questId)
+ public ITask With(IId questId)
{
QuestId = questId;
return this;
public ETaskResult Update()
{
- return QuestManager.IsQuestComplete(QuestId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+ return gameFunctions.IsQuestComplete(QuestId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
public override string ToString() => $"WaitQuestComplete({QuestId})";
}
- internal sealed class NextStep(ushort questId, int sequence) : ILastTask
+ internal sealed class NextStep(IId questId, int sequence) : ILastTask
{
- public ushort QuestId { get; } = questId;
+ public IId QuestId { get; } = questId;
public int Sequence { get; } = sequence;
public bool Start() => true;
internal sealed class EndAutomation : ILastTask
{
- public ushort QuestId => throw new InvalidOperationException();
+ public IId QuestId => throw new InvalidOperationException();
public int Sequence => throw new InvalidOperationException();
public bool Start() => true;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Questionable.Model;
+using Questionable.Model.Questing;
namespace Questionable.Data;
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((ushort)(x & 0xFFFF))).ToList());
+ .Select(x => 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((ushort)(x & 0xFFFF))).ToList());
+ .Select(x => 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((ushort)(x & 0xFFFF)))
+ .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
.ToList());
genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
genres.Single(x => x.Id == 1)
using System.Linq;
using Dalamud.Plugin.Services;
using Questionable.Model;
+using Questionable.Model.Questing;
using Quest = Lumina.Excel.GeneratedSheets.Quest;
namespace Questionable.Data;
internal sealed class QuestData
{
- private readonly ImmutableDictionary<ushort, QuestInfo> _quests;
+ private readonly ImmutableDictionary<QuestId, QuestInfo> _quests;
public QuestData(IDataManager dataManager)
{
.ToImmutableDictionary(x => x.QuestId, x => x);
}
- public QuestInfo GetQuestInfo(ushort questId)
+ public QuestInfo GetQuestInfo(IId id)
+ {
+ if (id is QuestId questId)
+ return GetQuestInfo(questId);
+
+ throw new ArgumentException("Invalid id", nameof(id));
+ }
+
+ public QuestInfo GetQuestInfo(QuestId questId)
{
return _quests[questId] ?? throw new ArgumentOutOfRangeException(nameof(questId));
}
public DateTime ReturnRequestedAt { get; set; } = DateTime.MinValue;
- public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
+ public (IId? CurrentQuest, byte Sequence) GetCurrentQuest()
{
var (currentQuest, sequence) = GetCurrentQuestInternal();
- QuestManager* questManager = QuestManager.Instance();
PlayerState* playerState = PlayerState.Instance();
- if (currentQuest == 0)
+ if (currentQuest == null || currentQuest.Value == 0)
{
if (_clientState.TerritoryType == 181) // Starting in Limsa
- return (107, 0);
+ return (new QuestId(107), 0);
if (_clientState.TerritoryType == 182) // Starting in Ul'dah
- return (594, 0);
+ return (new QuestId(594), 0);
if (_clientState.TerritoryType == 183) // Starting in Gridania
- return (39, 0);
+ return (new QuestId(39), 0);
return default;
}
- else if (currentQuest == 681)
+ else if (currentQuest.Value == 681)
{
// if we have already picked up the GC quest, just return the progress for it
- if (questManager->IsQuestAccepted(currentQuest) || QuestManager.IsQuestComplete(currentQuest))
+ if (IsQuestAccepted(currentQuest) || IsQuestComplete(currentQuest))
return (currentQuest, sequence);
// The company you keep...
return _configuration.General.GrandCompany switch
{
- GrandCompany.TwinAdder => (680, 0),
- GrandCompany.Maelstrom => (681, 0),
+ GrandCompany.TwinAdder => (new QuestId(680), 0),
+ GrandCompany.Maelstrom => (new QuestId(681), 0),
_ => default
};
}
- else if (currentQuest == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
+ else if (currentQuest.Value == 3856 && !playerState->IsMountUnlocked(1)) // we come in peace
{
ushort chocoboQuest = (GrandCompany)playerState->GrandCompany switch
{
};
if (chocoboQuest != 0 && !QuestManager.IsQuestComplete(chocoboQuest))
- return (chocoboQuest, QuestManager.GetQuestSequence(chocoboQuest));
+ return (new QuestId(chocoboQuest), QuestManager.GetQuestSequence(chocoboQuest));
}
- else if (currentQuest == 801)
+ else if (currentQuest.Value == 801)
{
// skeletons in her closet, finish 'broadening horizons' to unlock the white wolf gate
- ushort broadeningHorizons = 802;
- if (questManager->IsQuestAccepted(broadeningHorizons))
- return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons));
+ QuestId broadeningHorizons = new QuestId(802);
+ if (IsQuestAccepted(broadeningHorizons))
+ return (broadeningHorizons, QuestManager.GetQuestSequence(broadeningHorizons.Value));
}
return (currentQuest, sequence);
}
- public (ushort CurrentQuest, byte Sequence) GetCurrentQuestInternal()
+ public (IId? CurrentQuest, byte Sequence) GetCurrentQuestInternal()
{
var questManager = QuestManager.Instance();
if (questManager != null)
// always prioritize accepting MSQ quests, to make sure we don't turn in one MSQ quest and then go off to do
// side quests until the end of time.
var msqQuest = GetMainScenarioQuest(questManager);
- if (msqQuest.CurrentQuest != 0 && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
+ if (msqQuest.CurrentQuest is { Value: not 0 } && _questRegistry.IsKnownQuest(msqQuest.CurrentQuest))
return msqQuest;
// Use the quests in the same order as they're shown in the to-do list, e.g. if the MSQ is the first item,
// If no quests are marked as 'priority', accepting a new quest adds it to the top of the list.
for (int i = questManager->TrackedQuests.Length - 1; i >= 0; --i)
{
- ushort currentQuest;
+ IId currentQuest;
var trackedQuest = questManager->TrackedQuests[i];
switch (trackedQuest.QuestType)
{
continue;
case 1: // normal quest
- currentQuest = questManager->NormalQuests[trackedQuest.Index].QuestId;
+ currentQuest = new QuestId(questManager->NormalQuests[trackedQuest.Index].QuestId);
break;
}
if (_questRegistry.IsKnownQuest(currentQuest))
- return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
+ return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
// if we know no quest of those currently in the to-do list, just do MSQ
return default;
}
- private (ushort CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
+ private (QuestId? CurrentQuest, byte Sequence) GetMainScenarioQuest(QuestManager* questManager)
{
if (QuestManager.IsQuestComplete(3759)) // Memories Rekindled
{
// redoHud+44 is chapter
// redoHud+46 is quest
ushort questId = MemoryHelper.Read<ushort>((nint)questRedoHud + 46);
- return (questId, QuestManager.GetQuestSequence(questId));
+ return (new QuestId(questId), QuestManager.GetQuestSequence(questId));
}
}
}
if (scenarioTree->Data == null)
return default;
- ushort currentQuest = scenarioTree->Data->CurrentScenarioQuest;
- if (currentQuest == 0)
+ QuestId currentQuest = new QuestId(scenarioTree->Data->CurrentScenarioQuest);
+ if (currentQuest.Value == 0)
return default;
// if the MSQ is hidden, we generally ignore it
- if (questManager->IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest)->IsHidden)
+ if (IsQuestAccepted(currentQuest) && questManager->GetQuestById(currentQuest.Value)->IsHidden)
return default;
// it can sometimes happen (although this isn't reliably reproducible) that the quest returned here
&& quest.Info.Level > currentLevel)
return default;
- return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
+ return (currentQuest, QuestManager.GetQuestSequence(currentQuest.Value));
}
- public QuestWork? GetQuestEx(ushort questId)
+ public QuestWork? GetQuestEx(QuestId questId)
{
- QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId);
+ QuestWork* questWork = QuestManager.Instance()->GetQuestById(questId.Value);
return questWork != null ? *questWork : null;
}
- public bool IsReadyToAcceptQuest(ushort questId)
+ public bool IsReadyToAcceptQuest(IId id)
+ {
+ if (id is QuestId questId)
+ return IsReadyToAcceptQuest(questId);
+ return false;
+ }
+
+ public bool IsReadyToAcceptQuest(QuestId questId)
{
_questRegistry.TryGetQuest(questId, out var quest);
if (quest is { Info.IsRepeatable: true })
return true;
}
- public bool IsQuestAcceptedOrComplete(ushort questId)
+ public bool IsQuestAcceptedOrComplete(IId questId)
{
return IsQuestComplete(questId) || IsQuestAccepted(questId);
}
- public bool IsQuestAccepted(ushort questId)
+ public bool IsQuestAccepted(IId id)
+ {
+ if (id is QuestId questId)
+ return IsQuestAccepted(questId);
+ return false;
+ }
+
+ public bool IsQuestAccepted(QuestId questId)
{
QuestManager* questManager = QuestManager.Instance();
- return questManager->IsQuestAccepted(questId);
+ return questManager->IsQuestAccepted(questId.Value);
+ }
+
+ public bool IsQuestComplete(IId id)
+ {
+ if (id is QuestId questId)
+ return IsQuestComplete(questId);
+ return false;
}
[SuppressMessage("Performance", "CA1822")]
- public bool IsQuestComplete(ushort questId)
+ public bool IsQuestComplete(QuestId questId)
{
- return QuestManager.IsQuestComplete(questId);
+ return QuestManager.IsQuestComplete(questId.Value);
+ }
+
+ public bool IsQuestLocked(IId id, IId? extraCompletedQuest = null)
+ {
+ if (id is QuestId questId)
+ return IsQuestLocked(questId, extraCompletedQuest);
+ return false;
}
- public bool IsQuestLocked(ushort questId, ushort? extraCompletedQuest = null)
+ public bool IsQuestLocked(QuestId questId, IId? extraCompletedQuest = null)
{
var questInfo = _questData.GetQuestInfo(questId);
if (questInfo.QuestLocks.Count > 0)
{
- var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x == extraCompletedQuest);
+ var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests)
return true;
else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0)
return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo);
}
- private bool HasCompletedPreviousQuests(QuestInfo questInfo, ushort? extraCompletedQuest)
+ private bool HasCompletedPreviousQuests(QuestInfo questInfo, IId? extraCompletedQuest)
{
if (questInfo.PreviousQuests.Count == 0)
return true;
- var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x == extraCompletedQuest);
+ var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All &&
questInfo.PreviousQuests.Count == completedQuests)
return true;
if (_configuration.Advanced.NeverFly)
return false;
- if (IsQuestAccepted(3304) && _condition[ConditionFlag.Mounted])
+ if (IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted])
{
BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0);
if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one
if (excelSheetName == null)
{
var questRow =
- _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestId +
+ _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestId.Value +
0x10000);
if (questRow == null)
{
return null;
}
- excelSheetName = $"quest/{(currentQuest.QuestId / 100):000}/{questRow.Id}";
+ excelSheetName = $"quest/{(currentQuest.QuestId.Value / 100):000}/{questRow.Id}";
}
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
internal sealed class Quest
{
- public required ushort QuestId { get; init; }
+ public required IId QuestId { get; init; }
public required QuestRoot Root { get; init; }
public required QuestInfo Info { get; init; }
public required bool ReadOnly { get; init; }
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using JetBrains.Annotations;
+using Questionable.Model.Questing;
using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest;
namespace Questionable.Model;
{
public QuestInfo(ExcelQuest quest)
{
- QuestId = (ushort)(quest.RowId & 0xFFFF);
+ QuestId = new QuestId((ushort)(quest.RowId & 0xFFFF));
- string suffix = QuestId switch
+ string suffix = QuestId.Value switch
{
85 => " (Lancer)",
108 => " (Marauder)",
Level = quest.ClassJobLevel0;
IssuerDataId = quest.IssuerStart;
IsRepeatable = quest.IsRepeatable;
- PreviousQuests = quest.PreviousQuest.Select(x => (ushort)(x.Row & 0xFFFF)).Where(x => x != 0).ToImmutableList();
+ PreviousQuests = quest.PreviousQuest
+ .Select(x => new QuestId((ushort)(x.Row & 0xFFFF)))
+ .Where(x => x.Value != 0)
+ .ToImmutableList();
PreviousQuestJoin = (QuestJoin)quest.PreviousQuestJoin;
- QuestLocks = quest.QuestLock.Select(x => (ushort)(x.Row & 0xFFFFF)).Where(x => x != 0).ToImmutableList();
+ QuestLocks = quest.QuestLock
+ .Select(x => new QuestId((ushort)(x.Row & 0xFFFFF)))
+ .Where(x => x.Value != 0)
+ .ToImmutableList();
QuestLockJoin = (QuestJoin)quest.QuestLockJoin;
JournalGenre = quest.JournalGenre?.Row;
SortKey = quest.SortKey;
}
- public ushort QuestId { get; }
+ public QuestId QuestId { get; }
public string Name { get; }
public ushort Level { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable { get; }
- public ImmutableList<ushort> PreviousQuests { get; }
+ public ImmutableList<QuestId> PreviousQuests { get; }
public QuestJoin PreviousQuestJoin { get; }
- public ImmutableList<ushort> QuestLocks { get; }
+ public ImmutableList<QuestId> QuestLocks { get; }
public QuestJoin QuestLockJoin { get; }
public List<ushort> PreviousInstanceContent { get; }
public QuestJoin PreviousInstanceContentJoin { get; }
using Questionable.Model;
+using Questionable.Model.Questing;
namespace Questionable.Validation;
internal sealed record ValidationIssue
{
- public required ushort? QuestId { get; init; }
+ public required IId? QuestId { get; init; }
public required byte? Sequence { get; init; }
public required int? Step { get; init; }
public EBeastTribe BeastTribe { get; init; } = EBeastTribe.None;
.Cast<ValidationIssue>();
}
- private ValidationIssue? Validate(ushort questId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut)
+ private ValidationIssue? Validate(IId questId, int sequenceNo, int stepId, AethernetShortcut? aethernetShortcut)
{
if (aethernetShortcut == null)
return null;
using System.Text.Json.Nodes;
using Json.Schema;
using Questionable.Model;
+using Questionable.Model.Questing;
using Questionable.QuestPaths;
namespace Questionable.Validation.Validators;
internal sealed class JsonSchemaValidator : IQuestValidator
{
- private readonly Dictionary<ushort, JsonNode> _questNodes = new();
+ private readonly Dictionary<IId, JsonNode> _questNodes = new();
private JsonSchema? _questSchema;
public JsonSchemaValidator()
}
}
- public void Enqueue(ushort questId, JsonNode questNode) => _questNodes[questId] = questNode;
+ public void Enqueue(IId questId, JsonNode questNode) => _questNodes[questId] = questNode;
public void Reset() => _questNodes.Clear();
}
IsOpen = true;
}
- public ushort? HighlightedQuest { get; set; }
+ public IId? HighlightedQuest { get; set; }
public override bool DrawConditions() => _configuration.Advanced.DebugOverlay;
private void DrawHighlightedQuest()
{
- if (HighlightedQuest == null || !_questRegistry.TryGetQuest(HighlightedQuest.Value, out var quest))
+ if (HighlightedQuest == null || !_questRegistry.TryGetQuest(HighlightedQuest, out var quest))
return;
foreach (var sequence in quest.Root.QuestSequence)
if (ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
{
- _commandManager.DispatchCommand("/questinfo",
- questInfo.QuestId.ToString(CultureInfo.InvariantCulture), commandInfo);
+ _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString(), commandInfo);
}
if (ImGui.IsItemHovered())
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Common.Math;
using Questionable.Data;
+using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
internal sealed class ARealmRebornComponent
{
- private const ushort ATimeForEveryPurpose = 425;
- private const ushort TheUltimateWeapon = 524;
- private const ushort GoodIntentions = 363;
+ private static readonly QuestId ATimeForEveryPurpose = new(425);
+ private static readonly QuestId TheUltimateWeapon = new(524);
+ private static readonly QuestId GoodIntentions = new(363);
private static readonly ushort[] RequiredPrimalInstances = [20004, 20006, 20005];
- private static readonly ushort[] RequiredAllianceRaidQuests = [1709, 1200, 1201, 1202, 1203, 1474, 494, 495];
+
+ private static readonly QuestId[] RequiredAllianceRaidQuests =
+ [new(1709), new(1200), new(1201), new(1202), new(1203), new(1474), new(494), new(495)];
private readonly GameFunctions _gameFunctions;
private readonly QuestData _questData;
private QuestWork? DrawQuestWork(QuestController.QuestProgress currentQuest)
{
- var questWork = _gameFunctions.GetQuestEx(currentQuest.Quest.QuestId);
+ if (currentQuest.Quest.QuestId is not QuestId questId)
+ return null;
+
+ var questWork = _gameFunctions.GetQuestEx(questId);
if (questWork != null)
{
Vector4 color;
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Atlas))
_commandManager.DispatchCommand("/questinfo",
- currentQuest.Quest.QuestId.ToString(CultureInfo.InvariantCulture), commandInfo);
+ currentQuest.Quest.QuestId.ToString() ?? string.Empty, commandInfo);
}
bool autoAcceptNextQuest = _configuration.General.AutoAcceptNextQuest;
foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers)
{
- ushort questId = (ushort)(unacceptedQuest.ObjectiveId & 0xFFFF);
+ QuestId questId = new QuestId((ushort)(unacceptedQuest.ObjectiveId & 0xFFFF));
if (_quests.All(q => q.QuestId != questId))
_quests.Add(_questData.GetQuestInfo(questId));
}
{
ImGui.TableNextRow();
- string questId = quest.QuestId.ToString(CultureInfo.InvariantCulture);
+ string questId = quest.QuestId.ToString();
bool isKnownQuest = _questRegistry.TryGetQuest(quest.QuestId, out var knownQuest);
if (ImGui.TableNextColumn())
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
- ImGui.TextUnformatted(validationIssue.QuestId?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
+ ImGui.TextUnformatted(validationIssue.QuestId?.ToString() ?? string.Empty);
if (ImGui.TableNextColumn())
ImGui.TextUnformatted(validationIssue.QuestId != null
- ? _questData.GetQuestInfo(validationIssue.QuestId.Value).Name
+ ? _questData.GetQuestInfo(validationIssue.QuestId).Name
: validationIssue.BeastTribe.ToString());
if (ImGui.TableNextColumn())
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
+using Questionable.Model.Questing;
namespace Questionable.Windows;
_pluginInterface = pluginInterface;
}
- public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ushort questId)
+ public (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(IId questId)
{
if (_gameFunctions.IsQuestAccepted(questId))
return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active");