From cc6ba8ecf124bfd4729fafeb286ff7e8493969e7 Mon Sep 17 00:00:00 2001 From: Jackson <9527380+Jaksuhn@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:17:17 +0100 Subject: [PATCH] convert deliveroo and tank buying to async tasks --- Automaton/Features/ARCeruleum.cs | 266 +++++++++++----------- Automaton/Features/ARTurnIn.cs | 61 +---- Automaton/Features/AutoMerge.cs | 2 +- Automaton/Features/AutoPillion.cs | 2 +- Automaton/Features/AutoSelectGardening.cs | 2 +- Automaton/Features/ClickToMove.cs | 2 +- Automaton/Features/DateWithDestiny.cs | 8 +- Automaton/Features/DebugTools.cs | 2 +- Automaton/Tasks/AutoDeliveroo.cs | 84 +++++++ Automaton/Tasks/Automation.cs | 26 ++- Automaton/Tasks/BuyCeruleumTanks.cs | 75 ++++++ Automaton/Tasks/CommonTasks.cs | 79 ++++++- Automaton/Tasks/FateGrind.cs | 99 ++++++++ Automaton/Tasks/LeveTurnIn.cs | 7 + Automaton/Tasks/LoopMelding.cs | 15 ++ Automaton/UI/DebugWindow.cs | 6 +- Automaton/Utilities/DalamudReflection.cs | 45 ---- Automaton/Utilities/Events.cs | 5 + Automaton/Utilities/Extensions.cs | 2 +- Automaton/Utilities/Game.cs | 239 ++++++++++++++++++- Automaton/Utilities/ImGuiX.cs | 2 + Automaton/Utilities/Inventory.cs | 3 + Automaton/Utilities/Memory.cs | 26 +++ Automaton/Utilities/PlayerEx.cs | 2 +- Automaton/Utilities/Utils.cs | 11 - ECommons | 2 +- TODO.txt | 15 +- 27 files changed, 813 insertions(+), 275 deletions(-) create mode 100644 Automaton/Tasks/AutoDeliveroo.cs create mode 100644 Automaton/Tasks/BuyCeruleumTanks.cs create mode 100644 Automaton/Tasks/FateGrind.cs create mode 100644 Automaton/Tasks/LeveTurnIn.cs create mode 100644 Automaton/Tasks/LoopMelding.cs delete mode 100644 Automaton/Utilities/DalamudReflection.cs diff --git a/Automaton/Features/ARCeruleum.cs b/Automaton/Features/ARCeruleum.cs index d97a30a..2ccfb35 100644 --- a/Automaton/Features/ARCeruleum.cs +++ b/Automaton/Features/ARCeruleum.cs @@ -1,15 +1,11 @@ using Automaton.IPC; +using Automaton.Tasks; using Automaton.Utilities.Movement; -using ECommons.Automation.NeoTaskManager; -using ECommons.GameFunctions; -using ECommons.ImGuiMethods; -using ECommons.Throttlers; +using Dalamud.Interface; +using Dalamud.Interface.Components; using ECommons.UIHelpers.AddonMasterImplementations; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Control; -using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; -using Lumina.Excel.Sheets; namespace Automaton.Features; @@ -37,31 +33,22 @@ public override void DrawConfig() ImGuiX.DrawSection("Debug"); - ImGui.TextUnformatted($"AR:{P.AutoRetainer.IsBusy()} {P.AutoRetainer.GetSuppressed()}"); - if (Player.Available) - ImGui.TextUnformatted($"o:{PlayerEx.Occupied} m:{Player.IsMoving} mp:{Svc.Objects.FirstOrDefault(x => x.DataId == MammetVoyagerId) != null}"); - - if (ImGui.Button("request")) - AutoRetainer.RequestCharacterPostprocess(); - if (ImGui.Button("FinishCharacterPostProcess")) - AutoRetainer.FinishCharacterPostProcess(); - - if (ImGui.Button("BuyTanks")) - BuyTanks(); - - if (TaskManager.Tasks.Count > 0) + ImGuiX.TaskState(); + if (ImGuiComponents.IconButton(P.Automation.CurrentTask == null ? FontAwesomeIcon.Play : FontAwesomeIcon.Stop)) { - ImGuiX.DrawSection("Tasks"); - if (ImGui.Button($"Kill Tasks : {TaskManager.NumQueuedTasks}")) - TaskManager.Abort(); - ImGuiBB.Text($"[color=#33E6E6]{TaskManager.CurrentTask?.Name}:[/color] [color=#FFFFFF]{TaskManager.CurrentTask?.Function.Method.Name}[/color]"); - foreach (var task in TaskManager.Tasks) + if (P.Automation.CurrentTask == null) + P.Automation.Start(new BuyCeruleumTanks(), () => { AutoRetainer.FinishCharacterPostProcess(); P.UsingARPostProcess = false; }); + else { - ImGui.Indent(); - ImGuiBB.Text($"[color=#33E6E6]{task.Name}:[/color] [color=#FFFFFF]{task.Function.Method.Name}[/color]"); - ImGui.Unindent(); + P.Automation.Stop(); + AutoRetainer.FinishCharacterPostProcess(); + P.UsingARPostProcess = false; } } + + ImGui.TextUnformatted($"AR:{P.AutoRetainer.IsBusy()} {P.AutoRetainer.GetSuppressed()}"); + if (TryGetAddonMaster(out var am)) + ImGui.TextUnformatted($"FC:{am.Items[0].QuantityInInventory} {Inventory.GetItemCount(CeruleumTankId)}"); } private const uint CeruleumTankId = 10155; @@ -82,115 +69,116 @@ private unsafe void CheckCharacter() Svc.Log.Info("Skipping post process turn in for character: inventory above threshold or not in workshop."); } - private static uint Amount; - private unsafe void BuyTanks() - { - TaskManager.Enqueue(GoToMammet); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(OpenShop); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(SelectCreditExchange); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(WaitForShopOpen); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(() => // not sure how to get around being unable to make this its own function since it'd be a void return and that can't work - { - if (TryGetAddonMaster(out var m)) - { - if (m.Text.ContainsAny(TanksStr)) - if (EzThrottler.Throttle("CeruleumYesNo")) m.Yes(); - } - if (TryGetAddonMaster(out var am)) - { - if (Amount != am.CompanyCredits) - { - EzThrottler.Reset("CeruleumYesNo"); - EzThrottler.Reset("FCBuy"); - Amount = am.CompanyCredits; - } - if (am.CompanyCredits < 100) return true; - if (EzThrottler.Throttle("FCBuy", 2000)) - { - var tanks = am.Items[0]; - if (tanks.QuantityInInventory != InventoryManager.Instance()->GetInventoryItemCount(tanks.ItemId)) return false; // last purchase hasn't gone through yet - var desiredQty = (int)GetRow(tanks.ItemId)!.Value.StackSize - tanks.QuantityInInventory; - var maxCanBuyAtOnce = Math.Min(desiredQty, tanks.MaxPurchaseSize); - var maxAfford = (int)(am.CompanyCredits / tanks.Price); - var toBuy = Math.Min(maxCanBuyAtOnce, maxAfford); - if (toBuy >= 1) - tanks.Buy(toBuy); - else - { - am.Addon->Close(true); - return true; // we're at 999 or can't afford more - } - } - } - else - return null; - return false; - }, BuyConfig); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(WaitForShopClose); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(AutoRetainer.FinishCharacterPostProcess); - TaskManager.Enqueue(() => P.UsingARPostProcess = false); - } - - private bool GoToMammet() - { - if (Player.DistanceTo(MammetPos) < 0.5f) { movement.Enabled = false; return true; } - - movement.Enabled = true; - movement.DesiredPosition = MammetPos; - return false; - } - - private unsafe bool OpenShop() - { - var mammet = Svc.Objects.FirstOrDefault(x => x.DataId == MammetVoyagerId); - if (mammet == null) return false; - if (mammet.IsTarget()) - { - if (EzThrottler.Throttle(nameof(OpenShop))) - { - TargetSystem.Instance()->InteractWithObject(mammet.Struct(), false); - return true; - } - } - else - { - if (EzThrottler.Throttle("MammetSetTarget")) - { - Svc.Targets.Target = mammet; - return false; - } - } - return false; - } - - private bool SelectCreditExchange() - { - if (TryGetAddonMaster(out var m)) - { - foreach (var e in m.Entries) - { - if (e.Text.EqualsIgnoreCase(GetRow(2752515)!.Value.Name.ToString())) - { - if (EzThrottler.Throttle($"{nameof(SelectCreditExchange)}")) - { - e.Select(); - return true; - } - } - } - return false; - } - else return false; - } - - private unsafe bool WaitForShopOpen() => TryGetAddonByName("FreeCompanyCreditShop", out var addon) && IsAddonReady(addon); - private unsafe bool WaitForShopClose() => !TryGetAddonByName("FreeCompanyCreditShop", out _) && !PlayerEx.Occupied; - - private TaskManagerConfiguration BuyConfig => new(timeLimitMS: 10 * 60 * 1000); + private unsafe void BuyTanks() => P.Automation.Start(new BuyCeruleumTanks(), () => { AutoRetainer.FinishCharacterPostProcess(); P.UsingARPostProcess = false; }); + //private static uint Amount; + //private unsafe void BuyTanks() + //{ + // TaskManager.Enqueue(GoToMammet); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(OpenShop); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(SelectCreditExchange); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(WaitForShopOpen); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(() => // not sure how to get around being unable to make this its own function since it'd be a void return and that can't work + // { + // if (TryGetAddonMaster(out var m)) + // { + // if (m.Text.ContainsAny(TanksStr)) + // if (EzThrottler.Throttle("CeruleumYesNo")) m.Yes(); + // } + // if (TryGetAddonMaster(out var am)) + // { + // if (Amount != am.CompanyCredits) + // { + // EzThrottler.Reset("CeruleumYesNo"); + // EzThrottler.Reset("FCBuy"); + // Amount = am.CompanyCredits; + // } + // if (am.CompanyCredits < 100) return true; + // if (EzThrottler.Throttle("FCBuy", 2000)) + // { + // var tanks = am.Items[0]; + // if (tanks.QuantityInInventory != InventoryManager.Instance()->GetInventoryItemCount(tanks.ItemId)) return false; // last purchase hasn't gone through yet + // var desiredQty = (int)GetRow(tanks.ItemId)!.Value.StackSize - tanks.QuantityInInventory; + // var maxCanBuyAtOnce = Math.Min(desiredQty, tanks.MaxPurchaseSize); + // var maxAfford = (int)(am.CompanyCredits / tanks.Price); + // var toBuy = Math.Min(maxCanBuyAtOnce, maxAfford); + // if (toBuy >= 1) + // tanks.Buy(toBuy); + // else + // { + // am.Addon->Close(true); + // return true; // we're at 999 or can't afford more + // } + // } + // } + // else + // return null; + // return false; + // }, BuyConfig); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(WaitForShopClose); + // TaskManager.EnqueueDelay(1000); + // TaskManager.Enqueue(AutoRetainer.FinishCharacterPostProcess); + // TaskManager.Enqueue(() => P.UsingARPostProcess = false); + //} + + //private bool GoToMammet() + //{ + // if (Player.DistanceTo(MammetPos) < 0.5f) { movement.Enabled = false; return true; } + + // movement.Enabled = true; + // movement.DesiredPosition = MammetPos; + // return false; + //} + + //private unsafe bool OpenShop() + //{ + // var mammet = Svc.Objects.FirstOrDefault(x => x.DataId == MammetVoyagerId); + // if (mammet == null) return false; + // if (mammet.IsTarget()) + // { + // if (EzThrottler.Throttle(nameof(OpenShop))) + // { + // TargetSystem.Instance()->InteractWithObject(mammet.Struct(), false); + // return true; + // } + // } + // else + // { + // if (EzThrottler.Throttle("MammetSetTarget")) + // { + // Svc.Targets.Target = mammet; + // return false; + // } + // } + // return false; + //} + + //private bool SelectCreditExchange() + //{ + // if (TryGetAddonMaster(out var m)) + // { + // foreach (var e in m.Entries) + // { + // if (e.Text.EqualsIgnoreCase(GetRow(2752515)!.Value.Name.ToString())) + // { + // if (EzThrottler.Throttle($"{nameof(SelectCreditExchange)}")) + // { + // e.Select(); + // return true; + // } + // } + // } + // return false; + // } + // else return false; + //} + + //private unsafe bool WaitForShopOpen() => TryGetAddonByName("FreeCompanyCreditShop", out var addon) && IsAddonReady(addon); + //private unsafe bool WaitForShopClose() => !TryGetAddonByName("FreeCompanyCreditShop", out _) && !PlayerEx.IsBusy; + + //private TaskManagerConfiguration BuyConfig => new(timeLimitMS: 10 * 60 * 1000); } diff --git a/Automaton/Features/ARTurnIn.cs b/Automaton/Features/ARTurnIn.cs index 219cf73..46254d3 100644 --- a/Automaton/Features/ARTurnIn.cs +++ b/Automaton/Features/ARTurnIn.cs @@ -1,6 +1,7 @@ using Automaton.IPC; -using ECommons.Automation.NeoTaskManager; -using ECommons.ImGuiMethods; +using Automaton.Tasks; +using Dalamud.Interface; +using Dalamud.Interface.Components; using ImGuiNET; namespace Automaton.Features; @@ -48,36 +49,16 @@ public override void DrawConfig() ImGuiX.DrawSection("Debug"); - ImGui.TextUnformatted($"LS:{P.Lifestream.IsBusy()} AR:{P.AutoRetainer.IsBusy()} D:{P.Deliveroo.IsTurnInRunning()} N:{P.Navmesh.IsRunning()}"); - if (Player.Available) - ImGui.TextUnformatted($"o:{PlayerEx.Occupied} m:{Player.IsMoving} c:{PlayerEx.IsCasting} l:{PlayerEx.AnimationLock}"); - - if (ImGui.Button("FinishCharacterPostProcess")) - AutoRetainer.FinishCharacterPostProcess(); - - if (ImGui.Button("TurnIn (All Tasks Combined)")) - TurnIn(); - - if (ImGui.Button($"GoToGC")) - TaskManager.Enqueue(GoToGC); - - if (ImGui.Button($"TurnInGear")) - TaskManager.Enqueue(Deliveroo); - - if (ImGui.Button($"GoHome")) - TaskManager.Enqueue(GoHome); - - if (TaskManager.Tasks.Count > 0) + ImGuiX.TaskState(); + if (ImGuiComponents.IconButton(P.Automation.CurrentTask == null ? FontAwesomeIcon.Play : FontAwesomeIcon.Stop)) { - ImGuiX.DrawSection("Tasks"); - if (ImGui.Button($"Kill Tasks : {TaskManager.NumQueuedTasks}")) - TaskManager.Abort(); - ImGuiBB.Text($"[color=#33E6E6]{TaskManager.CurrentTask?.Name}:[/color] [color=#FFFFFF]{TaskManager.CurrentTask?.Function.Method.Name}[/color]"); - foreach (var task in TaskManager.Tasks) + if (P.Automation.CurrentTask == null) + P.Automation.Start(new AutoDeliveroo(), () => { AutoRetainer.FinishCharacterPostProcess(); P.UsingARPostProcess = false; }); + else { - ImGui.Indent(); - ImGuiBB.Text($"[color=#33E6E6]{task.Name}:[/color] [color=#FFFFFF]{task.Function.Method.Name}[/color]"); - ImGui.Unindent(); + P.Automation.Stop(); + AutoRetainer.FinishCharacterPostProcess(); + P.UsingARPostProcess = false; } } } @@ -98,23 +79,5 @@ private void CheckCharacter() } } - private void TurnIn() - { - TaskManager.Enqueue(GoToGC, configuration: LSConfig); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(Deliveroo, configuration: DConfig); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(GoHome, configuration: LSConfig); - TaskManager.EnqueueDelay(1000); - TaskManager.Enqueue(AutoRetainer.FinishCharacterPostProcess); - TaskManager.Enqueue(() => P.UsingARPostProcess = false); - } - - // bless lifestream for doing literally all the annoying work for me already - private void GoToGC() => TaskManager.InsertMulti([new(() => P.Lifestream.ExecuteCommand("gc")), new(() => P.Lifestream.IsBusy()), new(() => !P.Lifestream.IsBusy(), LSConfig)]); - private void Deliveroo() => TaskManager.InsertMulti([new(() => Svc.Commands.ProcessCommand("/deliveroo enable")), new(() => P.Deliveroo.IsTurnInRunning()), new(() => !P.Deliveroo.IsTurnInRunning(), DConfig)]); - private void GoHome() => TaskManager.InsertMulti([new(() => P.Lifestream.ExecuteCommand("auto")), new(() => P.Lifestream.IsBusy()), new(() => !P.Lifestream.IsBusy(), LSConfig)]); - - private TaskManagerConfiguration LSConfig => new(timeLimitMS: 2 * 60 * 1000); - private TaskManagerConfiguration DConfig => new(timeLimitMS: 10 * 60 * 1000, abortOnTimeout: false); + private void TurnIn() => P.Automation.Start(new AutoDeliveroo(), () => { AutoRetainer.FinishCharacterPostProcess(); P.UsingARPostProcess = false; }); } diff --git a/Automaton/Features/AutoMerge.cs b/Automaton/Features/AutoMerge.cs index a8981e4..0683783 100644 --- a/Automaton/Features/AutoMerge.cs +++ b/Automaton/Features/AutoMerge.cs @@ -37,7 +37,7 @@ public class InventorySlot private unsafe void OnSetup(string addonName) { - if (PlayerEx.Occupied || !inventoryAddonNames.Contains(addonName)) return; + if (PlayerEx.IsBusy || !inventoryAddonNames.Contains(addonName)) return; inventorySlots.Clear(); var inv = InventoryManager.Instance(); diff --git a/Automaton/Features/AutoPillion.cs b/Automaton/Features/AutoPillion.cs index 558bb63..79221b4 100644 --- a/Automaton/Features/AutoPillion.cs +++ b/Automaton/Features/AutoPillion.cs @@ -13,7 +13,7 @@ public class AutoPillion : Tweak private unsafe void OnUpdate(IFramework framework) { - if (!Player.Available || PlayerEx.Occupied || Svc.Condition[ConditionFlag.Mounted]) + if (!Player.Available || PlayerEx.IsBusy || Svc.Condition[ConditionFlag.Mounted]) { if (TaskManager.Tasks.Count > 0) TaskManager.Abort(); diff --git a/Automaton/Features/AutoSelectGardening.cs b/Automaton/Features/AutoSelectGardening.cs index 3248e00..3503d3e 100644 --- a/Automaton/Features/AutoSelectGardening.cs +++ b/Automaton/Features/AutoSelectGardening.cs @@ -62,7 +62,7 @@ public override void DrawConfig() { ImGuiX.DrawSection("Configuration"); - if (Utils.AnyNull(Seeds, Soils, Fertilizers)) return; + if (AnyNull(Seeds, Soils, Fertilizers)) return; ImGui.Checkbox("Show Only Inventory Items", ref Config.OnlyShowInventoryItems); diff --git a/Automaton/Features/ClickToMove.cs b/Automaton/Features/ClickToMove.cs index 6bb29c5..5d0d56d 100644 --- a/Automaton/Features/ClickToMove.cs +++ b/Automaton/Features/ClickToMove.cs @@ -31,7 +31,7 @@ public override void Disable() private Vector3 destination = Vector3.Zero; private void MoveTo(IFramework framework) { - if (!Player.Available || PlayerEx.Occupied) return; + if (!Player.Available || PlayerEx.IsBusy) return; if (Player.DistanceTo(destination) < 0.0025f) movement.Enabled = false; if (IsKeyPressed(ECommons.Interop.LimitedKeys.LeftMouseButton) && Utils.IsClickingInGameWorld()) diff --git a/Automaton/Features/DateWithDestiny.cs b/Automaton/Features/DateWithDestiny.cs index 37893c6..78410ef 100644 --- a/Automaton/Features/DateWithDestiny.cs +++ b/Automaton/Features/DateWithDestiny.cs @@ -46,7 +46,7 @@ public class DateWithDestinyConfiguration } [Tweak, Requirement(NavmeshIPC.Name, NavmeshIPC.Repo)] -internal class DateWithDestiny : Tweak +public class DateWithDestiny : Tweak { public override string Name => "Date with Destiny"; public override string Description => $"Fate tracker and mover. Doesn't handle combat. Open the menu with /vfate."; @@ -118,8 +118,8 @@ private enum Z .Zip(YokaiZones, (wxy, z) => (wxy.Minion, wxy.Medal, wxy.Weapon, z)) .ToList(); - private static readonly uint[] ForlornIDs = [7586, 7587]; - private static readonly uint[] TwistOfFateStatusIDs = [1288, 1289]; + internal static readonly uint[] ForlornIDs = [7586, 7587]; + internal static readonly uint[] TwistOfFateStatusIDs = [1288, 1289]; private ushort nextFateID; private byte fateMaxLevel; @@ -315,7 +315,7 @@ private void TargetAndMoveToEnemy(IGameObject target) private void ExecuteDismount() => ExecuteActionSafe(ActionType.GeneralAction, 23); private void ExecuteJump() => ExecuteActionSafe(ActionType.GeneralAction, 2); - private IOrderedEnumerable GetFates() => Svc.Fates.Where(FateConditions) + internal IOrderedEnumerable GetFates() => Svc.Fates.Where(FateConditions) .OrderByDescending(x => Config.PrioritizeBonusFates && x.HasBonus diff --git a/Automaton/Features/DebugTools.cs b/Automaton/Features/DebugTools.cs index e1a9559..b81adf2 100644 --- a/Automaton/Features/DebugTools.cs +++ b/Automaton/Features/DebugTools.cs @@ -94,7 +94,7 @@ private void OnEnterPvP() private bool ncActive; private unsafe void OnUpdate(IFramework framework) { - if (!Player.Available || PlayerEx.Occupied) return; + if (!Player.Available || PlayerEx.IsBusy) return; ShowMouseOverlay = false; if (Config.EnableTPClick && tpActive) { diff --git a/Automaton/Tasks/AutoDeliveroo.cs b/Automaton/Tasks/AutoDeliveroo.cs new file mode 100644 index 0000000..045a72e --- /dev/null +++ b/Automaton/Tasks/AutoDeliveroo.cs @@ -0,0 +1,84 @@ +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using System.Threading.Tasks; + +namespace Automaton.Tasks; +public sealed class AutoDeliveroo : CommonTasks +{ + protected override async Task Execute() + { + Status = "Going to GC"; + await GoToGC(); + Status = "Turning in Gear"; + await TurnIn(); + Status = "Going Home"; + await GoHome(); + } + + private async Task GoToGC() + { + P.Lifestream.ExecuteCommand("gc"); + await WaitUntilThenFalse(() => P.Lifestream.IsBusy(), $"{nameof(GoToGC)}"); + } + + /* + * Problems with this approach: + * - Inventory outside of armoury chest isn't considered + * - Could potentially overwrite gearsets on valuable characters (meant for an alt-only thing where they can gear up based on what they bring back from ventures) + */ + private async Task EquipRecommended() + { + var updating = false; + unsafe + { + var mod = RecommendEquipModule.Instance(); + if (mod == null) return; + updating = mod->IsUpdating; + } + await WaitUntil(() => !updating, $"WaitingFor{nameof(RecommendEquipModule)}Update"); + + unsafe + { + var mod = RecommendEquipModule.Instance(); + var equippedItems = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + var isAllEquipped = true; + foreach (var recommendedItemPtr in mod->RecommendedItems) + { + var recommendedItem = recommendedItemPtr.Value; + if (recommendedItem == null || recommendedItem->ItemId == 0) + continue; + + var isEquipped = false; + for (var i = 0; i < equippedItems->Size; ++i) + { + var equippedItem = equippedItems->Items[i]; + if (equippedItem.ItemId != 0 && equippedItem.ItemId == recommendedItem->ItemId) + { + isEquipped = true; + break; + } + } + + if (!isEquipped) + isAllEquipped = false; + } + + if (!isAllEquipped) + mod->EquipRecommendedGear(); + + } + await WaitUntil(() => !PlayerEx.IsBusy, $"WaitingForNotBusy"); + } + + private async Task TurnIn() + { + Svc.Commands.ProcessCommand("/deliveroo enable"); + await WaitUntilThenFalse(() => P.Deliveroo.IsTurnInRunning(), $"{nameof(TurnIn)}"); + } + + private async Task GoHome() + { + P.Lifestream.ExecuteCommand("auto"); + await WaitUntilThenFalse(() => P.Lifestream.IsBusy(), $"{nameof(GoHome)}"); + } +} diff --git a/Automaton/Tasks/Automation.cs b/Automaton/Tasks/Automation.cs index f8f664c..9cd5e5f 100644 --- a/Automaton/Tasks/Automation.cs +++ b/Automaton/Tasks/Automation.cs @@ -44,7 +44,7 @@ public void Rename(string newName) public void Cancel() => _cts.Cancel(); - public void Run(Action completed) + public void Run(Action completed, Action? OnCompleted = null) { Svc.Framework.Run(async () => { @@ -53,6 +53,7 @@ public void Run(Action completed) if (task.IsFaulted) PluginLog.Warning($"Task ended with error: {task.Exception}"); completed(); + OnCompleted?.Invoke(); _cts.Dispose(); }, _cts.Token); } @@ -83,6 +84,25 @@ protected async Task WaitWhile(Func condition, string scopeName, int check /// protected async Task WaitUntil(Func condition, string scopeName, int checkFrequency = 1) => await WaitWhile(() => !condition(), scopeName, checkFrequency); + /// + /// Wait until a condition function returns true, then wait until it returns false. + /// + /// Meant for functions like checking if an ipc is busy then checking til it's not. + protected async Task WaitUntilThenFalse(Func condition, string scopeName, int checkFrequency = 1) + { + using var scope = BeginScope(scopeName); + while (!condition()) + { + Log("waiting..."); + await NextFrame(checkFrequency); + } + while (condition()) + { + Log("waiting..."); + await NextFrame(checkFrequency); + } + } + protected void Log(string message) => PluginLog.Debug($"[{GetType().Name}] [{string.Join(" > ", _debugContext)}] {message}"); // start a new debug context; should be disposed, so usually should be assigned to RAII variable @@ -121,7 +141,7 @@ public void Stop() } // if any other task is running, it's cancelled - public void Start(AutoTask task) + public void Start(AutoTask task, Action? OnCompleted = null) { Stop(); CurrentTask = task; @@ -130,7 +150,7 @@ public void Start(AutoTask task) if (CurrentTask == task) CurrentTask = null; // else: some other task is now executing - }); + }, OnCompleted); } } diff --git a/Automaton/Tasks/BuyCeruleumTanks.cs b/Automaton/Tasks/BuyCeruleumTanks.cs new file mode 100644 index 0000000..9621b4d --- /dev/null +++ b/Automaton/Tasks/BuyCeruleumTanks.cs @@ -0,0 +1,75 @@ +using ECommons.UIHelpers.AddonMasterImplementations; +using System.Threading.Tasks; + +namespace Automaton.Tasks; +public sealed class BuyCeruleumTanks : CommonTasks +{ + private const uint CeruleumTankId = 10155; + private const uint MammetVoyagerENpcId = 1011274; + private readonly Memory.FreeCompanyDialogIPCReceive ipc = new(); + + protected override async Task Execute() + { + var npc = Game.GetNPCInfo(MammetVoyagerENpcId, Player.Territory, CeruleumTankId); + ErrorIf(npc == null, $"Failed to find NPC {MammetVoyagerENpcId} in {Player.Territory}"); + ErrorIf(npc!.ShopId == 0, $"Failed to find shop for NPC {MammetVoyagerENpcId} in {Player.Territory}"); + + Status = $"Moving to {npc.Location}"; + await MoveToDirectly(npc.Location, 0.5f); + await BuyFromFccShop(MammetVoyagerENpcId, npc!.ShopId, CeruleumTankId, 999 - Inventory.GetItemCount(CeruleumTankId, false), Game.ShopType.FreeCompanyCreditShop); + } + + private async Task BuyFromFccShop(ulong vendorInstanceId, uint shopId, uint itemId, int count, Game.ShopType shopType = Game.ShopType.None) + { + using var scope = BeginScope("Buy"); + Status = "Opening shop"; + if (!Game.IsShopOpen(shopId, shopType)) + { + Log("Opening shop..."); + ErrorIf(!Game.OpenShop(vendorInstanceId, shopId), $"Failed to open shop {vendorInstanceId:X}.{shopId:X}"); + await WaitWhile(() => !Game.IsShopOpen(shopId, shopType), "WaitForOpen"); + await WaitWhile(() => !Svc.Condition[ConditionFlag.OccupiedInEvent], "WaitForCondition"); + } + await WaitWhile(() => !Game.AddonActive("FreeCompanyCreditShop"), "WaitForFCCShop"); + + Log("Buying..."); + if (TryGetAddonMaster(out var am)) + { + var tanks = am.Items.First(x => x.ItemId == itemId); + while (count > 0) + { + Status = $"Buying x{count} ceruleum tanks"; + tanks.Buy(Math.Min(count, tanks.MaxPurchaseSize)); + count -= tanks.MaxPurchaseSize; + await WaitUntilSkipYesNo(() => GetAddonTankCount() != Inventory.GetItemCount(tanks.ItemId, false), "WaitingForPurchase"); + Status = "Waiting for purchase to go through"; + // I could just wait until the atkvalue equals the real inventory count again but this was a fun experiment. + using var stop = new OnDispose(ipc.FreeCompanyDialogPacketReceiveHook.Disable); + await WaitUntilServerIPC(); + } + } + + Status = "Closing shop"; + Log("Closing shop..."); + unsafe bool Close() => am.Base->Close(true); + ErrorIf(!Close(), $"Failed to close shop {vendorInstanceId:X}.{shopId:X}"); + await WaitWhile(() => Game.AddonActive("FreeCompanyCreditShop"), "WaitForClose"); + await WaitWhile(() => Svc.Condition[ConditionFlag.OccupiedInEvent], "WaitForCondition"); + await NextFrame(); + } + + private async Task WaitUntilServerIPC() + { + using var scope = BeginScope("WaitForPacketFreeCompanyDialog"); + ipc.FreeCompanyDialogPacketReceiveHook.Enable(); + var lastPacketTimestamp = ipc.LastPacketTimestamp; + while (ipc.LastPacketTimestamp == lastPacketTimestamp) + { + Log($"waiting..."); + await NextFrame(); + } + ipc.FreeCompanyDialogPacketReceiveHook.Disable(); + } + + private int GetAddonTankCount() => TryGetAddonMaster(out var am) ? am.Items.First(x => x.ItemId == CeruleumTankId).QuantityInInventory : 0; +} diff --git a/Automaton/Tasks/CommonTasks.cs b/Automaton/Tasks/CommonTasks.cs index 625cdee..a5c6a6d 100644 --- a/Automaton/Tasks/CommonTasks.cs +++ b/Automaton/Tasks/CommonTasks.cs @@ -1,9 +1,14 @@ -using Lumina.Excel.Sheets; +using Automaton.Utilities.Movement; +using Lumina.Excel.Sheets; using System.Threading.Tasks; +using Achievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement; namespace Automaton.Tasks; public abstract class CommonTasks : AutoTask { + private readonly OverrideMovement movement = new(); + private readonly Memory.AchievementProgress achv = new(); + protected async Task MoveTo(Vector3 dest, float tolerance, bool mount = false, bool fly = false) { using var scope = BeginScope("MoveTo"); @@ -21,6 +26,18 @@ protected async Task MoveTo(Vector3 dest, float tolerance, bool mount = false, b await WaitWhile(() => !(Player.DistanceTo(dest) < tolerance), "Navigate"); } + protected async Task MoveToDirectly(Vector3 dest, float tolerance) + { + using var scope = BeginScope("MoveToDirectly"); + if (Player.DistanceTo(dest) < tolerance) + return; + + movement.DesiredPosition = dest; + movement.Enabled = true; + using var stop = new OnDispose(() => movement.Enabled = false); + await WaitWhile(() => !(Player.DistanceTo(dest) < tolerance), "DirectNavigate"); + } + protected async Task TeleportTo(uint territoryId, Vector3 destination) { using var scope = BeginScope("Teleport"); @@ -33,8 +50,8 @@ protected async Task TeleportTo(uint territoryId, Vector3 destination) if (Player.Territory != GetRow(teleportAetheryteId)!.Value.Territory.RowId) { ErrorIf(!Coords.ExecuteTeleport(teleportAetheryteId), $"Failed to teleport to {teleportAetheryteId}"); - await WaitWhile(() => !PlayerEx.Occupied, "TeleportStart"); - await WaitWhile(() => PlayerEx.Occupied, "TeleportFinish"); + await WaitWhile(() => !PlayerEx.IsBusy, "TeleportStart"); + await WaitWhile(() => PlayerEx.IsBusy, "TeleportFinish"); } if (teleportAetheryteId != closestAetheryteId) @@ -44,8 +61,8 @@ protected async Task TeleportTo(uint territoryId, Vector3 destination) ErrorIf(!PlayerEx.InteractWith(aetheryteId), "Failed to interact with aetheryte"); await WaitUntilSkipTalk(() => Game.AddonActive("SelectString"), "WaitSelectAethernet"); Game.TeleportToAethernet(teleportAetheryteId, closestAetheryteId); - await WaitWhile(() => !PlayerEx.Occupied, "TeleportAethernetStart"); - await WaitWhile(() => PlayerEx.Occupied, "TeleportAethernetFinish"); + await WaitWhile(() => !PlayerEx.IsBusy, "TeleportAethernetStart"); + await WaitWhile(() => PlayerEx.IsBusy, "TeleportAethernetFinish"); } if (territoryId == 886) @@ -56,8 +73,8 @@ protected async Task TeleportTo(uint territoryId, Vector3 destination) ErrorIf(!PlayerEx.InteractWith(aetheryteId), "Failed to interact with aetheryte"); await WaitUntilSkipTalk(() => Game.AddonActive("SelectString"), "WaitSelectFirmament"); Game.TeleportToFirmament(teleportAetheryteId); - await WaitWhile(() => !PlayerEx.Occupied, "TeleportFirmamentStart"); - await WaitWhile(() => PlayerEx.Occupied, "TeleportFirmamentFinish"); + await WaitWhile(() => !PlayerEx.IsBusy, "TeleportFirmamentStart"); + await WaitWhile(() => PlayerEx.IsBusy, "TeleportFirmamentFinish"); } ErrorIf(Player.Territory != territoryId, "Failed to teleport to expected zone"); @@ -86,4 +103,52 @@ protected async Task WaitUntilSkipTalk(Func condition, string scopeName) await NextFrame(); } } + + protected async Task WaitUntilSkipYesNo(Func condition, string scopeName) + { + using var scope = BeginScope(scopeName); + while (!condition()) + { + if (Game.AddonActive("SelectYesno")) + { + Log("progressing yes/no..."); + Game.SelectYes(); + } + Log("waiting..."); + await NextFrame(); + } + } + + protected async Task<(uint, uint)> GetAchievementProgress(uint achievementId, string scopeName) + { + using var scope = BeginScope(scopeName); + achv.ReceiveAchievementProgressHook.Enable(); + unsafe { Achievement.Instance()->RequestAchievementProgress(achievementId); } + static unsafe bool IsState(Achievement.AchievementState state) => Achievement.Instance()->ProgressRequestState == state; + await WaitUntil(() => IsState(Achievement.AchievementState.Requested), "WaitingForRequestStart"); + await WaitUntil(() => IsState(Achievement.AchievementState.Loaded), "WaitingForRequestFinish"); + achv.ReceiveAchievementProgressHook.Disable(); + return achv.LastId == achievementId ? (achv.LastCurrent, achv.LastMax) : throw new Exception($"Expected data for achievement [#{achievementId}], got [#{achv.LastId}]"); + } + + protected async Task BuyFromShop(ulong vendorInstanceId, uint shopId, uint itemId, int count, Game.ShopType shopType = Game.ShopType.None) + { + using var scope = BeginScope("Buy"); + if (!Game.IsShopOpen(shopId, shopType)) + { + Log("Opening shop..."); + ErrorIf(!Game.OpenShop(vendorInstanceId, shopId), $"Failed to open shop {vendorInstanceId:X}.{shopId:X}"); + await WaitWhile(() => !Game.IsShopOpen(shopId, shopType), "WaitForOpen"); + await WaitWhile(() => !Svc.Condition[ConditionFlag.OccupiedInEvent], "WaitForCondition"); + } + + Log("Buying..."); + ErrorIf(!Game.BuyItemFromShop(shopId, itemId, count), $"Failed to buy {count}x {itemId} from shop {vendorInstanceId:X}.{shopId:X}"); + await WaitWhile(() => Game.ShopTransactionInProgress(shopId), "Transaction"); + Log("Closing shop..."); + ErrorIf(!Game.CloseShop(), $"Failed to close shop {vendorInstanceId:X}.{shopId:X}"); + await WaitWhile(() => Game.IsShopOpen(), "WaitForClose"); + await WaitWhile(() => Svc.Condition[ConditionFlag.OccupiedInEvent], "WaitForCondition"); + await NextFrame(); + } } diff --git a/Automaton/Tasks/FateGrind.cs b/Automaton/Tasks/FateGrind.cs new file mode 100644 index 0000000..657f62c --- /dev/null +++ b/Automaton/Tasks/FateGrind.cs @@ -0,0 +1,99 @@ +using Automaton.Features; +using Dalamud.Game.ClientState.Fates; +using FFXIVClientStructs.FFXIV.Client.Game.Fate; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using System.Threading.Tasks; +using FateState = Dalamud.Game.ClientState.Fates.FateState; + +namespace Automaton.Tasks; +public sealed class FateGrind(DateWithDestiny tweak) : CommonTasks +{ + // TODO: + // auto detect yokai event, set yokai mode accordingly + private static Vector3 TargetPos; + private ushort nextFateId; + private byte fateMaxLevel; + private unsafe bool InFate => FateManager.Instance()->CurrentFate != null; + + private ushort FateID + { + get; set + { + if (field != value) + SyncFate(value); + field = value; + } + } + + protected override async Task Execute() + { + while (true) + { + Status = "Swapping Zones"; + if (Svc.Fates.Count == 0) + { + if (tweak.Config.SwapZones) + await SwapZones(); + else + return; + } + + if (InFate) + { + unsafe + { + fateMaxLevel = FateManager.Instance()->CurrentFate->MaxLevel; + FateID = FateManager.Instance()->CurrentFate->FateId; + } + } + else + FateID = 0; + + if (!InFate) + { + var nextFate = GetFates().FirstOrDefault(); + if (nextFate is not null && Svc.Condition[ConditionFlag.InFlight] && !P.Navmesh.PathfindInProgress()) + { + nextFateId = nextFate.FateId; + TargetPos = GetRandomPointInFate(nextFateId); + Status = $"Moving to [{nextFateId}] @ {TargetPos}"; + await MoveTo(TargetPos, 5, true, true); + } + } + await NextFrame(); + } + } + + private async Task SwapZones() + { + // if we're achievement farming, find the next zone where the achievement isn't completed, otherwise, pick a random zone within the same expac + // if we're yokai farming, find the next zone where the yokai isn't completed + uint zone = 0; + await TeleportTo(zone, default); + } + private bool FateConditions(IFate f) => f.GameData.Value.Rule == 1 && f.State != FateState.Preparation && f.Duration <= tweak.Config.MaxDuration && f.Progress <= tweak.Config.MaxProgress && f.TimeRemaining > tweak.Config.MinTimeRemaining && !tweak.Config.blacklist.Contains(f.FateId); + + private IOrderedEnumerable GetFates() => Svc.Fates.Where(FateConditions) + .OrderByDescending(x => tweak.Config.PrioritizeBonusFates && x.HasBonus && (!tweak.Config.BonusWhenTwist || Player.Status.FirstOrDefault(x => DateWithDestiny.TwistOfFateStatusIDs.Contains(x.StatusId)) != null)) + .ThenByDescending(x => tweak.Config.PrioritizeStartedFates && x.Progress > 0) + .ThenBy(f => Vector3.Distance(PlayerEx.Position, f.Position)); + + private unsafe Vector3 GetRandomPointInFate(ushort fateID) + { + var fate = FateManager.Instance()->GetFateById(fateID); + var angle = new Random().NextDouble() * 2 * Math.PI; + // Get a random point in a circle within half its radius + var randomPoint = new Vector3((float)(fate->Location.X + fate->Radius / 2 * Math.Cos(angle)), fate->Location.Y, (float)(fate->Location.Z + fate->Radius / 2 * Math.Sin(angle))); + var point = P.Navmesh.NearestPoint(randomPoint, 5, 5); + return (Vector3)(point == null ? fate->Location : point); + } + + private unsafe void SyncFate(ushort value) + { + if (value != 0 && PlayerState.Instance()->IsLevelSynced == 0) + { + if (Player.Level > fateMaxLevel) + ECommons.Automation.Chat.Instance.SendMessage("/lsync"); + } + } +} diff --git a/Automaton/Tasks/LeveTurnIn.cs b/Automaton/Tasks/LeveTurnIn.cs new file mode 100644 index 0000000..54de1ce --- /dev/null +++ b/Automaton/Tasks/LeveTurnIn.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace Automaton.Tasks; +public sealed class LeveTurnIn : CommonTasks +{ + protected override async Task Execute() => throw new NotImplementedException(); +} diff --git a/Automaton/Tasks/LoopMelding.cs b/Automaton/Tasks/LoopMelding.cs new file mode 100644 index 0000000..a4820d3 --- /dev/null +++ b/Automaton/Tasks/LoopMelding.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; + +namespace Automaton.Tasks; +public sealed class LoopMelding : CommonTasks +{ + private static readonly uint GettingTooAttachedVII = 1905; + protected override async Task Execute() + { + var (current, max) = await GetAchievementProgress(GettingTooAttachedVII, $"GetProgress{nameof(GettingTooAttachedVII)}"); + while (current < max) + { + current++; + } + } +} diff --git a/Automaton/UI/DebugWindow.cs b/Automaton/UI/DebugWindow.cs index 34dbca3..9ff1a2c 100644 --- a/Automaton/UI/DebugWindow.cs +++ b/Automaton/UI/DebugWindow.cs @@ -93,7 +93,7 @@ public override unsafe void Draw() var nutsAmt = InventoryManager.Instance()->GetInventoryItemCount(nuts); var nutsCost = 25; var freeslots = InventoryManager.Instance()->GetEmptySlotsInBag() + Inventory.GetEmptySlots([InventoryType.ArmoryRings]); - uint tobuy = (uint)Math.Min(nutsAmt / nutsCost, freeslots); + var tobuy = (uint)Math.Min(nutsAmt / nutsCost, freeslots); Svc.Log.Info($"{InventoryManager.Instance()->GetEmptySlotsInBag()} {Inventory.GetEmptySlots([InventoryType.ArmoryRings])} {nutsAmt} {nutsAmt / nutsCost} {tobuy}"); Callback.Fire(addon, true, 0, 49, tobuy); } @@ -115,7 +115,7 @@ public override unsafe void Draw() if (item.Value.ItemSortCategory.Value.Param is 175 or 160) { P.TaskManager.Enqueue(() => AgentInventoryContext.Instance()->UseItem(slot->ItemId)); - P.TaskManager.Enqueue(() => !Player.IsAnimationLocked && !PlayerEx.Occupied && !PlayerEx.IsCasting); + P.TaskManager.Enqueue(() => !Player.IsAnimationLocked && !PlayerEx.IsBusy && !PlayerEx.IsCasting); } //ActionManager.Instance()->UseAction(ActionType.Item, slot->ItemId); } @@ -178,7 +178,7 @@ public override unsafe void Draw() using (ImRaii.Disabled(!P.Automation.Running)) if (ImGui.Button("Stop current task")) P.Automation.Stop(); - ImGui.TextUnformatted($"{P.Automation.CurrentTask?.Status ?? "Idle"}"); + ImGuiX.TaskState(); if (AgentMap.Instance()->IsFlagMarkerSet != 0) { diff --git a/Automaton/Utilities/DalamudReflection.cs b/Automaton/Utilities/DalamudReflection.cs deleted file mode 100644 index f44b7ec..0000000 --- a/Automaton/Utilities/DalamudReflection.cs +++ /dev/null @@ -1,45 +0,0 @@ -using ECommons.Reflection; -using System.Reflection; - -namespace Automaton.Utilities; -public class DalamudReflection -{ - public static bool HasRepo(string repository) - { - var conf = DalamudReflector.GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); - var repolist = (IEnumerable)conf.GetFoP("ThirdRepoList"); - if (repolist != null) - foreach (var r in repolist) - if ((string)r.GetFoP("Url") == repository) - return true; - return false; - } - - public static void AddRepo(string repository, bool enabled) - { - var conf = DalamudReflector.GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); - var repolist = (IEnumerable)conf.GetFoP("ThirdRepoList"); - if (repolist != null) - foreach (var r in repolist) - if ((string)r.GetFoP("Url") == repository) - return; - var instance = Activator.CreateInstance(Svc.PluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!); - instance.SetFoP("Url", repository); - instance.SetFoP("IsEnabled", enabled); - conf.GetFoP>("ThirdRepoList").Add(instance!); - } - - public static void ReloadPluginMasters() - { - var mgr = DalamudReflector.GetService("Dalamud.Plugin.Internal.PluginManager"); - var pluginReload = mgr?.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public); - pluginReload?.Invoke(mgr, [true]); - } - - public static void SaveDalamudConfig() - { - var conf = DalamudReflector.GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); - var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public); - configSave?.Invoke(conf, null); - } -} diff --git a/Automaton/Utilities/Events.cs b/Automaton/Utilities/Events.cs index a691b31..1173c65 100644 --- a/Automaton/Utilities/Events.cs +++ b/Automaton/Utilities/Events.cs @@ -23,4 +23,9 @@ public class Events public static event Action? EnteredPvPInstance; public static void OnEnteredPvPInstance() => EnteredPvPInstance?.Invoke(); + + public static event Action>? ServerIPCReceived; + public static void OnServerIPCReceived(DateTime sendTimestamp, uint sourceServerActor, uint targetServerActor, ushort opcode, uint epoch, Span payload) + => ServerIPCReceived?.Invoke(sendTimestamp, sourceServerActor, targetServerActor, opcode, epoch, payload.ToArray().AsMemory()); + public delegate void ServerIPCReceivedDelegate(); } diff --git a/Automaton/Utilities/Extensions.cs b/Automaton/Utilities/Extensions.cs index 9ad9a04..9c62e3c 100644 --- a/Automaton/Utilities/Extensions.cs +++ b/Automaton/Utilities/Extensions.cs @@ -56,7 +56,7 @@ public static List ToList(this StdVector stdVector) where T : unmanaged unsafe { - T* current = stdVector.First; + var current = stdVector.First; for (var i = 0; i < size; i++) { list.Add(current[i]); diff --git a/Automaton/Utilities/Game.cs b/Automaton/Utilities/Game.cs index 277945b..babdf01 100644 --- a/Automaton/Utilities/Game.cs +++ b/Automaton/Utilities/Game.cs @@ -1,7 +1,15 @@ -using FFXIVClientStructs.FFXIV.Client.Network; +using ECommons.GameFunctions; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.Interop; +using Lumina.Data.Files; +using Lumina.Data.Parsing.Layer; +using Lumina.Excel.Sheets; namespace Automaton.Utilities; public unsafe class Game @@ -21,6 +29,16 @@ public static void ProgressTalk() } } + public static void SelectYes() + { + if (TryGetAddonByName("SelectYesno", out var addon)) + { + var evt = new AtkEvent() { Listener = &addon->AtkEventListener, Target = &AtkStage.Instance()->AtkEventTarget }; + var data = new AtkEventData(); + addon->ReceiveEvent(AtkEventType.ButtonClick, 0, &evt, &data); + } + } + public static void TeleportToAethernet(uint currentAetheryte, uint destinationAetheryte) { Span payload = [4, destinationAetheryte]; @@ -32,4 +50,223 @@ public static void TeleportToFirmament(uint currentAetheryte) Span payload = [9]; PacketDispatcher.SendEventCompletePacket(0x50000 | currentAetheryte, 0, 0, payload.GetPointer(0), (byte)payload.Length, null); } + + public enum ShopType + { + None = 0, + GilShop = 1, + FreeCompanyCreditShop = 2 + } + + public static bool IsShopOpen(uint shopId = 0, ShopType shopType = ShopType.None) + { + AgentInterface* agent = null; + switch (shopType) + { + case ShopType.GilShop: + agent = &AgentShop.Instance()->AgentInterface; + break; + case ShopType.FreeCompanyCreditShop: + agent = AgentModule.Instance()->GetAgentByInternalId(AgentId.FreeCompanyCreditShop); + break; + } + if (agent == null || !agent->IsAgentActive() || &agent->AtkEventInterface == null || !agent->IsAddonReady()) + return false; + if (shopId == 0 || shopType == ShopType.None) + return true; // some shop is open... + if (!EventFramework.Instance()->EventHandlerModule.EventHandlerMap.TryGetValuePointer(shopId, out var eh) || eh == null || eh->Value == null) + return false; + var proxy = (ShopEventHandler.AgentProxy*)&agent->AtkEventInterface; + return proxy->Handler == eh->Value; + } + + public static bool OpenShop(GameObject* vendor, uint shopId) + { + PluginLog.Debug($"Interacting with {(ulong)vendor->GetGameObjectId():X}"); + TargetSystem.Instance()->InteractWithObject(vendor); + var selector = EventHandlerSelector.Instance(); + if (selector->Target == null) + return true; // assume interaction was successful without selector + + if (selector->Target != vendor) + { + PluginLog.Error($"Unexpected selector target {(ulong)selector->Target->GetGameObjectId():X} when trying to interact with {(ulong)vendor->GetGameObjectId():X}"); + return false; + } + + for (var i = 0; i < selector->OptionsCount; ++i) + { + if (selector->Options[i].Handler->Info.EventId.Id == shopId) + { + PluginLog.Debug($"Selecting selector option {i} for shop {shopId:X}"); + EventFramework.Instance()->InteractWithHandlerFromSelector(i); + return true; + } + } + + PluginLog.Error($"Failed to find shop {shopId:X} in selector for {(ulong)vendor->GetGameObjectId():X}"); + return false; + } + + public static bool OpenShop(ulong vendorInstanceId, uint shopId) + { + if (Svc.Objects.TryGetFirst(o => o.DataId == vendorInstanceId, out var vendor)) + return OpenShop(vendor.Struct(), shopId); + else + { + PluginLog.Error($"Failed to find vendor {vendorInstanceId:X}"); + return false; + } + } + + public static bool CloseShop() + { + var agent = AgentShop.Instance(); + if (agent == null || agent->EventReceiver == null) + return false; + AtkValue res = default, arg = default; + var proxy = (ShopEventHandler.AgentProxy*)agent->EventReceiver; + proxy->Handler->CancelInteraction(); + arg.SetInt(-1); + agent->ReceiveEvent(&res, &arg, 1, 0); + return true; + } + + public static bool BuyItemFromShop(uint shopId, uint itemId, int count) + { + if (!EventFramework.Instance()->EventHandlerModule.EventHandlerMap.TryGetValuePointer(shopId, out var eh) || eh == null || eh->Value == null) + { + PluginLog.Error($"Event handler for shop {shopId:X} not found"); + return false; + } + + if (!IsHandlerAShop(eh->Value->Info.EventId.ContentId)) + { + PluginLog.Error($"{shopId:X} is not a shop"); + return false; + } + + var shop = (ShopEventHandler*)eh->Value; + PluginLog.Debug($"{shop->VisibleItemsCount}"); + for (var i = 0; i < shop->VisibleItemsCount; ++i) + { + var index = shop->VisibleItems[i]; + if (shop->Items[index].ItemId == itemId) + { + PluginLog.Debug($"Buying {count}x {itemId} from {shopId:X}"); + shop->BuyItemIndex = index; + shop->ExecuteBuy(count); + return true; + } + } + + PluginLog.Error($"Did not find item {itemId} in shop {shopId:X}"); + return false; + } + + public static bool ShopTransactionInProgress(uint shopId) + { + if (!EventFramework.Instance()->EventHandlerModule.EventHandlerMap.TryGetValuePointer(shopId, out var eh) || eh == null || eh->Value == null) + { + PluginLog.Error($"Event handler for shop {shopId:X} not found"); + return false; + } + + if (!IsHandlerAShop(eh->Value->Info.EventId.ContentId)) + { + PluginLog.Error($"{shopId:X} is not a shop"); + return false; + } + + var shop = (ShopEventHandler*)eh->Value; + return shop->WaitingForTransactionToFinish; + } + + public static bool IsHandlerAShop(EventHandlerType contentId) => contentId is EventHandlerType.Shop or EventHandlerType.FreeCompanyCreditShop; + + public class NPCInfo(ulong id, Vector3 location, uint shopId) + { + public ulong Id = id; + public Vector3 Location = location; + public uint ShopId = shopId; + } + + public static NPCInfo? GetNPCInfo(uint enpcId, uint territoryId, uint itemId = 0) + { + var scene = GetRow(territoryId)!.Value.Bg.ToString(); + var filenameStart = scene.LastIndexOf('/') + 1; + var planeventLayerGroup = "bg/" + scene[0..filenameStart] + "planevent.lgb"; + PluginLog.Debug($"Territory {territoryId} -> {planeventLayerGroup}"); + var lvb = Svc.Data.GetFile(planeventLayerGroup); + if (lvb != null) + { + foreach (var layer in lvb.Layers) + { + foreach (var instance in layer.InstanceObjects) + { + if (instance.AssetType != LayerEntryType.EventNPC) + continue; + var baseId = ((LayerCommon.ENPCInstanceObject)instance.Object).ParentData.ParentData.BaseId; + if (baseId == enpcId) + { + var npcId = (1ul << 32) | instance.InstanceId; + Vector3 npcLocation = new(instance.Transform.Translation.X, instance.Transform.Translation.Y, instance.Transform.Translation.Z); + PluginLog.Debug($"Found npc {baseId} {instance.InstanceId} '{GetRow(baseId)?.Singular}' at {npcLocation}"); + if (itemId != 0) + { + var vendor = FindVendorItem(baseId, itemId); + if (vendor.itemIndex >= 0) + { + PluginLog.Debug($"Found shop #{vendor.shopId} and item index #{vendor.itemIndex}"); + return new NPCInfo(npcId, npcLocation, vendor.shopId); + } + } + return new NPCInfo(npcId, npcLocation, 0); + } + } + } + } + return null; + } + + private static (uint shopId, int itemIndex) FindVendorItem(uint enpcId, uint itemId) + { + var enpcBase = GetRow(enpcId); + if (enpcBase == null) + return (0, -1); + + foreach (var handler in enpcBase.Value.ENpcData) + { + var eventType = (EventHandlerType)(handler.RowId >> 16); + switch (eventType) + { + case EventHandlerType.Shop: + var gilItems = GetSubRow(handler.RowId); + if (gilItems == null) + continue; + + for (var i = 0; i < gilItems.Value.Count; ++i) + { + var shopItem = gilItems.Value[i]; + if (shopItem.Item.RowId == itemId) + return (handler.RowId, i); + } + break; + case EventHandlerType.FreeCompanyCreditShop: + var fccItems = GetRow(handler.RowId); + if (fccItems == null) + continue; + for (var i = 0; i < fccItems.Value.ItemData.Count; ++i) + { + var shopItem = fccItems.Value.ItemData[i]; + if (shopItem.Item.RowId == itemId) + return (handler.RowId, i); + } + break; + default: + continue; + } + } + return (0, -1); + } } diff --git a/Automaton/Utilities/ImGuiX.cs b/Automaton/Utilities/ImGuiX.cs index 1443cdc..cddac39 100644 --- a/Automaton/Utilities/ImGuiX.cs +++ b/Automaton/Utilities/ImGuiX.cs @@ -231,4 +231,6 @@ public static bool Enum(string label, ref T v) where T : Enum } return res; } + + public static void TaskState() => ImGui.TextUnformatted($"State: {P.Automation.CurrentTask?.Status ?? "Idle"}"); } diff --git a/Automaton/Utilities/Inventory.cs b/Automaton/Utilities/Inventory.cs index c9ef63f..28be073 100644 --- a/Automaton/Utilities/Inventory.cs +++ b/Automaton/Utilities/Inventory.cs @@ -53,6 +53,9 @@ public static unsafe (InventoryType inv, int slot)? GetItemLocationInInventory(u return null; } + private static unsafe int InternalGetItemCount(uint itemId, bool isHq) => InventoryManager.Instance()->GetInventoryItemCount(itemId, isHq); + public static unsafe int GetItemCount(uint itemId, bool includeHQ = true) => includeHQ ? InternalGetItemCount(itemId, true) + InternalGetItemCount(itemId, false) : InternalGetItemCount(itemId, false); + public static unsafe bool HasItem(uint itemId) => GetItemInInventory(itemId, Equippable) != null; public static unsafe bool HasItemEquipped(uint itemId) { diff --git a/Automaton/Utilities/Memory.cs b/Automaton/Utilities/Memory.cs index 66e22fd..e72dda3 100644 --- a/Automaton/Utilities/Memory.cs +++ b/Automaton/Utilities/Memory.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Common.Lua; using System.Runtime.InteropServices; using static Automaton.Utilities.Enums; @@ -39,6 +40,7 @@ public static class Signatures internal const string WorldTravelSetupInfo = "48 8B CB E8 ?? ?? ?? ?? 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 4C 8B 05 ?? ?? ?? ??"; internal const string InventoryManagerUniqueItemCheck = "E8 ?? ?? ?? ?? 44 8B E0 EB 29"; internal const string ItemIsUniqueConditionalJump = "75 4D"; + internal const string FreeCompanyDialogPacketReceive = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 42 31"; // +47: 75 57 // 270: 75 12 // 107: 0f 85 ea 00 00 00 @@ -207,10 +209,17 @@ public class AchievementProgress : Hook [EzHook(Signatures.ReceiveAchievementProgress, false)] internal EzHook ReceiveAchievementProgressHook = null!; + internal uint LastId; + internal uint LastCurrent; + internal uint LastMax; + private void ReceiveAchievementProgressDetour(Achievement* achievement, uint id, uint current, uint max) { try { + LastId = id; + LastCurrent = current; + LastMax = max; Svc.Log.Debug($"{nameof(ReceiveAchievementProgressDetour)}: [{id}] {current} / {max}"); Events.OnAchievementProgressUpdate(id, current, max); } @@ -424,4 +433,21 @@ public static void SetSpeed(float speedBase) private static unsafe void SetMoveControlData(float speed) => Dalamud.SafeMemory.Write(((delegate* unmanaged[Stdcall])Svc.SigScanner.ScanText(Signatures.MoveController))(1) + 8, speed); #endregion + + #region Server IPC Packet Receive + public class FreeCompanyDialogIPCReceive : Hook + { + internal delegate void FreeCompanyDialogPacketReceiveDelegate(InfoProxyInterface* ptr, byte* packetData); + [EzHook(Signatures.FreeCompanyDialogPacketReceive, false)] + internal readonly EzHook FreeCompanyDialogPacketReceiveHook = null!; + + internal DateTime LastPacketTimestamp = DateTime.MinValue; + private void FreeCompanyDialogPacketReceiveDetour(InfoProxyInterface* ptr, byte* packetData) + { + LastPacketTimestamp = DateTime.Now; + Svc.Log.Info($"{nameof(FreeCompanyDialogPacketReceiveDetour)}: Packet received at {LastPacketTimestamp}"); + FreeCompanyDialogPacketReceiveHook.Original(ptr, packetData); + } + } + #endregion } diff --git a/Automaton/Utilities/PlayerEx.cs b/Automaton/Utilities/PlayerEx.cs index 91a521f..1ed4514 100644 --- a/Automaton/Utilities/PlayerEx.cs +++ b/Automaton/Utilities/PlayerEx.cs @@ -20,7 +20,7 @@ public static unsafe class PlayerEx public static BattleChara* BattleChara => (BattleChara*)Svc.ClientState.LocalPlayer.Address; public static CSGameObject* GameObject => (CSGameObject*)Svc.ClientState.LocalPlayer.Address; - public static bool Occupied => IsOccupied() || IsCasting || AnimationLock > 0; + public static bool IsBusy => IsOccupied() || IsCasting || AnimationLock > 0; public static PlayerController* Controller => (PlayerController*)Svc.SigScanner.GetStaticAddressFromSig(Memory.Signatures.PlayerController); public static bool HasPenalty => FFXIVClientStructs.FFXIV.Client.Game.UI.InstanceContent.Instance()->GetPenaltyRemainingInMinutes(0) > 0; diff --git a/Automaton/Utilities/Utils.cs b/Automaton/Utilities/Utils.cs index 433f6a4..7f62d9f 100644 --- a/Automaton/Utilities/Utils.cs +++ b/Automaton/Utilities/Utils.cs @@ -96,15 +96,4 @@ private static unsafe void SynthesizeEvent(ulong eventKind, Span args) var eventData = stackalloc int[] { 0, 0, 0 }; Agent->AgentInterface.ReceiveEvent((AtkValue*)eventData, args.GetPointer(0), (uint)args.Length, eventKind); } - - public static T GetService() - { - Svc.Log.Info($"Requesting {typeof(T)}"); - var service = typeof(IDalamudPlugin).Assembly.GetType("Dalamud.Service`1")!.MakeGenericType(typeof(T)); - var get = service.GetMethod("Get", BindingFlags.Public | BindingFlags.Static)!; - return (T)get.Invoke(null, null)!; - } - - public static bool AllNull(params object[] objects) => objects.All(s => s == null); - public static bool AnyNull(params object[] objects) => objects.Any(s => s == null); } diff --git a/ECommons b/ECommons index c6a0bdd..014728e 160000 --- a/ECommons +++ b/ECommons @@ -1 +1 @@ -Subproject commit c6a0bdd06b852eca250d3b2652e679686dd0d17c +Subproject commit 014728e5a5ba6d825c9edb7da15d7e0c940d6d24 diff --git a/TODO.txt b/TODO.txt index 8b20ebd..a5ee54a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,9 +1,14 @@ framework: - - make it so one bad sig doesn't prevent all sigged tweaks from loading - migrate config name to something agnostic -relay helper: minion support, prevent double relaying -Some sort of go to npc command that incorporates itemvendorlocation/lifestream/navmesh -/tplast command to teleport to the last linked loc +existing tweaks: + - migrate taskmanager stuff to use the async system + - write up GTA requirements + - relay helper: minion support, prevent double relaying +new tweaks: + - leve turn in helper + - Some sort of go to npc command that incorporates itemvendorlocation/lifestream/navmesh + - /tplast command to teleport to the last linked loc + - a rank killer. Be able to take a flag and go to it and kill whatever A rank is there date with destiny adjustments - zone whitelist/blacklist @@ -12,4 +17,4 @@ date with destiny adjustments - fate whitelist/blacklist address book: move to lifestream -g2a/achievement tracker: move to their own plugins \ No newline at end of file +g2a/achievement tracker: move to their own plugins? \ No newline at end of file