muffin v6.12

This commit is contained in:
alydev 2025-10-09 07:53:51 +10:00
parent cfb4dea47e
commit c8197297b2
58 changed files with 40038 additions and 58059 deletions

View file

@ -1,13 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/gatheringlocation-v1.json",
"$id": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/GatheringPaths/gatheringlocation-v1.json",
"title": "Gathering Location V1",
"description": "A series of gathering locationsk",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"const": "https://qstxiv.github.io/schema/gatheringlocation-v1.json"
"const": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/GatheringPaths/gatheringlocation-v1.json"
},
"Author": {
"description": "Author of the gathering location data",
@ -22,7 +22,7 @@
"Steps": {
"type": "array",
"items": {
"$ref": "https://qstxiv.github.io/schema/quest-v1.json#/$defs/Step"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json#/$defs/Step"
},
"minItems": 1
},
@ -63,7 +63,7 @@
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"MinimumAngle": {
"type": "number",

View file

@ -1,13 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/quest-v1.json",
"$id": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json",
"title": "Questionable V1",
"description": "A series of quest sequences",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"const": "https://qstxiv.github.io/schema/quest-v1.json"
"const": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json"
},
"Author": {
"description": "Author of the quest sequence",
@ -91,7 +91,7 @@
"exclusiveMinimum": 0
},
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"StopDistance": {
"type": [
@ -139,7 +139,7 @@
"Emote",
"Action",
"StatusOff",
"WaitForNpcAtPosition",
"WaitForObjectAtPosition",
"WaitForManualProgress",
"Duty",
"SinglePlayerDuty",
@ -187,7 +187,7 @@
},
"AetheryteShortcut": {
"description": "The Aetheryte to teleport to (before moving)",
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"AethernetShortcut": {
"type": "array",
@ -195,7 +195,7 @@
"minItems": 2,
"maxItems": 2,
"items": {
"$ref": "https://qstxiv.github.io/schema/common-aethernetshard.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aethernetshard.json"
}
},
"ItemId": {
@ -217,7 +217,7 @@
"type": "boolean"
},
"CompletionQuestVariablesFlags": {
"$ref": "https://qstxiv.github.io/schema/common-completionflags.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json"
},
"Flying": {
"type": "string",
@ -287,16 +287,16 @@
}
},
"AetheryteLocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"AetheryteUnlocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"NearPosition": {
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"MaximumDistance": {
"type": "number"
@ -316,7 +316,7 @@
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"MaximumDistance": {
"type": "number"
@ -381,10 +381,10 @@
}
},
"AetheryteLocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"AetheryteUnlocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"RequiredQuestVariablesNotMet": {
"type": "boolean"
@ -393,7 +393,7 @@
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"MaximumDistance": {
"type": "number"
@ -413,7 +413,7 @@
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"MaximumDistance": {
"type": "number"
@ -448,10 +448,10 @@
"type": "boolean"
},
"AetheryteLocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"AetheryteUnlocked": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
}
},
"additionalProperties": false
@ -460,7 +460,7 @@
"additionalProperties": false
},
"CompletionQuestVariablesFlags": {
"$ref": "https://qstxiv.github.io/schema/common-completionflags.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json"
},
"RequiredQuestVariables": {
"type": "array",
@ -504,14 +504,14 @@
"description": "Which class or job you are using whenever this step gets executed",
"type": "array",
"items": {
"$ref": "https://qstxiv.github.io/schema/common-classjob.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json"
}
},
"RequiredQuestAcceptedJob": {
"description": "Which class or job you were using when accepting this quest (e.g. for beast tribes)",
"type": "array",
"items": {
"$ref": "https://qstxiv.github.io/schema/common-classjob.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json"
}
},
"DelaySecondsAtStart": {
@ -606,7 +606,7 @@
"then": {
"properties": {
"Aetheryte": {
"$ref": "https://qstxiv.github.io/schema/common-aetheryte.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"
},
"DataId": {
"type": "null"
@ -638,7 +638,7 @@
"then": {
"properties": {
"AethernetShard": {
"$ref": "https://qstxiv.github.io/schema/common-aethernetshard.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aethernetshard.json"
},
"DataId": {
"type": "null"
@ -738,7 +738,7 @@
"type": "integer"
},
"CompletionQuestVariablesFlags": {
"$ref": "https://qstxiv.github.io/schema/common-completionflags.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json"
},
"IgnoreQuestMarker": {
"type": "boolean"
@ -1386,7 +1386,7 @@
"type": "object",
"properties": {
"Position": {
"$ref": "https://qstxiv.github.io/schema/common-vector3.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"
},
"StopDistance": {
"type": [
@ -1667,7 +1667,7 @@
"if": {
"properties": {
"InteractionType": {
"const": "WaitForNpcAtPosition"
"const": "WaitForObjectAtPosition"
}
}
},
@ -1851,7 +1851,7 @@
"then": {
"properties": {
"TargetClass": {
"$ref": "https://qstxiv.github.io/schema/common-classjob.json"
"$ref": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json"
}
},
"required": [

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-aethernetshard.json",
"$id": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aethernetshard.json",
"type": "string",
"enum": [
"[Gridania] Aetheryte Plaza",

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-aetheryte.json",
"$id": "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json",
"type": "string",
"enum": [
"Gridania",

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-classjob.json",
"$id": "https://git.carvel.li//liza/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json",
"type": "string",
"enum": [
"Gladiator",

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-completionflags.json",
"$id": "https://git.carvel.li//liza/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json",
"type": "array",
"description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset",
"items": {
@ -24,6 +24,9 @@
"minimum": 0,
"maximum": 15
},
"Negative": {
"type": "boolean"
},
"Mode": {
"type": "string",
"enum": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qstxiv.github.io/schema/common-vector3.json",
"$id": "https://git.carvel.li//liza/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json",
"type": "object",
"description": "Position in the world",
"properties": {

View file

@ -73,7 +73,7 @@ public sealed class InteractionTypeConverter : EnumConverter<EInteractionType>
},
{
EInteractionType.WaitForObjectAtPosition,
"WaitForNpcAtPosition"
"WaitForObjectAtPosition"
},
{
EInteractionType.WaitForManualProgress,

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
@ -15,5 +16,11 @@ public sealed class QuestRoot
public string? Comment { get; set; }
[JsonIgnore(/*Could not decode attribute arguments.*/)]
public bool? IsSeasonalQuest { get; set; }
[JsonIgnore(/*Could not decode attribute arguments.*/)]
public DateTime? SeasonalQuestExpiry { get; set; }
public List<QuestSequence> QuestSequence { get; set; } = new List<QuestSequence>();
}

View file

@ -9,29 +9,100 @@ namespace Questionable.Controller.GameUi;
internal sealed class CreditsController : IDisposable
{
private static CreditsController? _instance;
private static IAddonLifecycle? _registeredLifecycle;
private static readonly IAddonLifecycle.AddonEventDelegate _creditScrollHandler = delegate(AddonEvent t, AddonArgs a)
{
_instance?.CreditScrollPostSetup(t, a);
};
private static readonly IAddonLifecycle.AddonEventDelegate _creditHandler = delegate(AddonEvent t, AddonArgs a)
{
_instance?.CreditPostSetup(t, a);
};
private static readonly IAddonLifecycle.AddonEventDelegate _creditPlayerHandler = delegate(AddonEvent t, AddonArgs a)
{
_instance?.CreditPlayerPostSetup(t, a);
};
private static readonly string[] CreditScrollArray = new string[1] { "CreditScroll" };
private static readonly string[] CreditArray = new string[1] { "Credit" };
private static readonly string[] CreditPlayerArray = new string[1] { "CreditPlayer" };
private static bool _deferDisposeUntilCutsceneEnds;
private static bool _pendingDispose;
private readonly IAddonLifecycle _addonLifecycle;
private readonly ILogger<CreditsController> _logger;
private static readonly object _lock = new object();
public CreditsController(IAddonLifecycle addonLifecycle, ILogger<CreditsController> logger)
{
_addonLifecycle = addonLifecycle;
_logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
lock (_lock)
{
if (_instance == null)
{
_instance = this;
_registeredLifecycle = _addonLifecycle;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler);
}
}
}
private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing Credits sequence");
_logger.LogInformation("Closing Credits sequence scroll post-setup");
if (args.Addon.Address == IntPtr.Zero)
{
_logger.LogInformation("CreditScrollPostSetup: Addon address is zero, skipping.");
return;
}
lock (_lock)
{
if (_registeredLifecycle != null)
{
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler);
_registeredLifecycle = null;
_instance = null;
}
}
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
}
private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing Credits sequence");
_logger.LogInformation("Closing Credits sequence post-setup");
if (args.Addon.Address == IntPtr.Zero)
{
_logger.LogInformation("CreditPostSetup: Addon address is zero, skipping.");
return;
}
lock (_lock)
{
if (_registeredLifecycle != null)
{
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler);
_registeredLifecycle = null;
_instance = null;
}
}
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
}
@ -39,14 +110,78 @@ internal sealed class CreditsController : IDisposable
private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing CreditPlayer");
if (args.Addon.Address == IntPtr.Zero)
{
return;
}
lock (_lock)
{
if (_registeredLifecycle != null)
{
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler);
_registeredLifecycle = null;
_instance = null;
}
}
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->Close(fireCallback: true);
}
public static void DeferDisposeUntilCutsceneEnds()
{
lock (_lock)
{
_deferDisposeUntilCutsceneEnds = true;
if (_instance != null)
{
_instance._logger.LogDebug("CreditsController: deferring dispose until cutscene ends.");
}
}
}
public static void NotifyCutsceneEnded()
{
lock (_lock)
{
_deferDisposeUntilCutsceneEnds = false;
if (_instance != null)
{
_instance._logger.LogDebug("CreditsController: cutscene ended, processing pending dispose state.");
}
if (_pendingDispose && _instance != null)
{
_instance._logger.LogDebug("CreditsController: pending dispose detected, performing unregister now.");
_instance.Dispose();
}
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
lock (_lock)
{
if (_instance != this)
{
return;
}
if (_deferDisposeUntilCutsceneEnds)
{
_pendingDispose = true;
_logger.LogInformation("CreditsController.Dispose deferred until cutscene end.");
return;
}
if (_registeredLifecycle != null)
{
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler);
_registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler);
}
_registeredLifecycle = null;
_instance = null;
_pendingDispose = false;
_logger.LogDebug("CreditsController listeners unregistered and disposed (primary instance).");
}
}
}

View file

@ -502,14 +502,14 @@ internal sealed class InteractionUiController : IDisposable
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", stringOrRegex, actualPrompt);
continue;
}
for (int num3 = 0; num3 < answers.Count; num3++)
if (dialogueChoice2.AnswerIsRegularExpression && stringOrRegex2 != null)
{
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", answers[num3], stringOrRegex2);
if (!IsMatch(answers[num3], stringOrRegex2))
int? num3 = FindBestRegexMatch(answers, stringOrRegex2, quest3, dialogueChoice2);
if (!num3.HasValue)
{
continue;
}
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", num3, answers[num3], actualPrompt);
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}' (best regex match)", num3.Value, answers[num3.Value], actualPrompt);
if (quest3?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null || _questController.GatheringQuest.Sequence == byte.MaxValue)
@ -519,13 +519,72 @@ internal sealed class InteractionUiController : IDisposable
_questController.GatheringQuest.SetSequence(1);
_questController.StartGatheringQuest("SatisfactionSupply turn in");
}
return num3;
return num3.Value;
}
for (int num4 = 0; num4 < answers.Count; num4++)
{
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", answers[num4], stringOrRegex2);
if (!IsMatch(answers[num4], stringOrRegex2))
{
continue;
}
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", num4, answers[num4], actualPrompt);
if (quest3?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null || _questController.GatheringQuest.Sequence == byte.MaxValue)
{
return null;
}
_questController.GatheringQuest.SetSequence(1);
_questController.StartGatheringQuest("SatisfactionSupply turn in");
}
return num4;
}
}
_logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
return null;
}
private static int? FindBestRegexMatch(List<string?> answers, StringOrRegex expectedAnswer, Questionable.Model.Quest? quest, DialogueChoice dialogueChoice)
{
List<int> list = (from x in answers.Select((string answer, int index) => new { answer, index })
where x.answer != null && expectedAnswer.IsMatch(x.answer)
select x.index).ToList();
return list.Count switch
{
0 => null,
1 => list[0],
_ => GetBestMatchFromQuestPath(answers, list, quest, dialogueChoice),
};
}
private static int GetBestMatchFromQuestPath(List<string?> answers, List<int> matchingIndexes, Questionable.Model.Quest? quest, DialogueChoice dialogueChoice)
{
if (quest != null && dialogueChoice.Answer != null)
{
try
{
string questExpectedAnswer = null;
if (dialogueChoice.Answer.Type == ExcelRef.EType.RawString)
{
questExpectedAnswer = dialogueChoice.Answer.AsRawString();
}
if (!string.IsNullOrEmpty(questExpectedAnswer) && !dialogueChoice.AnswerIsRegularExpression)
{
int num = matchingIndexes.FirstOrDefault((int i) => string.Equals(answers[i], questExpectedAnswer, StringComparison.OrdinalIgnoreCase));
if (num != 0)
{
return num;
}
}
}
catch
{
}
}
return matchingIndexes.OrderBy((int i) => answers[i]?.Length ?? int.MaxValue).First();
}
private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
{
if (actualAnswer == null && expectedAnswer == null)

View file

@ -96,6 +96,8 @@ internal static class Combat
break;
case EEnemySpawnType.OverworldEnemies:
case EEnemySpawnType.FateEnemies:
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.FinishCombatIfAny:
yield return CreateTask(quest, sequence, step);
break;

View file

@ -33,7 +33,12 @@ internal static class SinglePlayerDuty
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SinglePlayerDuty || !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
if (step.InteractionType != EInteractionType.SinglePlayerDuty)
{
yield break;
}
yield return new Mount.UnmountTask();
if (!bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
yield break;
}
@ -41,7 +46,6 @@ internal static class SinglePlayerDuty
{
throw new TaskException("Failed to get content finder condition for solo instance");
}
yield return new Mount.UnmountTask();
yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(2L));
yield return new EnableAi(cfcData.TerritoryId == 688);

View file

@ -39,7 +39,7 @@ internal sealed class TaskCreator
List<ITask> list2;
if (sequence == null)
{
_chatGui.PrintError($"Path for quest '{quest.Info.Name}' ({quest.Id}) does not contain sequence {sequenceNumber}, please report this: https://github.com/PunishXIV/Questionable/discussions/20", "Questionable", 576);
_chatGui.PrintError($"Path for quest '{quest.Info.Name}' ({quest.Id}) does not contain sequence {sequenceNumber}, please report this.", "Questionable", 576);
int num = 1;
List<ITask> list = new List<ITask>(num);
CollectionsMarshal.SetCount(list, num);

View file

@ -33,6 +33,8 @@ internal sealed class CombatController : IDisposable
public required CombatData Data { get; init; }
public required DateTime LastDistanceCheck { get; set; }
public bool HasAttemptedFateSync { get; set; }
}
public sealed class CombatData
@ -70,6 +72,8 @@ internal sealed class CombatController : IDisposable
private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly IObjectTable _objectTable;
@ -92,10 +96,11 @@ internal sealed class CombatController : IDisposable
public bool IsRunning => _currentFight != null;
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, ILogger<CombatController> logger)
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, GameFunctions gameFunctions, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, ILogger<CombatController> logger)
{
_combatModules = combatModules.ToList();
_movementController = movementController;
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_objectTable = objectTable;
_condition = condition;
@ -257,7 +262,7 @@ internal sealed class CombatController : IDisposable
}
}
}
return (from x in _objectTable
IGameObject gameObject = (from x in _objectTable
select new
{
GameObject = x,
@ -267,6 +272,17 @@ internal sealed class CombatController : IDisposable
where x.Priority > 0
orderby x.Priority descending, x.Distance
select x.GameObject).FirstOrDefault();
if (gameObject != null && _currentFight.Data.SpawnType == EEnemySpawnType.FateEnemies && !_currentFight.HasAttemptedFateSync)
{
ushort currentFateId = _gameFunctions.GetCurrentFateId();
if (currentFateId != 0)
{
_logger.LogInformation("Checking FATE sync for FATE {FateId}", currentFateId);
_gameFunctions.SyncToFate(currentFateId);
_currentFight.HasAttemptedFateSync = true;
}
}
return gameObject;
}
public unsafe (int Priority, string Reason) GetKillPriority(IGameObject gameObject)
@ -316,12 +332,12 @@ internal sealed class CombatController : IDisposable
}
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
GameObject* address = (GameObject*)gameObject.Address;
if (address->FateId != 0 && gameObject.TargetObjectId != _clientState.LocalPlayer?.GameObjectId)
if (address->FateId != 0 && _currentFight.Data.SpawnType != EEnemySpawnType.FateEnemies && gameObject.TargetObjectId != _clientState.LocalPlayer?.GameObjectId)
{
return (Priority: null, Reason: "FATE mob");
}
Vector3 value = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
bool flag = _currentFight.Data.SpawnType != EEnemySpawnType.FinishCombatIfAny && ((_currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || !(Vector3.Distance(value, battleNpc.Position) >= 50f)) ? true : false);
bool flag = _currentFight.Data.SpawnType != EEnemySpawnType.FinishCombatIfAny && (_currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || !(Vector3.Distance(value, battleNpc.Position) >= 50f)) && _currentFight.Data.SpawnType != EEnemySpawnType.FateEnemies;
if (complexCombatDatas.Count > 0)
{
for (int i = 0; i < complexCombatDatas.Count; i++)

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui.Toast;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
@ -289,11 +290,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_safeAnimationEnd = DateTime.Now.AddSeconds(1f + num);
}
}
UpdateCurrentQuest();
if (AutomationType == EAutomationType.Manual && !IsRunning && !IsQuestWindowOpen)
{
return;
}
UpdateCurrentQuest();
if (!_clientState.IsLoggedIn)
{
StopAllDueToConditionFailed("Logged out");
@ -320,6 +321,17 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return;
}
}
if (_configuration.Stop.Enabled && _configuration.Stop.SequenceToStopAfter && CurrentQuest != null)
{
int sequence = CurrentQuest.Sequence;
if (sequence >= _configuration.Stop.TargetSequence && IsRunning)
{
_logger.LogInformation("Reached quest sequence stop condition (sequence: {CurrentSequence}, target: {TargetSequence})", sequence, _configuration.Stop.TargetSequence);
_chatGui.Print($"Quest sequence {sequence} reached target sequence {_configuration.Stop.TargetSequence}.", "Questionable", 576);
Stop($"Sequence stop condition reached [{sequence}]");
return;
}
}
bool flag = AutomationType == EAutomationType.Automatic && (_taskQueue.AllTasksComplete || _taskQueue.CurrentTaskExecutor?.CurrentTask is WaitAtEnd.WaitQuestAccepted);
bool flag2;
if (flag)
@ -331,14 +343,14 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (step == 0 || step == 255)
{
flag2 = true;
goto IL_02de;
goto IL_0422;
}
}
flag2 = false;
goto IL_02de;
goto IL_0422;
}
goto IL_02e2;
IL_02e2:
goto IL_0426;
IL_0426:
if (flag && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15.0))
{
lock (_progressLock)
@ -354,23 +366,32 @@ internal sealed class QuestController : MiniTaskController<QuestController>
UpdateCurrentTask();
}
return;
IL_02de:
IL_0422:
flag = flag2;
goto IL_02e2;
goto IL_0426;
}
private void CheckAutoRefreshCondition()
{
if (!_configuration.General.AutoStepRefreshEnabled || AutomationType != EAutomationType.Automatic || !IsRunning || CurrentQuest == null || !_clientState.IsLoggedIn || _clientState.LocalPlayer == null || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0))
if (!ShouldCheckAutoRefresh() || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0))
{
return;
}
if (_condition[ConditionFlag.InCombat] || _condition[ConditionFlag.Unconscious] || _condition[ConditionFlag.BoundByDuty] || _condition[ConditionFlag.InDeepDungeon] || _condition[ConditionFlag.WatchingCutscene] || _condition[ConditionFlag.WatchingCutscene78] || _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || _gameFunctions.IsOccupied() || _movementController.IsPathfinding || _movementController.IsPathRunning || !_movementController.IsNavmeshReady || _taskQueue.CurrentTaskExecutor?.CurrentTask.GetType().Namespace == typeof(WaitAtEnd).Namespace || DateTime.Now < _safeAnimationEnd)
if (ShouldPreventAutoRefresh())
{
_lastProgressUpdate = DateTime.Now;
return;
}
Vector3 position = _clientState.LocalPlayer.Position;
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
if (localPlayer == null)
{
return;
}
Vector3 position = localPlayer.Position;
if (CurrentQuest == null)
{
return;
}
ElementId id = CurrentQuest.Quest.Id;
byte sequence = CurrentQuest.Sequence;
int step = CurrentQuest.Step;
@ -395,6 +416,79 @@ internal sealed class QuestController : MiniTaskController<QuestController>
}
}
private bool ShouldCheckAutoRefresh()
{
if (_configuration.General.AutoStepRefreshEnabled && AutomationType == EAutomationType.Automatic && IsRunning && CurrentQuest != null && _clientState.IsLoggedIn)
{
return _clientState.LocalPlayer != null;
}
return false;
}
private bool ShouldPreventAutoRefresh()
{
if (HasWaitingTasks())
{
return true;
}
if (HasManualInterventionStep())
{
return true;
}
if (HasSystemConditionsPreventingRefresh())
{
return true;
}
if (HasConfigurationConditionsPreventingRefresh())
{
return true;
}
return false;
}
private bool HasWaitingTasks()
{
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
if (task is WaitAtEnd.WaitObjectAtPosition || task is WaitAtEnd.WaitForCompletionFlags)
{
return true;
}
return false;
}
private bool HasManualInterventionStep()
{
switch (GetNextStep().Step?.InteractionType)
{
case EInteractionType.WaitForManualProgress:
case EInteractionType.Duty:
case EInteractionType.SinglePlayerDuty:
case EInteractionType.Snipe:
case EInteractionType.Instruction:
return true;
default:
return false;
}
}
private bool HasSystemConditionsPreventingRefresh()
{
if (_movementController.IsNavmeshReady && !_condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Unconscious] && !_condition[ConditionFlag.BoundByDuty] && !_condition[ConditionFlag.InDeepDungeon] && !_condition[ConditionFlag.WatchingCutscene] && !_condition[ConditionFlag.WatchingCutscene78] && !_condition[ConditionFlag.BetweenAreas] && !_condition[ConditionFlag.BetweenAreas51] && !_gameFunctions.IsOccupied() && !_movementController.IsPathfinding && !_movementController.IsPathRunning)
{
return DateTime.Now < _safeAnimationEnd;
}
return true;
}
private bool HasConfigurationConditionsPreventingRefresh()
{
if (_configuration.Advanced.PreventQuestCompletion)
{
return CurrentQuest?.Sequence == byte.MaxValue;
}
return false;
}
private void UpdateCurrentQuest()
{
lock (_progressLock)
@ -417,6 +511,20 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_pendingQuest = null;
CheckNextTasks("Pending quest accepted");
}
if (_startedQuest != null && !_questFunctions.IsQuestAccepted(_startedQuest.Quest.Id))
{
if (_startedQuest.Quest.Info.IsRepeatable)
{
_logger.LogInformation("Repeatable quest {QuestId} is no longer accepted, clearing started quest", _startedQuest.Quest.Id);
}
else if (!_questFunctions.IsQuestComplete(_startedQuest.Quest.Id))
{
_logger.LogInformation("Quest {QuestId} was abandoned, clearing started quest", _startedQuest.Quest.Id);
_startedQuest = null;
Stop("Quest abandoned");
return;
}
}
if (_simulatedQuest == null && _nextQuest != null && !((!_nextQuest.Quest.Info.IsRepeatable) ? (!_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id)) : (!_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id))))
{
_logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.Id);
@ -425,21 +533,37 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_startedQuest = _nextQuest;
AutomationType = EAutomationType.SingleQuestB;
}
else if (_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id))
{
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(_nextQuest.Quest.Id);
if (questProgressInfo != null)
{
_startedQuest = new QuestProgress(_nextQuest.Quest, questProgressInfo.Sequence);
_logger.LogInformation("Moving accepted next quest to started quest (sequence: {Sequence})", questProgressInfo.Sequence);
_nextQuest = null;
CheckNextTasks("Next quest already accepted");
return;
}
_logger.LogWarning("Could not get quest progress info for accepted quest {QuestId}", _nextQuest.Quest.Id);
}
_logger.LogDebug("Started: {StartedQuest}", _startedQuest?.Quest.Id);
_nextQuest = null;
}
byte b;
QuestProgress questProgress;
ElementId CurrentQuest;
byte Sequence;
MainScenarioQuestState State;
if (_simulatedQuest != null)
{
b = _simulatedQuest.Sequence;
questProgress = _simulatedQuest;
}
else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
else if (_nextQuest != null)
{
questProgress = _nextQuest;
b = _nextQuest.Sequence;
if (_nextQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic)
if (_questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id) && _nextQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic)
{
ExecuteNextStep();
}
@ -453,12 +577,21 @@ internal sealed class QuestController : MiniTaskController<QuestController>
ExecuteNextStep();
}
}
else
else if (_startedQuest != null)
{
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out ElementId CurrentQuest, out byte Sequence, out MainScenarioQuestState State);
questProgress = _startedQuest;
b = _startedQuest.Sequence;
QuestProgressInfo questProgressInfo2 = _questFunctions.GetQuestProgressInfo(_startedQuest.Quest.Id);
if (questProgressInfo2 != null && questProgressInfo2.Sequence != b)
{
_logger.LogInformation("Updating started quest sequence from {OldSequence} to {NewSequence}", b, questProgressInfo2.Sequence);
b = questProgressInfo2.Sequence;
}
if (AutomationType == EAutomationType.Manual || !IsRunning)
{
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State);
ElementId elementId = CurrentQuest;
b = Sequence;
MainScenarioQuestState mainScenarioQuestState = State;
byte sequence = Sequence;
(ElementId, byte)? tuple = (from x in ManualPriorityQuests
where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id)
select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
@ -467,10 +600,48 @@ internal sealed class QuestController : MiniTaskController<QuestController>
(ElementId, byte) valueOrDefault = tuple.GetValueOrDefault();
if ((object)valueOrDefault.Item1 != null)
{
(elementId, b) = valueOrDefault;
(elementId, sequence) = valueOrDefault;
}
}
if (elementId == null || elementId.Value == 0)
if (elementId != null && elementId.Value != 0 && _startedQuest.Quest.Id != elementId)
{
_logger.LogInformation("Game current quest changed from {OldQuest} to {NewQuest}, updating started quest", _startedQuest.Quest.Id, elementId);
if (_questRegistry.TryGetQuest(elementId, out Quest quest))
{
_logger.LogInformation("Switching to new quest: {QuestName}", quest.Info.Name);
_startedQuest = new QuestProgress(quest, sequence);
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest.Info.Level)
{
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest.Info.Level);
Stop("Quest level too high");
}
questProgress = _startedQuest;
}
else
{
_logger.LogInformation("New quest {QuestId} not found in registry", elementId);
}
}
}
}
else
{
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State);
ElementId elementId2 = CurrentQuest;
b = Sequence;
MainScenarioQuestState mainScenarioQuestState = State;
(ElementId, byte)? tuple3 = (from x in ManualPriorityQuests
where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id)
select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
if (tuple3.HasValue)
{
(ElementId, byte) valueOrDefault2 = tuple3.GetValueOrDefault();
if ((object)valueOrDefault2.Item1 != null)
{
(elementId2, b) = valueOrDefault2;
}
}
if (elementId2 == null || elementId2.Value == 0)
{
if (_startedQuest != null)
{
@ -491,9 +662,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>
}
else
{
if (_startedQuest == null || _startedQuest.Quest.Id != elementId)
if (_startedQuest == null || _startedQuest.Quest.Id != elementId2)
{
Quest quest;
Quest quest2;
if (_configuration.Stop.Enabled && _startedQuest != null && _configuration.Stop.QuestsToStopAfter.Contains(_startedQuest.Quest.Id) && _questFunctions.IsQuestComplete(_startedQuest.Quest.Id))
{
ElementId id = _startedQuest.Quest.Id;
@ -502,13 +673,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_startedQuest = null;
Stop($"Stopping point [{id}] reached");
}
else if (_questRegistry.TryGetQuest(elementId, out quest))
else if (_questRegistry.TryGetQuest(elementId2, out quest2))
{
_logger.LogInformation("New quest: {QuestName}", quest.Info.Name);
_startedQuest = new QuestProgress(quest, b);
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest.Info.Level)
_logger.LogInformation("New quest: {QuestName}", quest2.Info.Name);
_startedQuest = new QuestProgress(quest2, b);
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest2.Info.Level)
{
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest.Info.Level);
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest2.Info.Level);
Stop("Quest level too high");
return;
}
@ -533,7 +704,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
if (questProgress == null)
{
DebugState = "No quest active";
if (!IsRunning)
{
Stop("No quest active");
}
return;
}
if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest))
@ -739,6 +913,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
}
}
public void SetStartedQuest(Quest quest, byte sequence = 0)
{
_logger.LogInformation("Setting started quest: {QuestId}", quest.Id);
_startedQuest = new QuestProgress(quest, sequence);
_nextQuest = null;
}
public void SetGatheringQuest(Quest? quest)
{
_logger.LogInformation("GatheringQuest: {QuestId}", quest?.Id);

View file

@ -3,10 +3,12 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Services;
@ -45,6 +47,8 @@ internal sealed class QuestRegistry
private readonly List<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> _lowPriorityContentFinderConditionQuests = new List<(uint, ElementId, int)>();
private readonly Dictionary<ElementId, string> _questFolderNames = new Dictionary<ElementId, string>();
public IEnumerable<Quest> AllQuests => _quests.Values;
public int Count => _quests.Count<KeyValuePair<ElementId, Quest>>((KeyValuePair<ElementId, Quest> x) => !x.Value.Root.Disabled);
@ -75,6 +79,7 @@ internal sealed class QuestRegistry
_quests.Clear();
_contentFinderConditionIds.Clear();
_lowPriorityContentFinderConditionQuests.Clear();
_questFolderNames.Clear();
LoadQuestsFromAssembly();
try
{
@ -102,19 +107,69 @@ internal sealed class QuestRegistry
private void LoadQuestsFromAssembly()
{
_logger.LogInformation("Loading quests from assembly");
foreach (var (elementId2, root) in AssemblyQuestLoader.GetQuests())
foreach (var (elementId2, questRoot2) in AssemblyQuestLoader.GetQuests())
{
try
{
IQuestInfo questInfo = _questData.GetQuestInfo(elementId2);
bool? flag = null;
DateTime? dateTime = null;
bool flag2 = false;
bool flag3 = false;
try
{
flag = questRoot2.IsSeasonalQuest;
flag2 = flag.HasValue;
if (questRoot2.SeasonalQuestExpiry.HasValue)
{
dateTime = DateTime.SpecifyKind(questRoot2.SeasonalQuestExpiry.Value, DateTimeKind.Utc);
flag3 = true;
}
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to read seasonal fields from embedded QuestRoot for {QuestId}", elementId2);
}
if (_questData.TryGetQuestInfo(elementId2, out IQuestInfo questInfo))
{
goto IL_01c8;
}
if (elementId2 is UnlockLinkId unlockLinkId)
{
string text = unlockLinkId.ToString();
if (text.Length > 1 && text.StartsWith('U'))
{
string text2 = text.Substring(1);
string text3 = ((text2 == "568") ? "Patch 7.3 Fantasia" : ((!(text2 == "506")) ? ("U" + text2) : "Patch 7.2 Fantasia"));
text = text3;
}
else
{
text = $"Unlock Link {unlockLinkId.Value}";
}
questInfo = new UnlockLinkQuestInfo(unlockLinkId, text, 0u, dateTime);
_logger.LogDebug("Created UnlockLinkQuestInfo for {QuestId} from assembly", elementId2);
_questData.AddOrReplaceQuestInfo(questInfo);
goto IL_01c8;
}
_logger.LogWarning("Not loading unknown quest {QuestId} from assembly: Quest not found in quest data", elementId2);
goto end_IL_003d;
IL_01c8:
if (flag2 || flag3)
{
bool flag4 = flag ?? questInfo.IsSeasonalQuest;
_questData.ApplySeasonalOverride(elementId2, flag4, dateTime);
_logger.LogDebug("Applied seasonal override for quest {QuestId} from assembly: IsSeasonal={IsSeasonal}, Expiry={Expiry}", elementId2, flag4, dateTime?.ToString("o") ?? "(null)");
}
IQuestInfo questInfo2 = _questData.GetQuestInfo(elementId2);
Quest quest = new Quest
{
Id = elementId2,
Root = root,
Info = questInfo,
Root = questRoot2,
Info = questInfo2,
Source = Quest.ESource.Assembly
};
_quests[quest.Id] = quest;
end_IL_003d:;
}
catch (Exception ex)
{
@ -191,19 +246,140 @@ internal sealed class QuestRegistry
_questValidator.Validate(_quests.Values.Where((Quest x) => x.Source != Quest.ESource.Assembly).ToList());
}
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source, string directoryName)
{
if (source == Quest.ESource.UserDirectory)
{
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
}
ElementId elementId = ExtractQuestIdFromName(fileName);
if (!(elementId == null))
if (elementId == null)
{
JsonNode jsonNode = JsonNode.Parse(stream);
return;
}
JsonNode jsonNode;
try
{
jsonNode = JsonNode.Parse(stream);
}
catch (JsonException ex)
{
ValidationIssue issue = new ValidationIssue
{
ElementId = elementId,
Sequence = null,
Step = null,
Type = EIssueType.InvalidJsonSyntax,
Severity = EIssueSeverity.Error,
Description = $"JSON parsing error in file '{fileName}': {ex.Message}\n\nThis usually indicates a syntax error such as:\n\ufffd Missing comma between properties\n\ufffd Unclosed quotes or brackets\n\ufffd Invalid escape sequences\n\ufffd Trailing commas where not allowed\n\nPlease check the JSON syntax around the indicated position."
};
_questValidator.AddValidationIssue(issue);
return;
}
_jsonSchemaValidator.Enqueue(elementId, jsonNode);
bool? flag = null;
DateTime? dateTime = null;
bool flag2 = false;
bool flag3 = false;
if (jsonNode is JsonObject jsonObject)
{
if (jsonObject.TryGetPropertyValue("IsSeasonalQuest", out JsonNode jsonNode2) && jsonNode2 != null)
{
try
{
flag = jsonNode2.GetValue<bool>();
flag2 = true;
_logger.LogDebug("Quest {QuestId}: parsed IsSeasonalQuest override = {IsSeasonal}", elementId, flag);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Quest {QuestId}: failed to parse IsSeasonalQuest from JSON", elementId);
}
}
if (jsonObject.TryGetPropertyValue("SeasonalQuestExpiry", out JsonNode jsonNode3) && jsonNode3 != null)
{
try
{
string value = jsonNode3.GetValue<string>();
if (!string.IsNullOrEmpty(value))
{
dateTime = ((!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) ? new DateTime?(DateTime.Parse(value, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal)) : new DateTime?(DateTime.SpecifyKind(result, DateTimeKind.Utc)));
flag3 = true;
_logger.LogDebug("Quest {QuestId}: parsed SeasonalQuestExpiry override = {Expiry}", elementId, dateTime);
}
}
catch (Exception exception2)
{
_logger.LogWarning(exception2, "Quest {QuestId}: failed to parse SeasonalQuestExpiry from JSON", elementId);
}
}
}
QuestRoot root = jsonNode.Deserialize<QuestRoot>();
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
if (!_questData.TryGetQuestInfo(elementId, out IQuestInfo questInfo))
{
if (!(elementId is UnlockLinkId unlockLinkId))
{
_logger.LogWarning("Not loading unknown quest {QuestId} from project file {FileName}", elementId, fileName);
return;
}
string name;
try
{
string text = fileName.Substring(0, fileName.Length - ".json".Length);
int num = text.IndexOf('_', StringComparison.Ordinal);
string text2;
if (num < 0 || num + 1 >= text.Length)
{
text2 = text;
}
else
{
string text3 = text;
int num2 = num + 1;
text2 = text3.Substring(num2, text3.Length - num2);
}
name = text2;
}
catch
{
name = fileName.Substring(0, fileName.Length - ".json".Length);
}
name = NormalizeDerivedName(name);
uint issuerDataId = 0u;
string patch = null;
if (jsonNode is JsonObject jsonObject2)
{
if (jsonObject2.TryGetPropertyValue("DataId", out JsonNode jsonNode4) && jsonNode4 != null)
{
try
{
issuerDataId = jsonNode4.GetValue<uint>();
}
catch
{
issuerDataId = 0u;
}
}
if (jsonObject2.TryGetPropertyValue("Patch", out JsonNode jsonNode5) && jsonNode5 != null)
{
try
{
patch = jsonNode5.GetValue<string>();
}
catch
{
patch = null;
}
}
}
questInfo = new UnlockLinkQuestInfo(unlockLinkId, name, issuerDataId, dateTime, patch);
_logger.LogDebug("Created UnlockLinkQuestInfo for {QuestId} from project file '{FileName}'", elementId, fileName);
_questData.AddOrReplaceQuestInfo(questInfo);
}
if ((flag2 || flag3) && _questData.TryGetQuestInfo(elementId, out IQuestInfo questInfo2))
{
_questData.ApplySeasonalOverride(elementId, flag ?? questInfo2.IsSeasonalQuest, dateTime);
}
Quest quest = new Quest
{
Id = elementId,
@ -212,6 +388,9 @@ internal sealed class QuestRegistry
Source = source
};
_quests[quest.Id] = quest;
if (!string.IsNullOrEmpty(directoryName))
{
_questFolderNames[elementId] = directoryName;
}
}
@ -232,7 +411,7 @@ internal sealed class QuestRegistry
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
LoadQuestFromStream(fileInfo.Name, stream, source);
LoadQuestFromStream(fileInfo.Name, stream, source, directory.Name);
}
catch (Exception innerException)
{
@ -287,4 +466,26 @@ internal sealed class QuestRegistry
dutyOptions = null;
return false;
}
public IEnumerable<ElementId> GetAllQuestIds()
{
return _quests.Keys;
}
public bool TryGetQuestFolderName(ElementId questId, [NotNullWhen(true)] out string? folderName)
{
return _questFolderNames.TryGetValue(questId, out folderName);
}
private static string NormalizeDerivedName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return name ?? string.Empty;
}
name = name.Replace("_", " ", StringComparison.OrdinalIgnoreCase);
name = Regex.Replace(name, "\\s+", " ");
name = Regex.Replace(name, "\\b(Patch)\\s+(\\d+)\\s+(\\d+)\\b", "$1 $2.$3", RegexOptions.IgnoreCase);
return name;
}
}

View file

@ -192,28 +192,6 @@ internal sealed class AlliedSocietyData
case 5287:
case 5288:
return EAlliedSociety.MamoolJa;
case 5343:
case 5344:
case 5345:
case 5346:
case 5347:
case 5348:
case 5349:
case 5350:
case 5351:
case 5352:
case 5353:
case 5354:
case 5355:
case 5356:
case 5357:
case 5358:
case 5359:
case 5360:
case 5361:
case 5362:
case 5363:
return EAlliedSociety.YokHuy;
default:
return EAlliedSociety.None;
}

View file

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
@ -19,12 +21,15 @@ internal sealed class JournalData
public List<IQuestInfo> Quests { get; }
public bool IsUnderOtherQuests { get; set; }
public Genre(JournalGenre journalGenre, List<IQuestInfo> quests)
{
Id = journalGenre.RowId;
Name = journalGenre.Name.ToString();
CategoryId = journalGenre.JournalCategory.RowId;
Quests = quests;
IsUnderOtherQuests = false;
}
public Genre(uint id, string name, uint categoryId, List<IQuestInfo> quests)
@ -33,6 +38,16 @@ internal sealed class JournalData
Name = name;
CategoryId = categoryId;
Quests = quests;
IsUnderOtherQuests = false;
}
public Genre(uint id, string name, uint categoryId, List<IQuestInfo> quests, bool isUnderOtherQuests = false)
{
Id = id;
Name = name;
CategoryId = categoryId;
Quests = quests;
IsUnderOtherQuests = isUnderOtherQuests;
}
}
@ -68,15 +83,20 @@ internal sealed class JournalData
}
}
private readonly ILogger<JournalData> _logger;
public List<Genre> Genres { get; }
public List<Category> Categories { get; }
public List<Section> Sections { get; }
public JournalData(IDataManager dataManager, QuestData questData)
public int? OtherQuestsSectionRowId { get; private set; }
public JournalData(IDataManager dataManager, QuestData questData, ILogger<JournalData> logger)
{
JournalData journalData = this;
_logger = logger;
List<Genre> list = (from x in dataManager.GetExcelSheet<JournalGenre>()
where x.RowId != 0 && x.Icon > 0
select new Genre(x, questData.GetAllByJournalGenre(x.RowId))).ToList();
@ -92,7 +112,7 @@ internal sealed class JournalData
Genre genreUldah = new Genre(4294967294u, "Starting in Ul'dah", 1u, (from x in new uint[3] { 568u, 569u, 570u }.Concat(row3.QuestRedoParam.Select((QuestRedo.QuestRedoParamStruct x) => x.Quest.RowId))
where x != 0
select questData.GetQuestInfo(QuestId.FromRowId(x))).ToList());
list.InsertRange(0, new global::_003C_003Ez__ReadOnlyArray<Genre>(new Genre[3] { genreLimsa, genreGridania, genreUldah }));
list.InsertRange(0, new Genre[3] { genreLimsa, genreGridania, genreUldah });
list.Single((Genre x) => x.Id == 1).Quests.RemoveAll((IQuestInfo x) => genreLimsa.Quests.Contains(x) || genreGridania.Quests.Contains(x) || genreUldah.Quests.Contains(x));
Genres = list.ToList();
Categories = (from x in dataManager.GetExcelSheet<JournalCategory>()
@ -100,5 +120,51 @@ internal sealed class JournalData
select new Category(x, journalData.Genres.Where((Genre y) => y.CategoryId == x.RowId).ToList())).ToList();
Sections = (from x in dataManager.GetExcelSheet<JournalSection>()
select new Section(x, journalData.Categories.Where((Category y) => y.SectionId == x.RowId).ToList())).ToList();
_logger.LogDebug("Resolving OtherQuests section id...");
OtherQuestsSectionRowId = GetOtherQuestsSectionRowId(dataManager);
_logger.LogDebug("Resolved OtherQuestsSectionRowId = {Id}", OtherQuestsSectionRowId);
int? otherQuestsSectionRowId = OtherQuestsSectionRowId;
if (otherQuestsSectionRowId.HasValue)
{
int valueOrDefault = otherQuestsSectionRowId.GetValueOrDefault();
uint otherIdU = (uint)valueOrDefault;
Section section = Sections.FirstOrDefault((Section s) => s.Id == otherIdU);
if (section != null)
{
int num = 0;
foreach (Category category in section.Categories)
{
foreach (Genre genre in category.Genres)
{
genre.IsUnderOtherQuests = true;
num++;
}
}
_logger.LogInformation("Marked {Count} genres as under 'Other Quests' (section id {Id})", num, valueOrDefault);
}
else
{
_logger.LogWarning("OtherQuestsSectionRowId {Id} found but matching Section not present in constructed Sections", valueOrDefault);
}
}
else
{
_logger.LogDebug("OtherQuestsSectionRowId not found - falling back to localized name lookup when necessary");
}
}
private int? GetOtherQuestsSectionRowId(IDataManager dataManager)
{
JournalSection journalSection = dataManager.GetExcelSheet<JournalSection>().FirstOrDefault((JournalSection s) => s.Name.ToString() == "Other Quests");
int? result = ((journalSection.RowId != 0) ? new int?((int)journalSection.RowId) : ((int?)null));
if (!result.HasValue)
{
Section section = Sections.FirstOrDefault((Section s) => s.Name.Equals("Other Quests", StringComparison.OrdinalIgnoreCase));
if (section != null)
{
result = (int)section.Id;
}
}
return result;
}
}

View file

@ -8,8 +8,10 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib.GameData;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Windows.QuestComponents;
namespace Questionable.Data;
@ -52,6 +54,8 @@ internal sealed class QuestData
private readonly Dictionary<ElementId, IQuestInfo> _quests;
private readonly ILogger<QuestData> _logger;
public static ImmutableHashSet<QuestId> AetherCurrentQuests { get; }
public IReadOnlyList<QuestInfo> MainScenarioQuests { get; }
@ -60,9 +64,10 @@ internal sealed class QuestData
public QuestId LastMainScenarioQuestId { get; }
public QuestData(IDataManager dataManager, ClassJobUtils classJobUtils)
public QuestData(IDataManager dataManager, ClassJobUtils classJobUtils, ILogger<QuestData> logger)
{
QuestData questData = this;
_logger = logger ?? throw new ArgumentNullException("logger");
JournalGenreOverrides journalGenreOverrides = new JournalGenreOverrides
{
ARelicRebornQuests = dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>().GetRow(65742u).JournalGenre.RowId,
@ -93,18 +98,59 @@ internal sealed class QuestData
where x.RowId != 0 && !x.Name.IsEmpty
select x).SelectMany(delegate(BeastTribe x)
{
if (!Enum.IsDefined(typeof(EAlliedSociety), (byte)x.RowId))
{
questData._logger.LogWarning("Skipping unknown BeastTribe with RowId {RowId} (Name: {Name})", x.RowId, x.Name.ToString());
return Enumerable.Empty<AlliedSocietyDailyInfo>();
}
if (x.RowId < 5)
{
List<byte> list2 = new List<byte>();
list2.Add(0);
list2.AddRange((from QuestInfo y in quests.Where((IQuestInfo y) => (uint)y.AlliedSociety == (byte)x.RowId && y.IsRepeatable)
List<byte> list3 = new List<byte>();
list3.Add(0);
list3.AddRange((from QuestInfo y in quests.Where((IQuestInfo y) => (uint)y.AlliedSociety == (byte)x.RowId && y.IsRepeatable)
select (byte)y.AlliedSocietyRank).Distinct());
return new _003C_003Ez__ReadOnlyList<byte>(list2).Select((byte rank) => new AlliedSocietyDailyInfo(x, rank, classJobUtils));
return new _003C_003Ez__ReadOnlyList<byte>(list3).Select((byte rank) => new AlliedSocietyDailyInfo(x, rank, classJobUtils));
}
return new global::_003C_003Ez__ReadOnlySingleElementList<AlliedSocietyDailyInfo>(new AlliedSocietyDailyInfo(x, 0, classJobUtils));
}));
quests.Add(new UnlockLinkQuestInfo(new UnlockLinkId(506), "Patch 7.2 Fantasia", 1052475u));
quests.Add(new UnlockLinkQuestInfo(new UnlockLinkId(568), "Patch 7.3 Fantasia", 1052475u));
int num = 15;
List<AethernetQuestInfo> list2 = new List<AethernetQuestInfo>(num);
CollectionsMarshal.SetCount(list2, num);
Span<AethernetQuestInfo> span = CollectionsMarshal.AsSpan(list2);
int num2 = 0;
span[num2] = new AethernetQuestInfo(new AethernetId(1), "Limsa Lominsa");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(2), "Gridania");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(3), "Ul'dah");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(4), "The Gold Saucer");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(5), "Ishgard");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(6), "Idyllshire");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(7), "Rhalgr's Reach");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(8), "Kugane");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(9), "Doman Enclave");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(10), "The Crystarium");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(11), "Eulmore");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(12), "Old Sharlayan");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(13), "Radz-at-Han");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(14), "Tuliyollal");
num2++;
span[num2] = new AethernetQuestInfo(new AethernetId(15), "Solution Nine");
List<AethernetQuestInfo> collection = list2;
List<AetherCurrentQuestInfo> collection2 = new List<AetherCurrentQuestInfo>();
quests.AddRange(collection);
quests.AddRange(collection2);
_quests = quests.ToDictionary((IQuestInfo x) => x.QuestId, (IQuestInfo x) => x);
AddPreviousQuest(new QuestId(425), new QuestId(495));
AddPreviousQuest(new QuestId(1480), new QuestId(2373));
@ -265,7 +311,6 @@ internal sealed class QuestData
public List<IQuestInfo> GetAllByJournalGenre(uint journalGenre)
{
return (from x in _quests.Values
where !(x is QuestInfo { IsSeasonalEvent: not false })
where x.JournalGenre == journalGenre
orderby x.SortKey, x.QuestId
select x).ToList();
@ -1158,6 +1203,32 @@ internal sealed class QuestData
select new QuestId(x)).ToList();
}
public void ApplySeasonalOverride(ElementId questId, bool isSeasonal, DateTime? expiry)
{
if (_quests.TryGetValue(questId, out IQuestInfo value) && value is QuestInfo questInfo)
{
DateTime? seasonalQuestExpiry = null;
if (expiry.HasValue)
{
DateTime value2 = expiry.Value;
seasonalQuestExpiry = ((!(value2.TimeOfDay == TimeSpan.Zero)) ? new DateTime?((value2.Kind == DateTimeKind.Utc) ? value2 : value2.ToUniversalTime()) : new DateTime?(EventInfoComponent.AtDailyReset(DateOnly.FromDateTime(value2))));
}
questInfo.IsSeasonalQuest = isSeasonal;
questInfo.SeasonalQuestExpiry = seasonalQuestExpiry;
}
else
{
_logger.LogWarning("ApplySeasonalOverride: Quest {QuestId} not found in QuestData (could not apply seasonal override)", questId);
}
}
public void AddOrReplaceQuestInfo(IQuestInfo info)
{
ArgumentNullException.ThrowIfNull(info, "info");
_quests[info.QuestId] = info;
_logger.LogDebug("Added or replaced QuestInfo for {QuestId} in QuestData", info.QuestId);
}
static QuestData()
{
Dictionary<uint, List<ushort>> dictionary = new Dictionary<uint, List<ushort>>();

View file

@ -13,7 +13,7 @@ namespace Questionable.External;
internal sealed class PandorasBoxIpc : IDisposable
{
private static readonly ImmutableHashSet<string> ConflictingFeatures = new HashSet<string> { "Auto-Meditation", "Auto-Motif (Out of Combat)", "Auto-Mount after Combat", "Auto-Mount after Gathering", "Auto-Peleton", "Auto-Spring in Sanctuaries", "Auto-select Turn-ins", "Auto-Sync FATEs", "Auto-interact with Gathering Nodes", "Pandora Quick Gather" }.ToImmutableHashSet();
private static readonly ImmutableHashSet<string> ConflictingFeatures = new HashSet<string> { "Auto-Meditation", "Auto-Motif (Out of Combat)", "Auto-Mount after Combat", "Auto-Mount after Gathering", "Auto-Peleton", "Auto-Sprint in Sanctuaries", "Auto-interact with Gathering Nodes", "Auto-select Turn-ins", "Auto-Sync FATEs", "Pandora Quick Gather" }.ToImmutableHashSet();
private readonly IFramework _framework;

View file

@ -44,6 +44,14 @@ internal sealed class QuestionableIpc : IDisposable
private const string IpcIsQuestLocked = "Questionable.IsQuestLocked";
private const string IpcIsQuestCompleted = "Questionable.IsQuestCompleted";
private const string IpcIsQuestAvailable = "Questionable.IsQuestAvailable";
private const string IpcIsQuestAccepted = "Questionable.IsQuestAccepted";
private const string IpcIsQuestUnobtainable = "Questionable.IsQuestUnobtainable";
private const string IpcImportQuestPriority = "Questionable.ImportQuestPriority";
private const string IpcClearQuestPriority = "Questionable.ClearQuestPriority";
@ -60,6 +68,8 @@ internal sealed class QuestionableIpc : IDisposable
private readonly QuestFunctions _questFunctions;
private readonly ManualPriorityComponent _manualPriorityComponent;
private readonly ICallGateProvider<bool> _isRunning;
private readonly ICallGateProvider<string?> _getCurrentQuestId;
@ -74,6 +84,14 @@ internal sealed class QuestionableIpc : IDisposable
private readonly ICallGateProvider<string, bool> _isQuestLocked;
private readonly ICallGateProvider<string, bool> _isQuestCompleted;
private readonly ICallGateProvider<string, bool> _isQuestAvailable;
private readonly ICallGateProvider<string, bool> _isQuestAccepted;
private readonly ICallGateProvider<string, bool> _isQuestUnobtainable;
private readonly ICallGateProvider<string, bool> _importQuestPriority;
private readonly ICallGateProvider<string, bool> _addQuestPriority;
@ -84,12 +102,13 @@ internal sealed class QuestionableIpc : IDisposable
private readonly ICallGateProvider<string> _exportQuestPriority;
public QuestionableIpc(QuestController questController, EventInfoComponent eventInfoComponent, QuestRegistry questRegistry, QuestFunctions questFunctions, PriorityWindow priorityWindow, IDalamudPluginInterface pluginInterface)
public QuestionableIpc(QuestController questController, EventInfoComponent eventInfoComponent, QuestRegistry questRegistry, QuestFunctions questFunctions, ManualPriorityComponent manualPriorityComponent, IDalamudPluginInterface pluginInterface)
{
QuestionableIpc questionableIpc = this;
_questController = questController;
_questRegistry = questRegistry;
_questFunctions = questFunctions;
_manualPriorityComponent = manualPriorityComponent;
_isRunning = pluginInterface.GetIpcProvider<bool>("Questionable.IsRunning");
_isRunning.RegisterFunc(() => questController.AutomationType != QuestController.EAutomationType.Manual || questController.IsRunning);
_getCurrentQuestId = pluginInterface.GetIpcProvider<string>("Questionable.GetCurrentQuestId");
@ -105,6 +124,14 @@ internal sealed class QuestionableIpc : IDisposable
_startSingleQuest.RegisterFunc((string questId) => questionableIpc.StartQuest(questId, single: true));
_isQuestLocked = pluginInterface.GetIpcProvider<string, bool>("Questionable.IsQuestLocked");
_isQuestLocked.RegisterFunc(IsQuestLocked);
_isQuestCompleted = pluginInterface.GetIpcProvider<string, bool>("Questionable.IsQuestCompleted");
_isQuestCompleted.RegisterFunc(IsQuestCompleted);
_isQuestAvailable = pluginInterface.GetIpcProvider<string, bool>("Questionable.IsQuestAvailable");
_isQuestAvailable.RegisterFunc(IsQuestAvailable);
_isQuestAccepted = pluginInterface.GetIpcProvider<string, bool>("Questionable.IsQuestAccepted");
_isQuestAccepted.RegisterFunc(IsQuestAccepted);
_isQuestUnobtainable = pluginInterface.GetIpcProvider<string, bool>("Questionable.IsQuestUnobtainable");
_isQuestUnobtainable.RegisterFunc(IsQuestUnobtainable);
_importQuestPriority = pluginInterface.GetIpcProvider<string, bool>("Questionable.ImportQuestPriority");
_importQuestPriority.RegisterFunc(ImportQuestPriority);
_addQuestPriority = pluginInterface.GetIpcProvider<string, bool>("Questionable.AddQuestPriority");
@ -114,7 +141,7 @@ internal sealed class QuestionableIpc : IDisposable
_insertQuestPriority = pluginInterface.GetIpcProvider<int, string, bool>("Questionable.InsertQuestPriority");
_insertQuestPriority.RegisterFunc(InsertQuestPriority);
_exportQuestPriority = pluginInterface.GetIpcProvider<string>("Questionable.ExportQuestPriority");
_exportQuestPriority.RegisterFunc(priorityWindow.EncodeQuestPriority);
_exportQuestPriority.RegisterFunc(_manualPriorityComponent.EncodeQuestPriority);
}
private bool StartQuest(string questId, bool single)
@ -172,6 +199,42 @@ internal sealed class QuestionableIpc : IDisposable
return true;
}
private bool IsQuestCompleted(string questId)
{
if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null)
{
return _questFunctions.IsQuestComplete(elementId);
}
return false;
}
private bool IsQuestAvailable(string questId)
{
if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null)
{
return _questFunctions.IsReadyToAcceptQuest(elementId);
}
return false;
}
private bool IsQuestAccepted(string questId)
{
if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null)
{
return _questFunctions.IsQuestAccepted(elementId);
}
return false;
}
private bool IsQuestUnobtainable(string questId)
{
if (ElementId.TryFromString(questId, out ElementId elementId) && elementId != null)
{
return _questFunctions.IsQuestUnobtainable(elementId);
}
return false;
}
private bool ImportQuestPriority(string encodedQuestPriority)
{
List<ElementId> questElements = PriorityWindow.DecodeQuestPriority(encodedQuestPriority);
@ -210,6 +273,10 @@ internal sealed class QuestionableIpc : IDisposable
_clearQuestPriority.UnregisterFunc();
_addQuestPriority.UnregisterFunc();
_importQuestPriority.UnregisterFunc();
_isQuestUnobtainable.UnregisterFunc();
_isQuestAccepted.UnregisterFunc();
_isQuestAvailable.UnregisterFunc();
_isQuestCompleted.UnregisterFunc();
_isQuestLocked.UnregisterFunc();
_startSingleQuest.UnregisterFunc();
_startQuest.UnregisterFunc();

View file

@ -14,8 +14,12 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Fate;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
@ -540,4 +544,73 @@ internal sealed class GameFunctions
_logger.LogInformation("Unlocked unlock links: {UnlockedUnlockLinks}", string.Join(", ", list));
return list;
}
public unsafe bool SyncToFate(uint fateId)
{
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
if (localPlayer == null)
{
_logger.LogWarning("Cannot sync to FATE: LocalPlayer is null");
return false;
}
FateManager* ptr = FateManager.Instance();
if (ptr == null || ptr->CurrentFate == null)
{
return false;
}
FateContext* currentFate = ptr->CurrentFate;
byte maxLevel = currentFate->MaxLevel;
if (localPlayer.Level <= maxLevel)
{
return true;
}
try
{
_logger.LogInformation("Syncing to FATE {FateId} (max level {MaxLevel})", currentFate->FateId, maxLevel);
ExecuteCommand("/lsync");
return true;
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to sync to FATE {FateId}", fateId);
return false;
}
}
public unsafe ushort GetCurrentFateId()
{
FateManager* ptr = FateManager.Instance();
if (ptr == null || ptr->CurrentFate == null)
{
return 0;
}
return ptr->CurrentFate->FateId;
}
private unsafe void ExecuteCommand(string command)
{
try
{
UIModule* uIModule = Framework.Instance()->GetUIModule();
if (uIModule == null)
{
_logger.LogError("UIModule is null, cannot execute command: {Command}", command);
return;
}
Utf8String utf8String = new Utf8String(command);
try
{
uIModule->ProcessChatBoxEntry(&utf8String, (nint)utf8String.StringLength);
_logger.LogDebug("Executed chat command: {Command}", command);
}
finally
{
((IDisposable)utf8String/*cast due to .constrained prefix*/).Dispose();
}
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to execute command: {Command}", command);
}
}
}

View file

@ -16,11 +16,13 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameData;
using LLib.GameUI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
using Questionable.Windows.QuestComponents;
namespace Questionable.Functions;
@ -36,6 +38,8 @@ internal sealed class QuestFunctions
private readonly AlliedSocietyData _alliedSocietyData;
private readonly AetheryteData _aetheryteData;
private readonly Configuration _configuration;
private readonly IDataManager _dataManager;
@ -46,18 +50,24 @@ internal sealed class QuestFunctions
private readonly IAetheryteList _aetheryteList;
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui, IAetheryteList aetheryteList)
private readonly ILogger<QuestFunctions> _logger;
private readonly HashSet<ushort> _alreadyLoggedUnobtainableQuestsDetailed = new HashSet<ushort>();
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, AetheryteData aetheryteData, Configuration configuration, IDataManager dataManager, IClientState clientState, IGameGui gameGui, IAetheryteList aetheryteList, ILogger<QuestFunctions> logger)
{
_questRegistry = questRegistry;
_questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_alliedSocietyQuestFunctions = alliedSocietyQuestFunctions;
_alliedSocietyData = alliedSocietyData;
_aetheryteData = aetheryteData;
_configuration = configuration;
_dataManager = dataManager;
_clientState = clientState;
_gameGui = gameGui;
_aetheryteList = aetheryteList;
_logger = logger;
}
public unsafe QuestReference GetCurrentQuest(bool allowNewMsq = true)
@ -558,10 +568,20 @@ internal sealed class QuestFunctions
if (!ignoreLevel)
{
byte b = _clientState.LocalPlayer?.Level ?? 0;
if (b != 0 && quest != null && quest.Info.Level > b)
if (b == 0)
{
return false;
}
if (quest != null && quest.Info.Level > b)
{
_logger.LogDebug("Quest {QuestId} level requirement not met: required {RequiredLevel}, current {CurrentLevel}", questId, quest.Info.Level, b);
return false;
}
if (quest == null && questId is QuestId questId3 && _questData.TryGetQuestInfo(questId3, out IQuestInfo questInfo) && questInfo is QuestInfo questInfo2 && questInfo2.Level > b)
{
_logger.LogDebug("Quest {QuestId} (from data) level requirement not met: required {RequiredLevel}, current {CurrentLevel}", questId3, questInfo2.Level, b);
return false;
}
}
return true;
}
@ -593,6 +613,14 @@ internal sealed class QuestFunctions
{
return false;
}
if (elementId is AethernetId)
{
return false;
}
if (elementId is AetherCurrentId)
{
return false;
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -619,6 +647,14 @@ internal sealed class QuestFunctions
{
return IsQuestComplete(unlockLinkId);
}
if (elementId is AethernetId aethernetId)
{
return IsQuestComplete(aethernetId);
}
if (elementId is AetherCurrentId aetherCurrentId)
{
return IsQuestComplete(aetherCurrentId);
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -632,6 +668,29 @@ internal sealed class QuestFunctions
return UIState.Instance()->IsUnlockLinkUnlocked(unlockLinkId.Value);
}
public bool IsQuestComplete(AethernetId aethernetId)
{
if (!_questRegistry.TryGetQuest(aethernetId, out Questionable.Model.Quest quest))
{
_logger.LogWarning("Aethernet quest {AethernetId} not found in registry", aethernetId);
return false;
}
List<(QuestSequence, int, QuestStep)> list = (from x in quest.AllSteps()
where x.Step.InteractionType == EInteractionType.AttuneAethernetShard && x.Step.AethernetShard.HasValue
select x).ToList();
if (list.Count == 0)
{
_logger.LogWarning("Aethernet quest {AethernetId} has no aethernet shard attunement steps", aethernetId);
return false;
}
return list.All<(QuestSequence, int, QuestStep)>(((QuestSequence Sequence, int StepId, QuestStep Step) step) => _aetheryteFunctions.IsAetheryteUnlocked(step.Step.AethernetShard.Value));
}
public bool IsQuestComplete(AetherCurrentId aetherCurrentId)
{
return false;
}
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
{
if (elementId is QuestId questId)
@ -650,6 +709,14 @@ internal sealed class QuestFunctions
{
return IsQuestLocked(unlockLinkId);
}
if (elementId is AethernetId aethernetId)
{
return IsQuestLocked(aethernetId);
}
if (elementId is AetherCurrentId aetherCurrentId)
{
return IsQuestLocked(aetherCurrentId);
}
throw new ArgumentOutOfRangeException("elementId");
}
@ -708,6 +775,39 @@ internal sealed class QuestFunctions
return IsQuestUnobtainable(unlockLinkId);
}
private bool IsQuestLocked(AethernetId aethernetId)
{
EAetheryteLocation aetheryteLocation = aethernetId.Value switch
{
1 => EAetheryteLocation.Limsa,
2 => EAetheryteLocation.Gridania,
3 => EAetheryteLocation.Uldah,
4 => EAetheryteLocation.GoldSaucer,
5 => EAetheryteLocation.Ishgard,
6 => EAetheryteLocation.Idyllshire,
7 => EAetheryteLocation.RhalgrsReach,
8 => EAetheryteLocation.Kugane,
9 => EAetheryteLocation.DomanEnclave,
10 => EAetheryteLocation.Crystarium,
11 => EAetheryteLocation.Eulmore,
12 => EAetheryteLocation.OldSharlayan,
13 => EAetheryteLocation.RadzAtHan,
14 => EAetheryteLocation.Tuliyollal,
15 => EAetheryteLocation.SolutionNine,
_ => throw new ArgumentOutOfRangeException("aethernetId", $"Unknown AethernetId: {aethernetId.Value}"),
};
if (!_aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{
return true;
}
return false;
}
private static bool IsQuestLocked(AetherCurrentId aetherCurrentId)
{
return false;
}
public bool IsDailyAlliedSocietyQuest(QuestId questId)
{
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
@ -743,19 +843,110 @@ internal sealed class QuestFunctions
public unsafe bool IsQuestUnobtainable(QuestId questId, ElementId? extraCompletedQuest = null)
{
QuestInfo questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
if ((int)questInfo.Expansion > (int)PlayerState.Instance()->MaxExpansion)
IQuestInfo questInfo = _questData.GetQuestInfo(questId);
if (questInfo is UnlockLinkQuestInfo { QuestExpiry: { TimeOfDay: var timeOfDay } questExpiry })
{
TimeSpan timeSpan = new TimeSpan(23, 59, 59);
bool flag = false;
DateTime dateTime;
if (timeOfDay == TimeSpan.Zero || timeOfDay == timeSpan)
{
dateTime = EventInfoComponent.AtDailyReset(DateOnly.FromDateTime(questExpiry));
flag = true;
}
else
{
dateTime = ((questExpiry.Kind == DateTimeKind.Utc) ? questExpiry : questExpiry.ToUniversalTime());
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("UnlockLink quest {QuestId} expiry raw={ExpiryRaw} Kind={Kind} TimeOfDay={TimeOfDay}", questId, questExpiry.ToString("o"), questExpiry.Kind, questExpiry.TimeOfDay);
_logger.LogDebug("UnlockLink quest {QuestId} normalized expiryUtc={ExpiryUtc:o} treatedAsDailyReset={TreatedAsDailyReset}", questId, dateTime, flag);
}
if (DateTime.UtcNow > dateTime)
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("UnlockLink quest {QuestId} unobtainable: expiry {ExpiryUtc} (UTC) is before now {NowUtc}", questId, dateTime.ToString("o"), DateTime.UtcNow.ToString("o"));
}
return true;
}
}
QuestInfo questInfo2 = (QuestInfo)questInfo;
if ((int)questInfo2.Expansion > (int)PlayerState.Instance()->MaxExpansion)
{
return true;
}
if (questInfo.QuestLocks.Count > 0)
if (questInfo2.JournalGenre >= 234 && questInfo2.JournalGenre <= 247)
{
int num = questInfo.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo.QuestLockJoin == EQuestJoin.All && questInfo.QuestLocks.Count == num)
if (_questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
{
List<QuestSequence> list = quest?.Root?.QuestSequence;
if (list != null && list.Count > 0)
{
goto IL_0288;
}
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_questData.ApplySeasonalOverride(questId, isSeasonal: true, null);
_logger.LogDebug("Quest {QuestId} unobtainable: journal genre is 'event (seasonal)' and no quest path", questId);
}
return true;
}
goto IL_0288;
IL_0288:
if (questInfo2.QuestLocks.Count > 0)
{
int num = questInfo2.QuestLocks.Count((QuestId x) => IsQuestComplete(x) || x.Equals(extraCompletedQuest));
if (questInfo2.QuestLockJoin == EQuestJoin.All && questInfo2.QuestLocks.Count == num)
{
return true;
}
if (questInfo.QuestLockJoin == EQuestJoin.AtLeastOne && num > 0)
if (questInfo2.QuestLockJoin == EQuestJoin.AtLeastOne && num > 0)
{
return true;
}
}
DateTime? seasonalQuestExpiry = questInfo2.SeasonalQuestExpiry;
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault = seasonalQuestExpiry.GetValueOrDefault();
TimeSpan timeOfDay2 = valueOrDefault.TimeOfDay;
TimeSpan timeSpan2 = new TimeSpan(23, 59, 59);
bool flag2 = false;
DateTime dateTime2;
if (timeOfDay2 == TimeSpan.Zero || timeOfDay2 == timeSpan2)
{
dateTime2 = EventInfoComponent.AtDailyReset(DateOnly.FromDateTime(valueOrDefault));
flag2 = true;
}
else
{
dateTime2 = ((valueOrDefault.Kind == DateTimeKind.Utc) ? valueOrDefault : valueOrDefault.ToUniversalTime());
}
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} seasonal expiry raw={ExpiryRaw} Kind={Kind} TimeOfDay={TimeOfDay}", questId, valueOrDefault.ToString("o"), valueOrDefault.Kind, valueOrDefault.TimeOfDay);
_logger.LogDebug("Quest {QuestId} normalized expiryUtc={ExpiryUtc:o} treatedAsDailyReset={TreatedAsDailyReset}", questId, dateTime2, flag2);
_logger.LogTrace("Quest {QuestId} expiry check: nowUtc={Now:o}, expiryUtc={Expiry:o}, expired={Expired}", questId, DateTime.UtcNow, dateTime2, DateTime.UtcNow > dateTime2);
}
if (DateTime.UtcNow > dateTime2)
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} unobtainable: seasonal expiry {ExpiryUtc} (UTC) is before now {NowUtc}", questId, dateTime2.ToString("o"), DateTime.UtcNow.ToString("o"));
}
return true;
}
}
if ((questInfo2.IsSeasonalEvent || questInfo2.IsSeasonalQuest) && !(questInfo2.SeasonalQuestExpiry is DateTime))
{
if (_alreadyLoggedUnobtainableQuestsDetailed.Add(questId.Value))
{
_logger.LogDebug("Quest {QuestId} is seasonal/event with no expiry; ShowIncompleteSeasonalEvents={ShowIncomplete}", questId, _configuration.General.ShowIncompleteSeasonalEvents);
}
if (!_configuration.General.ShowIncompleteSeasonalEvents)
{
return true;
}
@ -765,7 +956,7 @@ internal sealed class QuestFunctions
return true;
}
byte startTown = PlayerState.Instance()->StartTown;
if (questInfo.StartingCity > 0 && questInfo.StartingCity != startTown)
if (questInfo2.StartingCity > 0 && questInfo2.StartingCity != startTown)
{
return true;
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using LLib.GameData;
using Questionable.Model.Questing;
namespace Questionable.Model;
internal sealed class AetherCurrentQuestInfo : IQuestInfo
{
public ElementId QuestId { get; }
public string Name { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable => false;
public ImmutableList<PreviousQuestInfo> PreviousQuests => ImmutableList.Create(default(ReadOnlySpan<PreviousQuestInfo>));
public EQuestJoin PreviousQuestJoin => EQuestJoin.All;
public ushort Level => 1;
public EAlliedSociety AlliedSociety => EAlliedSociety.None;
public uint? JournalGenre => null;
public ushort SortKey => 0;
public bool IsMainScenarioQuest => false;
public IReadOnlyList<EClassJob> ClassJobs => Array.Empty<EClassJob>();
public EExpansionVersion Expansion => EExpansionVersion.ARealmReborn;
public AetherCurrentQuestInfo(AetherCurrentId aetherCurrentId, string name, uint issuerDataId = 0u)
{
QuestId = aetherCurrentId;
Name = name;
IssuerDataId = issuerDataId;
}
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using LLib.GameData;
using Questionable.Model.Questing;
namespace Questionable.Model;
internal sealed class AethernetQuestInfo : IQuestInfo
{
public ElementId QuestId { get; }
public string Name { get; }
public uint IssuerDataId { get; }
public bool IsRepeatable => false;
public ImmutableList<PreviousQuestInfo> PreviousQuests => ImmutableList.Create(default(ReadOnlySpan<PreviousQuestInfo>));
public EQuestJoin PreviousQuestJoin => EQuestJoin.All;
public ushort Level => 1;
public EAlliedSociety AlliedSociety => EAlliedSociety.None;
public uint? JournalGenre => null;
public ushort SortKey => 0;
public bool IsMainScenarioQuest => false;
public IReadOnlyList<EClassJob> ClassJobs => Array.Empty<EClassJob>();
public EExpansionVersion Expansion => EExpansionVersion.ARealmReborn;
public AethernetQuestInfo(AethernetId aethernetId, string name, uint issuerDataId = 0u)
{
QuestId = aethernetId;
Name = name;
IssuerDataId = issuerDataId;
}
}

View file

@ -41,6 +41,9 @@ internal sealed class AlliedSocietyDailyInfo : IQuestInfo
{
QuestId = new AlliedSocietyDailyId((byte)beastTribe.RowId, rank);
Name = beastTribe.Name.ToString();
IReadOnlyList<EClassJob> readOnlyList = null;
try
{
List<EClassJob> list2;
switch ((EAlliedSociety)(byte)beastTribe.RowId)
{
@ -85,7 +88,16 @@ internal sealed class AlliedSocietyDailyInfo : IQuestInfo
default:
throw new ArgumentOutOfRangeException("beastTribe");
}
ClassJobs = list2;
readOnlyList = list2;
}
catch (ArgumentOutOfRangeException)
{
List<EClassJob> list4 = new List<EClassJob>();
list4.AddRange(classJobUtils.AsIndividualJobs(EExtendedClassJob.DoW, null));
list4.AddRange(classJobUtils.AsIndividualJobs(EExtendedClassJob.DoM, null));
readOnlyList = new _003C_003Ez__ReadOnlyList<EClassJob>(list4);
}
ClassJobs = readOnlyList;
Expansion = (EExpansionVersion)beastTribe.Expansion.RowId;
}
}

View file

@ -17,6 +17,10 @@ internal interface IQuestInfo
bool IsRepeatable { get; }
bool IsSeasonalQuest => false;
DateTime? SeasonalQuestExpiry => null;
ImmutableList<PreviousQuestInfo> PreviousQuests { get; }
EQuestJoin PreviousQuestJoin { get; }

View file

@ -66,7 +66,11 @@ internal sealed class QuestInfo : IQuestInfo
public EExpansionVersion Expansion { get; }
public QuestInfo(Lumina.Excel.Sheets.Quest quest, uint newGamePlusChapter, byte startingCity, JournalGenreOverrides journalGenreOverrides)
public DateTime? SeasonalQuestExpiry { get; internal set; }
public bool IsSeasonalQuest { get; internal set; }
public QuestInfo(Lumina.Excel.Sheets.Quest quest, uint newGamePlusChapter, byte startingCity, JournalGenreOverrides journalGenreOverrides, bool isSeasonalEventQuest = false, DateTime? seasonalQuestExpiry = null)
{
QuestId = Questionable.Model.Questing.QuestId.FromRowId(quest.RowId);
string value = QuestId.Value switch
@ -157,6 +161,9 @@ internal sealed class QuestInfo : IQuestInfo
AlliedSocietyRank = (int)quest.BeastReputationRank.RowId;
ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.ValueNullable);
IsSeasonalEvent = quest.Festival.RowId != 0;
IsSeasonalQuest = isSeasonalEventQuest;
SeasonalQuestExpiry = (IsSeasonalQuest ? seasonalQuestExpiry : ((DateTime?)null));
SeasonalQuestExpiry = seasonalQuestExpiry;
NewGamePlusChapter = newGamePlusChapter;
StartingCity = startingCity;
MoogleDeliveryLevel = (byte)quest.DeliveryQuest.RowId;

View file

@ -23,8 +23,6 @@ internal sealed class QuestProgressInfo
public EClassJob ClassJob { get; }
public string Tooltip { get; }
public QuestProgressInfo(QuestWork questWork)
{
Id = new QuestId(questWork.QuestId);
@ -33,20 +31,11 @@ internal sealed class QuestProgressInfo
Variables = questWork.Variables.ToArray().ToList();
IsHidden = questWork.IsHidden;
ClassJob = (EClassJob)questWork.AcceptClassJob;
Tooltip = "";
Span<byte> variables = questWork.Variables;
string text = "";
for (int i = 0; i < variables.Length; i++)
{
byte b = variables[i];
Tooltip = Tooltip + Convert.ToString(b, 2).PadLeft(8).Replace(" ", "0") + "\n";
int num = b & 0xF;
text += b;
if (num != 0)
{
text += $"({num})";
}
text += " ";
text = text + variables[i] + " ";
if (i % 2 == 1)
{
text += " ";

View file

@ -16,6 +16,10 @@ internal sealed class UnlockLinkQuestInfo : IQuestInfo
public bool IsRepeatable => false;
public DateTime? QuestExpiry { get; }
public string? Patch { get; }
public ImmutableList<PreviousQuestInfo> PreviousQuests => ImmutableList.Create(default(ReadOnlySpan<PreviousQuestInfo>));
public EQuestJoin PreviousQuestJoin => EQuestJoin.All;
@ -34,10 +38,12 @@ internal sealed class UnlockLinkQuestInfo : IQuestInfo
public EExpansionVersion Expansion => EExpansionVersion.ARealmReborn;
public UnlockLinkQuestInfo(UnlockLinkId unlockLinkId, string name, uint issuerDataId)
public UnlockLinkQuestInfo(UnlockLinkId unlockLinkId, string name, uint issuerDataId, DateTime? expiryTime, string? patch = null)
{
QuestId = unlockLinkId;
Name = name;
IssuerDataId = issuerDataId;
QuestExpiry = expiryTime;
Patch = patch;
}
}

View file

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using Json.Schema;
using Questionable.Model;
@ -17,11 +21,71 @@ internal sealed class JsonSchemaValidator : IQuestValidator
public JsonSchemaValidator()
{
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-aethernetshard.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAethernetShard).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-aetheryte.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAetheryte).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-classjob.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonClassJob).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-completionflags.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonCompletionFlags).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://qstxiv.github.io/schema/common-vector3.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonVector3).AsTask().Result);
bool flag = false;
DirectoryInfo directoryInfo = FindProjectRoot();
if (directoryInfo != null)
{
RegisterFolderForSchemas(directoryInfo);
flag = true;
}
if (!flag)
{
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aethernetshard.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAethernetShard).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonAetheryte).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonClassJob).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonCompletionFlags).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json"), JsonSchema.FromStream(AssemblyModelLoader.CommonVector3).AsTask().Result);
SchemaRegistry.Global.Register(new Uri("https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json"), JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result);
}
static DirectoryInfo? FindProjectRoot()
{
try
{
string location = Assembly.GetExecutingAssembly().Location;
if (string.IsNullOrEmpty(location))
{
return null;
}
for (DirectoryInfo directoryInfo2 = new DirectoryInfo(Path.GetDirectoryName(location) ?? location); directoryInfo2 != null; directoryInfo2 = directoryInfo2.Parent)
{
if (directoryInfo2.GetDirectories("QuestPaths", SearchOption.TopDirectoryOnly).Length != 0 || directoryInfo2.GetDirectories("Questionable.Model", SearchOption.TopDirectoryOnly).Length != 0 || directoryInfo2.GetDirectories("Questionable", SearchOption.TopDirectoryOnly).Length != 0)
{
return directoryInfo2;
}
}
}
catch
{
}
return null;
}
static void RegisterFolderForSchemas(DirectoryInfo folder)
{
try
{
RegisterLocalIfExistsFromPath(Find("common-aethernetshard.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aethernetshard.json");
RegisterLocalIfExistsFromPath(Find("common-aetheryte.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-aetheryte.json");
RegisterLocalIfExistsFromPath(Find("common-classjob.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-classjob.json");
RegisterLocalIfExistsFromPath(Find("common-completionflags.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-completionflags.json");
RegisterLocalIfExistsFromPath(Find("common-vector3.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/Questionable.Model/common-vector3.json");
RegisterLocalIfExistsFromPath(Find("quest-v1.json"), "https://github.com/WigglyMuffin/Questionable/raw/refs/heads/main/QuestPaths/quest-v1.json");
static void RegisterLocalIfExistsFromPath(string? path, string registrationUri)
{
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
JsonSchema document = JsonSchema.FromText(File.ReadAllText(path));
SchemaRegistry.Global.Register(new Uri(registrationUri), document);
}
}
}
catch
{
}
string? Find(string fileName)
{
return Directory.EnumerateFiles(folder.FullName, fileName, SearchOption.AllDirectories).FirstOrDefault();
}
}
}
public IEnumerable<ValidationIssue> Validate(Quest quest)
@ -30,12 +94,53 @@ internal sealed class JsonSchemaValidator : IQuestValidator
{
_questSchema = JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
}
if (_questNodes.TryGetValue(quest.Id, out JsonNode value) && !_questSchema.Evaluate(value, new EvaluationOptions
if (!_questNodes.TryGetValue(quest.Id, out JsonNode value))
{
yield break;
}
EvaluationResults evaluationResults = _questSchema.Evaluate(value, new EvaluationOptions
{
Culture = CultureInfo.InvariantCulture,
OutputFormat = OutputFormat.List
}).IsValid)
});
if (evaluationResults.IsValid)
{
yield break;
}
var array = (from r in GetInvalidResults(evaluationResults).ToArray()
group r by r.InstanceLocation?.ToString() ?? "<root>").Select(delegate(IGrouping<string, EvaluationResults> g)
{
string[] messages = (from m in g.SelectMany((EvaluationResults r) => r.Errors?.Values ?? Enumerable.Empty<string>())
where !string.IsNullOrWhiteSpace(m)
select m.Trim()).Distinct().ToArray();
return new
{
Path = g.Key,
Messages = messages
};
}).ToArray();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("JSON Validation failed:");
if (array.Length == 0)
{
stringBuilder.AppendLine(" - <unknown>: validation failed");
}
else
{
var array2 = array;
foreach (var anon in array2)
{
string value2 = ((anon.Messages.Length != 0) ? string.Join("; ", anon.Messages) : "validation failed");
StringBuilder stringBuilder2 = stringBuilder;
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 2, stringBuilder2, invariantCulture);
handler.AppendLiteral(" - ");
handler.AppendFormatted(anon.Path);
handler.AppendLiteral(": ");
handler.AppendFormatted(value2);
stringBuilder2.AppendLine(invariantCulture, ref handler);
}
}
yield return new ValidationIssue
{
ElementId = quest.Id,
@ -43,8 +148,24 @@ internal sealed class JsonSchemaValidator : IQuestValidator
Step = null,
Type = EIssueType.InvalidJsonSchema,
Severity = EIssueSeverity.Error,
Description = "JSON Validation failed"
Description = stringBuilder.ToString().TrimEnd()
};
static IEnumerable<EvaluationResults> GetInvalidResults(EvaluationResults result)
{
if (!result.IsValid)
{
yield return result;
}
if (result.HasDetails)
{
foreach (EvaluationResults detail in result.Details)
{
foreach (EvaluationResults invalidResult in GetInvalidResults(detail))
{
yield return invalidResult;
}
}
}
}
}

View file

@ -4,6 +4,7 @@ public enum EIssueType
{
None,
InvalidJsonSchema,
InvalidJsonSyntax,
MissingSequence0,
MissingSequence,
DuplicateSequence,

View file

@ -39,15 +39,21 @@ internal sealed class QuestValidator
_validationIssues.Clear();
}
public void AddValidationIssue(ValidationIssue issue)
{
_validationIssues.Add(issue);
}
public void Validate(IEnumerable<Quest> quests)
{
Task.Factory.StartNew(delegate
{
try
{
List<ValidationIssue> first = _validationIssues.ToList();
_validationIssues.Clear();
List<ValidationIssue> list = new List<ValidationIssue>();
Dictionary<EAlliedSociety, int> dictionary = new Dictionary<EAlliedSociety, int>();
Dictionary<EAlliedSociety, List<ElementId>> dictionary = new Dictionary<EAlliedSociety, List<ElementId>>();
foreach (Quest quest in quests)
{
foreach (IQuestValidator validator in _validators)
@ -56,8 +62,12 @@ internal sealed class QuestValidator
{
if (item.Type == EIssueType.QuestDisabled && quest.Info.AlliedSociety != EAlliedSociety.None)
{
dictionary.TryAdd(quest.Info.AlliedSociety, 0);
dictionary[quest.Info.AlliedSociety]++;
if (!dictionary.TryGetValue(quest.Info.AlliedSociety, out var value))
{
value = new List<ElementId>();
dictionary[quest.Info.AlliedSociety] = value;
}
value.Add(quest.Id);
}
else
{
@ -69,10 +79,9 @@ internal sealed class QuestValidator
List<ElementId> disabledQuests = (from x in list
where x.Type == EIssueType.QuestDisabled
select x.ElementId).ToList();
_validationIssues = (from x in list
where !disabledQuests.Contains(x.ElementId) || x.Type == EIssueType.QuestDisabled
_validationIssues = (from x in first.Concat(list.Where((ValidationIssue x) => !disabledQuests.Contains(x.ElementId) || x.Type == EIssueType.QuestDisabled)).Concat(DisabledTribesAsIssues(dictionary))
orderby x.ElementId, x.Sequence, x.Step, x.Description
select x).Concat(DisabledTribesAsIssues(dictionary)).ToList();
select x).ToList();
}
catch (Exception exception)
{
@ -86,11 +95,12 @@ internal sealed class QuestValidator
return _validationIssues.Where((ValidationIssue x) => x.ElementId == elementId).ToList();
}
private static IEnumerable<ValidationIssue> DisabledTribesAsIssues(Dictionary<EAlliedSociety, int> disabledTribeQuests)
private static IEnumerable<ValidationIssue> DisabledTribesAsIssues(Dictionary<EAlliedSociety, List<ElementId>> disabledTribeQuests)
{
return from x in disabledTribeQuests
orderby x.Key
select new ValidationIssue
return disabledTribeQuests.OrderBy<KeyValuePair<EAlliedSociety, List<ElementId>>, EAlliedSociety>((KeyValuePair<EAlliedSociety, List<ElementId>> x) => x.Key).Select(delegate(KeyValuePair<EAlliedSociety, List<ElementId>> x)
{
string value = ((x.Value.Count > 0) ? string.Join(", ", x.Value.Select((ElementId id) => id.ToString())) : "(none)");
return new ValidationIssue
{
ElementId = null,
Sequence = null,
@ -98,7 +108,8 @@ internal sealed class QuestValidator
AlliedSociety = x.Key,
Type = EIssueType.QuestDisabled,
Severity = EIssueSeverity.None,
Description = $"{x.Value} disabled quest(s)"
Description = $"{x.Value.Count} disabled quest(s): {value}"
};
});
}
}

View file

@ -110,22 +110,6 @@ internal sealed class DebugConfigComponent : ConfigComponent
}
ImGui.SameLine();
ImGuiComponents.HelpMarker("When enabled, Questionable will not attempt to turn-in and complete quests. This will do everything automatically except the final turn-in step.");
bool v11 = base.Configuration.Advanced.ShowWindowOnStart;
if (ImGui.Checkbox("Show window on start", ref v11))
{
base.Configuration.Advanced.ShowWindowOnStart = v11;
Save();
}
ImGui.SameLine();
ImGuiComponents.HelpMarker("When enabled, Questionable's progress window will show when the plugin is loaded.");
bool v12 = base.Configuration.Advanced.StartMinimized;
if (ImGui.Checkbox("Start minimized", ref v12))
{
base.Configuration.Advanced.StartMinimized = v12;
Save();
}
ImGui.SameLine();
ImGuiComponents.HelpMarker("When enabled, Questionable's progress window will be in its minimized state when loaded.");
}
}
}

View file

@ -127,21 +127,27 @@ internal sealed class GeneralConfigComponent : ConfigComponent
base.Configuration.General.ShowIncompleteSeasonalEvents = v3;
Save();
}
bool v4 = base.Configuration.General.HideSeasonalEventsFromJournalProgress;
if (ImGui.Checkbox("Hide Seasonal Events from Journal Progress", ref v4))
{
base.Configuration.General.HideSeasonalEventsFromJournalProgress = v4;
Save();
}
}
ImGui.Separator();
ImGui.Text("Questing");
using (ImRaii.PushIndent())
{
bool v4 = base.Configuration.General.ConfigureTextAdvance;
if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v4))
bool v5 = base.Configuration.General.ConfigureTextAdvance;
if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v5))
{
base.Configuration.General.ConfigureTextAdvance = v4;
base.Configuration.General.ConfigureTextAdvance = v5;
Save();
}
bool v5 = base.Configuration.General.SkipLowPriorityDuties;
if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v5))
bool v6 = base.Configuration.General.SkipLowPriorityDuties;
if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v6))
{
base.Configuration.General.SkipLowPriorityDuties = v5;
base.Configuration.General.SkipLowPriorityDuties = v6;
Save();
}
ImGui.SameLine();
@ -169,10 +175,10 @@ internal sealed class GeneralConfigComponent : ConfigComponent
}
}
ImGui.Spacing();
bool v6 = base.Configuration.General.AutoStepRefreshEnabled;
if (ImGui.Checkbox("Automatically refresh quest steps when stuck (WIP see tooltip)", ref v6))
bool v7 = base.Configuration.General.AutoStepRefreshEnabled;
if (ImGui.Checkbox("Automatically refresh quest steps when stuck", ref v7))
{
base.Configuration.General.AutoStepRefreshEnabled = v6;
base.Configuration.General.AutoStepRefreshEnabled = v7;
Save();
}
ImGui.SameLine();
@ -186,23 +192,22 @@ internal sealed class GeneralConfigComponent : ConfigComponent
{
ImGui.Text("Questionable will automatically refresh a quest step if it appears to be stuck after the configured delay.");
ImGui.Text("This helps resume automated quest completion when interruptions occur.");
ImGui.Text("WIP feature, rather than remove it, this is a warning that it isn't fully complete.");
}
}
using (ImRaii.Disabled(!v6))
using (ImRaii.Disabled(!v7))
{
ImGui.Indent();
int v7 = base.Configuration.General.AutoStepRefreshDelaySeconds;
int v8 = base.Configuration.General.AutoStepRefreshDelaySeconds;
ImGui.SetNextItemWidth(150f);
if (ImGui.SliderInt("Refresh delay (seconds)", ref v7, 10, 180))
if (ImGui.SliderInt("Refresh delay (seconds)", ref v8, 10, 180))
{
base.Configuration.General.AutoStepRefreshDelaySeconds = v7;
base.Configuration.General.AutoStepRefreshDelaySeconds = v8;
Save();
}
Vector4 col = new Vector4(0.7f, 0.7f, 0.7f, 1f);
ImU8String text = new ImU8String(77, 1);
text.AppendLiteral("Quest steps will refresh automatically after ");
text.AppendFormatted(v7);
text.AppendFormatted(v8);
text.AppendLiteral(" seconds if no progress is made.");
ImGui.TextColored(in col, text);
ImGui.Unindent();

View file

@ -85,8 +85,8 @@ internal sealed class PluginConfigComponent : ConfigComponent
num = 0;
span[num] = new PluginDetailInfo("'Auto Active Time Maneuver' enabled", "Automatically completes active time maneuvers in\nsingle player instances, trials and raids\"", () => pandorasBoxIpc.IsAutoActiveTimeManeuverEnabled);
array[1] = new PluginInfo("Pandora's Box", "PandorasBox", "Pandora's Box is a collection of tweaks.", websiteUri2, dalamudRepositoryUri2, "/pandora", list2);
array[2] = new PluginInfo("NotificationMaster", "NotificationMaster", "Sends a configurable out-of-game notification if a quest\nrequires manual actions.", new Uri("https://github.com/NightmareXIV/NotificationMaster"), null);
array[3] = new PluginInfo("Artisan", "Artisan", "Automates crafting", new Uri("https://github.com/PunishXIV/Artisan"), new Uri("https://puni.sh/api/plugins"), "/artisan");
array[2] = new PluginInfo("QuestMap", "QuestMap", "Displays quest objectives and markers on the map for\nbetter navigation and tracking.", new Uri("https://github.com/rreminy/QuestMap"), null);
array[3] = new PluginInfo("NotificationMaster", "NotificationMaster", "Sends a configurable out-of-game notification if a quest\nrequires manual actions.", new Uri("https://github.com/NightmareXIV/NotificationMaster"), null);
_recommendedPlugins = new global::_003C_003Ez__ReadOnlyArray<PluginInfo>(array);
}
@ -129,10 +129,10 @@ internal sealed class PluginConfigComponent : ConfigComponent
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.Text("Questionable recommends Boss Mod (VBM) for rotation/combat automation.");
using (ImRaii.Disabled(_combatController.IsRunning))
{
ImGui.Text("Questionable supports multiple rotation/combat plugins, please pick the one\nyou want to use:");
using (ImRaii.PushIndent())
{
using (ImRaii.Disabled(_combatController.IsRunning))
{
if (ImGui.RadioButton("No rotation/combat plugin (combat must be done manually)", _configuration.General.CombatModule == Questionable.Configuration.ECombatModule.None))
{
@ -141,10 +141,6 @@ internal sealed class PluginConfigComponent : ConfigComponent
}
allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.BossMod, checklistPadding);
allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.WrathCombo, checklistPadding);
}
ImGui.Text("The following rotation/combat plugin(s) are provided for compatibility and testing purposes:");
using (ImRaii.PushIndent())
{
allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.RotationSolverReborn, checklistPadding);
}
}

View file

@ -25,22 +25,28 @@ internal sealed class StopConditionComponent : ConfigComponent
private readonly QuestRegistry _questRegistry;
private readonly QuestFunctions _questFunctions;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
private readonly IClientState _clientState;
public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IClientState clientState, Configuration configuration)
private readonly QuestController _questController;
public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IClientState clientState, QuestController questController, Configuration configuration)
: base(pluginInterface, configuration)
{
StopConditionComponent stopConditionComponent = this;
_pluginInterface = pluginInterface;
_questSelector = questSelector;
_questRegistry = questRegistry;
_questFunctions = questFunctions;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
_clientState = clientState;
_questController = questController;
_questSelector.SuggestionPredicate = (Quest quest) => configuration.Stop.QuestsToStopAfter.All((ElementId x) => x != quest.Id);
_questSelector.DefaultPredicate = (Quest quest) => quest.Info.IsMainScenarioQuest && questFunctions.IsQuestAccepted(quest.Id);
_questSelector.QuestSelected = delegate(Quest quest)
@ -94,7 +100,37 @@ internal sealed class StopConditionComponent : ConfigComponent
}
}
ImGui.Separator();
ImGui.Text("Stop when quest sequence reaches:");
bool v3 = base.Configuration.Stop.SequenceToStopAfter;
if (ImGui.Checkbox("Enable quest sequence stop condition", ref v3))
{
base.Configuration.Stop.SequenceToStopAfter = v3;
Save();
}
using (ImRaii.Disabled(!v3))
{
int data2 = base.Configuration.Stop.TargetSequence;
ImGui.SetNextItemWidth(100f);
if (ImGui.InputInt("Stop at sequence", ref data2, 1, 1))
{
base.Configuration.Stop.TargetSequence = Math.Max(0, Math.Min(255, data2));
Save();
}
QuestController.QuestProgress currentQuest = _questController.CurrentQuest;
if (currentQuest != null)
{
int sequence = currentQuest.Sequence;
ImGui.SameLine();
ImU8String text = new ImU8String(11, 1);
text.AppendLiteral("(Current: ");
text.AppendFormatted(sequence);
text.AppendLiteral(")");
ImGui.TextDisabled(text);
}
}
ImGui.Separator();
ImGui.Text("Stop when completing any of the quests selected below:");
DrawCurrentlyAcceptedQuests();
_questSelector.DrawSelection();
List<ElementId> questsToStopAfter = base.Configuration.Stop.QuestsToStopAfter;
if (questsToStopAfter.Count > 0)
@ -159,4 +195,88 @@ internal sealed class StopConditionComponent : ConfigComponent
}
}
}
private void DrawCurrentlyAcceptedQuests()
{
List<Quest> currentlyAcceptedQuests = GetCurrentlyAcceptedQuests();
ImGui.Text("Currently Accepted Quests:");
using (ImRaii.IEndObject endObject = ImRaii.Child("AcceptedQuestsList", new Vector2(-1f, 120f), border: true))
{
if (endObject)
{
if (currentlyAcceptedQuests.Count > 0)
{
foreach (Quest item in currentlyAcceptedQuests)
{
ImU8String id = new ImU8String(13, 1);
id.AppendLiteral("AcceptedQuest");
id.AppendFormatted(item.Id);
using (ImRaii.PushId(id))
{
(Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(item.Id);
bool flag = false;
bool flag2 = base.Configuration.Stop.QuestsToStopAfter.Contains(item.Id);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(flag2 ? new Vector4(0.5f, 0.5f, 0.5f, 1f) : questStyle.Item1, questStyle.Item2.ToIconString());
flag = ImGui.IsItemHovered();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextColored(flag2 ? new Vector4(0.7f, 0.7f, 0.7f, 1f) : new Vector4(1f, 1f, 1f, 1f), item.Info.Name);
if (flag | ImGui.IsItemHovered())
{
_questTooltipComponent.Draw(item.Info);
}
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f);
}
using (ImRaii.Disabled(flag2))
{
if (ImGuiComponents.IconButton($"##Add{item.Id}", FontAwesomeIcon.Plus))
{
base.Configuration.Stop.QuestsToStopAfter.Add(item.Id);
Save();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.SetTooltip(flag2 ? "Quest already added to stop conditions" : "Add this quest to stop conditions");
}
}
}
}
else
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.7f, 0.7f, 0.7f, 1f));
ImGui.TextWrapped("No quests currently accepted");
ImGui.PopStyleColor();
}
}
}
ImGui.Spacing();
}
private List<Quest> GetCurrentlyAcceptedQuests()
{
List<Quest> list = new List<Quest>();
try
{
foreach (Quest allQuest in _questRegistry.AllQuests)
{
if (_questFunctions.IsQuestAccepted(allQuest.Id))
{
list.Add(allQuest);
}
}
list.Sort((Quest a, Quest b) => string.Compare(a.Info.Name, b.Info.Name, StringComparison.OrdinalIgnoreCase));
}
catch (Exception)
{
list.Clear();
}
return list;
}
}

View file

@ -6,8 +6,11 @@ using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Utility;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
@ -42,13 +45,15 @@ internal sealed class QuestJournalComponent
public bool HideNoPaths;
public bool HideUnobtainable;
public bool AdvancedFiltersActive
{
get
{
if (!AvailableOnly)
if (!AvailableOnly && !HideNoPaths)
{
return HideNoPaths;
return HideUnobtainable;
}
return true;
}
@ -59,7 +64,8 @@ internal sealed class QuestJournalComponent
return new FilterConfiguration
{
AvailableOnly = AvailableOnly,
HideNoPaths = HideNoPaths
HideNoPaths = HideNoPaths,
HideUnobtainable = HideUnobtainable
};
}
}
@ -86,11 +92,19 @@ internal sealed class QuestJournalComponent
private readonly QuestValidator _questValidator;
private readonly Configuration _configuration;
private readonly ILogger<QuestJournalComponent> _logger;
private const uint SeasonalJournalCategoryRowId = 96u;
private List<FilteredSection> _filteredSections = new List<FilteredSection>();
private bool _lastHideSeasonalGlobally;
internal FilterConfiguration Filter { get; } = new FilterConfiguration();
public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, QuestJournalUtils questJournalUtils, QuestValidator questValidator)
public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, QuestJournalUtils questJournalUtils, QuestValidator questValidator, Configuration configuration, ILogger<QuestJournalComponent> logger)
{
_journalData = journalData;
_questRegistry = questRegistry;
@ -100,6 +114,9 @@ internal sealed class QuestJournalComponent
_pluginInterface = pluginInterface;
_questJournalUtils = questJournalUtils;
_questValidator = questValidator;
_configuration = configuration;
_logger = logger;
_lastHideSeasonalGlobally = _configuration.General.HideSeasonalEventsFromJournalProgress;
}
public void DrawQuests()
@ -109,18 +126,34 @@ internal sealed class QuestJournalComponent
{
return;
}
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
if (hideSeasonalEventsFromJournalProgress != _lastHideSeasonalGlobally)
{
_lastHideSeasonalGlobally = hideSeasonalEventsFromJournalProgress;
_logger.LogDebug("Configuration change detected: HideSeasonalEventsFromJournalProgress={Hide} - refreshing journal", hideSeasonalEventsFromJournalProgress);
UpdateFilter();
}
if (ImGui.CollapsingHeader("Explanation", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.Text("The list below contains all quests that appear in your journal.");
ImGui.BulletText("'Supported' lists quests that Questionable can do for you");
ImGui.BulletText("'Completed' lists quests your current character has completed.");
ImGui.BulletText("Not all quests can be completed even if they're listed as available, e.g. starting city quest chains.");
ImGui.BulletText("Not all quests can be completed even if they're listed as available, e.g. starting city quest chains or past seasonal events.");
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
}
QuestJournalUtils.ShowFilterContextMenu(this);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.GlobeEurope))
{
Util.OpenLink("https://wigglymuffin.github.io/FFXIV-Tools/");
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("View All Quest Paths Online");
}
ImGui.SameLine();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref Filter.SearchText, 256))
{
@ -270,16 +303,57 @@ internal sealed class QuestJournalComponent
{
_uiUtils.ChecklistItem(string.Empty, complete: true);
}
goto IL_0215;
goto IL_0210;
}
}
_uiUtils.ChecklistItem(string.Empty, complete: false);
}
goto IL_0215;
IL_0215:
goto IL_0210;
IL_0210:
ImGui.TableNextColumn();
var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
_uiUtils.ChecklistItem(text, color, icon);
if (_questFunctions.IsQuestComplete(questInfo.QuestId))
{
if (questInfo.IsRepeatable && _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Complete", ImGuiColors.ParsedBlue, FontAwesomeIcon.Check);
}
else
{
_uiUtils.ChecklistItem("Complete", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
}
return;
}
if (_questFunctions.IsQuestAccepted(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Active", ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight);
return;
}
bool flag = false;
bool flag2 = _questFunctions.IsQuestUnobtainable(questInfo.QuestId);
bool flag3 = _questFunctions.IsQuestLocked(questInfo.QuestId);
bool flag4 = _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId);
DateTime? seasonalQuestExpiry = questInfo.SeasonalQuestExpiry;
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault = seasonalQuestExpiry.GetValueOrDefault();
DateTime dateTime = ((valueOrDefault.Kind == DateTimeKind.Utc) ? valueOrDefault : valueOrDefault.ToUniversalTime());
if (DateTime.UtcNow > dateTime)
{
flag = true;
}
}
if (flag || flag2)
{
_uiUtils.ChecklistItem("Unobtainable", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
}
else if (flag3 || !flag4 || !_questRegistry.IsKnownQuest(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Locked", ImGuiColors.DalamudRed, FontAwesomeIcon.Times);
}
else
{
_uiUtils.ChecklistItem("Available", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
}
}
private static void DrawCount(int count, int total)
@ -321,19 +395,34 @@ internal sealed class QuestJournalComponent
private FilteredSection FilterSection(JournalData.Section section, FilterConfiguration filter)
{
IEnumerable<FilteredCategory> source = ((!IsCategorySectionGenreMatch(filter, section.Name)) ? section.Categories.Select((JournalData.Category category) => FilterCategory(category, filter)) : section.Categories.Select((JournalData.Category x) => FilterCategory(x, filter.WithoutName())));
return new FilteredSection(section, source.Where((FilteredCategory x) => x.Genres.Count > 0).ToList());
IEnumerable<JournalData.Category> enumerable;
if (!_configuration.General.HideSeasonalEventsFromJournalProgress)
{
IEnumerable<JournalData.Category> categories = section.Categories;
enumerable = categories;
}
else
{
enumerable = section.Categories.Where((JournalData.Category c) => c.Id != 96);
}
IEnumerable<JournalData.Category> source = enumerable;
return new FilteredSection(Categories: ((!IsCategorySectionGenreMatch(filter, section.Name)) ? source.Select((JournalData.Category category) => FilterCategory(category, filter, section)) : source.Select((JournalData.Category x) => FilterCategory(x, filter.WithoutName(), section))).Where((FilteredCategory x) => x.Genres.Count > 0).ToList(), Section: section);
}
private FilteredCategory FilterCategory(JournalData.Category category, FilterConfiguration filter)
private FilteredCategory FilterCategory(JournalData.Category category, FilterConfiguration filter, JournalData.Section? parentSection = null)
{
IEnumerable<FilteredGenre> source = ((!IsCategorySectionGenreMatch(filter, category.Name)) ? category.Genres.Select((JournalData.Genre genre) => FilterGenre(genre, filter)) : category.Genres.Select((JournalData.Genre x) => FilterGenre(x, filter.WithoutName())));
IEnumerable<FilteredGenre> source = ((!IsCategorySectionGenreMatch(filter, category.Name)) ? category.Genres.Select((JournalData.Genre genre) => FilterGenre(genre, filter, parentSection)) : category.Genres.Select((JournalData.Genre x) => FilterGenre(x, filter.WithoutName(), parentSection)));
return new FilteredCategory(category, source.Where((FilteredGenre x) => x.Quests.Count > 0).ToList());
}
private FilteredGenre FilterGenre(JournalData.Genre genre, FilterConfiguration filter)
private FilteredGenre FilterGenre(JournalData.Genre genre, FilterConfiguration filter, JournalData.Section? parentSection = null)
{
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
IEnumerable<IQuestInfo> source = ((!IsCategorySectionGenreMatch(filter, genre.Name)) ? genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter, x)) : genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter.WithoutName(), x)));
if (hideSeasonalEventsFromJournalProgress && genre.CategoryId == 96)
{
source = source.Where((IQuestInfo q) => !IsSeasonal(q));
}
return new FilteredGenre(genre, source.ToList());
}
@ -342,37 +431,50 @@ internal sealed class QuestJournalComponent
_genreCounts.Clear();
_categoryCounts.Clear();
_sectionCounts.Clear();
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
_logger.LogInformation("Refreshing journal counts. HideSeasonalEventsFromJournalProgress={Hide}", hideSeasonalEventsFromJournalProgress);
foreach (JournalData.Genre genre in _journalData.Genres)
{
List<IQuestInfo> source = ((hideSeasonalEventsFromJournalProgress && genre.CategoryId == 96) ? genre.Quests.Where((IQuestInfo q) => !IsSeasonal(q)).ToList() : genre.Quests.ToList());
Quest quest;
int available = genre.Quests.Count((IQuestInfo x) => _questRegistry.TryGetQuest(x.QuestId, out quest) && !quest.Root.Disabled && !_questFunctions.IsQuestRemoved(x.QuestId));
int total = genre.Quests.Count((IQuestInfo x) => !_questFunctions.IsQuestRemoved(x.QuestId));
int obtainable = genre.Quests.Count((IQuestInfo x) => !_questFunctions.IsQuestUnobtainable(x.QuestId));
int completed = genre.Quests.Count((IQuestInfo x) => _questFunctions.IsQuestComplete(x.QuestId));
int available = source.Count((IQuestInfo x) => _questRegistry.TryGetQuest(x.QuestId, out quest) && !quest.Root.Disabled && !_questFunctions.IsQuestRemoved(x.QuestId));
int total = source.Count((IQuestInfo x) => !_questFunctions.IsQuestRemoved(x.QuestId));
int obtainable = source.Count((IQuestInfo x) => !_questFunctions.IsQuestUnobtainable(x.QuestId));
int completed = source.Count((IQuestInfo x) => _questFunctions.IsQuestComplete(x.QuestId));
_genreCounts[genre] = new JournalCounts(available, total, obtainable, completed);
}
foreach (JournalData.Category category in _journalData.Categories)
{
List<JournalCounts> source = (from x in _genreCounts
where category.Genres.Contains(x.Key)
select x.Value).ToList();
int available2 = source.Sum((JournalCounts x) => x.Available);
int total2 = source.Sum((JournalCounts x) => x.Total);
int obtainable2 = source.Sum((JournalCounts x) => x.Obtainable);
int completed2 = source.Sum((JournalCounts x) => x.Completed);
if (!hideSeasonalEventsFromJournalProgress || category.Id != 96)
{
List<JournalCounts> source2 = _genre_counts_or_default(category);
int available2 = source2.Sum((JournalCounts x) => x.Available);
int total2 = source2.Sum((JournalCounts x) => x.Total);
int obtainable2 = source2.Sum((JournalCounts x) => x.Obtainable);
int completed2 = source2.Sum((JournalCounts x) => x.Completed);
_categoryCounts[category] = new JournalCounts(available2, total2, obtainable2, completed2);
}
}
foreach (JournalData.Section section in _journalData.Sections)
{
List<JournalCounts> source2 = (from x in _categoryCounts
List<JournalCounts> source3 = (from x in _categoryCounts
where section.Categories.Contains(x.Key)
select x.Value).ToList();
int available3 = source2.Sum((JournalCounts x) => x.Available);
int total3 = source2.Sum((JournalCounts x) => x.Total);
int obtainable3 = source2.Sum((JournalCounts x) => x.Obtainable);
int completed3 = source2.Sum((JournalCounts x) => x.Completed);
int available3 = source3.Sum((JournalCounts x) => x.Available);
int total3 = source3.Sum((JournalCounts x) => x.Total);
int obtainable3 = source3.Sum((JournalCounts x) => x.Obtainable);
int completed3 = source3.Sum((JournalCounts x) => x.Completed);
_sectionCounts[section] = new JournalCounts(available3, total3, obtainable3, completed3);
}
int num = _sectionCounts.Values.Sum((JournalCounts x) => x.Total);
_logger.LogDebug("RefreshCounts complete. Sections={Sections}, Categories={Categories}, Genres={Genres}, TotalQuests={Total}", _sectionCounts.Count, _categoryCounts.Count, _genreCounts.Count, num);
}
private List<JournalCounts> _genre_counts_or_default(JournalData.Category category)
{
return (from x in _genreCounts
where category.Genres.Contains(x.Key)
select x.Value).ToList();
}
internal void ClearCounts(int type, int code)
@ -423,6 +525,31 @@ internal sealed class QuestJournalComponent
{
return false;
}
if (filter.HideUnobtainable && _questFunctions.IsQuestUnobtainable(questInfo.QuestId))
{
return false;
}
return true;
}
private static bool IsSeasonal(IQuestInfo q)
{
if (q == null)
{
return false;
}
if (q.IsSeasonalQuest)
{
return true;
}
if (q.SeasonalQuestExpiry.HasValue)
{
return true;
}
if (q is UnlockLinkQuestInfo { QuestExpiry: not null })
{
return true;
}
return false;
}
}

View file

@ -43,14 +43,30 @@ internal sealed class QuestJournalUtils
{
return;
}
using (ImRaii.Disabled(!_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId)))
using (ImRaii.Disabled(quest == null || (!_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId) && !_questFunctions.IsQuestAccepted(questInfo.QuestId))))
{
if (ImGui.MenuItem("Start as next quest"))
{
if (quest == null)
{
return;
}
if (_questFunctions.IsQuestAccepted(questInfo.QuestId))
{
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(questInfo.QuestId);
if (questProgressInfo != null)
{
_questController.SetStartedQuest(quest, questProgressInfo.Sequence);
_questController.Start(label);
}
}
else
{
_questController.SetNextQuest(quest);
_questController.Start(label);
}
}
}
bool flag = _commandManager.Commands.ContainsKey("/questinfo");
using (ImRaii.Disabled(!(questInfo.QuestId is QuestId) || !flag))
{
@ -68,7 +84,7 @@ internal sealed class QuestJournalUtils
ImGui.OpenPopup("##QuestFilters");
}
using ImRaii.IEndObject endObject = ImRaii.Popup("##QuestFilters");
if (!(!endObject) && (ImGui.Checkbox("Show only Available Quests", ref journalUi.Filter.AvailableOnly) || ImGui.Checkbox("Hide Quests Without Path", ref journalUi.Filter.HideNoPaths)))
if (!(!endObject) && (ImGui.Checkbox("Show only Available Quests", ref journalUi.Filter.AvailableOnly) || ImGui.Checkbox("Hide Quests Without Path", ref journalUi.Filter.HideNoPaths) || ImGui.Checkbox("Hide Unobtainable Quests", ref journalUi.Filter.HideUnobtainable)))
{
journalUi.UpdateFilter();
}

View file

@ -55,7 +55,7 @@ internal sealed class ActiveQuestComponent
[GeneratedCode("System.Text.RegularExpressions.Generator", "9.0.12.41916")]
private static Regex MultipleWhitespaceRegex()
{
return _003CRegexGenerator_g_003EFF909AF37F4C319C8940E7DA0E71D9E470824ECE485FE299B23B08984F5D534F6__MultipleWhitespaceRegex_0.Instance;
return _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0.Instance;
}
public ActiveQuestComponent(QuestController questController, MovementController movementController, CombatController combatController, GatheringController gatheringController, QuestFunctions questFunctions, ICommandManager commandManager, Configuration configuration, QuestRegistry questRegistry, PriorityWindow priorityWindow, UiUtils uiUtils, IClientState clientState, IChatGui chatGui, ILogger<ActiveQuestComponent> logger)
@ -236,7 +236,8 @@ internal sealed class ActiveQuestComponent
}
bool flag = _configuration.Stop.Enabled && _configuration.Stop.LevelToStopAfter;
bool flag2 = _configuration.Stop.Enabled && _configuration.Stop.QuestsToStopAfter.Any((ElementId x) => !_questFunctions.IsQuestComplete(x) && !_questFunctions.IsQuestUnobtainable(x));
if (flag || flag2)
bool flag3 = _configuration.Stop.Enabled && _configuration.Stop.SequenceToStopAfter;
if (flag || flag2 || flag3)
{
ImGui.SameLine();
Vector4 col = ImGuiColors.ParsedPurple;
@ -252,6 +253,10 @@ internal sealed class ActiveQuestComponent
col = ImGuiColors.ParsedBlue;
}
}
if (flag3)
{
col = ((startedQuest.Sequence < _configuration.Stop.TargetSequence) ? ImGuiColors.ParsedBlue : ImGuiColors.ParsedGreen);
}
ImGui.TextColored(in col, SeIconChar.Clock.ToIconString());
if (ImGui.IsItemHovered())
{
@ -290,12 +295,43 @@ internal sealed class ActiveQuestComponent
}
}
}
if (flag2)
if (flag3)
{
if (flag)
{
ImGui.Spacing();
}
int sequence = startedQuest.Sequence;
text = new ImU8String(23, 1);
text.AppendLiteral("Stop at quest sequence ");
text.AppendFormatted(_configuration.Stop.TargetSequence);
ImGui.BulletText(text);
ImGui.SameLine();
if (sequence >= _configuration.Stop.TargetSequence)
{
Vector4 col2 = ImGuiColors.ParsedGreen;
text = new ImU8String(22, 1);
text.AppendLiteral("(Current: ");
text.AppendFormatted(sequence);
text.AppendLiteral(" - Reached!)");
ImGui.TextColored(in col2, text);
}
else
{
Vector4 col2 = ImGuiColors.ParsedBlue;
text = new ImU8String(11, 1);
text.AppendLiteral("(Current: ");
text.AppendFormatted(sequence);
text.AppendLiteral(")");
ImGui.TextColored(in col2, text);
}
}
if (flag2)
{
if (flag || flag3)
{
ImGui.Spacing();
}
ImGui.BulletText("Stop after completing any of these quests:");
ImGui.Indent();
foreach (ElementId item in _configuration.Stop.QuestsToStopAfter)
@ -415,7 +451,6 @@ internal sealed class ActiveQuestComponent
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(questProgressInfo.Tooltip);
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text(FontAwesomeIcon.Copy.ToIconString());

View file

@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Humanizer;
using Humanizer.Localisation;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
@ -20,9 +19,7 @@ namespace Questionable.Windows.QuestComponents;
internal sealed class EventInfoComponent
{
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc);
private readonly List<EventQuest> _eventQuests;
private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc, string? Patch);
private readonly QuestData _questData;
@ -38,42 +35,33 @@ internal sealed class EventInfoComponent
private readonly Configuration _configuration;
private readonly IDataManager _dataManager;
private List<IQuestInfo> _cachedActiveSeasonalQuests = new List<IQuestInfo>();
private DateTime _cachedAtUtc = DateTime.MinValue;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5L);
private readonly ILogger<EventInfoComponent> _logger;
private readonly HashSet<ushort> _alreadyLoggedActiveSeasonalSkip = new HashSet<ushort>();
public bool ShouldDraw
{
get
{
if (_configuration.General.ShowIncompleteSeasonalEvents)
if (!_configuration.General.ShowIncompleteSeasonalEvents)
{
return _eventQuests.Any(IsIncomplete);
}
return false;
}
UpdateCacheIfNeeded();
return _cachedActiveSeasonalQuests.Count > 0;
}
}
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration)
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, QuestTooltipComponent questTooltipComponent, Configuration configuration, IDataManager dataManager, ILogger<EventInfoComponent> logger)
{
int num = 2;
List<EventQuest> list = new List<EventQuest>(num);
CollectionsMarshal.SetCount(list, num);
Span<EventQuest> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
ref EventQuest reference = ref span[num2];
int num3 = 1;
List<ElementId> list2 = new List<ElementId>(num3);
CollectionsMarshal.SetCount(list2, num3);
CollectionsMarshal.AsSpan(list2)[0] = new UnlockLinkId(568);
reference = new EventQuest("Limited Time Items", list2, DateTime.MaxValue);
ref EventQuest reference2 = ref span[num2 + 1];
int num4 = 2;
List<ElementId> list3 = new List<ElementId>(num4);
CollectionsMarshal.SetCount(list3, num4);
Span<ElementId> span2 = CollectionsMarshal.AsSpan(list3);
num3 = 0;
span2[num3] = new QuestId(5297);
span2[num3 + 1] = new QuestId(5298);
reference2 = new EventQuest("The Rising 2025", list3, AtDailyReset(new DateOnly(2025, 9, 11)));
_eventQuests = list;
base._002Ector();
_questData = questData;
_questRegistry = questRegistry;
_questFunctions = questFunctions;
@ -81,39 +69,97 @@ internal sealed class EventInfoComponent
_questController = questController;
_questTooltipComponent = questTooltipComponent;
_configuration = configuration;
}
private static DateTime AtDailyReset(DateOnly date)
{
return new DateTime(date, new TimeOnly(14, 59), DateTimeKind.Utc);
_dataManager = dataManager;
_logger = logger ?? throw new ArgumentNullException("logger");
}
public void Draw()
{
foreach (EventQuest eventQuest in _eventQuests)
UpdateCacheIfNeeded();
foreach (IGrouping<string, IQuestInfo> item in _cachedActiveSeasonalQuests.GroupBy(delegate(IQuestInfo q)
{
if (IsIncomplete(eventQuest))
if (q.QuestId is UnlockLinkId)
{
return "Limited Unlocks";
}
if (_questRegistry.TryGetQuestFolderName(q.QuestId, out string folderName) && !string.IsNullOrEmpty(folderName))
{
return folderName;
}
return q.JournalGenre.HasValue ? GetJournalGenreName(q.JournalGenre.Value) : q.Name;
}))
{
if (item.All((IQuestInfo q) => _questFunctions.IsQuestComplete(q.QuestId)))
{
continue;
}
DateTime endsAtUtc = item.Select(delegate(IQuestInfo q)
{
DateTime? dateTime = (q as QuestInfo)?.SeasonalQuestExpiry ?? ((q is UnlockLinkQuestInfo unlockLinkQuestInfo) ? unlockLinkQuestInfo.QuestExpiry : ((DateTime?)null));
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
return NormalizeExpiry(valueOrDefault);
}
return DateTime.MaxValue;
}).DefaultIfEmpty(DateTime.MaxValue).Min();
List<string> list = (from q in item
select (q as UnlockLinkQuestInfo)?.Patch into p
where !string.IsNullOrEmpty(p)
select p).Distinct().ToList();
string patch = ((list.Count == 1) ? list[0] : null);
EventQuest eventQuest = new EventQuest(item.Key, item.Select((IQuestInfo q) => q.QuestId).ToList(), endsAtUtc, patch);
DrawEventQuest(eventQuest);
}
}
private string GetJournalGenreName(uint journalGenreId)
{
try
{
JournalGenre row = _dataManager.GetExcelSheet<JournalGenre>().GetRow(journalGenreId);
if (!row.Equals(default(JournalGenre)))
{
return row.Name.ExtractText();
}
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to get journal genre name for id {JournalGenreId}", journalGenreId);
}
return $"Event {journalGenreId}";
}
private void DrawEventQuest(EventQuest eventQuest)
{
string text = eventQuest.Name;
if (!string.IsNullOrEmpty(eventQuest.Patch))
{
text = text + " [" + eventQuest.Patch + "]";
}
if (eventQuest.EndsAtUtc != DateTime.MaxValue)
{
string value = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(1, CultureInfo.InvariantCulture, TimeUnit.Day, TimeUnit.Minute);
ImU8String text = new ImU8String(3, 2);
text.AppendFormatted(eventQuest.Name);
text.AppendLiteral(" (");
text.AppendFormatted(value);
text.AppendLiteral(")");
ImGui.Text(text);
TimeSpan timeSpan = eventQuest.EndsAtUtc - DateTime.UtcNow;
if (timeSpan < TimeSpan.Zero)
{
timeSpan = TimeSpan.Zero;
}
string value = FormatRemainingDays(timeSpan);
string text2 = FormatRemainingFull(timeSpan);
ImU8String text3 = new ImU8String(3, 2);
text3.AppendFormatted(text);
text3.AppendLiteral(" (");
text3.AppendFormatted(value);
text3.AppendLiteral(")");
ImGui.Text(text3);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(text2);
}
}
else
{
ImGui.Text(eventQuest.Name);
ImGui.Text(text);
}
List<ElementId> list = eventQuest.QuestIds.Where((ElementId x) => _questRegistry.IsKnownQuest(x) && _questFunctions.IsReadyToAcceptQuest(x) && x != _questController.StartedQuest?.Quest.Id && x != _questController.NextQuest?.Quest.Id).ToList();
foreach (ElementId questId in eventQuest.QuestIds)
@ -122,13 +168,13 @@ internal sealed class EventInfoComponent
{
continue;
}
ImU8String text = new ImU8String(21, 1);
text.AppendLiteral("##EventQuestSelection");
text.AppendFormatted(questId);
using (ImRaii.PushId(text))
ImU8String text3 = new ImU8String(21, 1);
text3.AppendLiteral("##EventQuestSelection");
text3.AppendFormatted(questId);
using (ImRaii.PushId(text3))
{
string name = _questData.GetQuestInfo(questId).Name;
if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Quest quest))
if (list.Contains(questId) && _questRegistry.TryGetQuest(questId, out Questionable.Model.Quest quest))
{
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
@ -157,18 +203,23 @@ internal sealed class EventInfoComponent
}
}
private bool IsIncomplete(EventQuest eventQuest)
{
if (eventQuest.EndsAtUtc <= DateTime.UtcNow)
{
return false;
}
return eventQuest.QuestIds.Any(ShouldShowQuest);
}
public IEnumerable<ElementId> GetCurrentlyActiveEventQuests()
{
return _eventQuests.Where((EventQuest x) => x.EndsAtUtc >= DateTime.UtcNow).SelectMany((EventQuest x) => x.QuestIds).Where(ShouldShowQuest);
UpdateCacheIfNeeded();
return (from q in _cachedActiveSeasonalQuests.Where(delegate(IQuestInfo q)
{
DateTime? dateTime = (q as QuestInfo)?.SeasonalQuestExpiry;
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
if (NormalizeExpiry(valueOrDefault) >= DateTime.UtcNow)
{
return true;
}
}
return (q is UnlockLinkQuestInfo { QuestExpiry: { } questExpiry } && NormalizeExpiry(questExpiry) >= DateTime.UtcNow) ? true : false;
})
select q.QuestId).Where(ShouldShowQuest);
}
private bool ShouldShowQuest(ElementId elementId)
@ -179,4 +230,208 @@ internal sealed class EventInfoComponent
}
return false;
}
private IEnumerable<IQuestInfo> GetActiveSeasonalQuestsNoCache()
{
IEnumerable<ElementId> allQuestIds = _questRegistry.GetAllQuestIds();
foreach (ElementId item in allQuestIds)
{
if (!_questData.TryGetQuestInfo(item, out IQuestInfo questInfo))
{
if (!_questRegistry.TryGetQuest(item, out Questionable.Model.Quest quest))
{
if (_alreadyLoggedActiveSeasonalSkip.Add(item.Value))
{
_logger.LogDebug("Skipping quest {QuestId}: no QuestInfo", item);
}
continue;
}
questInfo = quest.Info;
}
try
{
bool flag = false;
DateTime? dateTime = null;
if (questInfo is QuestInfo questInfo2)
{
flag = questInfo2.IsSeasonalQuest || questInfo2.IsSeasonalEvent || questInfo2.SeasonalQuestExpiry is DateTime || (questInfo2.JournalGenre >= 234 && questInfo2.JournalGenre <= 247);
dateTime = questInfo2.SeasonalQuestExpiry;
}
if (flag)
{
if (dateTime.HasValue)
{
DateTime valueOrDefault = dateTime.GetValueOrDefault();
DateTime dateTime2 = NormalizeExpiry(valueOrDefault);
_logger.LogInformation("Seasonal details: Quest {QuestId} '{Name}' rawExpiry={Raw:o} Kind={Kind} TimeOfDay={TimeOfDay} normalizedUtc={Normalized:o}", questInfo.QuestId, questInfo.Name, valueOrDefault, valueOrDefault.Kind, valueOrDefault.TimeOfDay, dateTime2);
}
else
{
_logger.LogInformation("Seasonal details: Quest {QuestId} '{Name}' has no expiry (seasonal flag present). IsSeasonalEvent={IsSeasonalEvent} IsSeasonalQuest={IsSeasonalQuest} JournalGenre={JournalGenre} SeasonalQuestExpiry={SeasonalQuestExpiry}", questInfo.QuestId, questInfo.Name, questInfo is QuestInfo questInfo3 && questInfo3.IsSeasonalEvent, questInfo is QuestInfo questInfo4 && questInfo4.IsSeasonalQuest, (questInfo is QuestInfo questInfo5) ? questInfo5.JournalGenre : ((uint?)null), (questInfo is QuestInfo questInfo6) ? questInfo6.SeasonalQuestExpiry : ((DateTime?)null));
}
}
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Failed to log seasonal details for {QuestId}", questInfo.QuestId);
}
if (_questFunctions.IsQuestUnobtainable(questInfo.QuestId))
{
if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping quest {QuestId} '{Name}': marked unobtainable", questInfo.QuestId, questInfo.Name);
}
}
else if (questInfo is UnlockLinkQuestInfo { QuestExpiry: var questExpiry })
{
if (questExpiry.HasValue)
{
DateTime valueOrDefault2 = questExpiry.GetValueOrDefault();
DateTime dateTime3 = NormalizeExpiry(valueOrDefault2);
if (dateTime3 > DateTime.UtcNow)
{
yield return questInfo;
}
else if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping UnlockLink quest {QuestId} '{Name}': expiry {Expiry:o} UTC is not in the future", questInfo.QuestId, questInfo.Name, dateTime3);
}
}
else
{
yield return questInfo;
}
}
else
{
if (!(questInfo is QuestInfo { SeasonalQuestExpiry: var seasonalQuestExpiry } questInfo7))
{
continue;
}
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault3 = seasonalQuestExpiry.GetValueOrDefault();
DateTime dateTime4 = NormalizeExpiry(valueOrDefault3);
if (dateTime4 > DateTime.UtcNow)
{
yield return questInfo;
}
else if (_alreadyLoggedActiveSeasonalSkip.Add(questInfo.QuestId.Value))
{
_logger.LogDebug("Skipping quest {QuestId} '{Name}': seasonal expiry {Expiry:o} UTC is not in the future", questInfo.QuestId, questInfo.Name, dateTime4);
}
}
else if (questInfo7.IsSeasonalQuest && !questInfo7.SeasonalQuestExpiry.HasValue)
{
yield return questInfo;
}
}
}
}
private void UpdateCacheIfNeeded()
{
if (DateTime.UtcNow - _cachedAtUtc < _cacheDuration)
{
return;
}
_cachedActiveSeasonalQuests = GetActiveSeasonalQuestsNoCache().ToList();
_cachedAtUtc = DateTime.UtcNow;
_logger.LogDebug("Refreshed seasonal quest cache: {Count} active seasonal quests (UTC now {UtcNow:o})", _cachedActiveSeasonalQuests.Count, _cachedAtUtc);
foreach (IGrouping<string, IQuestInfo> item in _cachedActiveSeasonalQuests.GroupBy(delegate(IQuestInfo q)
{
if (q.QuestId is UnlockLinkId)
{
return "Limited Unlocks";
}
if (_questRegistry.TryGetQuestFolderName(q.QuestId, out string folderName) && !string.IsNullOrEmpty(folderName))
{
return folderName;
}
return q.JournalGenre.HasValue ? GetJournalGenreName(q.JournalGenre.Value) : q.Name;
}))
{
DateTime dateTime = item.Select(delegate(IQuestInfo q)
{
DateTime? dateTime2 = (q as QuestInfo)?.SeasonalQuestExpiry ?? ((q is UnlockLinkQuestInfo unlockLinkQuestInfo) ? unlockLinkQuestInfo.QuestExpiry : ((DateTime?)null));
if (dateTime2.HasValue)
{
DateTime valueOrDefault = dateTime2.GetValueOrDefault();
return NormalizeExpiry(valueOrDefault);
}
return DateTime.MaxValue;
}).DefaultIfEmpty(DateTime.MaxValue).Min();
List<string> list = (from q in item
select (q as UnlockLinkQuestInfo)?.Patch into p
where !string.IsNullOrEmpty(p)
select p).Distinct().ToList();
string text = ((list.Count == 1) ? list[0] : null);
if (dateTime != DateTime.MaxValue)
{
_logger.LogInformation("Seasonal event '{Name}' ends at {Expiry:o} UTC (patch={Patch})", item.Key, dateTime, text ?? "n/a");
}
else
{
_logger.LogInformation("Seasonal event '{Name}' has no expiry (patch={Patch})", item.Key, text ?? "n/a");
}
}
}
public void RefreshAndLogSeasonalExpiries()
{
_cachedAtUtc = DateTime.MinValue;
UpdateCacheIfNeeded();
}
public static DateTime AtDailyReset(DateOnly date)
{
return new DateTime(date, new TimeOnly(14, 59, 59), DateTimeKind.Utc);
}
private static DateTime NormalizeExpiry(DateTime d)
{
TimeSpan timeOfDay = d.TimeOfDay;
TimeSpan timeSpan = new TimeSpan(23, 59, 59);
if (timeOfDay == TimeSpan.Zero || timeOfDay == timeSpan)
{
return AtDailyReset(DateOnly.FromDateTime(d));
}
if (d.Kind != DateTimeKind.Utc)
{
return d.ToUniversalTime();
}
return d;
}
private static string FormatRemainingDays(TimeSpan remaining)
{
int num = (int)Math.Ceiling(Math.Max(0.0, remaining.TotalSeconds));
int num2 = num / 86400;
if (num2 >= 1)
{
if (num2 != 1)
{
return $"{num2} days";
}
return "1 day";
}
int value = num % 86400 / 3600;
int value2 = num % 3600 / 60;
int value3 = num % 60;
return $"{value:D2}:{value2:D2}:{value3:D2}";
}
private static string FormatRemainingFull(TimeSpan remaining)
{
int num = (int)Math.Ceiling(Math.Max(0.0, remaining.TotalSeconds));
int num2 = num / 86400;
int value = num % 86400 / 3600;
int value2 = num % 3600 / 60;
int value3 = num % 60;
if (num2 < 1)
{
return $"Ends in {value:D2}d {value2:D2}m {value3:D2}s";
}
return $"Ends in {num2}d {value:D2}h {value2:D2}m {value3:D2}s";
}
}

View file

@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Questionable.Controller;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Windows.Utils;
namespace Questionable.Windows.QuestComponents;
internal sealed class ManualPriorityComponent
{
private readonly QuestController _questController;
private readonly QuestFunctions _questFunctions;
private readonly QuestSelector _questSelector;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface;
private ElementId? _draggedItem;
public ManualPriorityComponent(QuestController questController, QuestFunctions questFunctions, QuestSelector questSelector, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IChatGui chatGui, IDalamudPluginInterface pluginInterface)
{
ManualPriorityComponent manualPriorityComponent = this;
_questController = questController;
_questFunctions = questFunctions;
_questSelector = questSelector;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
_chatGui = chatGui;
_pluginInterface = pluginInterface;
_questSelector.SuggestionPredicate = (Quest quest) => !quest.Info.IsMainScenarioQuest && !questFunctions.IsQuestUnobtainable(quest.Id) && questController.ManualPriorityQuests.All((Quest x) => x.Id != quest.Id);
_questSelector.DefaultPredicate = (Quest quest) => questFunctions.IsQuestAccepted(quest.Id);
_questSelector.QuestSelected = delegate(Quest quest)
{
manualPriorityComponent._questController.ManualPriorityQuests.Add(quest);
};
}
public void Draw()
{
ImGui.TextWrapped("When you have an active Main Scenario Quest, Questionable will prioritise quests in this order:");
ImGui.BulletText("Priority quests: class quests, A Realm Reborn primals and raids");
ImGui.BulletText("Supported quests from your Journal's To-Do list (always visible on-screen quests)");
ImGui.BulletText("Main Scenario Quest (if available and not marked as ignored in your Journal)");
ImGui.TextWrapped("Without an active Main Scenario Quest, it will always try to pick up the next MSQ first.");
ImGui.Spacing();
ImGui.Text("Custom priority quests:");
_questSelector.DrawSelection();
ImGui.Spacing();
if (_questController.ManualPriorityQuests.Count > 0)
{
ImGui.Text("Priority queue (drag arrow buttons to reorder):");
}
using (ImRaii.IEndObject endObject = ImRaii.Child("ManualPriorityList", new Vector2(-1f, -27f), border: true))
{
if (endObject)
{
DrawQuestList();
}
}
List<ElementId> list = ParseClipboardItems();
using (ImRaii.Disabled(list.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import from Clipboard"))
{
ImportFromClipboard(list);
}
}
ImGui.SameLine();
using (ImRaii.Disabled(_questController.ManualPriorityQuests.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Upload, "Export to Clipboard"))
{
ExportToClipboard();
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove finished Quests"))
{
_questController.ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id));
}
ImGui.SameLine();
using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Clear All"))
{
_questController.ClearQuestPriority();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.SetTooltip("Hold CTRL to enable this button.");
}
}
}
private void DrawQuestList()
{
List<Quest> manualPriorityQuests = _questController.ManualPriorityQuests;
if (manualPriorityQuests.Count == 0)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextWrapped("No quests in priority queue. Add quests using the selector above to prioritise them over other available quests.");
ImGui.PopStyleColor();
return;
}
Quest quest = null;
Quest quest2 = null;
int index = 0;
float x = ImGui.GetContentRegionAvail().X;
List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>();
for (int i = 0; i < manualPriorityQuests.Count; i++)
{
Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f);
Quest quest3 = manualPriorityQuests[i];
ImU8String id = new ImU8String(5, 1);
id.AppendLiteral("Quest");
id.AppendFormatted(quest3.Id);
using (ImRaii.PushId(id))
{
ImGui.AlignTextToFramePadding();
id = new ImU8String(1, 1);
id.AppendFormatted(i + 1);
id.AppendLiteral(".");
ImGui.Text(id);
ImGui.SameLine();
(Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(quest3.Id);
bool flag;
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString());
flag = ImGui.IsItemHovered();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(quest3.Info.Name);
flag |= ImGui.IsItemHovered();
if (flag)
{
_questTooltipComponent.Draw(quest3.Info);
}
if (manualPriorityQuests.Count > 1)
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 4f - ImGui.GetStyle().ItemSpacing.X);
}
if (_draggedItem == quest3.Id)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
}
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = quest3.Id;
}
ImGui.SameLine();
}
else
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f);
}
}
if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
{
quest = quest3;
}
}
Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f);
list.Add((item, item2));
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = null;
}
else if (_draggedItem != null)
{
Quest item3 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
int num = manualPriorityQuests.IndexOf(item3);
var (pMin, pMax) = list[num];
ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f, ImDrawFlags.RoundCornersAll);
int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true));
if (num2 >= 0 && num != num2)
{
quest2 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
index = num2;
}
}
if (quest != null)
{
manualPriorityQuests.Remove(quest);
}
if (quest2 != null)
{
manualPriorityQuests.Remove(quest2);
manualPriorityQuests.Insert(index, quest2);
}
}
public string EncodeQuestPriority()
{
return "qst:priority:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(';', _questController.ManualPriorityQuests.Select((Quest x) => x.Id.ToString()))));
}
private static List<ElementId> ParseClipboardItems()
{
return PriorityWindow.DecodeQuestPriority(ImGui.GetClipboardText().Trim());
}
private void ExportToClipboard()
{
ImGui.SetClipboardText(EncodeQuestPriority());
_chatGui.Print("Copied quests to clipboard.", "Questionable", 576);
}
private void ImportFromClipboard(List<ElementId> questElements)
{
_questController.ImportQuestPriority(questElements);
}
}

View file

@ -0,0 +1,788 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Windows.QuestComponents;
internal sealed class PresetBuilderComponent
{
private sealed class QuestPreset
{
public required string Name { get; init; }
public required string Description { get; init; }
public required int DisplayOrder { get; init; }
public required List<ElementId> QuestIds { get; init; }
public List<ElementId> GetQuestIds()
{
return QuestIds;
}
}
private readonly QuestController _questController;
private readonly QuestFunctions _questFunctions;
private readonly QuestData _questData;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly UiUtils _uiUtils;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly QuestRegistry _questRegistry;
private readonly ILogger<PresetBuilderComponent> _logger;
private readonly Dictionary<string, QuestPreset> _availablePresets;
public PresetBuilderComponent(QuestController questController, QuestFunctions questFunctions, QuestData questData, AetheryteFunctions aetheryteFunctions, IClientState clientState, IChatGui chatGui, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestRegistry questRegistry, ILogger<PresetBuilderComponent> logger)
{
_questController = questController;
_questFunctions = questFunctions;
_questData = questData;
_aetheryteFunctions = aetheryteFunctions;
_clientState = clientState;
_chatGui = chatGui;
_uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent;
_questRegistry = questRegistry;
_logger = logger;
_availablePresets = BuildPresetList();
}
public void Draw()
{
ImGui.TextWrapped("Quest presets allow you to quickly add related quests to your priority list. These are useful for unlocking content that may be required later.");
ImGui.Spacing();
using (ImRaii.IEndObject endObject = ImRaii.Child("PresetList", new Vector2(-1f, -27f), border: true))
{
if (endObject)
{
DrawPresetGroups();
}
}
List<ElementId> list = (from questId in GetAllPresetQuests()
where _questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
select questId).ToList();
using (ImRaii.Disabled(list.Count == 0))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, $"Clear All Preset Quests ({list.Count})"))
{
ClearAllPresetQuests(list);
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
if (list.Count == 0)
{
ImGui.SetTooltip("No preset quests are currently in the priority list.");
return;
}
ImU8String tooltip = new ImU8String(59, 1);
tooltip.AppendLiteral("Remove all ");
tooltip.AppendFormatted(list.Count);
tooltip.AppendLiteral(" preset-related quest(s) from the priority list.");
ImGui.SetTooltip(tooltip);
}
}
private void DrawPresetGroups()
{
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable = from x in _availablePresets
where x.Key.StartsWith("aether_currents_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable2 = from x in _availablePresets
where x.Key.StartsWith("aethernet_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
IOrderedEnumerable<KeyValuePair<string, QuestPreset>> orderedEnumerable3 = from x in _availablePresets
where !x.Key.StartsWith("aether_currents_", StringComparison.Ordinal) && !x.Key.StartsWith("aethernet_", StringComparison.Ordinal)
orderby x.Value.DisplayOrder
select x;
string key;
QuestPreset value;
if (DrawGroupHeader("Aether Currents", "Unlock aether currents in various expansion zones to enable flying.", orderedEnumerable))
{
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item in orderedEnumerable)
{
item.Deconstruct(out key, out value);
string key2 = key;
QuestPreset preset = value;
DrawPreset(key2, preset);
}
}
}
if (DrawGroupHeader("City Aethernet", "Unlock aethernet shards in major cities to enable city teleports.", orderedEnumerable2))
{
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item2 in orderedEnumerable2)
{
item2.Deconstruct(out key, out value);
string key3 = key;
QuestPreset preset2 = value;
DrawPreset(key3, preset2);
}
}
}
if (!orderedEnumerable3.Any() || !DrawGroupHeader("Content Unlocks", "Essential quest series and unlocks that may be required for progression.", orderedEnumerable3))
{
return;
}
using (ImRaii.PushIndent())
{
foreach (KeyValuePair<string, QuestPreset> item3 in orderedEnumerable3)
{
item3.Deconstruct(out key, out value);
string key4 = key;
QuestPreset preset3 = value;
DrawPreset(key4, preset3);
}
}
}
private bool DrawGroupHeader(string groupName, string groupDescription, IEnumerable<KeyValuePair<string, QuestPreset>> presets)
{
int num = 0;
int num2 = 0;
int num3 = 0;
foreach (KeyValuePair<string, QuestPreset> preset2 in presets)
{
preset2.Deconstruct(out var _, out var value);
QuestPreset preset = value;
num += GetAvailableQuestsForPreset(preset).Count;
num2 += GetCompletedQuestsForPreset(preset).Count;
num3 += GetAlreadyPriorityQuestsForPreset(preset).Count;
}
string text = groupName;
if (num > 0 || num2 > 0 || num3 > 0)
{
List<string> list = new List<string>();
if (num > 0)
{
list.Add($"{num} available");
}
if (num3 > 0)
{
list.Add($"{num3} priority");
}
if (num2 > 0)
{
list.Add($"{num2} completed");
}
if (list.Count > 0)
{
text = text + " (" + string.Join(", ", list) + ")";
}
}
ImU8String label = new ImU8String(9, 2);
label.AppendFormatted(text);
label.AppendLiteral("###Group_");
label.AppendFormatted(groupName);
bool result = ImGui.CollapsingHeader(label);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(groupDescription);
ImGui.EndTooltip();
}
return result;
}
private void DrawPreset(string key, QuestPreset preset)
{
using (ImRaii.PushId(key))
{
List<ElementId> availableQuestsForPreset = GetAvailableQuestsForPreset(preset);
List<ElementId> completedQuestsForPreset = GetCompletedQuestsForPreset(preset);
List<ElementId> alreadyPriorityQuestsForPreset = GetAlreadyPriorityQuestsForPreset(preset);
string text = preset.Name;
if (availableQuestsForPreset.Count > 0 || completedQuestsForPreset.Count > 0 || alreadyPriorityQuestsForPreset.Count > 0)
{
List<string> list = new List<string>();
if (availableQuestsForPreset.Count > 0)
{
list.Add($"{availableQuestsForPreset.Count} available");
}
if (alreadyPriorityQuestsForPreset.Count > 0)
{
list.Add($"{alreadyPriorityQuestsForPreset.Count} priority");
}
if (completedQuestsForPreset.Count > 0)
{
list.Add($"{completedQuestsForPreset.Count} completed");
}
if (list.Count > 0)
{
text = text + " (" + string.Join(", ", list) + ")";
}
}
ImU8String label = new ImU8String(3, 2);
label.AppendFormatted(text);
label.AppendLiteral("###");
label.AppendFormatted(key);
bool num = ImGui.CollapsingHeader(label);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted(preset.Description);
ImGui.EndTooltip();
}
if (!num)
{
return;
}
using (ImRaii.PushIndent())
{
ImGui.TextWrapped(preset.Description);
ImGui.Spacing();
bool flag = key.StartsWith("aethernet_", StringComparison.Ordinal);
if (flag && availableQuestsForPreset.Count == 0 && completedQuestsForPreset.Count == 0 && alreadyPriorityQuestsForPreset.Count == 0)
{
EAetheryteLocation? mainAetheryteForAethernetPreset = GetMainAetheryteForAethernetPreset(key);
if (mainAetheryteForAethernetPreset.HasValue && !_aetheryteFunctions.IsAetheryteUnlocked(mainAetheryteForAethernetPreset.Value))
{
_uiUtils.ChecklistItem("Main aetheryte must be attuned first", ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle);
ImGui.Spacing();
}
}
if (availableQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{availableQuestsForPreset.Count} {((availableQuestsForPreset.Count == 1) ? "quest" : "quests")} available", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
}
if (alreadyPriorityQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{alreadyPriorityQuestsForPreset.Count} {((alreadyPriorityQuestsForPreset.Count == 1) ? "quest" : "quests")} already in priority list", ImGuiColors.DalamudOrange, FontAwesomeIcon.PersonWalkingArrowRight);
}
if (completedQuestsForPreset.Count > 0)
{
_uiUtils.ChecklistItem($"{completedQuestsForPreset.Count} {((completedQuestsForPreset.Count == 1) ? "quest" : "quests")} already completed", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
}
if (availableQuestsForPreset.Count == 0 && completedQuestsForPreset.Count == 0 && alreadyPriorityQuestsForPreset.Count == 0)
{
if (!flag)
{
goto IL_03b8;
}
if (flag)
{
EAetheryteLocation? mainAetheryteForAethernetPreset2 = GetMainAetheryteForAethernetPreset(key);
if (mainAetheryteForAethernetPreset2.HasValue && _aetheryteFunctions.IsAetheryteUnlocked(mainAetheryteForAethernetPreset2.Value))
{
goto IL_03b8;
}
}
}
goto IL_03d8;
IL_03d8:
if (availableQuestsForPreset.Count > 0)
{
ImGui.Spacing();
string text2 = ((availableQuestsForPreset.Count == 1) ? "Add Quest to Priority" : $"Add All {availableQuestsForPreset.Count} Quests to Priority");
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, text2))
{
AddPresetToPriority(preset, availableQuestsForPreset);
}
}
if (availableQuestsForPreset.Count > 0 || completedQuestsForPreset.Count > 0 || alreadyPriorityQuestsForPreset.Count > 0)
{
ImGui.Spacing();
ImGui.Separator();
foreach (ElementId item in availableQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item, out IQuestInfo questInfo))
{
if (_uiUtils.ChecklistItem($"{questInfo.Name} ({item})", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running))
{
_questTooltipComponent.Draw(questInfo);
}
ImGui.SameLine(0f, 5f);
if (ImGuiComponents.IconButton($"##AddQuest{item}", FontAwesomeIcon.Plus))
{
AddIndividualQuestToPriority(questInfo, item);
}
if (ImGui.IsItemHovered())
{
label = new ImU8String(23, 1);
label.AppendLiteral("Add '");
label.AppendFormatted(questInfo.Name);
label.AppendLiteral("' to priority list");
ImGui.SetTooltip(label);
}
}
}
foreach (ElementId item2 in alreadyPriorityQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item2, out IQuestInfo questInfo2) && _uiUtils.ChecklistItem($"{questInfo2.Name} ({item2})", ImGuiColors.DalamudOrange, FontAwesomeIcon.PersonWalkingArrowRight))
{
_questTooltipComponent.Draw(questInfo2);
}
}
foreach (ElementId item3 in completedQuestsForPreset)
{
if (_questData.TryGetQuestInfo(item3, out IQuestInfo questInfo3) && _uiUtils.ChecklistItem($"{questInfo3.Name} ({item3})", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check))
{
_questTooltipComponent.Draw(questInfo3);
}
}
}
ImGui.Spacing();
return;
IL_03b8:
_uiUtils.ChecklistItem("No applicable quests found", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
goto IL_03d8;
}
}
}
private List<ElementId> GetAvailableQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questFunctions.IsReadyToAcceptQuest(questId) || _questFunctions.IsQuestAccepted(questId)
where !_questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
where _questRegistry.IsKnownQuest(questId)
select questId).ToList();
}
private List<ElementId> GetCompletedQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questFunctions.IsQuestComplete(questId)
select questId).ToList();
}
private List<ElementId> GetAlreadyPriorityQuestsForPreset(QuestPreset preset)
{
return (from questId in preset.GetQuestIds()
where _questController.ManualPriorityQuests.Any((Quest q) => q.Id.Equals(questId))
where !_questFunctions.IsQuestComplete(questId)
select questId).ToList();
}
private List<ElementId> GetAllPresetQuests()
{
return _availablePresets.Values.SelectMany((QuestPreset preset) => preset.GetQuestIds()).Distinct().ToList();
}
private void ClearAllPresetQuests(List<ElementId> questsToRemove)
{
int num = 0;
foreach (ElementId questId in questsToRemove)
{
Quest quest = _questController.ManualPriorityQuests.FirstOrDefault((Quest q) => q.Id.Equals(questId));
if (quest != null)
{
_questController.ManualPriorityQuests.Remove(quest);
num++;
}
}
_logger.LogInformation("Removed {Count} preset quests from priority list", num);
}
private void AddPresetToPriority(QuestPreset preset, List<ElementId> questIds)
{
int num = 0;
foreach (ElementId questId in questIds)
{
if (_questController.AddQuestPriority(questId))
{
num++;
}
}
_logger.LogInformation("Added {Count} quests from preset '{PresetName}' to priority list", num, preset.Name);
}
private void AddIndividualQuestToPriority(IQuestInfo questInfo, ElementId questId)
{
if (_questController.AddQuestPriority(questId))
{
_logger.LogInformation("Added individual quest '{QuestName}' ({QuestId}) to priority list", questInfo.Name, questId);
}
}
private static Dictionary<string, QuestPreset> BuildPresetList()
{
return new Dictionary<string, QuestPreset>
{
["aether_currents_hw"] = new QuestPreset
{
Name = "Heavensward",
Description = "Unlock all missed aether currents in Heavensward zones to enable flying.",
DisplayOrder = 10,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Heavensward)
},
["aether_currents_sb"] = new QuestPreset
{
Name = "Stormblood",
Description = "Unlock all missed aether currents in Stormblood zones to enable flying.",
DisplayOrder = 11,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Stormblood)
},
["aether_currents_shb"] = new QuestPreset
{
Name = "Shadowbringers",
Description = "Unlock all missed aether currents in Shadowbringers zones to enable flying.",
DisplayOrder = 12,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Shadowbringers)
},
["aether_currents_ew"] = new QuestPreset
{
Name = "Endwalker",
Description = "Unlock all missed aether currents in Endwalker zones to enable flying.",
DisplayOrder = 13,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Endwalker)
},
["aether_currents_dt"] = new QuestPreset
{
Name = "Dawntrail",
Description = "Unlock all missed aether currents in Dawntrail zones to enable flying.",
DisplayOrder = 14,
QuestIds = GetAetherCurrentQuestsByExpansion(EExpansionVersion.Dawntrail)
},
["aethernet_limsa"] = new QuestPreset
{
Name = "Limsa Lominsa",
Description = "Unlock all aethernet shards in Limsa Lominsa for convenient city travel.",
DisplayOrder = 20,
QuestIds = GetAethernetQuestsByCity("Limsa")
},
["aethernet_gridania"] = new QuestPreset
{
Name = "Gridania",
Description = "Unlock all aethernet shards in Gridania for convenient city travel.",
DisplayOrder = 21,
QuestIds = GetAethernetQuestsByCity("Gridania")
},
["aethernet_uldah"] = new QuestPreset
{
Name = "Ul'dah",
Description = "Unlock all aethernet shards in Ul'dah for convenient city travel.",
DisplayOrder = 22,
QuestIds = GetAethernetQuestsByCity("Uldah")
},
["aethernet_goldsaucer"] = new QuestPreset
{
Name = "The Gold Saucer",
Description = "Unlock all aethernet shards in The Gold Saucer for convenient city travel.",
DisplayOrder = 23,
QuestIds = GetAethernetQuestsByCity("GoldSaucer")
},
["aethernet_ishgard"] = new QuestPreset
{
Name = "Ishgard",
Description = "Unlock all aethernet shards in Ishgard for convenient city travel.",
DisplayOrder = 24,
QuestIds = GetAethernetQuestsByCity("Ishgard")
},
["aethernet_idyllshire"] = new QuestPreset
{
Name = "Idyllshire",
Description = "Unlock all aethernet shards in Idyllshire for convenient city travel.",
DisplayOrder = 25,
QuestIds = GetAethernetQuestsByCity("Idyllshire")
},
["aethernet_rhalgrs_reach"] = new QuestPreset
{
Name = "Rhalgr's Reach",
Description = "Unlock all aethernet shards in Rhalgr's Reach for convenient city travel.",
DisplayOrder = 26,
QuestIds = GetAethernetQuestsByCity("Rhalgr's Reach")
},
["aethernet_kugane"] = new QuestPreset
{
Name = "Kugane",
Description = "Unlock all aethernet shards in Kugane for convenient city travel.",
DisplayOrder = 27,
QuestIds = GetAethernetQuestsByCity("Kugane")
},
["aethernet_doman_enclave"] = new QuestPreset
{
Name = "Doman Enclave",
Description = "Unlock all aethernet shards in Doman Enclave for convenient city travel.",
DisplayOrder = 28,
QuestIds = GetAethernetQuestsByCity("Doman Enclave")
},
["aethernet_the_crystarium"] = new QuestPreset
{
Name = "The Crystarium",
Description = "Unlock all aethernet shards in The Crystarium for convenient city travel.",
DisplayOrder = 29,
QuestIds = GetAethernetQuestsByCity("The Crystarium")
},
["aethernet_eulmore"] = new QuestPreset
{
Name = "Eulmore",
Description = "Unlock all aethernet shards in Eulmore for convenient city travel.",
DisplayOrder = 30,
QuestIds = GetAethernetQuestsByCity("Eulmore")
},
["aethernet_old_sharlayan"] = new QuestPreset
{
Name = "Old Sharlayan",
Description = "Unlock all aethernet shards in Old Sharlayan for convenient city travel.",
DisplayOrder = 31,
QuestIds = GetAethernetQuestsByCity("Old Sharlayan")
},
["aethernet_radz_at_han"] = new QuestPreset
{
Name = "Radz-at-Han",
Description = "Unlock all aethernet shards in Radz-at-Han for convenient city travel.",
DisplayOrder = 32,
QuestIds = GetAethernetQuestsByCity("Radz-at-Han")
},
["aethernet_tuliyollal"] = new QuestPreset
{
Name = "Tuliyollal",
Description = "Unlock all aethernet shards in Tuliyollal for convenient city travel.",
DisplayOrder = 33,
QuestIds = GetAethernetQuestsByCity("Tuliyollal")
},
["aethernet_solution_nine"] = new QuestPreset
{
Name = "Solution Nine",
Description = "Unlock all aethernet shards in Solution Nine for convenient city travel.",
DisplayOrder = 34,
QuestIds = GetAethernetQuestsByCity("Solution Nine")
},
["crystal_tower"] = new QuestPreset
{
Name = "Crystal Tower Raids",
Description = "Complete the Crystal Tower raid series (required for A Realm Reborn MSQ).",
DisplayOrder = 40,
QuestIds = QuestData.CrystalTowerQuests.Cast<ElementId>().ToList()
},
["hard_primals"] = new QuestPreset
{
Name = "A Realm Reborn Hard Mode Primals",
Description = "Unlock hard mode primal fights from A Realm Reborn.",
DisplayOrder = 41,
QuestIds = QuestData.HardModePrimals.Cast<ElementId>().ToList()
}
};
}
private static List<ElementId> GetAetherCurrentQuestsByExpansion(EExpansionVersion expansion)
{
uint[] territoryRanges = expansion switch
{
EExpansionVersion.Heavensward => new uint[5] { 397u, 398u, 399u, 400u, 401u },
EExpansionVersion.Stormblood => new uint[6] { 612u, 613u, 614u, 620u, 621u, 622u },
EExpansionVersion.Shadowbringers => new uint[6] { 813u, 814u, 815u, 816u, 817u, 818u },
EExpansionVersion.Endwalker => new uint[6] { 956u, 957u, 958u, 959u, 960u, 961u },
EExpansionVersion.Dawntrail => new uint[6] { 1187u, 1188u, 1189u, 1190u, 1191u, 1192u },
_ => Array.Empty<uint>(),
};
return QuestData.AetherCurrentQuestsByTerritory.Where<KeyValuePair<uint, ImmutableList<QuestId>>>((KeyValuePair<uint, ImmutableList<QuestId>> kvp) => territoryRanges.Contains(kvp.Key)).SelectMany((KeyValuePair<uint, ImmutableList<QuestId>> kvp) => kvp.Value).Cast<ElementId>()
.ToList();
}
private static List<ElementId> GetAethernetQuestsByCity(string cityName)
{
switch (cityName)
{
case "Limsa":
{
int num = 1;
List<ElementId> list15 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list15, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list15);
int index = 0;
span[index] = new AethernetId(1);
return list15;
}
case "Gridania":
{
int index = 1;
List<ElementId> list14 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list14, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list14);
int num = 0;
span[num] = new AethernetId(2);
return list14;
}
case "Uldah":
{
int num = 1;
List<ElementId> list13 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list13, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list13);
int index = 0;
span[index] = new AethernetId(3);
return list13;
}
case "GoldSaucer":
{
int index = 1;
List<ElementId> list12 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list12, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list12);
int num = 0;
span[num] = new AethernetId(4);
return list12;
}
case "Ishgard":
{
int num = 1;
List<ElementId> list11 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list11, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list11);
int index = 0;
span[index] = new AethernetId(5);
return list11;
}
case "Idyllshire":
{
int index = 1;
List<ElementId> list10 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list10, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list10);
int num = 0;
span[num] = new AethernetId(6);
return list10;
}
case "Rhalgr's Reach":
{
int num = 1;
List<ElementId> list9 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list9, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list9);
int index = 0;
span[index] = new AethernetId(7);
return list9;
}
case "Kugane":
{
int index = 1;
List<ElementId> list8 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list8, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list8);
int num = 0;
span[num] = new AethernetId(8);
return list8;
}
case "Doman Enclave":
{
int num = 1;
List<ElementId> list7 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list7, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list7);
int index = 0;
span[index] = new AethernetId(9);
return list7;
}
case "The Crystarium":
{
int index = 1;
List<ElementId> list6 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list6, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list6);
int num = 0;
span[num] = new AethernetId(10);
return list6;
}
case "Eulmore":
{
int num = 1;
List<ElementId> list5 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list5, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list5);
int index = 0;
span[index] = new AethernetId(11);
return list5;
}
case "Old Sharlayan":
{
int index = 1;
List<ElementId> list4 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list4, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list4);
int num = 0;
span[num] = new AethernetId(12);
return list4;
}
case "Radz-at-Han":
{
int num = 1;
List<ElementId> list3 = new List<ElementId>(num);
CollectionsMarshal.SetCount(list3, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list3);
int index = 0;
span[index] = new AethernetId(13);
return list3;
}
case "Tuliyollal":
{
int index = 1;
List<ElementId> list2 = new List<ElementId>(index);
CollectionsMarshal.SetCount(list2, index);
Span<ElementId> span = CollectionsMarshal.AsSpan(list2);
int num = 0;
span[num] = new AethernetId(14);
return list2;
}
case "Solution Nine":
{
int num = 1;
List<ElementId> list = new List<ElementId>(num);
CollectionsMarshal.SetCount(list, num);
Span<ElementId> span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = new AethernetId(15);
return list;
}
default:
return new List<ElementId>();
}
}
private static EAetheryteLocation? GetMainAetheryteForAethernetPreset(string presetKey)
{
return presetKey switch
{
"aethernet_limsa" => EAetheryteLocation.Limsa,
"aethernet_gridania" => EAetheryteLocation.Gridania,
"aethernet_uldah" => EAetheryteLocation.Uldah,
"aethernet_goldsaucer" => EAetheryteLocation.GoldSaucer,
"aethernet_ishgard" => EAetheryteLocation.Ishgard,
"aethernet_idyllshire" => EAetheryteLocation.Idyllshire,
"aethernet_rhalgrs_reach" => EAetheryteLocation.RhalgrsReach,
"aethernet_kugane" => EAetheryteLocation.Kugane,
"aethernet_doman_enclave" => EAetheryteLocation.DomanEnclave,
"aethernet_the_crystarium" => EAetheryteLocation.Crystarium,
"aethernet_eulmore" => EAetheryteLocation.Eulmore,
"aethernet_old_sharlayan" => EAetheryteLocation.OldSharlayan,
"aethernet_radz_at_han" => EAetheryteLocation.RadzAtHan,
"aethernet_tuliyollal" => EAetheryteLocation.Tuliyollal,
"aethernet_solution_nine" => EAetheryteLocation.SolutionNine,
_ => null,
};
}
}

View file

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Questionable.Data;
using Questionable.Validation;
namespace Questionable.Windows.QuestComponents;
internal sealed class QuestValidationComponent
{
private readonly QuestValidator _questValidator;
private readonly QuestData _questData;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly ValidationDetailsRenderer _detailsRenderer;
public bool ShouldDraw => _questValidator.Issues.Count > 0;
public QuestValidationComponent(QuestValidator questValidator, QuestData questData, IDalamudPluginInterface pluginInterface)
{
_questValidator = questValidator;
_questData = questData;
_pluginInterface = pluginInterface;
_detailsRenderer = new ValidationDetailsRenderer(questData, pluginInterface);
}
public void Draw()
{
DrawSummaryHeader();
ImGui.Separator();
DrawValidationTable();
_detailsRenderer.DrawDetailWindows();
}
private void DrawSummaryHeader()
{
int issueCount = _questValidator.IssueCount;
int errorCount = _questValidator.ErrorCount;
int num = issueCount - errorCount;
ImU8String text = new ImU8String(33, 1);
text.AppendLiteral("Validation Results: ");
text.AppendFormatted(issueCount);
text.AppendLiteral(" total issues");
ImGui.Text(text);
if (errorCount > 0)
{
ImGui.SameLine();
ImGui.Text("(");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
text = new ImU8String(7, 1);
text.AppendFormatted(errorCount);
text.AppendLiteral(" errors");
ImGui.Text(text);
}
}
if (num > 0)
{
if (errorCount > 0)
{
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
{
text = new ImU8String(12, 1);
text.AppendLiteral(", ");
text.AppendFormatted(num);
text.AppendLiteral(" warnings)");
ImGui.Text(text);
return;
}
}
ImGui.SameLine();
ImGui.Text("(");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
{
text = new ImU8String(10, 1);
text.AppendFormatted(num);
text.AppendLiteral(" warnings)");
ImGui.Text(text);
return;
}
}
if (errorCount > 0)
{
ImGui.SameLine();
ImGui.Text(")");
}
}
private void DrawValidationTable()
{
using ImRaii.IEndObject endObject = ImRaii.Table("ValidationIssues", 6, ImGuiTableFlags.Borders | ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY);
if (!(!endObject))
{
ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, 60f);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 250f);
ImGui.TableSetupColumn("Seq", ImGuiTableColumnFlags.WidthFixed, 40f);
ImGui.TableSetupColumn("Step", ImGuiTableColumnFlags.WidthFixed, 40f);
ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 60f);
ImGui.TableHeadersRow();
IReadOnlyList<ValidationIssue> issues = _questValidator.Issues;
for (int i = 0; i < issues.Count; i++)
{
DrawValidationRow(issues[i], i);
}
}
}
private void DrawValidationRow(ValidationIssue issue, int index)
{
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted(issue.ElementId?.ToString() ?? string.Empty);
}
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted((issue.ElementId != null) ? _questData.GetQuestInfo(issue.ElementId).Name : issue.AlliedSociety.ToString());
}
if (ImGui.TableNextColumn())
{
if (issue.Sequence.HasValue)
{
ImGui.TextUnformatted(issue.Sequence.Value.ToString(CultureInfo.InvariantCulture));
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2))
{
ImGui.TextUnformatted("\ufffd");
}
}
}
if (ImGui.TableNextColumn())
{
if (issue.Step.HasValue)
{
ImGui.TextUnformatted(issue.Step.Value.ToString(CultureInfo.InvariantCulture));
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2))
{
ImGui.TextUnformatted("\ufffd");
}
}
}
if (ImGui.TableNextColumn())
{
DrawIssueCell(issue, index);
}
if (ImGui.TableNextColumn())
{
DrawActionsCell(issue, index);
}
}
private void DrawIssueCell(ValidationIssue issue, int index)
{
Vector4 color = ((issue.Severity == EIssueSeverity.Error) ? ImGuiColors.DalamudRed : ImGuiColors.DalamudOrange);
FontAwesomeIcon icon = ((issue.Severity == EIssueSeverity.Error) ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextUnformatted(icon.ToIconString());
}
}
ImGui.SameLine();
string issueSummary = GetIssueSummary(issue);
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextWrapped(issueSummary);
}
}
private void DrawActionsCell(ValidationIssue issue, int index)
{
if (HasDetailedDescription(issue) && ImGui.SmallButton($"Details##{index}"))
{
_detailsRenderer.OpenDetails(issue, index);
}
}
private static string GetIssueSummary(ValidationIssue issue)
{
string text = ValidationDetailsRenderer.CleanJsonText(issue.Description ?? string.Empty);
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
string[] array = text.Split(':', 2);
if (array.Length == 0)
{
return text;
}
return array[0];
}
if (issue.Type == EIssueType.InvalidJsonSchema)
{
string[] array2 = text.Split('\n', 2, StringSplitOptions.RemoveEmptyEntries);
if (array2.Length == 0)
{
return text;
}
return array2[0];
}
if (issue.Type == EIssueType.InvalidJsonSyntax)
{
string[] array3 = text.Split('\n', 2, StringSplitOptions.RemoveEmptyEntries);
if (array3.Length == 0)
{
return text;
}
return array3[0];
}
string[] array4 = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (array4.Length <= 2)
{
return text;
}
return array4[0] + ((array4.Length > 1) ? "..." : "");
}
private static bool HasDetailedDescription(ValidationIssue issue)
{
string text = issue.Description ?? string.Empty;
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
return true;
}
if (issue.Type == EIssueType.InvalidJsonSchema)
{
return true;
}
if (issue.Type == EIssueType.InvalidJsonSyntax)
{
return true;
}
if (!text.Contains('\n', StringComparison.Ordinal))
{
return text.Length > 100;
}
return true;
}
}

View file

@ -0,0 +1,567 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Validation;
namespace Questionable.Windows.QuestComponents;
internal sealed class ValidationDetailsRenderer
{
private sealed record JsonValidationError
{
public string Path { get; init; } = string.Empty;
public List<string> Messages { get; init; } = new List<string>();
}
private readonly QuestData _questData;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Dictionary<int, ValidationIssue> _storedIssues = new Dictionary<int, ValidationIssue>();
private readonly Dictionary<int, bool> _openDetailWindows = new Dictionary<int, bool>();
private static readonly Regex JsonPropertyPathRegex = new Regex("#/([^:]*)", RegexOptions.Compiled);
private static readonly Regex UnicodeEscapeRegex = new Regex("\\\\u([0-9A-Fa-f]{4})", RegexOptions.Compiled);
private static readonly Regex JsonEscapeRegex = new Regex("\\\\(.)", RegexOptions.Compiled);
private static readonly Regex ConsecutiveQuotesRegex = new Regex("\"{2,}", RegexOptions.Compiled);
public ValidationDetailsRenderer(QuestData questData, IDalamudPluginInterface pluginInterface)
{
_questData = questData;
_pluginInterface = pluginInterface;
}
public static string CleanJsonText(string text)
{
if (string.IsNullOrEmpty(text))
{
return text;
}
text = UnicodeEscapeRegex.Replace(text, (Match match) => int.TryParse(match.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result) ? ((char)result).ToString() : match.Value);
text = JsonEscapeRegex.Replace(text, (Match match) => match.Groups[1].Value switch
{
"\"" => "\"",
"\\" => "\\",
"/" => "/",
"b" => "\b",
"f" => "\f",
"n" => "\n",
"r" => "\r",
"t" => "\t",
_ => match.Value,
});
if (text.Contains("\"\"", StringComparison.Ordinal))
{
text = ConsecutiveQuotesRegex.Replace(text, "\"");
text = text.Replace("Expected \"\"", "Expected \"\"", StringComparison.Ordinal);
}
return text;
}
public void OpenDetails(ValidationIssue issue, int index)
{
_storedIssues[index] = issue;
_openDetailWindows[index] = true;
}
public void DrawDetailWindows()
{
List<int> list = new List<int>();
foreach (KeyValuePair<int, bool> item in _openDetailWindows.ToList())
{
if (item.Value && _storedIssues.TryGetValue(item.Key, out ValidationIssue value))
{
string obj = $"Validation Details##{item.Key}";
bool open = true;
ImGui.SetNextWindowSize(new Vector2(800f, 600f), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSizeConstraints(new Vector2(500f, 300f), new Vector2(1200f, 800f));
if (ImGui.Begin(obj, ref open))
{
DrawIssueDetails(value);
ImGui.End();
}
if (!open)
{
list.Add(item.Key);
}
}
}
foreach (int item2 in list)
{
_openDetailWindows.Remove(item2);
_storedIssues.Remove(item2);
}
}
private void DrawIssueDetails(ValidationIssue issue)
{
Vector4 color = ((issue.Severity == EIssueSeverity.Error) ? ImGuiColors.DalamudRed : ImGuiColors.DalamudOrange);
FontAwesomeIcon icon = ((issue.Severity == EIssueSeverity.Error) ? FontAwesomeIcon.ExclamationTriangle : FontAwesomeIcon.InfoCircle);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImGui.TextUnformatted(icon.ToIconString());
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
ImU8String text = new ImU8String(2, 2);
text.AppendFormatted(issue.Severity);
text.AppendLiteral(": ");
text.AppendFormatted(issue.Type);
ImGui.Text(text);
}
ImGui.Separator();
if (issue.ElementId != null)
{
IQuestInfo questInfo = _questData.GetQuestInfo(issue.ElementId);
ImU8String text = new ImU8String(10, 2);
text.AppendLiteral("Quest: ");
text.AppendFormatted(issue.ElementId);
text.AppendLiteral(" - ");
text.AppendFormatted(questInfo.Name);
ImGui.Text(text);
}
else if (issue.AlliedSociety != EAlliedSociety.None)
{
ImU8String text = new ImU8String(16, 1);
text.AppendLiteral("Allied Society: ");
text.AppendFormatted(issue.AlliedSociety);
ImGui.Text(text);
}
if (issue.Sequence.HasValue)
{
ImU8String text = new ImU8String(10, 1);
text.AppendLiteral("Sequence: ");
text.AppendFormatted(issue.Sequence);
ImGui.Text(text);
}
if (issue.Step.HasValue)
{
ImU8String text = new ImU8String(6, 1);
text.AppendLiteral("Step: ");
text.AppendFormatted(issue.Step);
ImGui.Text(text);
}
ImGui.Separator();
ImGui.Text("Description:");
string description = CleanJsonText(issue.Description ?? "(no description)");
if (issue.Type == EIssueType.QuestDisabled && issue.ElementId == null)
{
DrawDisabledTribesDetails(description);
}
else if (issue.Type == EIssueType.InvalidJsonSchema)
{
DrawEnhancedJsonSchemaDetails(description);
}
else if (issue.Type == EIssueType.InvalidJsonSyntax)
{
DrawJsonSyntaxErrorDetails(description);
}
else
{
DrawGenericDetails(description);
}
}
private static void DrawJsonSyntaxErrorDetails(string description)
{
string[] array = description.Split('\n');
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (string.IsNullOrEmpty(text))
{
ImGui.Spacing();
}
else if (text.StartsWith("JSON parsing error", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith("This usually indicates", StringComparison.Ordinal) || text.StartsWith("Please check", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith("\ufffd ", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImGui.TextWrapped(text);
}
}
else
{
ImGui.TextWrapped(text);
}
}
}
private void DrawDisabledTribesDetails(string description)
{
string[] array = description.Split(':', 2);
if (array.Length < 2)
{
ImGui.TextWrapped(description);
return;
}
ImGui.TextWrapped(array[0]);
ImGui.Spacing();
List<string> list = (from x in array[1].Split(',', StringSplitOptions.RemoveEmptyEntries)
select x.Trim() into x
where !string.IsNullOrEmpty(x)
select x).Distinct().ToList();
if (list.Count == 0)
{
ImGui.TextWrapped("(no disabled quests listed)");
return;
}
ImGui.Text("Disabled Quests:");
ImGui.Indent();
Vector4[] array2 = new Vector4[6]
{
ImGuiColors.TankBlue,
ImGuiColors.HealerGreen,
ImGuiColors.DPSRed,
ImGuiColors.ParsedGreen,
ImGuiColors.ParsedBlue,
ImGuiColors.DalamudViolet
};
for (int num = 0; num < list.Count; num++)
{
string value = list[num];
Vector4 color = array2[num % array2.Length];
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
if (ElementId.TryFromString(value, out ElementId elementId) && elementId != null)
{
try
{
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
ImU8String text = new ImU8String(5, 2);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
text.AppendLiteral(" - ");
text.AppendFormatted(questInfo.Name);
ImGui.TextWrapped(text);
}
catch
{
ImU8String text = new ImU8String(18, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
text.AppendLiteral(" (unknown quest)");
ImGui.TextWrapped(text);
}
}
else
{
ImU8String text = new ImU8String(2, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(value);
ImGui.TextWrapped(text);
}
}
}
ImGui.Unindent();
}
private void DrawEnhancedJsonSchemaDetails(string description)
{
if (description.Split('\n').Length == 0)
{
ImGui.TextWrapped("No validation details available.");
return;
}
List<JsonValidationError> list = ParseJsonValidationErrors(description);
if (list.Count > 0)
{
ImGui.Text("JSON Schema Validation Errors:");
ImGui.Spacing();
for (int i = 0; i < list.Count; i++)
{
DrawJsonValidationError(list[i], i);
if (i < list.Count - 1)
{
ImGui.Separator();
}
}
}
else
{
DrawSimpleJsonSchemaDetails(description);
}
}
private void DrawJsonValidationError(JsonValidationError error, int index)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImU8String text = new ImU8String(8, 1);
text.AppendLiteral("Error #");
text.AppendFormatted(index + 1);
text.AppendLiteral(":");
ImGui.Text(text);
}
ImGui.Indent(12f);
if (!string.IsNullOrEmpty(error.Path))
{
ImGui.AlignTextToFramePadding();
ImGui.Text("Location:");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.TextWrapped(FormatJsonPath(error.Path));
}
}
if (error.Messages.Count > 0)
{
ImGui.AlignTextToFramePadding();
ImGui.Text((error.Messages.Count == 1) ? "Issue:" : "Issues:");
foreach (string message in error.Messages)
{
ImGui.Indent(12f);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
string text2 = CleanJsonText(message);
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
{
text2 = "JSON schema validation failed - check that all properties match the expected format and values";
}
ImU8String text = new ImU8String(2, 1);
text.AppendLiteral("\ufffd ");
text.AppendFormatted(text2);
ImGui.TextWrapped(text);
ImGui.Unindent(12f);
}
}
}
List<string> validationSuggestions = GetValidationSuggestions(error);
if (validationSuggestions.Count > 0)
{
ImGui.Spacing();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.Text("Suggestions:");
foreach (string item in validationSuggestions)
{
ImGui.Indent(12f);
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow))
{
ImGui.Text(FontAwesomeIcon.Lightbulb.ToIconString());
}
}
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen))
{
ImGui.TextWrapped(item);
ImGui.Unindent(12f);
}
}
}
}
ImGui.Unindent(12f);
}
private static void DrawSimpleJsonSchemaDetails(string description)
{
string[] array = description.Split('\n');
foreach (string text in array)
{
if (text.StartsWith("JSON Validation failed:", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.StartsWith(" - ", StringComparison.Ordinal))
{
int num = text.IndexOf(':', 3);
if (num > 0)
{
string path = text.Substring(3, num - 3).Trim();
string text2 = CleanJsonText(text.Substring(num + 1).Trim());
if (string.Equals(text2, "validation failed", StringComparison.OrdinalIgnoreCase))
{
text2 = "Schema validation failed - check property format and values";
}
ImGui.Text("\ufffd");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.Text(FormatJsonPath(path));
}
ImGui.SameLine();
ImGui.Text(":");
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text2);
}
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(CleanJsonText(text));
}
}
}
else
{
ImGui.TextWrapped(CleanJsonText(text));
}
}
}
private static List<JsonValidationError> ParseJsonValidationErrors(string description)
{
List<JsonValidationError> list = new List<JsonValidationError>();
string[] array = description.Split('\n');
foreach (string text in array)
{
if (!text.StartsWith(" - ", StringComparison.Ordinal))
{
continue;
}
int num = text.IndexOf(':', 3);
if (num > 0)
{
string path = text.Substring(3, num - 3).Trim();
List<string> messages = (from m in text.Substring(num + 1).Trim().Split(';', StringSplitOptions.RemoveEmptyEntries)
select CleanJsonText(m.Trim()) into m
where !string.IsNullOrEmpty(m)
select m).ToList();
list.Add(new JsonValidationError
{
Path = path,
Messages = messages
});
}
}
return list;
}
private static string FormatJsonPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return "<root>";
}
Match match = JsonPropertyPathRegex.Match(path);
if (match.Success)
{
string value = match.Groups[1].Value;
if (string.IsNullOrEmpty(value))
{
return "<root>";
}
return value.Replace('/', '.');
}
if (!(path == "<root>"))
{
return path;
}
return "<root>";
}
private static List<string> GetValidationSuggestions(JsonValidationError error)
{
List<string> list = new List<string>();
foreach (string message in error.Messages)
{
string text = message.ToUpperInvariant();
if (text.Contains("REQUIRED", StringComparison.Ordinal))
{
list.Add("Add the missing required property to your JSON.");
}
else if (text.Contains("TYPE", StringComparison.Ordinal))
{
list.Add("Check that the property value has the correct data type (string, number, boolean, etc.).");
}
else if (text.Contains("ENUM", StringComparison.Ordinal) || text.Contains("ALLOWED VALUES", StringComparison.Ordinal))
{
list.Add("Use one of the allowed enumeration values for this property.");
}
else if (text.Contains("FORMAT", StringComparison.Ordinal))
{
list.Add("Ensure the property value follows the expected format.");
}
else if (text.Contains("MINIMUM", StringComparison.Ordinal) || text.Contains("MAXIMUM", StringComparison.Ordinal))
{
list.Add("Check that numeric values are within the allowed range.");
}
else if (text.Contains("ADDITIONAL", StringComparison.Ordinal) && text.Contains("NOT ALLOWED", StringComparison.Ordinal))
{
list.Add("Remove any extra properties that are not defined in the schema.");
}
else if (text.Contains("VALIDATION FAILED", StringComparison.Ordinal))
{
list.Add("Review the JSON structure and ensure all properties match the expected schema format.");
}
}
return list.Distinct().ToList();
}
private static void DrawGenericDetails(string description)
{
string[] array = description.Split('\n');
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (string.IsNullOrEmpty(text))
{
ImGui.Spacing();
}
else if (text.StartsWith("Error:", StringComparison.Ordinal) || text.StartsWith("Invalid", StringComparison.Ordinal) || text.StartsWith("Missing", StringComparison.Ordinal))
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextWrapped(text);
}
}
else if (text.Contains(':', StringComparison.Ordinal))
{
int num = text.IndexOf(':', StringComparison.Ordinal);
string text2 = text.Substring(0, num);
string text3 = text.Substring(num + 1).TrimStart();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.Text(text2 + ":");
}
ImGui.SameLine();
ImGui.TextWrapped(text3);
}
else
{
ImGui.TextWrapped(text);
}
}
}
}

View file

@ -1,12 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
@ -14,7 +10,6 @@ using Dalamud.Plugin.Services;
using LLib.ImGui;
using Questionable.Controller;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
using Questionable.Windows.QuestComponents;
using Questionable.Windows.Utils;
@ -23,198 +18,49 @@ namespace Questionable.Windows;
internal sealed class PriorityWindow : LWindow
{
private const string ClipboardPrefix = "qst:priority:";
public const string ClipboardPrefix = "qst:priority:";
private const string LegacyClipboardPrefix = "qst:v1:";
public const string LegacyClipboardPrefix = "qst:v1:";
private const char ClipboardSeparator = ';';
public const char ClipboardSeparator = ';';
private readonly QuestController _questController;
private readonly ManualPriorityComponent _manualPriorityComponent;
private readonly QuestFunctions _questFunctions;
private readonly PresetBuilderComponent _presetBuilderComponent;
private readonly QuestSelector _questSelector;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface;
private ElementId? _draggedItem;
public PriorityWindow(QuestController questController, QuestFunctions questFunctions, QuestSelector questSelector, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IChatGui chatGui, IDalamudPluginInterface pluginInterface)
public PriorityWindow(QuestController questController, QuestFunctions questFunctions, QuestSelector questSelector, QuestTooltipComponent questTooltipComponent, PresetBuilderComponent presetBuilderComponent, UiUtils uiUtils, IChatGui chatGui, IDalamudPluginInterface pluginInterface)
: base("Quest Priority###QuestionableQuestPriority")
{
PriorityWindow priorityWindow = this;
_questController = questController;
_questFunctions = questFunctions;
_questSelector = questSelector;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
_chatGui = chatGui;
_pluginInterface = pluginInterface;
_questSelector.SuggestionPredicate = (Quest quest) => !quest.Info.IsMainScenarioQuest && !questFunctions.IsQuestUnobtainable(quest.Id) && questController.ManualPriorityQuests.All((Quest x) => x.Id != quest.Id);
_questSelector.DefaultPredicate = (Quest quest) => questFunctions.IsQuestAccepted(quest.Id);
_questSelector.QuestSelected = delegate(Quest quest)
{
priorityWindow._questController.ManualPriorityQuests.Add(quest);
};
base.Size = new Vector2(400f, 400f);
_manualPriorityComponent = new ManualPriorityComponent(questController, questFunctions, questSelector, questTooltipComponent, uiUtils, chatGui, pluginInterface);
_presetBuilderComponent = presetBuilderComponent;
base.Size = new Vector2(500f, 500f);
base.SizeCondition = ImGuiCond.Once;
base.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(400f, 400f),
MaximumSize = new Vector2(400f, 999f)
MinimumSize = new Vector2(615f, 500f),
MaximumSize = new Vector2(800f, 1000f)
};
}
public override void DrawContent()
{
ImGui.Text("Quests to do first:");
_questSelector.DrawSelection();
DrawQuestList();
List<ElementId> list = ParseClipboardItems();
ImGui.BeginDisabled(list.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import from Clipboard"))
using ImRaii.IEndObject endObject = ImRaii.TabBar("PriorityTabs");
if (!endObject)
{
ImportFromClipboard(list);
return;
}
ImGui.EndDisabled();
ImGui.SameLine();
ImGui.BeginDisabled(_questController.ManualPriorityQuests.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Upload, "Export to Clipboard"))
using (ImRaii.IEndObject endObject2 = ImRaii.TabItem("Manual Priority"))
{
ExportToClipboard();
}
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove finished Quests"))
if (endObject2)
{
_questController.ManualPriorityQuests.RemoveAll((Quest q) => _questFunctions.IsQuestComplete(q.Id));
}
ImGui.SameLine();
using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Trash, "Clear All"))
{
_questController.ClearQuestPriority();
_manualPriorityComponent.Draw();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
using ImRaii.IEndObject endObject3 = ImRaii.TabItem("Quest Presets");
if (endObject3)
{
ImGui.SetTooltip("Hold CTRL to enable this button.");
_presetBuilderComponent.Draw();
}
ImGui.EndDisabled();
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextWrapped("If you have an active MSQ quest, Questionable will generally try to do:");
ImGui.BulletText("'Priority' quests: class quests, ARR primals, ARR raids");
ImGui.BulletText("Supported quests in your 'To-Do list'\n(quests from your Journal that are always on-screen)");
ImGui.BulletText("MSQ quest (if available, unless it is marked as 'ignored'\nin your Journal)");
ImGui.TextWrapped("If you don't have any active MSQ quest, it will always try to pick up the next quest in the MSQ first.");
}
private void DrawQuestList()
{
List<Quest> manualPriorityQuests = _questController.ManualPriorityQuests;
Quest quest = null;
Quest quest2 = null;
int index = 0;
float x = ImGui.GetContentRegionAvail().X;
List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>();
for (int i = 0; i < manualPriorityQuests.Count; i++)
{
Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f);
Quest quest3 = manualPriorityQuests[i];
ImU8String id = new ImU8String(5, 1);
id.AppendLiteral("Quest");
id.AppendFormatted(quest3.Id);
using (ImRaii.PushId(id))
{
(Vector4, FontAwesomeIcon, string) questStyle = _uiUtils.GetQuestStyle(quest3.Id);
bool flag;
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(in questStyle.Item1, questStyle.Item2.ToIconString());
flag = ImGui.IsItemHovered();
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.Text(quest3.Info.Name);
flag |= ImGui.IsItemHovered();
if (flag)
{
_questTooltipComponent.Draw(quest3.Info);
}
if (manualPriorityQuests.Count > 1)
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 4f - ImGui.GetStyle().ItemSpacing.X);
}
if (_draggedItem == quest3.Id)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
}
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = quest3.Id;
}
ImGui.SameLine();
}
else
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.SameLine(ImGui.GetContentRegionAvail().X + ImGui.GetStyle().WindowPadding.X - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - ImGui.GetStyle().FramePadding.X * 2f);
}
}
if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
{
quest = quest3;
}
}
Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f);
list.Add((item, item2));
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedItem = null;
}
else if (_draggedItem != null)
{
Quest item3 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
int num = manualPriorityQuests.IndexOf(item3);
var (pMin, pMax) = list[num];
ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f, ImDrawFlags.RoundCornersAll);
int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true));
if (num2 >= 0 && num != num2)
{
quest2 = manualPriorityQuests.Single((Quest quest4) => quest4.Id == _draggedItem);
index = num2;
}
}
if (quest != null)
{
manualPriorityQuests.Remove(quest);
}
if (quest2 != null)
{
manualPriorityQuests.Remove(quest2);
manualPriorityQuests.Insert(index, quest2);
}
}
private static List<ElementId> ParseClipboardItems()
{
return DecodeQuestPriority(ImGui.GetClipboardText().Trim());
}
public static List<ElementId> DecodeQuestPriority(string clipboardText)
@ -251,20 +97,4 @@ internal sealed class PriorityWindow : LWindow
}
return list;
}
public string EncodeQuestPriority()
{
return "qst:priority:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(';', _questController.ManualPriorityQuests.Select((Quest x) => x.Id.ToString()))));
}
private void ExportToClipboard()
{
ImGui.SetClipboardText(EncodeQuestPriority());
_chatGui.Print("Copied quests to clipboard.", "Questionable", 576);
}
private void ImportFromClipboard(List<ElementId> questElements)
{
_questController.ImportQuestPriority(questElements);
}
}

View file

@ -225,8 +225,10 @@ internal sealed class QuestSelectionWindow : LWindow
continue;
}
EInteractionType? eInteractionType = quest.FindSequence(0)?.LastStep()?.InteractionType;
if (eInteractionType.HasValue && eInteractionType == EInteractionType.AcceptQuest && _questFunctions.IsReadyToAcceptQuest(item.QuestId))
if (!eInteractionType.HasValue || eInteractionType != EInteractionType.AcceptQuest || !_questFunctions.IsReadyToAcceptQuest(item.QuestId))
{
continue;
}
ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null);
bool num2 = ImGuiComponents.IconButton(FontAwesomeIcon.Play);
if (ImGui.IsItemHovered())
@ -236,6 +238,10 @@ internal sealed class QuestSelectionWindow : LWindow
if (num2)
{
_questController.SetNextQuest(quest);
if (!_questController.ManualPriorityQuests.Contains(quest))
{
_questController.ManualPriorityQuests.Insert(0, quest);
}
_questController.Start("QuestSelectionWindow");
}
ImGui.SameLine();
@ -252,7 +258,6 @@ internal sealed class QuestSelectionWindow : LWindow
}
}
}
}
private void CopyToClipboard(IQuestInfo quest, bool suffix)
{

View file

@ -1,95 +1,48 @@
using System.Globalization;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Common.Math;
using LLib.ImGui;
using Questionable.Data;
using Questionable.Validation;
using Questionable.Windows.QuestComponents;
namespace Questionable.Windows;
internal sealed class QuestValidationWindow : LWindow
internal sealed class QuestValidationWindow : LWindow, IPersistableWindowConfig
{
private readonly QuestValidator _questValidator;
private readonly QuestData _questData;
private readonly IDalamudPluginInterface _pluginInterface;
public QuestValidationWindow(QuestValidator questValidator, QuestData questData, IDalamudPluginInterface pluginInterface)
private readonly Configuration _configuration;
private readonly QuestValidationComponent _questValidationComponent;
public WindowConfig WindowConfig => _configuration.QuestValidationWindowConfig;
public QuestValidationWindow(QuestValidationComponent questValidationComponent, IDalamudPluginInterface pluginInterface, Configuration configuration)
: base("Quest Validation###QuestionableValidator")
{
_questValidator = questValidator;
_questData = questData;
_questValidationComponent = questValidationComponent;
_pluginInterface = pluginInterface;
base.Size = new Vector2(600f, 200f);
base.SizeCondition = ImGuiCond.Once;
_configuration = configuration;
base.Size = new Vector2(800f, 400f);
base.SizeCondition = ImGuiCond.FirstUseEver;
base.SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600f, 200f)
MinimumSize = new Vector2(600f, 300f)
};
}
public void SaveWindowConfig()
{
_pluginInterface.SavePluginConfig(_configuration);
}
public override bool DrawConditions()
{
return _questValidationComponent.ShouldDraw;
}
public override void DrawContent()
{
using ImRaii.IEndObject endObject = ImRaii.Table("QuestSelection", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY);
if (!endObject)
{
ImGui.Text("Not table");
return;
}
ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthFixed, 50f);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 200f);
ImGui.TableSetupColumn("Seq", ImGuiTableColumnFlags.WidthFixed, 30f);
ImGui.TableSetupColumn("Step", ImGuiTableColumnFlags.WidthFixed, 30f);
ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.None, 200f);
ImGui.TableHeadersRow();
foreach (ValidationIssue issue in _questValidator.Issues)
{
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted(issue.ElementId?.ToString() ?? string.Empty);
}
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted((issue.ElementId != null) ? _questData.GetQuestInfo(issue.ElementId).Name : issue.AlliedSociety.ToString());
}
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted(issue.Sequence?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
}
if (ImGui.TableNextColumn())
{
ImGui.TextUnformatted(issue.Step?.ToString(CultureInfo.InvariantCulture) ?? string.Empty);
}
if (!ImGui.TableNextColumn())
{
continue;
}
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
{
if (issue.Severity == EIssueSeverity.Error)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationTriangle.ToIconString());
}
}
else
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
}
}
ImGui.SameLine();
ImGui.TextUnformatted(issue.Description);
}
_questValidationComponent.Draw();
}
}

View file

@ -51,7 +51,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig
public bool IsMinimized { get; set; }
public QuestWindow(IDalamudPluginInterface pluginInterface, QuestController questController, IClientState clientState, Configuration configuration, TerritoryData territoryData, ActiveQuestComponent activeQuestComponent, ARealmRebornComponent aRealmRebornComponent, EventInfoComponent eventInfoComponent, CreationUtilsComponent creationUtilsComponent, QuickAccessButtonsComponent quickAccessButtonsComponent, RemainingTasksComponent remainingTasksComponent, IFramework framework, InteractionUiController interactionUiController, ConfigWindow configWindow)
: base("Questionable v" + PluginVersion.ToString(4) + "###Questionable", ImGuiWindowFlags.AlwaysAutoResize)
: base("Questionable v" + PluginVersion.ToString(2) + "###Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{
QuestWindow questWindow = this;
_pluginInterface = pluginInterface;

View file

@ -4,6 +4,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Questionable.Controller;
using Questionable.Functions;
using Questionable.Model.Questing;
@ -13,11 +14,14 @@ internal sealed class UiUtils
{
private readonly QuestFunctions _questFunctions;
private readonly QuestRegistry _questRegistry;
private readonly IDalamudPluginInterface _pluginInterface;
public UiUtils(QuestFunctions questFunctions, IDalamudPluginInterface pluginInterface)
public UiUtils(QuestFunctions questFunctions, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface)
{
_questFunctions = questFunctions;
_questRegistry = questRegistry;
_pluginInterface = pluginInterface;
}
@ -47,7 +51,7 @@ internal sealed class UiUtils
{
return (Color: ImGuiColors.DalamudGrey, Icon: FontAwesomeIcon.Minus, Status: "Unobtainable");
}
if (_questFunctions.IsQuestLocked(elementId))
if (_questFunctions.IsQuestLocked(elementId) || !_questFunctions.IsReadyToAcceptQuest(elementId) || !_questRegistry.IsKnownQuest(elementId))
{
return (Color: ImGuiColors.DalamudRed, Icon: FontAwesomeIcon.Times, Status: "Locked");
}

View file

@ -62,17 +62,17 @@
<Reference Include="Microsoft.Extensions.Logging">
<HintPath>..\..\Microsoft.Extensions.Logging.dll</HintPath>
</Reference>
<Reference Include="Humanizer">
<HintPath>..\..\Humanizer.dll</HintPath>
<Reference Include="QuestPaths">
<HintPath>..\..\QuestPaths.dll</HintPath>
</Reference>
<Reference Include="GatheringPaths">
<HintPath>..\..\GatheringPaths.dll</HintPath>
</Reference>
<Reference Include="QuestPaths">
<HintPath>..\..\QuestPaths.dll</HintPath>
</Reference>
<Reference Include="Dalamud.Extensions.MicrosoftLogging">
<HintPath>..\..\Dalamud.Extensions.MicrosoftLogging.dll</HintPath>
</Reference>
<Reference Include="JsonPointer.Net">
<HintPath>..\..\JsonPointer.Net.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -34,7 +34,9 @@ internal sealed class Configuration : IPluginConfiguration
public bool AutoStepRefreshEnabled { get; set; } = true;
public int AutoStepRefreshDelaySeconds { get; set; } = 30;
public int AutoStepRefreshDelaySeconds { get; set; } = 10;
public bool HideSeasonalEventsFromJournalProgress { get; set; }
}
internal sealed class StopConfiguration
@ -47,6 +49,10 @@ internal sealed class Configuration : IPluginConfiguration
public bool LevelToStopAfter { get; set; }
public int TargetLevel { get; set; } = 50;
public bool SequenceToStopAfter { get; set; }
public int TargetSequence { get; set; } = 1;
}
internal sealed class DutyConfiguration
@ -105,10 +111,6 @@ internal sealed class Configuration : IPluginConfiguration
public bool SkipCrystalTowerRaids { get; set; }
public bool PreventQuestCompletion { get; set; }
public bool ShowWindowOnStart { get; set; }
public bool StartMinimized { get; set; }
}
internal enum ECombatModule
@ -159,6 +161,8 @@ internal sealed class Configuration : IPluginConfiguration
public WindowConfig ConfigWindowConfig { get; } = new WindowConfig();
public WindowConfig QuestValidationWindowConfig { get; set; } = new WindowConfig();
internal bool IsPluginSetupComplete()
{
return PluginSetupCompleteVersion == 5;

View file

@ -66,14 +66,6 @@ internal sealed class DalamudInitializer : IDisposable
_toastGui.Toast += OnToast;
_toastGui.ErrorToast += OnErrorToast;
_toastGui.QuestToast += OnQuestToast;
if (_configuration.Advanced.StartMinimized)
{
_questWindow.IsMinimized = true;
}
if (_configuration.Advanced.ShowWindowOnStart)
{
ToggleQuestWindow();
}
}
private void FrameworkUpdate(IFramework framework)

View file

@ -231,8 +231,11 @@ public sealed class QuestionablePlugin : IDalamudPlugin, IDisposable
serviceCollection.AddSingleton<ARealmRebornComponent>();
serviceCollection.AddSingleton<CreationUtilsComponent>();
serviceCollection.AddSingleton<EventInfoComponent>();
serviceCollection.AddSingleton<ManualPriorityComponent>();
serviceCollection.AddSingleton<PresetBuilderComponent>();
serviceCollection.AddSingleton<QuestTooltipComponent>();
serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
serviceCollection.AddSingleton<QuestValidationComponent>();
serviceCollection.AddSingleton<RemainingTasksComponent>();
serviceCollection.AddSingleton<QuestJournalUtils>();
serviceCollection.AddSingleton<QuestJournalComponent>();

View file

@ -0,0 +1,92 @@
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
namespace System.Text.RegularExpressions.Generated;
[GeneratedCode("System.Text.RegularExpressions.Generator", "9.0.12.41916")]
[SkipLocalsInit]
internal sealed class _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0 : Regex
{
private sealed class RunnerFactory : RegexRunnerFactory
{
private sealed class Runner : RegexRunner
{
protected override void Scan(ReadOnlySpan<char> inputSpan)
{
while (TryFindNextPossibleStartingPosition(inputSpan) && !TryMatchAtCurrentPosition(inputSpan) && runtextpos != inputSpan.Length)
{
runtextpos++;
if (_003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.s_hasTimeout)
{
CheckTimeout();
}
}
}
private bool TryFindNextPossibleStartingPosition(ReadOnlySpan<char> inputSpan)
{
int num = runtextpos;
if (num <= inputSpan.Length - 2)
{
ReadOnlySpan<char> readOnlySpan = inputSpan.Slice(num);
int num2;
for (num2 = 0; num2 < readOnlySpan.Length - 1; num2++)
{
int num3 = readOnlySpan.Slice(num2).IndexOfAny(_003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.s_whitespace);
if (num3 < 0)
{
break;
}
num2 += num3;
if ((uint)(num2 + 1) >= (uint)readOnlySpan.Length)
{
break;
}
if (char.IsWhiteSpace(readOnlySpan[num2 + 1]))
{
runtextpos = num + num2;
return true;
}
}
}
runtextpos = inputSpan.Length;
return false;
}
private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
{
int num = runtextpos;
int start = num;
ReadOnlySpan<char> readOnlySpan = inputSpan.Slice(num);
int i;
for (i = 0; (uint)i < (uint)readOnlySpan.Length && char.IsWhiteSpace(readOnlySpan[i]); i++)
{
}
if (i < 2)
{
return false;
}
readOnlySpan = readOnlySpan.Slice(i);
Capture(0, start, runtextpos = num + i);
return true;
}
}
protected override RegexRunner CreateInstance()
{
return new Runner();
}
}
internal static readonly _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0 Instance = new _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0();
private _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__MultipleWhitespaceRegex_0()
{
pattern = "\\s\\s+";
roptions = RegexOptions.IgnoreCase;
Regex.ValidateMatchTimeout(_003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.s_defaultTimeout);
internalMatchTimeout = _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities.s_defaultTimeout;
factory = new RunnerFactory();
capsize = 1;
}
}

View file

@ -0,0 +1,14 @@
using System.Buffers;
using System.CodeDom.Compiler;
namespace System.Text.RegularExpressions.Generated;
[GeneratedCode("System.Text.RegularExpressions.Generator", "9.0.12.41916")]
internal static class _003CRegexGenerator_g_003EFBB8301322196CF81C64F1652C2FA6E1D6BF3907141F781E9D97ABED51BF056C4__Utilities
{
internal static readonly TimeSpan s_defaultTimeout = ((AppContext.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") is TimeSpan timeSpan) ? timeSpan : Regex.InfiniteMatchTimeout);
internal static readonly bool s_hasTimeout = s_defaultTimeout != Regex.InfiniteMatchTimeout;
internal static readonly SearchValues<char> s_whitespace = SearchValues.Create("\t\n\v\f\r \u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000");
}