diff --git a/Deliveroo/Deliveroo.csproj b/Deliveroo/Deliveroo.csproj index 8d7561d..b7bc4c0 100644 --- a/Deliveroo/Deliveroo.csproj +++ b/Deliveroo/Deliveroo.csproj @@ -1,7 +1,7 @@ net7.0-windows - 2.3 + 2.4 11.0 enable true diff --git a/Deliveroo/DeliverooPlugin.Exchange.cs b/Deliveroo/DeliverooPlugin.Exchange.cs index acc991b..5c2faf7 100644 --- a/Deliveroo/DeliverooPlugin.Exchange.cs +++ b/Deliveroo/DeliverooPlugin.Exchange.cs @@ -165,26 +165,6 @@ partial class DeliverooPlugin return false; } - private void ConfirmReward() - { - PurchaseItemRequest? item = GetNextItemToPurchase(); - if (item == null) - { - CurrentStage = Stage.CloseGcExchange; - return; - } - - if (SelectSelectYesno(0, s => s.StartsWith("Exchange "))) - { - var nextItem = GetNextItemToPurchase(item); - if (nextItem != null && GetCurrentSealCount() >= _configuration.ReservedSealCount + nextItem.SealCost) - CurrentStage = Stage.SelectRewardTier; - else - CurrentStage = Stage.CloseGcExchange; - _continueAt = DateTime.Now.AddSeconds(0.5); - } - } - private unsafe void CloseGcExchange() { if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && diff --git a/Deliveroo/DeliverooPlugin.GameFunctions.cs b/Deliveroo/DeliverooPlugin.GameFunctions.cs index 3f6ac53..16ab066 100644 --- a/Deliveroo/DeliverooPlugin.GameFunctions.cs +++ b/Deliveroo/DeliverooPlugin.GameFunctions.cs @@ -9,7 +9,6 @@ using Deliveroo.GameData; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.UI; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -52,17 +51,25 @@ partial class DeliverooPlugin private float GetDistanceToNpc(int npcId, out GameObject? o) { - foreach (var obj in _objectTable) + try { - if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c) + foreach (var obj in _objectTable) { - if (GetNpcId(obj) == npcId) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (obj != null && obj.ObjectKind == ObjectKind.EventNpc && obj is Character c) { - o = obj; - return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position); + if (GetNpcId(obj) == npcId) + { + o = obj; + return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position); + } } } } + catch (Exception) + { + // ignore + } o = null; return float.MaxValue; @@ -196,32 +203,4 @@ partial class DeliverooPlugin { return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; } - - private unsafe bool SelectSelectString(int choice) - { - if (TryGetAddonByName("SelectString", out var addonSelectString) && - IsAddonReady(&addonSelectString->AtkUnitBase)) - { - addonSelectString->AtkUnitBase.FireCallbackInt(choice); - return true; - } - - return false; - } - - private unsafe bool SelectSelectYesno(int choice, Predicate predicate) - { - if (TryGetAddonByName("SelectYesno", out var addonSelectYesno) && - IsAddonReady(&addonSelectYesno->AtkUnitBase) && - predicate(MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString())) - { - _pluginLog.Information( - $"Selecting choice={choice} for '{MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText)}'"); - - addonSelectYesno->AtkUnitBase.FireCallbackInt(choice); - return true; - } - - return false; - } } diff --git a/Deliveroo/DeliverooPlugin.SelectString.cs b/Deliveroo/DeliverooPlugin.SelectString.cs new file mode 100644 index 0000000..9d9d7fe --- /dev/null +++ b/Deliveroo/DeliverooPlugin.SelectString.cs @@ -0,0 +1,100 @@ +using System; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Deliveroo; + +partial class DeliverooPlugin +{ + private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args) + { + _pluginLog.Verbose("SelectString post-setup"); + + string desiredText; + Action followUp; + if (CurrentStage == Stage.OpenGcSupply) + { + desiredText = _gameStrings.UndertakeSupplyAndProvisioningMission; + followUp = OpenGcSupplyFollowUp; + } + else if (CurrentStage == Stage.CloseGcSupply) + { + desiredText = _gameStrings.ClosePersonnelOfficerTalk; + followUp = CloseGcSupplyFollowUp; + } + else if (CurrentStage == Stage.CloseGcSupplyThenStop) + { + desiredText = _gameStrings.ClosePersonnelOfficerTalk; + followUp = CloseGcSupplyThenCloseFollowUp; + } + else + return; + + _pluginLog.Verbose($"Looking for '{desiredText}' in prompt"); + AddonSelectString* addonSelectString = (AddonSelectString*)args.Addon; + int entries = addonSelectString->PopupMenu.PopupMenu.EntryCount; + + for (int i = 0; i < entries; ++i) + { + var textPointer = addonSelectString->PopupMenu.PopupMenu.EntryNames[i]; + if (textPointer == null) + continue; + + var text = MemoryHelper.ReadSeStringNullTerminated((nint)textPointer).ToString(); + _pluginLog.Verbose($" Choice {i} → {text}"); + if (text == desiredText) + { + + _pluginLog.Information($"Selecting choice {i} ({text})"); + addonSelectString->AtkUnitBase.FireCallbackInt(i); + + followUp(); + return; + } + } + + _pluginLog.Verbose($"Text '{desiredText}' was not found in prompt."); + } + + private void OpenGcSupplyFollowUp() + { + CurrentStage = Stage.SelectExpertDeliveryTab; + } + + private void CloseGcSupplyFollowUp() + { + if (GetNextItemToPurchase() == null) + { + _turnInWindow.State = false; + CurrentStage = Stage.RequestStop; + } + else + { + // you can occasionally get a 'not enough seals' warning lol + _continueAt = DateTime.Now.AddSeconds(1); + CurrentStage = Stage.TargetQuartermaster; + } + } + + private void CloseGcSupplyThenCloseFollowUp() + { + if (GetNextItemToPurchase() == null) + { + _turnInWindow.State = false; + CurrentStage = Stage.RequestStop; + } + else if (GetCurrentSealCount() <= + _configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost) + { + _turnInWindow.State = false; + CurrentStage = Stage.RequestStop; + } + else + { + _continueAt = DateTime.Now.AddSeconds(1); + CurrentStage = Stage.TargetQuartermaster; + } + } +} diff --git a/Deliveroo/DeliverooPlugin.SelectYesNo.cs b/Deliveroo/DeliverooPlugin.SelectYesNo.cs new file mode 100644 index 0000000..61d61a9 --- /dev/null +++ b/Deliveroo/DeliverooPlugin.SelectYesNo.cs @@ -0,0 +1,47 @@ +using System; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Deliveroo; + +partial class DeliverooPlugin +{ + private unsafe void SelectYesNoPostSetup(AddonEvent type, AddonArgs args) + { + _pluginLog.Verbose("SelectYesNo post-setup"); + + AddonSelectYesno* addonSelectYesNo = (AddonSelectYesno*)args.Addon; + string text = MemoryHelper.ReadSeString(&addonSelectYesNo->PromptText->NodeText).ToString().Replace("\n", "").Replace("\r", ""); + _pluginLog.Verbose($"YesNo prompt: '{text}'"); + + if (CurrentStage == Stage.ConfirmReward && + _gameStrings.ExchangeItems.IsMatch(text)) + { + PurchaseItemRequest? item = GetNextItemToPurchase(); + if (item == null) + { + addonSelectYesNo->AtkUnitBase.FireCallbackInt(1); + CurrentStage = Stage.CloseGcExchange; + return; + } + + _pluginLog.Information($"Selecting 'yes' ({text})"); + addonSelectYesNo->AtkUnitBase.FireCallbackInt(0); + + var nextItem = GetNextItemToPurchase(item); + if (nextItem != null && GetCurrentSealCount() >= _configuration.ReservedSealCount + nextItem.SealCost) + CurrentStage = Stage.SelectRewardTier; + else + CurrentStage = Stage.CloseGcExchange; + _continueAt = DateTime.Now.AddSeconds(0.5); + } + else if (CurrentStage == Stage.TurnInSelected && + _gameStrings.TradeHighQualityItem == text) + { + _pluginLog.Information($"Selecting 'yes' ({text})"); + addonSelectYesNo->AtkUnitBase.FireCallbackInt(0); + } + } +} diff --git a/Deliveroo/DeliverooPlugin.Supply.cs b/Deliveroo/DeliverooPlugin.Supply.cs index f4d40d6..f21786f 100644 --- a/Deliveroo/DeliverooPlugin.Supply.cs +++ b/Deliveroo/DeliverooPlugin.Supply.cs @@ -20,12 +20,6 @@ partial class DeliverooPlugin CurrentStage = Stage.OpenGcSupply; } - private void OpenGcSupply() - { - if (SelectSelectString(0)) - CurrentStage = Stage.SelectExpertDeliveryTab; - } - private unsafe void SelectExpertDeliveryTab() { var agentInterface = AgentModule.Instance()->GetAgentByInternalId(AgentId.GrandCompanySupply); @@ -147,9 +141,6 @@ partial class DeliverooPlugin private unsafe void TurnInSelectedItem() { - if (SelectSelectYesno(0, s => s == "Do you really want to trade a high-quality item?")) - return; - if (TryGetAddonByName("GrandCompanySupplyReward", out var addonSupplyReward) && IsAddonReady(&addonSupplyReward->AtkUnitBase)) { @@ -201,47 +192,6 @@ partial class DeliverooPlugin } } - private void CloseGcSupply() - { - if (SelectSelectString(3)) - { - if (GetNextItemToPurchase() == null) - { - _turnInWindow.State = false; - CurrentStage = Stage.RequestStop; - } - else - { - // you can occasionally get a 'not enough seals' warning lol - _continueAt = DateTime.Now.AddSeconds(1); - CurrentStage = Stage.TargetQuartermaster; - } - } - } - - private void CloseGcSupplyThenStop() - { - if (SelectSelectString(3)) - { - if (GetNextItemToPurchase() == null) - { - _turnInWindow.State = false; - CurrentStage = Stage.RequestStop; - } - else if (GetCurrentSealCount() <= - _configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost) - { - _turnInWindow.State = false; - CurrentStage = Stage.RequestStop; - } - else - { - _continueAt = DateTime.Now.AddSeconds(1); - CurrentStage = Stage.TargetQuartermaster; - } - } - } - private ItemFilterType ResolveSelectedSupplyFilter() { if (CharacterConfiguration is { UseHideArmouryChestItemsFilter: true }) diff --git a/Deliveroo/DeliverooPlugin.cs b/Deliveroo/DeliverooPlugin.cs index 0d74a36..0cb2b85 100644 --- a/Deliveroo/DeliverooPlugin.cs +++ b/Deliveroo/DeliverooPlugin.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; @@ -31,10 +32,12 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin private readonly ICondition _condition; private readonly ICommandManager _commandManager; private readonly IPluginLog _pluginLog; + private readonly IAddonLifecycle _addonLifecycle; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly Configuration _configuration; + private readonly GameStrings _gameStrings; private readonly ExternalPluginHandler _externalPluginHandler; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable @@ -50,7 +53,8 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin public DeliverooPlugin(DalamudPluginInterface pluginInterface, IChatGui chatGui, IGameGui gameGui, IFramework framework, IClientState clientState, IObjectTable objectTable, ITargetManager targetManager, - IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog) + IDataManager dataManager, ICondition condition, ICommandManager commandManager, IPluginLog pluginLog, + IAddonLifecycle addonLifecycle) { _pluginInterface = pluginInterface; _chatGui = chatGui; @@ -62,7 +66,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin _condition = condition; _commandManager = commandManager; _pluginLog = pluginLog; + _addonLifecycle = addonLifecycle; + _gameStrings = new GameStrings(dataManager, _pluginLog); _externalPluginHandler = new ExternalPluginHandler(_pluginInterface, _framework, _pluginLog); _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _gcRewardsCache = new GcRewardsCache(dataManager); @@ -88,6 +94,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin if (_configuration.AddVentureIfNoItemToPurchaseSelected()) _pluginInterface.SavePluginConfig(_configuration); + + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup); } internal CharacterConfiguration? CharacterConfiguration { get; set; } @@ -215,7 +224,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin break; case Stage.OpenGcSupply: - OpenGcSupply(); + // see SelectStringPostSetup break; case Stage.SelectExpertDeliveryTab: @@ -243,11 +252,11 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin break; case Stage.CloseGcSupply: - CloseGcSupply(); + // see SelectStringPostSetup break; case Stage.CloseGcSupplyThenStop: - CloseGcSupplyThenStop(); + // see SelectStringPostSetup break; case Stage.TargetQuartermaster: @@ -267,7 +276,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin break; case Stage.ConfirmReward: - ConfirmReward(); + // see SelectYesNoPostSetup break; case Stage.CloseGcExchange: @@ -292,6 +301,9 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin public void Dispose() { + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesNoPostSetup); + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup); + _commandManager.RemoveHandler("/deliveroo"); _clientState.Logout -= Logout; _clientState.Login -= Login; diff --git a/Deliveroo/GameData/GameStrings.cs b/Deliveroo/GameData/GameStrings.cs new file mode 100644 index 0000000..1d621b9 --- /dev/null +++ b/Deliveroo/GameData/GameStrings.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Dalamud.Plugin.Services; +using Lumina.Excel; +using Lumina.Excel.CustomSheets; +using Lumina.Excel.GeneratedSheets; +using Lumina.Text; +using Lumina.Text.Payloads; + +namespace Deliveroo.GameData; + +internal sealed class GameStrings +{ + private readonly IDataManager _dataManager; + private readonly IPluginLog _pluginLog; + + public GameStrings(IDataManager dataManager, IPluginLog pluginLog) + { + _dataManager = dataManager; + _pluginLog = pluginLog; + + UndertakeSupplyAndProvisioningMission = + GetDialogue("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_002"); + ClosePersonnelOfficerTalk = + GetDialogue("TEXT_COMDEFGRANDCOMPANYOFFICER_00073_A4_004"); + ExchangeItems = GetRegex(4928, addon => addon.Text) + ?? throw new Exception($"Unable to resolve {nameof(ExchangeItems)}"); + TradeHighQualityItem = GetString(102434, addon => addon.Text) + ?? throw new Exception($"Unable to resolve {nameof(TradeHighQualityItem)}"); + } + + + public string UndertakeSupplyAndProvisioningMission { get; } + public string ClosePersonnelOfficerTalk { get; } + public Regex ExchangeItems { get; } + public string TradeHighQualityItem { get; } + + private string GetDialogue(string key) + where T : QuestDialogueText + { + string result = _dataManager.GetExcelSheet()! + .Single(x => x.Key == key) + .Value + .ToString(); + _pluginLog.Verbose($"{typeof(T).Name}.{key} => {result}"); + return result; + } + + private SeString? GetSeString(uint rowId, Func mapper) + where T : ExcelRow + { + var row = _dataManager.GetExcelSheet()?.GetRow(rowId); + if (row == null) + return null; + + return mapper(row); + } + + private string? GetString(uint rowId, Func mapper) + where T : ExcelRow + { + string? text = GetSeString(rowId, mapper)?.ToString(); + + _pluginLog.Verbose($"{typeof(T).Name}.{rowId} => {text}"); + return text; + } + + private Regex? GetRegex(uint rowId, Func mapper) + where T : ExcelRow + { + SeString? text = GetSeString(rowId, mapper); + string regex = string.Join("", text.Payloads.Select(payload => + { + if (payload is TextPayload) + return Regex.Escape(payload.RawString); + else + return ".*"; + })); + _pluginLog.Verbose($"{typeof(T).Name}.{rowId} => /{regex}/"); + return new Regex(regex); + } + + [Sheet("custom/000/ComDefGrandCompanyOfficer_00073")] + private class ComDefGrandCompanyOfficer : QuestDialogueText + { + } +}