From f3b006b2762604d7dedff2e81e69c1d05f80abb6 Mon Sep 17 00:00:00 2001 From: Zeffuro Date: Tue, 30 Dec 2025 02:58:02 +0100 Subject: [PATCH] First iteration of retainer --- AetherBags.sln.DotSettings.user | 2 + .../AddonLifecycles/InventoryLifecycles.cs | 19 ++ AetherBags/Addons/AddonRetainerWindow.cs | 197 ++++++++++++++++++ AetherBags/Addons/InventoryAddonBase.cs | 5 +- AetherBags/Commands/CommandHandler.cs | 7 +- AetherBags/Configuration/GeneralSettings.cs | 2 + .../Extensions/AgentInterfaceExtensions.cs | 23 ++ .../Inventory/Scanning/InventoryScanner.cs | 2 + AetherBags/Inventory/State/RetainerState.cs | 66 ++++++ AetherBags/Plugin.cs | 10 +- AetherBags/System.cs | 2 +- 11 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 AetherBags/Addons/AddonRetainerWindow.cs create mode 100644 AetherBags/Extensions/AgentInterfaceExtensions.cs create mode 100644 AetherBags/Inventory/State/RetainerState.cs diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user index 6ef2c30..7a0bf99 100644 --- a/AetherBags.sln.DotSettings.user +++ b/AetherBags.sln.DotSettings.user @@ -1,5 +1,7 @@  ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/AddonLifecycles/InventoryLifecycles.cs index 8f4e53f..c966fc6 100644 --- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs +++ b/AetherBags/AddonLifecycles/InventoryLifecycles.cs @@ -19,6 +19,7 @@ public class InventoryLifecycles : IDisposable { Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], InventoryPreRefreshHandler); Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["InventoryBuddy"], InventoryBuddyPreRefreshHandler); + //Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["RetainerGrid0"], InventoryRetainerPreRefreshHandler); Services.Logger.Verbose("InventoryLifecycles initialized"); } @@ -70,6 +71,7 @@ public class InventoryLifecycles : IDisposable } } + // TODO: Inventory/Retainers are not perma open, need some way to close it too. private void InventoryBuddyPreRefreshHandler(AddonEvent type, AddonArgs args) { if (args is not AddonRefreshArgs refreshArgs) @@ -84,9 +86,26 @@ public class InventoryLifecycles : IDisposable } } + // TODO: Inventory/Retainers are not perma open, need some way to close it too. + // TODO: Don't have the right retainer prerefresh handler yet. + private void InventoryRetainerPreRefreshHandler(AddonEvent type, AddonArgs args) + { + if (args is not AddonRefreshArgs refreshArgs) + return; + + GeneralSettings config = System.Config.General; + + if (config.HideGameRetainer) refreshArgs.AtkValueCount = 0; + if (config.OpenRetainerWithGameInventory) + { + System.AddonRetainerWindow.Toggle(); + } + } + public void Dispose() { Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"]); Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["InventoryBuddy"]); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["RetainerGrid0"]); } } \ No newline at end of file diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs new file mode 100644 index 0000000..d80e90c --- /dev/null +++ b/AetherBags/Addons/AddonRetainerWindow.cs @@ -0,0 +1,197 @@ +using System. Numerics; +using AetherBags.Inventory.State; +using AetherBags. Nodes. Input; +using AetherBags. Nodes. Inventory; +using AetherBags. Nodes.Layout; +using Dalamud.Game. Addon.Lifecycle; +using Dalamud.Game. Addon.Lifecycle.AddonArgTypes; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs. FFXIV. Component. GUI; +using KamiToolKit.Classes; +using KamiToolKit. Nodes; + +namespace AetherBags.Addons; + +public unsafe class AddonRetainerWindow : InventoryAddonBase +{ + private readonly RetainerState _inventoryState = new(); + private TextNode _slotCounterNode = null!; + private TextNode _retainerNameNode = null!; + private TextButtonNode _entrustDuplicatesButton = null!; + + protected override InventoryStateBase InventoryState => _inventoryState; + + protected override bool HasFooter => false; + protected override bool HasSlotCounter => true; + + protected override float MinWindowWidth => 400; + protected override float MaxWindowWidth => 700; + + protected override void OnSetup(AtkUnitBase* addon) + { + WindowNode?.AddColor = new Vector3(8f / 255f, -8f / 255f, -4f / 255f); + + CategoriesNode = new WrappingGridNode + { + Position = ContentStartPosition, + Size = ContentSize, + HorizontalSpacing = CategorySpacing, + VerticalSpacing = CategorySpacing, + TopPadding = 4.0f, + BottomPadding = 4.0f, + }; + CategoriesNode.AttachNode(this); + + var size = new Vector2(addon->Size. X / 2.0f, 28.0f); + + var header = addon->WindowHeaderCollisionNode; + + float headerX = header->X; + float headerY = header->Y; + float headerW = header->Width; + float headerH = header->Height; + + float x = headerX + (headerW - size.X) * 0.5f; + float y = headerY + (headerH - size.Y) * 0.5f; + + SearchInputNode = new TextInputWithHintNode + { + Position = new Vector2(x, y), + Size = size, + OnInputReceived = _ => RefreshCategoriesCore(autosize: false), + }; + SearchInputNode. AttachNode(this); + + SettingsButtonNode = new CircleButtonNode + { + Position = new Vector2(headerW - 48f, y), + Size = new Vector2(28f), + Icon = ButtonIcon.GearCog, + OnClick = System.AddonConfigurationWindow.Toggle + }; + SettingsButtonNode. AttachNode(this); + + // Retainer name display + _retainerNameNode = new TextNode + { + Position = new Vector2(8f, 0), + Size = new Vector2(200, 20), + AlignmentType = AlignmentType.Left, + FontType = FontType.MiedingerMed, + TextFlags = TextFlags.Glare, + TextColor = ColorHelper.GetColor(50), + TextOutlineColor = ColorHelper.GetColor(32), + }; + _retainerNameNode.AttachNode(this); + + _entrustDuplicatesButton = new TextButtonNode + { + Size = new Vector2(120, 28), + String = "Entrust Duplicates", + OnClick = OnEntrustDuplicates, + }; + _entrustDuplicatesButton.AttachNode(this); + + // Slot counter + _slotCounterNode = new TextNode + { + Position = new Vector2(Size.X - 10, 0), + Size = new Vector2(82, 20), + AlignmentType = AlignmentType.Right, + FontType = FontType.MiedingerMed, + TextFlags = TextFlags.Glare, + TextColor = ColorHelper.GetColor(50), + TextOutlineColor = ColorHelper.GetColor(32), + }; + _slotCounterNode.AttachNode(this); + SlotCounterNode = _slotCounterNode; + + LayoutContent(); + + Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate); + + _inventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + + base.OnSetup(addon); + } + + protected override void RefreshCategoriesCore(bool autosize) + { + _slotCounterNode. String = _inventoryState.GetEmptySlotsString(); + _retainerNameNode.String = RetainerState.CurrentRetainerName; + + base.RefreshCategoriesCore(autosize); + } + + protected override void LayoutContent() + { + base.LayoutContent(); + + Vector2 contentPos = ContentStartPosition; + Vector2 contentSize = ContentSize; + + float footerY = contentPos.Y + contentSize.Y - FooterHeight + 4f; + + _retainerNameNode. Position = new Vector2(contentPos.X + 8f, footerY); + + float buttonWidth = _entrustDuplicatesButton.Width; + float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f; + _entrustDuplicatesButton.Position = new Vector2(buttonX, footerY - 2f); + + if (SlotCounterNode != null) + SlotCounterNode.Position = new Vector2(contentSize.X - 80f, footerY); + } + + protected override void OnUpdate(AtkUnitBase* addon) + { + if (RefreshQueued) + { + bool doAutosize = RefreshAutosizeQueued; + RefreshQueued = false; + RefreshAutosizeQueued = false; + + RefreshCategoriesCore(doAutosize); + } + + base. OnUpdate(addon); + } + + private void OnEntrustDuplicates() + { + // TODO: Implement checking if the retainer bag is def open + var agent = AgentModule.Instance()->GetAgentByInternalId(AgentId.Retainer); + agent->SendCommand(0, [0]); + } + + private void OnRetainerInventoryUpdate(AddonEvent type, AddonArgs args) + { + _inventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + + protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); + + _inventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + + public void SetSearchText(string searchText) + { + Services.Framework.RunOnTick(() => + { + if (IsOpen) SearchInputNode.SearchString = searchText; + RefreshCategoriesCore(autosize: true); + }, delayTicks: 1); + } + + protected override void OnFinalize(AtkUnitBase* addon) + { + Services.AddonLifecycle.UnregisterListener(OnRetainerInventoryUpdate); + + base.OnFinalize(addon); + } +} \ No newline at end of file diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index 3ab3810..83cd39f 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -47,6 +47,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon public void ManualRefresh() { + if (!IsOpen) return; if (!Services.ClientState.IsLoggedIn) return; if (_isRefreshing) return; try @@ -61,13 +62,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon } } - protected virtual void RefreshAllInventoryWindows() + protected static void RefreshAllInventoryWindows() { Services.Framework.RunOnTick(() => { System.AddonInventoryWindow?.ManualRefresh(); System.AddonSaddleBagWindow?.ManualRefresh(); - //AetherBags.System.AddonRetainerWindow?.ManualRefresh(); + System.AddonRetainerWindow?.ManualRefresh(); }, delayTicks: 2); } diff --git a/AetherBags/Commands/CommandHandler.cs b/AetherBags/Commands/CommandHandler.cs index e3971a2..921f984 100644 --- a/AetherBags/Commands/CommandHandler.cs +++ b/AetherBags/Commands/CommandHandler.cs @@ -3,6 +3,7 @@ using AetherBags.Helpers; using AetherBags.Inventory; using AetherBags.Inventory.State; using Dalamud.Game.Command; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; namespace AetherBags.Commands; @@ -29,7 +30,7 @@ public class CommandHandler : IDisposable }); } - private void OnCommand(string command, string args) + private unsafe void OnCommand(string command, string args) { var argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty; @@ -95,6 +96,10 @@ public class CommandHandler : IDisposable System.AddonSaddleBagWindow.Toggle(); break; + case "retainer": + System.AddonRetainerWindow.Toggle(); + break; + case "help": case "?": PrintHelp(); diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs index e1dd4e1..4845fbc 100644 --- a/AetherBags/Configuration/GeneralSettings.cs +++ b/AetherBags/Configuration/GeneralSettings.cs @@ -12,6 +12,8 @@ public class GeneralSettings public bool HideGameInventory { get; set; } = false; public bool OpenSaddleBagsWithGameInventory { get; set; } = true; public bool HideGameSaddleBags { get; set; } = false; + public bool OpenRetainerWithGameInventory { get; set; } = true; + public bool HideGameRetainer { get; set; } = false; public bool ShowCategoryItemCount { get; set; } = false; public bool LinkItemEnabled { get; set; } = false; } diff --git a/AetherBags/Extensions/AgentInterfaceExtensions.cs b/AetherBags/Extensions/AgentInterfaceExtensions.cs new file mode 100644 index 0000000..2236e0a --- /dev/null +++ b/AetherBags/Extensions/AgentInterfaceExtensions.cs @@ -0,0 +1,23 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace AetherBags.Extensions; + +public static unsafe class AgentInterfaceExtensions { + + extension(ref AgentInterface agent) + { + public void SendCommand(uint eventKind, int[] commandValues) + { + using var returnValue = new AtkValue(); + var command = stackalloc AtkValue[commandValues.Length]; + + for (var index = 0; index < commandValues.Length; index++) + { + command[index].SetInt(commandValues[index]); + } + + agent.ReceiveEvent(&returnValue, command, (uint)commandValues.Length, eventKind); + } + } +} diff --git a/AetherBags/Inventory/Scanning/InventoryScanner.cs b/AetherBags/Inventory/Scanning/InventoryScanner.cs index c957669..ac77300 100644 --- a/AetherBags/Inventory/Scanning/InventoryScanner.cs +++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs @@ -186,6 +186,8 @@ public static unsafe class InventoryScanner { InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(), InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), + InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), + InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer), _ => 0, }; diff --git a/AetherBags/Inventory/State/RetainerState.cs b/AetherBags/Inventory/State/RetainerState.cs new file mode 100644 index 0000000..dfba24e --- /dev/null +++ b/AetherBags/Inventory/State/RetainerState.cs @@ -0,0 +1,66 @@ +using AetherBags. Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace AetherBags. Inventory.State; + +public class RetainerState : InventoryStateBase +{ + public override InventorySourceType SourceType => InventorySourceType.Retainer; + public override InventoryType[] Inventories => InventorySourceDefinitions.Retainer; + + + public static unsafe ulong CurrentRetainerId + { + get + { + var retainerManager = RetainerManager.Instance(); + if (retainerManager == null) return 0; + + return retainerManager->LastSelectedRetainerId; + } + } + + public static unsafe string CurrentRetainerName + { + get + { + var retainerManager = RetainerManager.Instance(); + if (retainerManager == null) return string.Empty; + + var retainer = retainerManager->GetActiveRetainer(); + if (retainer == null) return string.Empty; + + return retainer->NameString; + } + } + + public static unsafe bool IsRetainerActive + { + get + { + if (! Services.ClientState.IsLoggedIn) return false; + + var retainerManager = RetainerManager. Instance(); + if (retainerManager == null) return false; + + return retainerManager->LastSelectedRetainerId != 0; + } + } + + public static unsafe bool AreContainersLoaded + { + get + { + if (!IsRetainerActive) return false; + + var inventoryManager = InventoryManager.Instance(); + if (inventoryManager == null) return false; + + var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1); + return container != null && container->Size > 0; + } + } + + public static bool CanMoveItems => AreContainersLoaded; +} \ No newline at end of file diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index b688706..a9eb598 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -43,6 +43,13 @@ public unsafe class Plugin : IDalamudPlugin Size = new Vector2(750, 750), }; + System.AddonRetainerWindow = new AddonRetainerWindow + { + InternalName = "AetherBags_Retainer", + Title = "AetherBags", + Size = new Vector2(750, 750), + }; + System.AddonConfigurationWindow = new AddonConfigurationWindow { InternalName = "AetherBags Config", @@ -81,7 +88,7 @@ public unsafe class Plugin : IDalamudPlugin System.AddonInventoryWindow.Dispose(); System.AddonSaddleBagWindow.Dispose(); - //System.AddonRetainerWindow.Dispose(); + System.AddonRetainerWindow.Dispose(); System.AddonConfigurationWindow.Dispose(); KamiToolKitLibrary.Dispose(); @@ -107,6 +114,7 @@ public unsafe class Plugin : IDalamudPlugin InventoryState.TrackLootedItems = false; System.AddonInventoryWindow.Close(); System.AddonSaddleBagWindow.Close(); + System.AddonRetainerWindow.Close(); System.AddonConfigurationWindow.Close(); } } \ No newline at end of file diff --git a/AetherBags/System.cs b/AetherBags/System.cs index db4b448..bc10aa3 100644 --- a/AetherBags/System.cs +++ b/AetherBags/System.cs @@ -7,7 +7,7 @@ public static class System { public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!; public static AddonSaddleBagWindow AddonSaddleBagWindow { get; set; } = null!; - //public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!; + public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!; public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!; public static SystemConfiguration Config { get; set; } = null!; } \ No newline at end of file