qstbak/Questionable/Questionable.Controller.GameUi/InteractionUiController.cs
2025-10-09 08:41:52 +10:00

1016 lines
36 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
using LLib.GameData;
using LLib.GameUI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.GameUi;
internal sealed class InteractionUiController : IDisposable
{
private sealed record DialogueChoiceInfo(Questionable.Model.Quest? Quest, DialogueChoice DialogueChoice);
private readonly IAddonLifecycle _addonLifecycle;
private readonly IDataManager _dataManager;
private readonly QuestFunctions _questFunctions;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly ExcelFunctions _excelFunctions;
private readonly QuestController _questController;
private readonly GatheringPointRegistry _gatheringPointRegistry;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly TerritoryData _territoryData;
private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
private readonly IClientState _clientState;
private readonly ShopController _shopController;
private readonly BossModIpc _bossModIpc;
private readonly Configuration _configuration;
private readonly ILogger<InteractionUiController> _logger;
private readonly Regex _returnRegex;
private readonly Regex _purchaseItemRegex;
private bool _isInitialCheck;
private bool ShouldHandleUiInteractions
{
get
{
if (!_isInitialCheck && !_questController.IsRunning)
{
return _territoryData.IsQuestBattleInstance(_clientState.TerritoryType);
}
return true;
}
}
public unsafe InteractionUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, QuestFunctions questFunctions, AetheryteFunctions aetheryteFunctions, ExcelFunctions excelFunctions, QuestController questController, GatheringPointRegistry gatheringPointRegistry, QuestRegistry questRegistry, QuestData questData, TerritoryData territoryData, IGameGui gameGui, ITargetManager targetManager, IPluginLog pluginLog, IClientState clientState, ShopController shopController, BossModIpc bossModIpc, Configuration configuration, ILogger<InteractionUiController> logger)
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
_questFunctions = questFunctions;
_aetheryteFunctions = aetheryteFunctions;
_excelFunctions = excelFunctions;
_questController = questController;
_gatheringPointRegistry = gatheringPointRegistry;
_questRegistry = questRegistry;
_questData = questData;
_territoryData = territoryData;
_gameGui = gameGui;
_targetManager = targetManager;
_clientState = clientState;
_shopController = shopController;
_bossModIpc = bossModIpc;
_configuration = configuration;
_logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196u).GetRegex((Addon addon) => addon.Text, pluginLog);
_purchaseItemRegex = _dataManager.GetRegex(3406u, (Addon addon) => addon.Text, pluginLog);
_questController.AutomationTypeChanged += HandleCurrentDialogueChoices;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
if (_gameGui.TryGetAddonByName<AtkUnitBase>("RhythmAction", out var addonPtr))
{
addonPtr->Close(fireCallback: true);
}
}
private void HandleCurrentDialogueChoices(object sender, QuestController.EAutomationType automationType)
{
if (automationType != QuestController.EAutomationType.Manual)
{
HandleCurrentDialogueChoices();
}
}
internal unsafe void HandleCurrentDialogueChoices()
{
try
{
_isInitialCheck = true;
if (_gameGui.TryGetAddonByName<AddonSelectString>("SelectString", out var addonPtr))
{
_logger.LogInformation("SelectString window is open");
SelectStringPostSetup(addonPtr, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonCutSceneSelectString>("CutSceneSelectString", out var addonPtr2))
{
_logger.LogInformation("CutSceneSelectString window is open");
CutsceneSelectStringPostSetup(addonPtr2, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonSelectIconString>("SelectIconString", out var addonPtr3))
{
_logger.LogInformation("SelectIconString window is open");
SelectIconStringPostSetup(addonPtr3, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonSelectYesno>("SelectYesno", out var addonPtr4))
{
_logger.LogInformation("SelectYesno window is open");
SelectYesnoPostSetup(addonPtr4, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("DifficultySelectYesNo", out var addonPtr5))
{
_logger.LogInformation("DifficultySelectYesNo window is open");
DifficultySelectYesNoPostSetup(addonPtr5, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("PointMenu", out var addonPtr6))
{
_logger.LogInformation("PointMenu is open");
PointMenuPostSetup(addonPtr6);
}
}
finally
{
_isInitialCheck = false;
}
}
private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectString* address = (AddonSelectString*)args.Addon.Address;
SelectStringPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
if (text == null)
{
return;
}
List<string> list = new List<string>();
for (ushort num = 7; num < addonSelectString->AtkUnitBase.AtkValuesCount; num++)
{
if (addonSelectString->AtkUnitBase.AtkValues[(int)num].Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String)
{
list.Add(addonSelectString->AtkUnitBase.AtkValues[(int)num].ReadAtkString());
}
}
int? num2 = HandleListChoice(text, list, checkAllSteps) ?? HandleInstanceListChoice(text);
if (num2.HasValue)
{
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", num2, text);
addonSelectString->AtkUnitBase.FireCallbackInt(num2.Value);
}
}
private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonCutSceneSelectString* address = (AddonCutSceneSelectString*)args.Addon.Address;
CutsceneSelectStringPostSetup(address, checkAllSteps: false);
}
private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
if (text != null)
{
List<string> list = new List<string>();
for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; i++)
{
list.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
}
int? num = HandleListChoice(text, list, checkAllSteps);
if (num.HasValue)
{
addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(num.Value);
}
}
}
private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectIconString* address = (AddonSelectIconString*)args.Addon.Address;
SelectIconStringPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
if (string.IsNullOrEmpty(text))
{
text = null;
}
List<string> choices = GetChoices(addonSelectIconString);
int? num = HandleListChoice(text, choices, checkAllSteps);
if (num.HasValue)
{
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", num, text);
addonSelectIconString->AtkUnitBase.FireCallbackInt(num.Value);
return;
}
string text2 = (*addonSelectIconString->AtkValues).ReadAtkString();
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest != null && (text == null || text2 != null))
{
_logger.LogInformation("Checking if current quest {Name} is on the list", startedQuest.Quest.Info.Name);
if (CheckQuestSelection(addonSelectIconString, startedQuest.Quest, choices))
{
return;
}
QuestStep questStep = startedQuest.Quest.FindSequence(startedQuest.Sequence)?.FindStep(startedQuest.Step);
if (questStep != null && questStep.InteractionType == EInteractionType.AcceptQuest && (object)questStep.PickUpQuestId != null && _questRegistry.TryGetQuest(questStep.PickUpQuestId, out Questionable.Model.Quest quest))
{
_logger.LogInformation("Checking if current picked-up {Name} is on the list", quest.Info.Name);
if (CheckQuestSelection(addonSelectIconString, quest, choices))
{
return;
}
}
}
QuestController.QuestProgress nextQuest = _questController.NextQuest;
if (nextQuest != null && (text == null || text2 != null))
{
_logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
CheckQuestSelection(addonSelectIconString, nextQuest.Quest, choices);
}
}
private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Questionable.Model.Quest quest, List<string?> answers)
{
string questName = quest.Info.Name;
int num = answers.FindIndex((string? x) => GameFunctions.GameStringEquals(questName, x));
if (num >= 0)
{
_logger.LogInformation("Selecting quest {QuestName}", questName);
addonSelectIconString->AtkUnitBase.FireCallbackInt(num);
return true;
}
return false;
}
public unsafe static List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
{
List<string> list = new List<string>();
for (ushort num = 0; num < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; num++)
{
list.Add(addonSelectIconString->AtkValues[num * 3 + 7].ReadAtkString());
}
return list;
}
private unsafe int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
{
List<DialogueChoiceInfo> list = new List<DialogueChoiceInfo>();
QuestController.QuestProgress questProgress = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest;
if (questProgress != null)
{
Questionable.Model.Quest quest = questProgress.Quest;
bool flag = false;
List<EAetheryteLocation> source = new List<EAetheryteLocation>();
if (checkAllSteps)
{
QuestSequence? questSequence = quest.FindSequence(questProgress.Sequence);
IEnumerable<DialogueChoice> enumerable = questSequence?.Steps.SelectMany((QuestStep x) => x.DialogueChoices);
if (enumerable != null)
{
list.AddRange(enumerable.Select((DialogueChoice x) => new DialogueChoiceInfo(quest, x)));
}
flag = questSequence?.Steps.Any((QuestStep x) => x.InteractionType == EInteractionType.UnlockTaxiStand) ?? false;
source = (from x in questSequence?.Steps
where x != null && x.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte && x.Aetheryte.HasValue
select x.Aetheryte.Value).ToList() ?? new List<EAetheryteLocation>();
}
else
{
QuestStep questStep = null;
if (_territoryData.IsQuestBattleInstance(_clientState.TerritoryType))
{
questStep = quest.FindSequence(questProgress.Sequence)?.Steps.FirstOrDefault((QuestStep x) => x.InteractionType == EInteractionType.SinglePlayerDuty);
}
if (questStep == null)
{
questStep = quest.FindSequence(questProgress.Sequence)?.FindStep(questProgress.Step);
}
if (questStep == null)
{
_logger.LogDebug("Ignoring current quest dialogue choices, no active step");
}
else
{
list.AddRange(questStep.DialogueChoices.Select((DialogueChoice x) => new DialogueChoiceInfo(quest, x)));
if (questStep.PurchaseMenu != null)
{
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = questStep.PurchaseMenu.ExcelSheet,
Prompt = null,
Answer = questStep.PurchaseMenu.Key
}));
}
if (questStep != null && questStep.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte)
{
EAetheryteLocation? aetheryte = questStep.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault = aetheryte.GetValueOrDefault();
int num = 1;
List<EAetheryteLocation> list2 = new List<EAetheryteLocation>(num);
CollectionsMarshal.SetCount(list2, num);
Span<EAetheryteLocation> span = CollectionsMarshal.AsSpan(list2);
int index = 0;
span[index] = valueOrDefault;
source = list2;
}
}
flag = questStep.InteractionType == EInteractionType.UnlockTaxiStand;
}
}
if (flag)
{
_logger.LogInformation("Adding chocobo taxi stand unlock dialogue choices");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/ChocoboTaxiStand",
Prompt = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_Q1_000_1"),
Answer = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_A1_000_3")
}));
}
if (source.Any((EAetheryteLocation x) => _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable))
{
_logger.LogInformation("Adding security token aetheryte unlock dialogue choice");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/Aetheryte",
Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"),
PromptIsRegularExpression = true,
Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_TOKEN_FAVORITE"),
AnswerIsRegularExpression = true
}));
}
else if (source.Any((EAetheryteLocation x) => _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == AetheryteRegistrationResult.FavoredDestinationAvailable))
{
_logger.LogInformation("Adding favored aetheryte unlock dialogue choice");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/Aetheryte",
Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"),
PromptIsRegularExpression = true,
Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_FAVORITE"),
AnswerIsRegularExpression = true
}));
}
ushort? num2 = FindTargetTerritoryFromQuestStep(questProgress);
if (num2.HasValue)
{
foreach (string answer in answers)
{
if (answer != null && TryFindWarp(num2.Value, answer, out uint? warpId, out string warpText))
{
_logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = null,
Prompt = null,
Answer = ExcelRef.FromSheetValue(warpText)
}));
}
}
}
}
else
{
_logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
}
IGameObject target = _targetManager.Target;
if (target != null)
{
foreach (IQuestInfo item in from x in _questData.GetAllByIssuerDataId(target.BaseId)
where x.QuestId is QuestId
select x)
{
if (!_questFunctions.IsReadyToAcceptQuest(item.QuestId) || !_questRegistry.TryGetQuest(item.QuestId, out Questionable.Model.Quest knownQuest))
{
continue;
}
List<DialogueChoice> list3 = knownQuest.FindSequence(0)?.Steps.SelectMany((QuestStep x) => x.DialogueChoices).ToList();
if (list3 != null && list3.Count > 0)
{
_logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}", list3.Count, item.Name);
list.AddRange(list3.Select((DialogueChoice x) => new DialogueChoiceInfo(knownQuest, x)));
}
}
}
if (list.Count == 0)
{
_logger.LogDebug("No dialogue choices to check");
return null;
}
foreach (var (quest3, dialogueChoice2) in list)
{
if (dialogueChoice2.Type != EDialogChoiceType.List)
{
continue;
}
if (dialogueChoice2.SpecialCondition == "NoDutyActions")
{
try
{
ContentDirector* contentDirector = EventFramework.Instance()->GetContentDirector();
if (contentDirector != null && contentDirector->DutyActionManager.ActionsPresent)
{
_logger.LogInformation("NoDutyActions: actions present, skipping dialogue choice");
continue;
}
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to check for duty actions");
continue;
}
}
if (dialogueChoice2.Answer == null)
{
_logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
continue;
}
if (dialogueChoice2.DataId.HasValue && dialogueChoice2.DataId != _targetManager.Target?.BaseId)
{
_logger.LogDebug("Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", dialogueChoice2.DataId, _targetManager.Target?.BaseId);
continue;
}
StringOrRegex stringOrRegex = ResolveReference(quest3, dialogueChoice2.ExcelSheet, dialogueChoice2.Prompt, dialogueChoice2.PromptIsRegularExpression);
StringOrRegex stringOrRegex2 = ResolveReference(quest3, dialogueChoice2.ExcelSheet, dialogueChoice2.Answer, dialogueChoice2.AnswerIsRegularExpression);
if (actualPrompt == null && stringOrRegex != null)
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", stringOrRegex);
continue;
}
if (actualPrompt != null && (stringOrRegex == null || !IsMatch(actualPrompt, stringOrRegex)))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", stringOrRegex, actualPrompt);
continue;
}
if (dialogueChoice2.AnswerIsRegularExpression && stringOrRegex2 != null)
{
int? num3 = FindBestRegexMatch(answers, stringOrRegex2, quest3, dialogueChoice2);
if (!num3.HasValue)
{
continue;
}
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}' (best regex match)", num3.Value, answers[num3.Value], actualPrompt);
if (quest3?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null || _questController.GatheringQuest.Sequence == byte.MaxValue)
{
return null;
}
_questController.GatheringQuest.SetSequence(1);
_questController.StartGatheringQuest("SatisfactionSupply turn in");
}
return num3.Value;
}
for (int num4 = 0; num4 < answers.Count; num4++)
{
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", answers[num4], stringOrRegex2);
if (!IsMatch(answers[num4], stringOrRegex2))
{
continue;
}
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", num4, answers[num4], actualPrompt);
if (quest3?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null || _questController.GatheringQuest.Sequence == byte.MaxValue)
{
return null;
}
_questController.GatheringQuest.SetSequence(1);
_questController.StartGatheringQuest("SatisfactionSupply turn in");
}
return num4;
}
}
_logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
return null;
}
private static int? FindBestRegexMatch(List<string?> answers, StringOrRegex expectedAnswer, Questionable.Model.Quest? quest, DialogueChoice dialogueChoice)
{
List<int> list = (from x in answers.Select((string answer, int index) => new { answer, index })
where x.answer != null && expectedAnswer.IsMatch(x.answer)
select x.index).ToList();
return list.Count switch
{
0 => null,
1 => list[0],
_ => GetBestMatchFromQuestPath(answers, list, quest, dialogueChoice),
};
}
private static int GetBestMatchFromQuestPath(List<string?> answers, List<int> matchingIndexes, Questionable.Model.Quest? quest, DialogueChoice dialogueChoice)
{
if (quest != null && dialogueChoice.Answer != null)
{
try
{
string questExpectedAnswer = null;
if (dialogueChoice.Answer.Type == ExcelRef.EType.RawString)
{
questExpectedAnswer = dialogueChoice.Answer.AsRawString();
}
if (!string.IsNullOrEmpty(questExpectedAnswer) && !dialogueChoice.AnswerIsRegularExpression)
{
int num = matchingIndexes.FirstOrDefault((int i) => string.Equals(answers[i], questExpectedAnswer, StringComparison.OrdinalIgnoreCase));
if (num != 0)
{
return num;
}
}
}
catch
{
}
}
return matchingIndexes.OrderBy((int i) => answers[i]?.Length ?? int.MaxValue).First();
}
private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
{
if (actualAnswer == null && expectedAnswer == null)
{
return true;
}
if (actualAnswer == null || expectedAnswer == null)
{
return false;
}
return expectedAnswer.IsMatch(actualAnswer);
}
private int? HandleInstanceListChoice(string? actualPrompt)
{
string b = _excelFunctions.GetDialogueTextByRowId("Addon", 2090u, isRegex: false).GetString();
if (GameFunctions.GameStringEquals(actualPrompt, b))
{
_logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
return 0;
}
return null;
}
private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectYesno* address = (AddonSelectYesno*)args.Addon.Address;
SelectYesnoPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = (*addonSelectYesno->AtkUnitBase.AtkValues).ReadAtkString();
if (text == null)
{
return;
}
_logger.LogTrace("Prompt: '{Prompt}'", text);
if (_shopController.IsAwaitingYesNo && _purchaseItemRegex.IsMatch(text))
{
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
_shopController.IsAwaitingYesNo = false;
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest != null && CheckQuestYesNo(addonSelectYesno, startedQuest, text, checkAllSteps))
{
return;
}
QuestController.QuestProgress simulatedQuest = _questController.SimulatedQuest;
if (simulatedQuest == null || !HandleTravelYesNo(addonSelectYesno, simulatedQuest, text))
{
QuestController.QuestProgress nextQuest = _questController.NextQuest;
if (nextQuest != null)
{
CheckQuestYesNo(addonSelectYesno, nextQuest, text, checkAllSteps);
}
}
}
private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest, string actualPrompt, bool checkAllSteps)
{
Questionable.Model.Quest quest = currentQuest.Quest;
if (checkAllSteps)
{
QuestSequence questSequence = quest.FindSequence(currentQuest.Sequence);
if (questSequence != null && questSequence.Steps.Any((QuestStep step) => HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt)))
{
return true;
}
}
else
{
QuestStep questStep = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
if (questStep != null && HandleDefaultYesNo(addonSelectYesno, quest, questStep, questStep.DialogueChoices, actualPrompt))
{
return true;
}
}
if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
{
return true;
}
return false;
}
private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Questionable.Model.Quest quest, QuestStep? step, List<DialogueChoice> dialogueChoices, string actualPrompt)
{
if (step != null && step.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault = aetheryte.GetValueOrDefault();
Span<DialogueChoice> span2;
Span<DialogueChoice> span;
switch (_aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(valueOrDefault))
{
case AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable:
{
List<DialogueChoice> list = dialogueChoices;
int num2 = 1 + list.Count;
List<DialogueChoice> list3 = new List<DialogueChoice>(num2);
CollectionsMarshal.SetCount(list3, num2);
span2 = CollectionsMarshal.AsSpan(list3);
int num = 0;
span = CollectionsMarshal.AsSpan(list);
span.CopyTo(span2.Slice(num, span.Length));
num += span.Length;
span2[num] = new DialogueChoice
{
Type = EDialogChoiceType.YesNo,
ExcelSheet = "Addon",
Prompt = ExcelRef.FromRowId(102334u),
Yes = true
};
dialogueChoices = list3;
break;
}
case AetheryteRegistrationResult.FavoredDestinationAvailable:
{
List<DialogueChoice> list = dialogueChoices;
int num = 1 + list.Count;
List<DialogueChoice> list2 = new List<DialogueChoice>(num);
CollectionsMarshal.SetCount(list2, num);
span = CollectionsMarshal.AsSpan(list2);
int num2 = 0;
span2 = CollectionsMarshal.AsSpan(list);
span2.CopyTo(span.Slice(num2, span2.Length));
num2 += span2.Length;
span[num2] = new DialogueChoice
{
Type = EDialogChoiceType.YesNo,
ExcelSheet = "Addon",
Prompt = ExcelRef.FromRowId(102306u),
Yes = true
};
dialogueChoices = list2;
break;
}
}
}
}
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (DialogueChoice dialogueChoice in dialogueChoices)
{
if (dialogueChoice.Type != EDialogChoiceType.YesNo)
{
continue;
}
if (dialogueChoice.DataId.HasValue && dialogueChoice.DataId != _targetManager.Target?.BaseId)
{
_logger.LogDebug("Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", dialogueChoice.DataId, _targetManager.Target?.BaseId);
continue;
}
StringOrRegex stringOrRegex = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, dialogueChoice.PromptIsRegularExpression);
if (stringOrRegex == null || !IsMatch(actualPrompt, stringOrRegex))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", stringOrRegex, actualPrompt);
continue;
}
_logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt((!dialogueChoice.Yes) ? 1 : 0);
return true;
}
if (CheckSinglePlayerDutyYesNo(quest.Id, step))
{
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
return false;
}
private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step)
{
if (step != null && step.InteractionType == EInteractionType.SinglePlayerDuty && _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyOptions))
{
_logger.LogInformation("SinglePlayerDutyYesNo: probably Single Player Duty");
return true;
}
return false;
}
private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest, string actualPrompt)
{
_logger.LogInformation("TravelYesNo");
if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2.0) && _returnRegex.IsMatch(actualPrompt))
{
_logger.LogInformation("Automatically confirming return...");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
if (_questController.IsRunning && _gameGui.TryGetAddonByName<AtkUnitBase>("HousingSelectBlock", out var _))
{
_logger.LogInformation("Automatically confirming ward selection");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
ushort? num = FindTargetTerritoryFromQuestStep(currentQuest);
if (num.HasValue && TryFindWarp(num.Value, actualPrompt, out uint? warpId, out string warpText))
{
_logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
return false;
}
private unsafe void DifficultySelectYesNoPostSetup(AddonEvent type, AddonArgs args)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
DifficultySelectYesNoPostSetup(address, checkAllSteps: false);
}
private unsafe void DifficultySelectYesNoPostSetup(AtkUnitBase* addonDifficultySelectYesNo, bool checkAllSteps)
{
if (!_questController.IsRunning)
{
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest == null)
{
return;
}
Questionable.Model.Quest quest = startedQuest.Quest;
bool flag;
if (checkAllSteps)
{
flag = quest.FindSequence(startedQuest.Sequence)?.Steps.Any((QuestStep step) => CheckSinglePlayerDutyYesNo(quest.Id, step)) ?? false;
}
else
{
QuestStep questStep = quest.FindSequence(startedQuest.Sequence)?.FindStep(startedQuest.Step);
flag = questStep != null && CheckSinglePlayerDutyYesNo(quest.Id, questStep);
}
if (flag)
{
_logger.LogInformation("Confirming difficulty ({Difficulty}) for quest battle", _configuration.SinglePlayerDuties.RetryDifficulty);
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 0
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = _configuration.SinglePlayerDuties.RetryDifficulty
}
};
addonDifficultySelectYesNo->FireCallback(2u, values);
}
}
private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
{
QuestSequence questSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
if (questSequence == null)
{
return null;
}
QuestStep questStep = questSequence.FindStep(currentQuest.Step);
if (questStep != null)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}", questStep.TerritoryId, questStep.TargetTerritoryId);
}
if (questStep != null && (questStep.TerritoryId != _clientState.TerritoryType || !questStep.TargetTerritoryId.HasValue) && questStep.InteractionType == EInteractionType.Gather && _gatheringPointRegistry.TryGetGatheringPointId(questStep.ItemsToGather[0].ItemId, ((EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId).GetValueOrDefault(), out GatheringPointId gatheringPointId) && _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot gatheringRoot))
{
foreach (QuestStep step in gatheringRoot.Steps)
{
if (step.TerritoryId == _clientState.TerritoryType && step.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId);
return step.TargetTerritoryId;
}
}
}
if (questStep == null || !questStep.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
questStep = questSequence.FindStep((currentQuest.Step == 255) ? (questSequence.Steps.Count - 1) : (currentQuest.Step - 1));
if (questStep != null)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}", questStep.TerritoryId, questStep.TargetTerritoryId);
}
}
if (questStep == null || !questStep.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
return null;
}
_logger.LogDebug("Target territory for quest step: {TargetTerritory}", questStep.TargetTerritoryId);
return questStep.TargetTerritoryId;
}
private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId, [NotNullWhen(true)] out string? warpText)
{
foreach (Warp item in from x in _dataManager.GetExcelSheet<Warp>()
where x.RowId != 0 && x.TerritoryType.RowId == targetTerritoryId
select x)
{
string text = item.Name.WithCertainMacroCodeReplacements();
string text2 = item.Question.WithCertainMacroCodeReplacements();
if (!string.IsNullOrEmpty(text2) && GameFunctions.GameStringEquals(text2, actualPrompt))
{
warpId = item.RowId;
warpText = text2;
return true;
}
if (!string.IsNullOrEmpty(text) && GameFunctions.GameStringEquals(text, actualPrompt))
{
warpId = item.RowId;
warpText = text;
return true;
}
_logger.LogDebug("Ignoring prompt '{Prompt}'", text2);
}
warpId = null;
warpText = null;
return false;
}
private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
PointMenuPostSetup(address);
}
private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
{
if (!ShouldHandleUiInteractions)
{
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest == null)
{
_logger.LogInformation("Ignoring point menu, no active quest");
return;
}
QuestSequence questSequence = startedQuest.Quest.FindSequence(startedQuest.Sequence);
if (questSequence == null)
{
return;
}
QuestStep questStep = questSequence.FindStep(startedQuest.Step);
if (questStep == null)
{
return;
}
if (questStep.PointMenuChoices.Count == 0)
{
_logger.LogWarning("No point menu choices");
return;
}
int pointMenuCounter = startedQuest.StepProgress.PointMenuCounter;
if (pointMenuCounter >= questStep.PointMenuChoices.Count)
{
_logger.LogWarning("No remaining point menu choices");
return;
}
uint num = questStep.PointMenuChoices[pointMenuCounter];
_logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", num, pointMenuCounter);
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 13
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt,
UInt = num
}
};
addonPointMenu->FireCallback(2u, values);
startedQuest.IncreasePointMenuCounter();
}
private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
{
if (ShouldHandleUiInteractions)
{
_logger.LogInformation("Confirming selected housing ward");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(0);
}
}
private StringOrRegex? ResolveReference(Questionable.Model.Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
{
if (excelRef == null)
{
return null;
}
if (excelRef.Type == ExcelRef.EType.Key)
{
return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
}
if (excelRef.Type == ExcelRef.EType.RowId)
{
return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
}
if (excelRef.Type == ExcelRef.EType.RawString)
{
return new StringOrRegex(excelRef.AsRawString());
}
return null;
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_questController.AutomationTypeChanged -= HandleCurrentDialogueChoices;
}
}