Automatically redeem untradeable mounts/minions/orchestrion rolls/TT cards/fashion accessories from quest rewards

This commit is contained in:
Liza 2025-01-02 22:50:50 +01:00
parent 40a2507573
commit c722abb6df
Signed by: liza
GPG Key ID: 2C41B84815CF6445
9 changed files with 332 additions and 47 deletions

View File

@ -25,6 +25,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=mnemo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nightsoil/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ondo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=orchestrion/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ostall/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=palaka_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rostra/@EntryIndexedValue">True</s:Boolean>

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class RedeemRewardItems
{
internal sealed class Factory(QuestData questData) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AcceptQuest)
return [];
List<ITask> tasks = [];
unsafe
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
return tasks;
foreach (var itemReward in questData.RedeemableItems)
{
if (inventoryManager->GetInventoryItemCount(itemReward.ItemId) > 0 &&
!itemReward.IsUnlocked())
{
tasks.Add(new Task(itemReward));
}
}
}
return tasks;
}
}
internal sealed record Task(ItemReward ItemReward) : ITask
{
public override string ToString() => $"TryRedeem({ItemReward.Name})";
}
internal sealed class Executor(
GameFunctions gameFunctions,
ICondition condition) : TaskExecutor<Task>
{
private DateTime _continueAt;
protected override bool Start()
{
if (condition[ConditionFlag.Mounted])
return false;
_continueAt = DateTime.Now.Add(Task.ItemReward.CastTime).AddSeconds(1);
return gameFunctions.UseItem(Task.ItemReward.ItemId);
}
public override ETaskResult Update()
{
if (condition[ConditionFlag.Casting])
return ETaskResult.StillRunning;
return DateTime.Now <= _continueAt ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
@ -218,8 +219,15 @@ internal sealed class QuestData
quest.JournalGenre = 82;
quest.SortKey = 0;
}
RedeemableItems = quests.Where(x => x is QuestInfo)
.Cast<QuestInfo>()
.SelectMany(x => x.ItemRewards)
.ToImmutableHashSet();
}
public ImmutableHashSet<ItemReward> RedeemableItems { get; }
private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId)
{
QuestInfo quest = (QuestInfo)_quests[questToUpdate];

View File

@ -0,0 +1,99 @@
using System;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Lumina.Excel.Sheets;
using Questionable.Model.Questing;
namespace Questionable.Model;
public enum EItemRewardType
{
Mount,
Minion,
OrchestrionRoll,
TripleTriadCard,
FashionAccessory,
}
public sealed class ItemRewardDetails(Item item, ElementId elementId)
{
public uint ItemId { get; } = item.RowId;
public string Name { get; } = item.Name.ToDalamudString().ToString();
public TimeSpan CastTime { get; } = TimeSpan.FromSeconds(item.CastTimeSeconds);
public ElementId ElementId { get; } = elementId;
}
public abstract record ItemReward(ItemRewardDetails Item)
{
internal static ItemReward? CreateFromItem(Item item, ElementId elementId)
{
if (item.ItemAction.ValueNullable?.Type is 1322)
return new MountReward(new ItemRewardDetails(item, elementId), item.ItemAction.Value.Data[0]);
if (item.ItemAction.ValueNullable?.Type is 853)
return new MinionReward(new ItemRewardDetails(item, elementId), item.ItemAction.Value.Data[0]);
if (item.AdditionalData.GetValueOrDefault<Orchestrion>() is { } orchestrionRoll)
return new OrchestrionRollReward(new ItemRewardDetails(item, elementId), orchestrionRoll.RowId);
if (item.AdditionalData.GetValueOrDefault<TripleTriadCard>() is { } tripleTriadCard)
return new TripleTriadCardReward(new ItemRewardDetails(item, elementId), (ushort)tripleTriadCard.RowId);
if (item.ItemAction.ValueNullable?.Type is 20086)
return new FashionAccessoryReward(new ItemRewardDetails(item, elementId), item.ItemAction.Value.Data[0]);
return null;
}
public uint ItemId => Item.ItemId;
public string Name => Item.Name;
public ElementId ElementId => Item.ElementId;
public TimeSpan CastTime => Item.CastTime;
public abstract EItemRewardType Type { get; }
public abstract bool IsUnlocked();
}
public sealed record MountReward(ItemRewardDetails Item, uint MountId)
: ItemReward(Item)
{
public override EItemRewardType Type => EItemRewardType.Mount;
public override unsafe bool IsUnlocked()
=> PlayerState.Instance()->IsMountUnlocked(MountId);
}
public sealed record MinionReward(ItemRewardDetails Item, uint MinionId)
: ItemReward(Item)
{
public override EItemRewardType Type => EItemRewardType.Minion;
public override unsafe bool IsUnlocked()
=> UIState.Instance()->IsCompanionUnlocked(MinionId);
}
public sealed record OrchestrionRollReward(ItemRewardDetails Item, uint OrchestrionRollId)
: ItemReward(Item)
{
public override EItemRewardType Type => EItemRewardType.OrchestrionRoll;
public override unsafe bool IsUnlocked() =>
PlayerState.Instance()->IsOrchestrionRollUnlocked(OrchestrionRollId);
}
public sealed record TripleTriadCardReward(ItemRewardDetails Item, ushort TripleTriadCardId)
: ItemReward(Item)
{
public override EItemRewardType Type => EItemRewardType.TripleTriadCard;
public override unsafe bool IsUnlocked() =>
UIState.Instance()->IsTripleTriadCardUnlocked(TripleTriadCardId);
}
public sealed record FashionAccessoryReward(ItemRewardDetails Item, uint AccessoryId)
: ItemReward(Item)
{
public override EItemRewardType Type => EItemRewardType.FashionAccessory;
public override unsafe bool IsUnlocked() =>
PlayerState.Instance()->IsOrnamentUnlocked(AccessoryId);
}

View File

@ -2,10 +2,11 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.GameData;
using Lumina.Excel.Sheets;
using Questionable.Model.Questing;
using ExcelQuest = Lumina.Excel.Sheets.Quest;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Questionable.Model;
@ -54,7 +55,8 @@ internal sealed class QuestInfo : IQuestInfo
QuestLockJoin = (EQuestJoin)quest.QuestLockJoin;
JournalGenre = quest.JournalGenre.ValueNullable?.RowId;
SortKey = quest.SortKey;
IsMainScenarioQuest = quest.JournalGenre.ValueNullable?.JournalCategory.ValueNullable?.JournalSection.ValueNullable?.RowId is 0 or 1;
IsMainScenarioQuest = quest.JournalGenre.ValueNullable?.JournalCategory.ValueNullable?.JournalSection
.ValueNullable?.RowId is 0 or 1;
CompletesInstantly = quest.TodoParams[0].ToDoCompleteSeq == 0;
PreviousInstanceContent = quest.InstanceContent.Select(x => (ushort)x.RowId).Where(x => x != 0).ToList();
PreviousInstanceContentJoin = (EQuestJoin)quest.InstanceContentJoin;
@ -67,6 +69,15 @@ internal sealed class QuestInfo : IQuestInfo
NewGamePlusChapter = newGamePlusChapter;
StartingCity = startingCity;
MoogleDeliveryLevel = (byte)quest.DeliveryQuest.RowId;
ItemRewards = quest.Reward.Where(x => x.RowId > 0 && x.Is<Item>())
.Select(x => x.GetValueOrDefault<Item>())
.Where(x => x != null)
.Cast<Item>()
.Where(x => x.IsUntradable)
.Select(x => ItemReward.CreateFromItem(x, QuestId))
.Where(x => x != null)
.Cast<ItemReward>()
.ToList();
Expansion = (EExpansionVersion)quest.Expansion.RowId;
}
@ -79,7 +90,6 @@ internal sealed class QuestInfo : IQuestInfo
});
}
public ElementId QuestId { get; }
public string Name { get; }
public ushort Level { get; }
@ -105,6 +115,7 @@ internal sealed class QuestInfo : IQuestInfo
public byte StartingCity { get; set; }
public byte MoogleDeliveryLevel { get; }
public bool IsMoogleDeliveryQuest => JournalGenre == 87;
public IReadOnlyList<ItemReward> ItemRewards { get; }
public EExpansionVersion Expansion { get; }
public void AddPreviousQuest(PreviousQuestInfo questId)

View File

@ -138,6 +138,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskFactory<QuestCleanUp.CheckAlliedSocietyMount>();
serviceCollection
.AddTaskExecutor<MoveToLandingLocation.Task, MoveToLandingLocation.MoveToLandingLocationExecutor>();
serviceCollection
.AddTaskFactoryAndExecutor<RedeemRewardItems.Task, RedeemRewardItems.Factory, RedeemRewardItems.Executor>();
serviceCollection.AddTaskExecutor<DoGather.Task, DoGather.GatherExecutor>();
serviceCollection.AddTaskExecutor<DoGatherCollectable.Task, DoGatherCollectable.GatherCollectableExecutor>();
serviceCollection.AddTaskFactoryAndExecutor<SwitchClassJob.Task, SwitchClassJob.Factory,
@ -155,7 +157,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
.AddTaskFactoryAndExecutor<AetheryteShortcut.Task, AetheryteShortcut.Factory,
AetheryteShortcut.UseAetheryteShortcut>();
serviceCollection
.AddTaskExecutor<AetheryteShortcut.MoveAwayFromAetheryte, AetheryteShortcut.MoveAwayFromAetheryteExecutor>();
.AddTaskExecutor<AetheryteShortcut.MoveAwayFromAetheryte,
AetheryteShortcut.MoveAwayFromAetheryteExecutor>();
serviceCollection
.AddTaskFactoryAndExecutor<SkipCondition.SkipTask, SkipCondition.Factory, SkipCondition.CheckSkip>();
serviceCollection.AddTaskFactoryAndExecutor<Gather.GatheringTask, Gather.Factory, Gather.StartGathering>();
@ -179,7 +182,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin
.AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
serviceCollection.AddTaskFactoryAndExecutor<Duty.OpenDutyFinderTask, Duty.Factory, Duty.OpenDutyFinderExecutor>();
serviceCollection
.AddTaskFactoryAndExecutor<Duty.OpenDutyFinderTask, Duty.Factory, Duty.OpenDutyFinderExecutor>();
serviceCollection.AddTaskExecutor<Duty.StartAutoDutyTask, Duty.StartAutoDutyExecutor>();
serviceCollection.AddTaskExecutor<Duty.WaitAutoDutyTask, Duty.WaitAutoDutyExecutor>();
serviceCollection.AddTaskFactory<Emote.Factory>();
@ -269,6 +273,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<QuestJournalUtils>();
serviceCollection.AddSingleton<QuestJournalComponent>();
serviceCollection.AddSingleton<QuestRewardComponent>();
serviceCollection.AddSingleton<GatheringJournalComponent>();
serviceCollection.AddSingleton<AlliedSocietyJournalComponent>();

View File

@ -0,0 +1,65 @@
using System;
using System.Linq;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using Questionable.Data;
using Questionable.Model;
using Questionable.Windows.QuestComponents;
namespace Questionable.Windows.JournalComponents;
internal sealed class QuestRewardComponent
{
private readonly QuestData _questData;
private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils;
public QuestRewardComponent(
QuestData questData,
QuestTooltipComponent questTooltipComponent,
UiUtils uiUtils)
{
_questData = questData;
_questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils;
}
public void DrawItemRewards()
{
using var tab = ImRaii.TabItem("Item Rewards");
if (!tab)
return;
ImGui.BulletText("Only untradeable items are listed (you can e.g. sell your Wind-up Airship from the enovy quest).");
DrawGroup("Mounts", EItemRewardType.Mount);
DrawGroup("Minions", EItemRewardType.Minion);
DrawGroup("Orchestrion Rolls", EItemRewardType.OrchestrionRoll);
DrawGroup("Triple Triad Cards", EItemRewardType.TripleTriadCard);
DrawGroup("Fashion Accessories", EItemRewardType.FashionAccessory);
}
private void DrawGroup(string label, EItemRewardType type)
{
if (!ImGui.CollapsingHeader($"{label}###Reward{type}"))
return;
foreach (var item in _questData.RedeemableItems.Where(x => x.Type == type)
.OrderBy(x => x.Name, StringComparer.CurrentCultureIgnoreCase))
{
if (_uiUtils.ChecklistItem(item.Name, item.IsUnlocked()))
{
if (_questData.TryGetQuestInfo(item.ElementId, out var questInfo))
{
using var tooltip = ImRaii.Tooltip();
if (!tooltip)
continue;
ImGui.Text($"Obtained from: {questInfo.Name}");
using (ImRaii.PushIndent())
_questTooltipComponent.DrawInner(questInfo, false);
}
}
}
}
}

View File

@ -12,12 +12,14 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
{
private readonly QuestJournalComponent _questJournalComponent;
private readonly AlliedSocietyJournalComponent _alliedSocietyJournalComponent;
private readonly QuestRewardComponent _questRewardComponent;
private readonly GatheringJournalComponent _gatheringJournalComponent;
private readonly QuestRegistry _questRegistry;
private readonly IClientState _clientState;
public JournalProgressWindow(
QuestJournalComponent questJournalComponent,
QuestRewardComponent questRewardComponent,
AlliedSocietyJournalComponent alliedSocietyJournalComponent,
GatheringJournalComponent gatheringJournalComponent,
QuestRegistry questRegistry,
@ -26,6 +28,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
{
_questJournalComponent = questJournalComponent;
_alliedSocietyJournalComponent = alliedSocietyJournalComponent;
_questRewardComponent = questRewardComponent;
_gatheringJournalComponent = gatheringJournalComponent;
_questRegistry = questRegistry;
_clientState = clientState;
@ -64,6 +67,7 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
_questJournalComponent.DrawQuests();
_alliedSocietyJournalComponent.DrawAlliedSocietyQuests();
_questRewardComponent.DrawItemRewards();
_gatheringJournalComponent.DrawGatheringItems();
}

View File

@ -40,48 +40,58 @@ internal sealed class QuestTooltipComponent
{
using var tooltip = ImRaii.Tooltip();
if (tooltip)
{
ImGui.Text($"{SeIconChar.LevelEn.ToIconString()}{questInfo.Level}");
ImGui.SameLine();
var (color, _, tooltipText) = _uiUtils.GetQuestStyle(questInfo.QuestId);
ImGui.TextColored(color, tooltipText);
if (questInfo.IsRepeatable)
{
ImGui.SameLine();
ImGui.TextUnformatted("Repeatable");
}
if (questInfo is QuestInfo { CompletesInstantly: true })
{
ImGui.SameLine();
ImGui.TextUnformatted("Instant");
}
if (_questRegistry.TryGetQuest(questInfo.QuestId, out Quest? quest))
{
if (quest.Root.Disabled)
{
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled");
}
if (quest.Root.Author.Count == 1)
ImGui.Text($"Author: {quest.Root.Author[0]}");
else
ImGui.Text($"Authors: {string.Join(", ", quest.Root.Author)}");
}
else
{
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudRed, "NoQuestPath");
}
DrawQuestUnlocks(questInfo, 0);
}
DrawInner(questInfo, true);
}
private void DrawQuestUnlocks(IQuestInfo questInfo, int counter)
public void DrawInner(IQuestInfo questInfo, bool showItemRewards)
{
ImGui.Text($"{SeIconChar.LevelEn.ToIconString()}{questInfo.Level}");
ImGui.SameLine();
var (color, _, tooltipText) = _uiUtils.GetQuestStyle(questInfo.QuestId);
ImGui.TextColored(color, tooltipText);
if (questInfo is QuestInfo { IsSeasonalEvent: true })
{
ImGui.SameLine();
ImGui.TextUnformatted("Event");
}
if (questInfo.IsRepeatable)
{
ImGui.SameLine();
ImGui.TextUnformatted("Repeatable");
}
if (questInfo is QuestInfo { CompletesInstantly: true })
{
ImGui.SameLine();
ImGui.TextUnformatted("Instant");
}
if (_questRegistry.TryGetQuest(questInfo.QuestId, out Quest? quest))
{
if (quest.Root.Disabled)
{
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled");
}
if (quest.Root.Author.Count == 1)
ImGui.Text($"Author: {quest.Root.Author[0]}");
else
ImGui.Text($"Authors: {string.Join(", ", quest.Root.Author)}");
}
else
{
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudRed, "NoQuestPath");
}
DrawQuestUnlocks(questInfo, 0, showItemRewards);
}
private void DrawQuestUnlocks(IQuestInfo questInfo, int counter, bool showItemRewards)
{
if (counter >= 10)
return;
@ -118,12 +128,13 @@ internal sealed class QuestTooltipComponent
_questFunctions.IsQuestComplete(q.QuestId) ? byte.MinValue : q.Sequence), iconColor, icon);
if (qInfo is QuestInfo qstInfo && (counter <= 2 || icon != FontAwesomeIcon.Check))
DrawQuestUnlocks(qstInfo, counter + 1);
DrawQuestUnlocks(qstInfo, counter + 1, false);
}
else
{
using var _ = ImRaii.Disabled();
_uiUtils.ChecklistItem($"Unknown Quest ({q.QuestId})", ImGuiColors.DalamudGrey, FontAwesomeIcon.Question);
_uiUtils.ChecklistItem($"Unknown Quest ({q.QuestId})", ImGuiColors.DalamudGrey,
FontAwesomeIcon.Question);
}
}
}
@ -193,6 +204,16 @@ internal sealed class QuestTooltipComponent
GrandCompany currentGrandCompany = _questFunctions.GetGrandCompany();
_uiUtils.ChecklistItem($"Grand Company: {gcName}", actualQuestInfo.GrandCompany == currentGrandCompany);
}
if (showItemRewards && actualQuestInfo.ItemRewards.Count > 0)
{
ImGui.Separator();
ImGui.Text("Item Rewards:");
foreach (var reward in actualQuestInfo.ItemRewards)
{
ImGui.BulletText(reward.Name);
}
}
}
if (counter > 0)