qstbak/Questionable/Questionable.Controller.Steps.Shared/RedeemRewardItems.cs
2026-08-19 13:19:57 +10:00

283 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib.Gear;
using LLib.Inventory;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
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, Configuration configuration, ILogger<Factory> logger) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AcceptQuest)
{
return Array.Empty<ITask>();
}
return CreateTasks(questData, configuration, logger);
}
}
internal sealed class CompletionFactory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.CompleteQuest)
{
return Array.Empty<ITask>();
}
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new ScanAfterQuestCompletion(step.TurnInQuestId ?? quest.Id));
}
}
internal sealed record ScanAfterQuestCompletion(ElementId ElementId) : ITask
{
public override string ToString()
{
return $"ScanRewardsAfterQuestCompletion({ElementId})";
}
}
internal sealed class ScanAfterQuestCompletionExecutor(QuestFunctions questFunctions, QuestData questData, Configuration configuration, ILogger<ScanAfterQuestCompletionExecutor> logger) : TaskExecutor<ScanAfterQuestCompletion>(), IExtraTaskCreator, ITaskExecutor
{
private const long InventorySettleDelayMs = 1000L;
private long? _scanAt;
private List<ITask> _tasks = new List<ITask>();
protected override bool Start()
{
_scanAt = null;
_tasks = new List<ITask>();
return true;
}
public override ETaskResult Update()
{
if (!questFunctions.IsQuestComplete(base.Task.ElementId))
{
return ETaskResult.StillRunning;
}
long tickCount = Environment.TickCount64;
if (!_scanAt.HasValue)
{
_scanAt = tickCount + 1000;
return ETaskResult.StillRunning;
}
if (tickCount < _scanAt)
{
return ETaskResult.StillRunning;
}
_tasks = CreateTasks(questData, configuration, logger);
if (_tasks.Count <= 0)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.CreateNewTasks;
}
public IEnumerable<ITask> CreateExtraTasks()
{
return _tasks;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record Task(ItemReward ItemReward) : ITask
{
public int OriginalGearsetIndex { get; set; } = -1;
public override string ToString()
{
return "TryRedeem(" + ItemReward.Name + ")";
}
}
internal sealed class Executor(GameFunctions gameFunctions, Configuration configuration, ICondition condition, ILogger<Executor> logger) : TaskExecutor<Task>(), IStoppableTaskExecutor, ITaskExecutor
{
private static readonly TimeSpan MinimumCastTime = TimeSpan.FromSeconds(4L);
private const long GearsetSettleDelayMs = 1000L;
private const long UseItemRetryDelayMs = 1000L;
private const int MaxUseItemAttempts = 5;
private long _useItemAt;
private long _continueAt;
private int _useItemAttempts;
private bool _itemUsed;
protected override bool Start()
{
if (condition[ConditionFlag.Mounted] && base.Task.OriginalGearsetIndex < 0)
{
return false;
}
long num = (_useItemAt = Environment.TickCount64);
if (base.Task.ItemReward is CofferReward)
{
int? cofferGearsetIndex = configuration.General.CofferGearsetIndex;
if (cofferGearsetIndex.HasValue)
{
int valueOrDefault = cofferGearsetIndex.GetValueOrDefault();
if (base.Task.OriginalGearsetIndex < 0)
{
int currentGearsetIndex = GearsetHelper.GetCurrentGearsetIndex();
if (currentGearsetIndex != valueOrDefault && GearsetHelper.TryEquipGearset(valueOrDefault))
{
base.Task.OriginalGearsetIndex = currentGearsetIndex;
_useItemAt = num + 1000;
}
}
else if (GearsetHelper.GetCurrentGearsetIndex() != valueOrDefault)
{
GearsetHelper.TryEquipGearset(valueOrDefault);
_useItemAt = num + 1000;
}
}
}
return true;
}
public override ETaskResult Update()
{
long tickCount = Environment.TickCount64;
if (!_itemUsed)
{
if (tickCount < _useItemAt)
{
return ETaskResult.StillRunning;
}
if (base.Task.ItemReward.IsUnlocked() || !HasItemInInventory(base.Task.ItemReward.ItemId))
{
RestoreGearset();
return ETaskResult.TaskComplete;
}
if (gameFunctions.UseItem(base.Task.ItemReward.ItemId))
{
_itemUsed = true;
TimeSpan timeSpan = base.Task.ItemReward.CastTime;
if (timeSpan < MinimumCastTime)
{
timeSpan = MinimumCastTime;
}
_continueAt = tickCount + (long)timeSpan.TotalMilliseconds + 3000;
return ETaskResult.StillRunning;
}
_useItemAttempts++;
if (_useItemAttempts >= 5)
{
logger.LogWarning("Unable to use {ItemName} after {Attempts} attempts, giving up", base.Task.ItemReward.Name, _useItemAttempts);
RestoreGearset();
return ETaskResult.TaskComplete;
}
_useItemAt = tickCount + 1000;
return ETaskResult.StillRunning;
}
if (condition[ConditionFlag.Casting])
{
return ETaskResult.StillRunning;
}
if (tickCount <= _continueAt)
{
return ETaskResult.StillRunning;
}
RestoreGearset();
return ETaskResult.TaskComplete;
}
public void StopNow()
{
RestoreGearset();
}
private void RestoreGearset()
{
if (base.Task.OriginalGearsetIndex >= 0 && GearsetHelper.TryEquipGearset(base.Task.OriginalGearsetIndex))
{
base.Task.OriginalGearsetIndex = -1;
}
}
private unsafe static bool HasItemInInventory(uint itemId)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr != null)
{
return ptr->GetInventoryItemCount(itemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
private unsafe static List<ITask> CreateTasks(QuestData questData, Configuration configuration, ILogger logger)
{
List<ITask> list = new List<ITask>();
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return list;
}
bool flag = InventoryHelper.HasFreeInventorySlot();
foreach (ItemReward redeemableItem in questData.RedeemableItems)
{
if (ptr->GetInventoryItemCount(redeemableItem.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) != 0 && !redeemableItem.IsUnlocked())
{
if (!PassesRedemptionFilters(redeemableItem.Type, redeemableItem.IsUntradable, configuration.General, flag))
{
logger.LogDebug("Skipping quest reward {ItemName}: type={RewardType}, untradable={IsUntradable}, freeInventorySlot={HasFreeInventorySlot}", redeemableItem.Name, redeemableItem.Type, redeemableItem.IsUntradable, flag);
}
else
{
list.Add(new Task(redeemableItem));
}
}
}
if (list.Count > 0)
{
list.Insert(0, new Mount.UnmountTask());
}
return list;
}
internal static bool PassesRedemptionFilters(EItemRewardType type, bool isUntradable, Configuration.GeneralConfiguration general, bool hasFreeInventorySlot)
{
if (general.DisabledRewardTypes.Contains(type))
{
return false;
}
if (general.SkipTradableRewards && !isUntradable)
{
return false;
}
if (type == EItemRewardType.Coffer && !hasFreeInventorySlot)
{
return false;
}
return true;
}
}