qstbak/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs
2026-08-17 20:29:32 +10:00

424 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib.Inventory;
using Microsoft.Extensions.Logging;
using Questionable.Controller.GameUi;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Interact
{
internal sealed class Factory(Configuration configuration) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 30) <= 1u) ? true : false)
{
if ((step.InteractionType == EInteractionType.CompleteQuest && configuration.Advanced.PreventQuestCompletion) || step.Emote.HasValue || step.ChatMessage != null || step.ItemId.HasValue || !step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.PurchaseItem)
{
if (!step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.Snipe)
{
if (!configuration.General.AutoSnipe)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.UnlockTaxiStand)
{
if (!step.TaxiStandId.HasValue)
{
yield break;
}
}
else if (step.InteractionType != EInteractionType.Interact)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
{
yield return new WaitAtEnd.WaitDelay();
}
uint value = step.DataId.Value;
EInteractionType interactionType2 = step.InteractionType;
int skipMarkerCheck;
if (!step.IgnoreQuestMarker && !step.TargetTerritoryId.HasValue)
{
SkipConditions skipConditions = step.SkipConditions;
if (skipConditions != null)
{
SkipStepConditions stepIf = skipConditions.StepIf;
if (stepIf != null && stepIf.Never)
{
goto IL_024a;
}
}
if (step.InteractionType != EInteractionType.PurchaseItem)
{
skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0);
goto IL_024b;
}
}
goto IL_024a;
IL_024a:
skipMarkerCheck = 1;
goto IL_024b;
IL_024b:
uint? pickUpItemId = step.PickUpItemId ?? step.GCPurchase?.ItemId;
byte? taxiStandId = step.TaxiStandId;
SkipStepConditions? skipConditions2 = step.SkipConditions?.StepIf;
List<QuestWorkValue?> completionQuestVariablesFlags = step.CompletionQuestVariablesFlags;
uint? purchaseItemId = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemId : ((uint?)null));
int purchaseItemCount = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemCount.GetValueOrDefault() : 0);
yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, pickUpItemId, taxiStandId, skipConditions2, completionQuestVariablesFlags, null, purchaseItemId, purchaseItemCount);
}
}
internal sealed record Task : ITask
{
public uint DataId { get; init; }
public Quest? Quest { get; init; }
public EInteractionType InteractionType { get; init; }
public bool SkipMarkerCheck { get; init; }
public uint? PickUpItemId { get; init; }
public byte? TaxiStandId { get; init; }
public SkipStepConditions? SkipConditions { get; init; }
public EStatus? CompletionStatusId { get; init; }
public uint? PurchaseItemId { get; init; }
public int PurchaseItemCount { get; init; }
public List<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
public bool HasCompletionQuestVariablesFlags { get; }
public Task(uint DataId, Quest? Quest, EInteractionType InteractionType, bool SkipMarkerCheck = false, uint? PickUpItemId = null, byte? TaxiStandId = null, SkipStepConditions? SkipConditions = null, List<QuestWorkValue?>? CompletionQuestVariablesFlags = null, EStatus? CompletionStatusId = null, uint? PurchaseItemId = null, int PurchaseItemCount = 0)
{
this.DataId = DataId;
this.Quest = Quest;
this.InteractionType = InteractionType;
this.SkipMarkerCheck = SkipMarkerCheck;
this.PickUpItemId = PickUpItemId;
this.TaxiStandId = TaxiStandId;
this.SkipConditions = SkipConditions;
this.CompletionStatusId = CompletionStatusId;
this.PurchaseItemId = PurchaseItemId;
this.PurchaseItemCount = PurchaseItemCount;
this.CompletionQuestVariablesFlags = CompletionQuestVariablesFlags ?? new List<QuestWorkValue>();
HasCompletionQuestVariablesFlags = Quest != null && CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags);
base._002Ector();
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Interact{(HasCompletionQuestVariablesFlags ? "*" : "")}({DataId})";
}
[CompilerGenerated]
public void Deconstruct(out uint DataId, out Quest? Quest, out EInteractionType InteractionType, out bool SkipMarkerCheck, out uint? PickUpItemId, out byte? TaxiStandId, out SkipStepConditions? SkipConditions, out List<QuestWorkValue?>? CompletionQuestVariablesFlags, out EStatus? CompletionStatusId, out uint? PurchaseItemId, out int PurchaseItemCount)
{
DataId = this.DataId;
Quest = this.Quest;
InteractionType = this.InteractionType;
SkipMarkerCheck = this.SkipMarkerCheck;
PickUpItemId = this.PickUpItemId;
TaxiStandId = this.TaxiStandId;
SkipConditions = this.SkipConditions;
CompletionQuestVariablesFlags = this.CompletionQuestVariablesFlags;
CompletionStatusId = this.CompletionStatusId;
PurchaseItemId = this.PurchaseItemId;
PurchaseItemCount = this.PurchaseItemCount;
}
}
internal sealed class DoInteract(GameFunctions gameFunctions, QuestFunctions questFunctions, ShopController shopController, ICondition condition, ILogger<DoInteract> logger) : TaskExecutor<Task>(), IConditionChangeAware, ITaskExecutor
{
private enum EInteractionState
{
None,
InteractionTriggered,
InteractionConfirmed
}
private const long GatheringNodeLoadTimeoutMs = 1000L;
private bool _needsUnmount;
private EInteractionState _interactionState;
private long _continueAt;
private int _interactionAttempts;
private long _gatheringNodeMissingSince;
private bool _delayedFinalCheck;
public Quest? Quest => base.Task.Quest;
public EInteractionType InteractionType { get; set; }
protected override bool Start()
{
InteractionType = base.Task.InteractionType;
_interactionState = EInteractionState.None;
_needsUnmount = false;
_delayedFinalCheck = false;
_continueAt = 0L;
_interactionAttempts = 0;
_gatheringNodeMissingSince = 0L;
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId, null, warnIfMissing: false);
if (gameObject == null)
{
if (InteractionType == EInteractionType.Gather)
{
_gatheringNodeMissingSince = Environment.TickCount64;
}
logger.LogDebug("Object with dataId {DataId} not found yet, waiting", base.Task.DataId);
return true;
}
if (!gameObject.IsTargetable)
{
SkipStepConditions skipConditions = base.Task.SkipConditions;
if (skipConditions != null && !skipConditions.Never && skipConditions.NotTargetable)
{
logger.LogDebug("Not interacting with {DataId} because it is not targetable (but skippable)", base.Task.DataId);
return false;
}
}
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint)
{
logger.LogDebug("Preparing interaction for {DataId} by unmounting", base.Task.DataId);
_needsUnmount = true;
gameFunctions.Unmount();
_continueAt = Environment.TickCount64 + 1000;
return true;
}
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{
TriggerInteraction(gameObject);
return true;
}
return true;
}
public unsafe override ETaskResult Update()
{
if (Environment.TickCount64 <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (_needsUnmount)
{
if (condition[ConditionFlag.Mounted])
{
gameFunctions.Unmount();
_continueAt = Environment.TickCount64 + 1000;
return ETaskResult.StillRunning;
}
_needsUnmount = false;
}
EStatus? completionStatusId = base.Task.CompletionStatusId;
if (completionStatusId.HasValue)
{
EStatus valueOrDefault = completionStatusId.GetValueOrDefault();
if (gameFunctions.HasStatus(valueOrDefault))
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
}
uint? pickUpItemId = base.Task.PickUpItemId;
if (pickUpItemId.HasValue)
{
uint valueOrDefault2 = pickUpItemId.GetValueOrDefault();
if (InventoryManager.Instance()->GetInventoryItemCount(valueOrDefault2, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0)
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
}
else
{
pickUpItemId = base.Task.PurchaseItemId;
if (pickUpItemId.HasValue)
{
uint valueOrDefault3 = pickUpItemId.GetValueOrDefault();
if (base.Task.PurchaseItemCount > 0)
{
if (((ItemHandle)valueOrDefault3).GetCount(checkEquipped: true, checkArmory: true) >= base.Task.PurchaseItemCount)
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
if (shopController.IsOpen)
{
return ETaskResult.StillRunning;
}
goto IL_0252;
}
}
byte? taxiStandId = base.Task.TaxiStandId;
if (taxiStandId.HasValue)
{
byte valueOrDefault4 = taxiStandId.GetValueOrDefault();
if (UIState.Instance()->IsChocoboTaxiStandUnlocked((uint)(valueOrDefault4 + 1179648)))
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
}
else
{
if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
if (Quest != null && base.Task.HasCompletionQuestVariablesFlags)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(Quest.Id);
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
ResetProgressTimer();
return ETaskResult.TaskComplete;
}
}
else if (base.ProgressContext != null)
{
if (base.ProgressContext.WasInterrupted())
{
logger.LogDebug("Interaction with {DataId} was interrupted", base.Task.DataId);
return ETaskResult.StillRunning;
}
if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed)
{
ResetProgressTimer();
if (_delayedFinalCheck)
{
return ETaskResult.TaskComplete;
}
_continueAt = Environment.TickCount64 + 200;
_delayedFinalCheck = true;
return ETaskResult.StillRunning;
}
}
}
}
goto IL_0252;
IL_0252:
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId, null, warnIfMissing: false);
if (gameObject == null || !gameObject.IsTargetable)
{
if (InteractionType == EInteractionType.Gather)
{
if (_gatheringNodeMissingSince == 0L)
{
_gatheringNodeMissingSince = Environment.TickCount64;
}
if (Environment.TickCount64 - _gatheringNodeMissingSince >= 1000)
{
logger.LogDebug("Gathering node {DataId} is not active, selecting another node", base.Task.DataId);
return ETaskResult.SkipRemainingTasksForStep;
}
}
return ETaskResult.StillRunning;
}
_gatheringNodeMissingSince = 0L;
if (!HasAnyMarker(gameObject))
{
return ETaskResult.StillRunning;
}
_interactionAttempts++;
if (_interactionAttempts > 10)
{
logger.LogWarning("Interaction with {DataId} failed after {Attempts} attempts, retrying step", base.Task.DataId, _interactionAttempts);
return ETaskResult.RetryStep;
}
TriggerInteraction(gameObject);
ResetProgressTimer();
return ETaskResult.StillRunning;
}
private void TriggerInteraction(IGameObject gameObject)
{
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(delegate
{
_interactionState = (gameFunctions.InteractWith(gameObject) ? EInteractionState.InteractionTriggered : EInteractionState.None);
return _interactionState != EInteractionState.None;
});
_continueAt = Environment.TickCount64 + 500;
}
private unsafe bool HasAnyMarker(IGameObject gameObject)
{
if (base.Task.SkipMarkerCheck || gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.EventNpc)
{
return true;
}
GameObject* address = (GameObject*)gameObject.Address;
return address->NamePlateIconId != 0;
}
public void OnConditionChange(ConditionFlag flag, bool value)
{
if (base.ProgressContext == null || (!base.ProgressContext.WasInterrupted() && !base.ProgressContext.WasSuccessful()))
{
logger.LogDebug("Condition change: {Flag} = {Value}", flag, value);
bool flag2 = _interactionState == EInteractionState.InteractionTriggered;
if (flag2)
{
bool flag3 = (uint)(flag - 31) <= 1u;
flag2 = flag3;
}
if (flag2 && value)
{
logger.LogDebug("Interaction was most likely triggered");
_interactionState = EInteractionState.InteractionConfirmed;
}
}
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}