muffin v6.12
This commit is contained in:
parent
cfb4dea47e
commit
c8197297b2
58 changed files with 40038 additions and 58059 deletions
|
@ -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",
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -73,7 +73,7 @@ public sealed class InteractionTypeConverter : EnumConverter<EInteractionType>
|
|||
},
|
||||
{
|
||||
EInteractionType.WaitForObjectAtPosition,
|
||||
"WaitForNpcAtPosition"
|
||||
"WaitForObjectAtPosition"
|
||||
},
|
||||
{
|
||||
EInteractionType.WaitForManualProgress,
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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++)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
43
Questionable/Questionable.Model/AetherCurrentQuestInfo.cs
Normal file
43
Questionable/Questionable.Model/AetherCurrentQuestInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
43
Questionable/Questionable.Model/AethernetQuestInfo.cs
Normal file
43
Questionable/Questionable.Model/AethernetQuestInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@ internal interface IQuestInfo
|
|||
|
||||
bool IsRepeatable { get; }
|
||||
|
||||
bool IsSeasonalQuest => false;
|
||||
|
||||
DateTime? SeasonalQuestExpiry => null;
|
||||
|
||||
ImmutableList<PreviousQuestInfo> PreviousQuests { get; }
|
||||
|
||||
EQuestJoin PreviousQuestJoin { get; }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 += " ";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ public enum EIssueType
|
|||
{
|
||||
None,
|
||||
InvalidJsonSchema,
|
||||
InvalidJsonSyntax,
|
||||
MissingSequence0,
|
||||
MissingSequence,
|
||||
DuplicateSequence,
|
||||
|
|
|
@ -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}"
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue