--- /dev/null
+{
+ "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/QuestPaths/quest-v1.json",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1052475,
+ "Position": {
+ "X": -22.354492,
+ "Y": 10.13581,
+ "Z": -241.41296
+ },
+ "TerritoryId": 133,
+ "InteractionType": "AcceptQuest",
+ "AetheryteShortcut": "Gridania",
+ "AethernetShortcut": [
+ "[Gridania] Aetheryte Plaza",
+ "[Gridania] Mih Khetto's Amphitheatre"
+ ],
+ "SkipConditions": {
+ "AetheryteShortcutIf": {
+ "InSameTerritory": true,
+ "InTerritory": [
+ 133
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using Dalamud.Game.Command;
.AddUiForeground(stepData?.TerritoryId.ToString() ?? "?", 61)
.Build());
}
+ else if (arguments == "events")
+ {
+ var eventQuests = _pluginInterface.GetIpcSubscriber<List<string>>("Questionable.GetCurrentlyActiveEventQuests").InvokeFunc();
+ _chatGui.Print(new SeStringBuilder()
+ .AddUiForeground("[IPC]", 576)
+ .AddText(": Quests: ")
+ .AddUiForeground(string.Join(", ", eventQuests), 61)
+ .Build());
+ }
else
_chatGui.PrintError("Unknown subcommand");
}
{ EExtraSkipCondition.RoguesGuild, "RoguesGuild"},
{ EExtraSkipCondition.NotRoguesGuild, "NotRoguesGuild"},
{ EExtraSkipCondition.DockStorehouse, "DockStorehouse"},
- { EExtraSkipCondition.SkipFreeFantasia, "SkipFreeFantasia" },
};
}
/// Location for NIN quests in Eastern La Noscea; located far underneath the actual zone.
/// </summary>
DockStorehouse,
-
- SkipFreeFantasia,
}
{
if (value.StartsWith("S"))
return new SatisfactionSupplyNpcId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
+ else if (value.StartsWith("U"))
+ return new UnlockLinkId(ushort.Parse(value.Substring(1), CultureInfo.InvariantCulture));
else if (value.StartsWith("A"))
{
value = value.Substring(1);
throw;
}
}
+
+ public abstract override string ToString();
}
public sealed class QuestId(ushort value) : ElementId(value)
}
}
+public sealed class UnlockLinkId(ushort value) : ElementId(value)
+{
+ public override string ToString()
+ {
+ return "U" + Value.ToString(CultureInfo.InvariantCulture);
+ }
+}
+
public sealed class AlliedSocietyDailyId(byte alliedSociety, byte rank = 0) : ElementId((ushort)(alliedSociety * 10 + rank))
{
public byte AlliedSociety { get; } = alliedSociety;
public bool UseEscToCancelQuesting { get; set; } = true;
public bool ShowIncompleteSeasonalEvents { get; set; } = true;
public bool ConfigureTextAdvance { get; set; } = true;
-
- // TODO Temporary setting, 7.2 adds another fantasia
- public bool PickUpFreeFantasia { get; set; } = true;
}
internal sealed class DutyConfiguration
MatchesExtraCondition(skipCondition, position.Value, _clientState.TerritoryType);
}
- public bool MatchesExtraCondition(EExtraSkipCondition skipCondition, Vector3 position, ushort territoryType)
+ public static bool MatchesExtraCondition(EExtraSkipCondition skipCondition, Vector3 position, ushort territoryType)
{
return skipCondition switch
{
EExtraSkipCondition.RoguesGuild => territoryType == 129 && position.Y <= -115,
EExtraSkipCondition.NotRoguesGuild => territoryType == 129 && position.Y > -115,
EExtraSkipCondition.DockStorehouse => territoryType == 137 && position.Y <= -20,
- EExtraSkipCondition.SkipFreeFantasia => ShouldSkipFreeFantasia(),
_ => throw new ArgumentOutOfRangeException(nameof(skipCondition), skipCondition, null)
};
}
-
- private unsafe bool ShouldSkipFreeFantasia()
- {
- if (!_configuration.General.PickUpFreeFantasia)
- {
- _logger.LogInformation("Skipping fantasia step, as free fantasia is disabled in the configuration");
- return true;
- }
-
- bool foundFestival = false;
- for (int i = 0; i < GameMain.Instance()->ActiveFestivals.Length; ++i)
- {
- if (GameMain.Instance()->ActiveFestivals[i].Id == 160)
- {
- foundFestival = true;
- break;
- }
- }
-
- if (!foundFestival)
- {
- _logger.LogInformation("Skipping fantasia step, as free fantasia moogle is not available");
- return true;
- }
-
- UIState* uiState = UIState.Instance();
- if (uiState != null && uiState->IsUnlockLinkUnlocked(505))
- {
- _logger.LogInformation("Already picked up free fantasia");
- return true;
- }
-
- return false;
- }
}
}
}));
+ quests.Add(new UnlockLinkQuestInfo(new UnlockLinkId(506), "Patch 7.2 Fantasia", 1052475));
+
_quests = quests.ToDictionary(x => x.QuestId, x => x);
// workaround because the game doesn't require completion of the CT questline through normal means
return false;
else if (elementId is AlliedSocietyDailyId)
return false;
+ else if (elementId is UnlockLinkId)
+ return false;
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
return false;
else if (elementId is AlliedSocietyDailyId)
return false;
+ else if (elementId is UnlockLinkId unlockLinkId)
+ return IsQuestComplete(unlockLinkId);
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
return QuestManager.IsQuestComplete(questId.Value);
}
+ public bool IsQuestComplete(UnlockLinkId unlockLinkId)
+ {
+ return UIState.Instance()->IsUnlockLinkUnlocked(unlockLinkId.Value);
+ }
+
public bool IsQuestLocked(ElementId elementId, ElementId? extraCompletedQuest = null)
{
if (elementId is QuestId questId)
return IsQuestLocked(satisfactionSupplyNpcId);
else if (elementId is AlliedSocietyDailyId alliedSocietyDailyId)
return IsQuestLocked(alliedSocietyDailyId);
+ else if (elementId is UnlockLinkId unlockLinkId)
+ return IsQuestLocked(unlockLinkId);
else
throw new ArgumentOutOfRangeException(nameof(elementId));
}
return currentRank == 0 || currentRank < alliedSocietyDailyId.Rank;
}
+ private static bool IsQuestLocked(UnlockLinkId unlockLinkId)
+ {
+ return IsQuestUnobtainable(unlockLinkId);
+ }
+
public bool IsDailyAlliedSocietyQuest(QuestId questId)
{
var questInfo = (QuestInfo)_questData.GetQuestInfo(questId);
{
if (elementId is QuestId questId)
return IsQuestUnobtainable(questId, extraCompletedQuest);
+ else if (elementId is UnlockLinkId unlockLinkId)
+ return IsQuestUnobtainable(unlockLinkId);
else
return false;
}
return false;
}
+ /// <summary>
+ /// All unlock links (presumably) have unique conditions, be that quests or otherwise.
+ /// </summary>
+ private static bool IsQuestUnobtainable(UnlockLinkId unlockLinkId)
+ {
+ if (unlockLinkId.Value == 506)
+ return !IsFestivalActive(160, 2);
+ else
+ return true;
+ }
+
+ private static bool IsFestivalActive(ushort id, ushort? phase = null)
+ {
+ for (int i = 0; i < GameMain.Instance()->ActiveFestivals.Length; ++i)
+ {
+ var festival = GameMain.Instance()->ActiveFestivals[i];
+ if (festival.Id == id)
+ return phase == null || festival.Phase == phase;
+ }
+
+ return false;
+ }
+
public bool IsQuestRemoved(ElementId elementId)
{
if (elementId is QuestId questId)
--- /dev/null
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using LLib.GameData;
+using Questionable.Model.Questing;
+
+namespace Questionable.Model;
+
+internal sealed class UnlockLinkQuestInfo : IQuestInfo
+{
+ public UnlockLinkQuestInfo(UnlockLinkId unlockLinkId, string name, uint issuerDataId)
+ {
+ QuestId = unlockLinkId;
+ Name = name;
+ IssuerDataId = issuerDataId;
+ }
+
+ public ElementId QuestId { get; }
+ public string Name { get; }
+ public uint IssuerDataId { get; }
+ public bool IsRepeatable => false;
+ public ImmutableList<PreviousQuestInfo> PreviousQuests => [];
+ 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 => [];
+ public EExpansionVersion Expansion => EExpansionVersion.ARealmReborn;
+}
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
private readonly List<EventQuest> _eventQuests =
[
+ new EventQuest("Limited Time Items", [new UnlockLinkId(506)], DateTime.MaxValue),
];
private readonly QuestData _questData;
private void DrawEventQuest(EventQuest eventQuest)
{
- string time = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(
- precision: 1,
- culture: CultureInfo.InvariantCulture,
- minUnit: TimeUnit.Minute,
- maxUnit: TimeUnit.Day);
- ImGui.Text($"{eventQuest.Name} ({time})");
+ if (eventQuest.EndsAtUtc != DateTime.MaxValue)
+ {
+ string time = (eventQuest.EndsAtUtc - DateTime.UtcNow).Humanize(
+ precision: 1,
+ culture: CultureInfo.InvariantCulture,
+ minUnit: TimeUnit.Minute,
+ maxUnit: TimeUnit.Day);
+ ImGui.Text($"{eventQuest.Name} ({time})");
+ }
+ else
+ ImGui.Text(eventQuest.Name);
float width;
using (var _ = _pluginInterface.UiBuilder.IconFontHandle.Push())
using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
width -= ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
- List<QuestId> startableQuests = eventQuest.QuestIds.Where(x =>
+ List<ElementId> startableQuests = eventQuest.QuestIds.Where(x =>
_questRegistry.IsKnownQuest(x) &&
_questFunctions.IsReadyToAcceptQuest(x) &&
x != _questController.StartedQuest?.Quest.Id &&
if (eventQuest.EndsAtUtc <= DateTime.UtcNow)
return false;
- return !eventQuest.QuestIds.All(x => _questFunctions.IsQuestComplete(x));
+ return eventQuest.QuestIds.Any(ShouldShowQuest);
}
- public IEnumerable<QuestId> GetCurrentlyActiveEventQuests()
+ public IEnumerable<ElementId> GetCurrentlyActiveEventQuests()
{
return _eventQuests
.Where(x => x.EndsAtUtc >= DateTime.UtcNow)
- .SelectMany(x => x.QuestIds);
+ .SelectMany(x => x.QuestIds)
+ .Where(ShouldShowQuest);
}
- private sealed record EventQuest(string Name, List<QuestId> QuestIds, DateTime EndsAtUtc);
+ private bool ShouldShowQuest(ElementId elementId) => !_questFunctions.IsQuestComplete(elementId) &&
+ !_questFunctions.IsQuestUnobtainable(elementId);
+
+ private sealed record EventQuest(string Name, List<ElementId> QuestIds, DateTime EndsAtUtc);
}