424 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|