Merge remote-tracking branch 'origin/master' into dev/pie-lover

This commit is contained in:
Shawrkie Williams
2025-12-28 11:39:20 -05:00
18 changed files with 555 additions and 285 deletions
@@ -0,0 +1,64 @@
using System;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.ReadOnly;
namespace AetherBags.AddonLifecycles;
public class InventoryLifecycles : IDisposable
{
public InventoryLifecycles()
{
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], PreRefreshHandler);
Services.Logger.Verbose("InventoryLifecycles initialized");
}
private unsafe void PreRefreshHandler(AddonEvent type, AddonArgs args)
{
if (args is not AddonRefreshArgs refreshArgs)
return;
GeneralSettings config = System.Config.General;
Services.Logger.Debug("PreRefresh event for Inventory detected");
AtkValuePtr[] atkValues = refreshArgs.AtkValueEnumerable.ToArray();
if (atkValues.Length < 7) return;
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
AtkValue* value5 = (AtkValue*)atkValues[5].Address;
AtkValue* value6 = (AtkValue*)atkValues[6].Address;
int openTitleId = value1->Int;
ReadOnlySeString title = value5->String.AsReadOnlySeString();
ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString();
System.AddonInventoryWindow.SetNotification(new InventoryNotificationInfo(title, upperTitle));
if (config.HideGameInventory) refreshArgs.AtkValueCount = 0;
if (config.OpenWithGameInventory)
{
if (openTitleId == 0)
{
System.AddonInventoryWindow.Toggle();
}
else
{
System.AddonInventoryWindow.Open();
}
}
}
public void Dispose()
{
Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"]);
}
}
@@ -119,6 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
listNode.AddOption(newWrapper);
RefreshSelectionList();
System.AddonInventoryWindow.ManualInventoryRefresh();
}
private void OnRemoveCategory(CategoryWrapper categoryWrapper)
@@ -134,6 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
{
OnOptionChanged(null);
}
System.AddonInventoryWindow.ManualInventoryRefresh();
}
private void RefreshSelectionList()
+8 -3
View File
@@ -125,9 +125,6 @@ public class AddonInventoryWindow : NativeAddon
RefreshCategoriesCore(doAutosize);
}
InventoryNotificationType currentNotificationType = (InventoryNotificationType) AgentInventory.Instance()->OpenTitleId;
if(currentNotificationType != _notificationNode.NotificationType) _notificationNode.NotificationType = currentNotificationType;
base.OnUpdate(addon);
}
@@ -292,6 +289,14 @@ public class AddonInventoryWindow : NativeAddon
private void ResizeWindow(float width, float height)
=> ResizeWindow(width, height, recalcLayout: true);
public void SetNotification(InventoryNotificationInfo info)
{
Services.Framework.RunOnTick(() =>
{
if(IsOpen) _notificationNode.NotificationInfo = info;
}, delayTicks: 1);
}
protected override unsafe void OnFinalize(AtkUnitBase* addon)
{
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
+5 -1
View File
@@ -1,5 +1,8 @@
using AetherBags.Configuration;
using AetherBags.Inventory;
using Dalamud.Game.Text.SeStringHandling;
using KamiToolKit.Premade;
using SeStringBuilder = Lumina.Text.SeStringBuilder;
namespace AetherBags.Addons;
@@ -13,7 +16,8 @@ public class CategoryWrapper(UserCategoryDefinition categoryDefinition) : IInfoN
}
public string GetSubLabel() {
return CategoryDefinition!.Enabled ? "Enabled" : "Disabled";
if(UserCategoryMatcher.IsCatchAll(CategoryDefinition!)) return " No valid rules!";
return CategoryDefinition!.Enabled ? "✓ Enabled" : " Disabled";
}
public uint? GetId() => null;
@@ -11,6 +11,8 @@ public class GeneralSettings
public int CompactLookahead { get; set; } = 24;
public bool CompactPreferLargestFit { get; set; } = true;
public bool CompactStableInsert { get; set; } = true;
public bool OpenWithGameInventory { get; set; } = true;
public bool HideGameInventory { get; set; } = false;
}
public enum InventoryStackMode : byte
+82
View File
@@ -1,6 +1,9 @@
using System;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace AetherBags.Hooks;
@@ -17,7 +20,14 @@ public sealed unsafe class InventoryHooks : IDisposable
ushort dstSlot,
bool unk);
private delegate void HandleInventoryEventDelegate(AgentInterface* eventInterface, AtkValue* atkValue, int valueCount);
private readonly Hook<MoveItemSlotDelegate>? _moveItemSlotHook;
/*
private readonly Hook<UIModule.Delegates.OpenInventory>? _openInventoryHook;
private readonly Hook<HandleInventoryEventDelegate>? _handleInventoryEventHook;
private readonly Hook<RaptureAtkModule.Delegates.OpenAddon>? _openAddonHook;
*/
public InventoryHooks()
{
@@ -34,6 +44,47 @@ public sealed unsafe class InventoryHooks : IDisposable
{
Services.Logger.Error(e, "Failed to hook MoveItemSlot");
}
/*
try
{
_openInventoryHook = Services.GameInteropProvider.HookFromAddress<UIModule.Delegates.OpenInventory>(
UIModule.Instance()->VirtualTable->OpenInventory,
OpenInventoryDetour);
_openInventoryHook.Enable();
Services.Logger.Debug("OpenInventory hooked successfully.");
}
catch (Exception e)
{
Services.Logger.Error(e, "Failed to hook OpenInventory");
}
try
{
_handleInventoryEventHook = Services.GameInteropProvider.HookFromSignature<HandleInventoryEventDelegate>(
"E8 ?? ?? ?? ?? 48 8B 74 24 ?? 33 C0 ?? ?? 89 43",
HandleInventoryEventDetour);
_handleInventoryEventHook.Enable();
Services.Logger.Debug("HandleInventoryEvent hooked successfully.");
}
catch (Exception e)
{
Services.Logger.Error(e, "Failed to hook HandleInventoryEvent");
}
try
{
_openAddonHook = Services.GameInteropProvider.HookFromAddress<RaptureAtkModule.Delegates.OpenAddon>(
RaptureAtkModule.MemberFunctionPointers.OpenAddon,
OpenAddonDetour);
_openAddonHook.Enable();
Services.Logger.Debug("OpenAddon hooked successfully.");
}
catch (Exception e)
{
Services.Logger.Error(e, "Failed to hook MoveItemSlot");
}
*/
}
private int MoveItemSlotDetour(InventoryManager* manager,
@@ -51,8 +102,39 @@ public sealed unsafe class InventoryHooks : IDisposable
return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
}
/*
private void OpenInventoryDetour(UIModule* uiModule, byte type)
{
Services.Logger.Debug($"[OpenInventory Hook] Opening inventory of type {type}");
_openInventoryHook?.Original(uiModule, type);
}
private void HandleInventoryEventDetour(AgentInterface* eventInterface, AtkValue* atkValue, int valueCount)
{
for(int i = 0; i < valueCount; i++)
{
Services.Logger.Debug($"[HandleInventoryEvent Hook] AtkValue[{i}]: Type={atkValue[i].Type}, ToString: {atkValue[i].ToString()} ");
}
_handleInventoryEventHook?.Original(eventInterface, atkValue, valueCount);
}
private ushort OpenAddonDetour(RaptureAtkModule* thisPtr, uint addonNameId, uint valueCount, AtkValue* values, AtkModuleInterface.AtkEventInterface* eventInterface, ulong eventKind, ushort parentAddonId, int depthLayer)
{
for(int i = 0; i < valueCount; i++)
{
Services.Logger.Debug($"[OpenAddon Hook] AtkValue[{i}]: ToString: {values[i].ToString()} ");
}
return _openAddonHook!.Original(thisPtr, addonNameId, valueCount, values, eventInterface, eventKind, parentAddonId, depthLayer);
}
*/
public void Dispose()
{
_moveItemSlotHook?.Dispose();
/*
_openInventoryHook?.Dispose();
_handleInventoryEventHook?.Dispose();
_openAddonHook?.Dispose();
*/
}
}
@@ -54,6 +54,13 @@ public static class CategoryBucketManager
for (int i = 0; i < sortedScratch.Count; i++)
{
UserCategoryDefinition category = sortedScratch[i];
if (!category.Enabled)
continue;
if (UserCategoryMatcher.IsCatchAll(category))
continue;
uint bucketKey = MakeUserCategoryKey(category.Order);
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
@@ -56,8 +56,8 @@ public class InventoryNotificationState
return notificationCache.GetValueOrDefault((InventoryNotificationType)openTitleId);
}
public record InventoryNotificationInfo(ReadOnlySeString Title, ReadOnlySeString Message);
}
public record InventoryNotificationInfo(ReadOnlySeString Title, ReadOnlySeString Message);
public enum InventoryNotificationType : uint
{
+72 -31
View File
@@ -11,6 +11,44 @@ internal static class UserCategoryMatcher
{
var rules = userCategory.Rules;
bool hasIdentificationFilters = rules.AllowedItemIds.Count > 0 || rules.AllowedItemNamePatterns.Count > 0;
if (hasIdentificationFilters)
{
bool matchesAnyIdentification = false;
if (rules.AllowedItemIds.Count > 0 && rules.AllowedItemIds.Contains(item.Item.ItemId))
{
matchesAnyIdentification = true;
}
if (!matchesAnyIdentification && rules.AllowedItemNamePatterns.Count > 0)
{
for (int i = 0; i < rules.AllowedItemNamePatterns.Count; i++)
{
string pattern = rules.AllowedItemNamePatterns[i];
if (string.IsNullOrWhiteSpace(pattern))
continue;
try
{
if (Regex.IsMatch(item.Name, pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
{
matchesAnyIdentification = true;
break;
}
}
catch
{
// Invalid regex: ignore it.
}
}
}
if (!matchesAnyIdentification)
return false;
}
if (rules.AllowedUiCategoryIds.Count > 0)
{
uint uiCategoryId = item.UiCategory.RowId;
@@ -18,9 +56,6 @@ internal static class UserCategoryMatcher
return false;
}
if (rules.AllowedItemIds.Count > 0 && !rules.AllowedItemIds.Contains(item.Item.ItemId))
return false;
if (rules.AllowedRarities.Count > 0 && !rules.AllowedRarities.Contains(item.Rarity))
return false;
@@ -39,40 +74,46 @@ internal static class UserCategoryMatcher
if (!MatchesToggle(rules.Dyeable, item.IsDyeable)) return false;
if (!MatchesToggle(rules.Repairable, item.IsRepairable)) return false;
if (rules.AllowedItemNamePatterns.Count > 0)
{
bool any = false;
for (int i = 0; i < rules.AllowedItemNamePatterns.Count; i++)
{
string pattern = rules.AllowedItemNamePatterns[i];
if (string.IsNullOrWhiteSpace(pattern))
continue;
// Treat patterns as regex for now.
try
{
if (Regex.IsMatch(item.Name, pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
{
any = true;
break;
}
}
catch
{
// Invalid regex: ignore it.
}
}
if (!any)
return false;
}
return true;
}
private static bool InRange<T>(T value, T min, T max) where T : struct, IComparable<T>
=> value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
public static bool IsCatchAll(UserCategoryDefinition userCategory)
{
var rules = userCategory.Rules;
if (rules.AllowedItemIds.Count > 0)
return false;
if (rules.AllowedItemNamePatterns.Count > 0)
return false;
if (rules.AllowedUiCategoryIds.Count > 0)
return false;
if (rules.AllowedRarities.Count > 0)
return false;
if (rules.Level.Enabled)
return false;
if (rules.ItemLevel.Enabled)
return false;
if (rules.VendorPrice.Enabled)
return false;
if (rules.Untradable.ToggleState != ToggleFilterState.Ignored)
return false;
if (rules.Unique.ToggleState != ToggleFilterState.Ignored)
return false;
if (rules.Collectable.ToggleState != ToggleFilterState.Ignored)
return false;
if (rules.Dyeable.ToggleState != ToggleFilterState.Ignored)
return false;
if (rules.Repairable.ToggleState != ToggleFilterState.Ignored)
return false;
return true;
}
private static bool MatchesToggle(StateFilter filter, bool itemHasProperty)
=> filter.ToggleState switch
{
+4 -6
View File
@@ -8,17 +8,15 @@ namespace AetherBags.Nodes.Color;
public class ColorInputRow : HorizontalListNode
{
private readonly GridNode _gridNode;
private ColorPickerAddon? _colorPickerAddon;
private readonly LabelTextNode _labelTextNode;
private ColorPreviewButtonNode _colorPreview;
private Vector4 _initialColor;
private readonly ColorPreviewButtonNode _colorPreview;
public ColorInputRow()
{
InitializeColorPicker();
_initialColor = CurrentColor;
var initialColor = CurrentColor;
_colorPreview = new ColorPreviewButtonNode
{
@@ -33,7 +31,7 @@ public class ColorInputRow : HorizontalListNode
{
CurrentColor = color;
_colorPreview?.Color = color;
_initialColor = color;
initialColor = color;
OnColorConfirmed?.Invoke(color);
};
_colorPickerAddon?.OnColorPreviewed = color =>
@@ -41,7 +39,7 @@ public class ColorInputRow : HorizontalListNode
_colorPreview?.Color = color;
OnColorChange?.Invoke(color);
};
_colorPickerAddon?.OnColorCancelled = () => OnColorCanceled?.Invoke(_initialColor);
_colorPickerAddon?.OnColorCancelled = () => OnColorCanceled?.Invoke(initialColor);
}
};
_colorPreview.AttachNode(this);
@@ -1,6 +1,7 @@
using System;
using System.Numerics;
using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Nodes.Color;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -8,6 +9,7 @@ using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Action = System.Action;
namespace AetherBags.Nodes.Configuration.Category;
@@ -65,6 +67,17 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
FitContents = true;
ItemSpacing = 4.0f;
var catchAllWarningNode = new TextNode
{
Size = new Vector2(300, 40),
TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize,
SeString = new SeStringBuilder().Append(" Warning: No rules configured\nThis category won't match anything!").ToReadOnlySeString(),
TextColor = ColorHelper.GetColor(17),
LineSpacing = 20,
IsVisible = UserCategoryMatcher.IsCatchAll(CategoryDefinition),
};
AddNode(catchAllWarningNode);
AddNode(CreateSectionHeader("Basic Settings"));
_enabledCheckbox = new CheckboxNode
@@ -297,7 +310,7 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
private static void NotifyChanged()
{
System.AddonInventoryWindow?.ManualInventoryRefresh();
System.AddonInventoryWindow.ManualInventoryRefresh();
}
private void NotifyCategoryPropertyChanged()
@@ -50,7 +50,7 @@ public sealed class StringListEditorNode : VerticalListNode
if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value))
{
_list.Add(value);
_addInput.String = "";
_addInput?.String = "";
RefreshItems();
_onChanged?.Invoke();
}
@@ -0,0 +1,77 @@
using System;
using System.Linq;
using System.Numerics;
using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.General;
internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
{
private readonly CheckboxNode _hideDefaultBagsCheckboxNode;
private readonly LabeledDropdownNode _stackDropDown;
public FunctionalConfigurationNode()
{
GeneralSettings config = System.Config.General;
var titleNode = new CategoryTextNode
{
Height = 18,
String = "Functional Configuration",
};
AddNode(titleNode);
AddTab(1);
var showWithGameCheckBox = new CheckboxNode
{
Size = Size with { Y = 18 },
IsVisible = true,
String = "Auto-open with game inventory",
IsChecked = config.OpenWithGameInventory,
OnClick = isChecked =>
{
config.OpenWithGameInventory = isChecked;
_hideDefaultBagsCheckboxNode?.IsEnabled = isChecked;
}
};
AddNode(showWithGameCheckBox);
AddTab(1);
_hideDefaultBagsCheckboxNode = new CheckboxNode
{
Size = Size with { Y = 18 },
IsVisible = true,
String = "Hide default inventory bags",
IsEnabled = config.OpenWithGameInventory,
IsChecked = config.HideGameInventory,
OnClick = isChecked =>
{
config.HideGameInventory = isChecked;
}
};
AddNode(_hideDefaultBagsCheckboxNode);
SubtractTab(1);
_stackDropDown = new LabeledDropdownNode
{
Size = new Vector2(300, 20),
IsEnabled = true,
LabelText = "Stack Mode",
LabelTextFlags = TextFlags.AutoAdjustNodeSize,
Options = Enum.GetNames(typeof(InventoryStackMode)).ToList(),
SelectedOption = config.StackMode.ToString(),
OnOptionSelected = selected =>
{
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
{
config.StackMode = parsed;
System.AddonInventoryWindow.ManualInventoryRefresh();
}
}
};
AddNode(_stackDropDown);
}
}
@@ -11,7 +11,6 @@ namespace AetherBags.Nodes.Configuration.General;
public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{
private readonly CheckboxNode _debugCheckboxNode = null!;
private readonly LabeledDropdownNode _stackDropDown = null!;
public GeneralScrollingAreaNode()
{
@@ -19,24 +18,7 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNod
ContentNode.ItemSpacing = 32;
_stackDropDown = new LabeledDropdownNode
{
Size = new Vector2(300, 20),
IsEnabled = true,
LabelText = "Stack Mode",
LabelTextFlags = TextFlags.AutoAdjustNodeSize,
Options = Enum.GetNames(typeof(InventoryStackMode)).ToList(),
SelectedOption = config.StackMode.ToString(),
OnOptionSelected = selected =>
{
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
{
config.StackMode = parsed;
System.AddonInventoryWindow.ManualInventoryRefresh();
}
}
};
ContentNode.AddNode(_stackDropDown);
ContentNode.AddNode(new FunctionalConfigurationNode());
ContentNode.AddNode(new LayoutConfigurationNode());
+228 -228
View File
@@ -1,244 +1,244 @@
using System;
using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Interop;
using FFXIVClientStructs.FFXIV.Client.Enums;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
using System;
using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Interop;
using FFXIVClientStructs.FFXIV.Client.Enums;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace AetherBags.Nodes;
namespace AetherBags.Nodes;
public unsafe class DragDropNode : ComponentNode<AtkComponentDragDrop, AtkUldComponentDataDragDrop> {
public unsafe class DragDropNode : ComponentNode<AtkComponentDragDrop, AtkUldComponentDataDragDrop> {
// FIX: Manually expose the pointers that are 'internal' in KamiToolKit
// We access the raw AtkComponentNode* via 'this.ResNode' and cast from there.
private new AtkComponentDragDrop* Component => (AtkComponentDragDrop*)this.InternalComponentNode->Component;
private new AtkUldComponentDataDragDrop* Data => (AtkUldComponentDataDragDrop*)Component->UldManager.ComponentData;
// FIX: Manually expose the pointers that are 'internal' in KamiToolKit
// We access the raw AtkComponentNode* via 'this.ResNode' and cast from there.
private AtkComponentDragDrop* Component => (AtkComponentDragDrop*)Node->Component;
private AtkUldComponentDataDragDrop* Data => (AtkUldComponentDataDragDrop*)Component->UldManager.ComponentData;
public readonly ImageNode DragDropBackgroundNode;
public readonly IconNode IconNode;
public readonly ImageNode DragDropBackgroundNode;
public readonly IconNode IconNode;
public DragDropNode() {
SetInternalComponentType(ComponentType.DragDrop);
public DragDropNode() {
SetInternalComponentType(ComponentType.DragDrop);
DragDropBackgroundNode = new SimpleImageNode {
NodeId = 3,
Size = new Vector2(44.0f, 44.0f),
TexturePath = "ui/uld/DragTargetA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(44.0f, 44.0f),
WrapMode = WrapMode.Tile,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
};
DragDropBackgroundNode.AttachNode(this);
DragDropBackgroundNode = new SimpleImageNode {
NodeId = 3,
Size = new Vector2(44.0f, 44.0f),
TexturePath = "ui/uld/DragTargetA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(44.0f, 44.0f),
WrapMode = WrapMode.Tile,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
};
DragDropBackgroundNode.AttachNode(this);
IconNode = new IconNode {
NodeId = 2,
Size = new Vector2(44.0f, 48.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
};
IconNode.AttachNode(this);
IconNode = new IconNode {
NodeId = 2,
Size = new Vector2(44.0f, 48.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
};
IconNode.AttachNode(this);
LoadTimelines();
LoadTimelines();
Data->Nodes[0] = IconNode.NodeId;
Data->Nodes[0] = IconNode.NodeId;
AcceptedType = DragDropType.Everything;
Payload = new DragDropPayload();
AcceptedType = DragDropType.Everything;
Payload = new DragDropPayload();
// Use the fixed shadow struct for writing initial values if needed,
// though direct field access on the struct usually works for simple fields.
// However, to be safe with the VTable fix, we just set fields directly here
// as they are standard offsets, or use the pointer.
Component->AtkDragDropInterface.DragDropType = DragDropType.Everything;
Component->AtkDragDropInterface.DragDropReferenceIndex = 0;
// Use the fixed shadow struct for writing initial values if needed,
// though direct field access on the struct usually works for simple fields.
// However, to be safe with the VTable fix, we just set fields directly here
// as they are standard offsets, or use the pointer.
Component->AtkDragDropInterface.DragDropType = DragDropType.Everything;
Component->AtkDragDropInterface.DragDropReferenceIndex = 0;
InitializeComponentEvents();
InitializeComponentEvents();
AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler);
AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler);
AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler);
AddEvent(AtkEventType.DragDropClick, DragDropClickHandler);
AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler);
AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler);
AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler);
AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler);
AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler);
AddEvent(AtkEventType.DragDropClick, DragDropClickHandler);
AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler);
AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler);
}
private bool IsDragDropEndRegistered { get; set; }
public Action<DragDropNode>? OnBegin { get; set; }
public Action<DragDropNode>? OnEnd { get; set; }
public Action<DragDropNode, DragDropPayload>? OnPayloadAccepted { get; set; }
public Action<DragDropNode>? OnDiscard { get; set; }
public Action<DragDropNode>? OnClicked { get; set; }
public Action<DragDropNode>? OnRollOver { get; set; }
public Action<DragDropNode>? OnRollOut { get; set; }
public DragDropPayload Payload { get; set; }
public uint IconId {
get => IconNode.IconId;
set {
IconNode.IconId = value;
IconNode.IsVisible = value != 0;
}
}
public bool IsIconDisabled {
get => IconNode.IsIconDisabled;
set => IconNode.IsIconDisabled = value;
}
public int Quantity {
get => int.Parse(Component->GetQuantityText().ToString());
set => Component->SetQuantity(value);
}
public string QuantityString {
get => Component->GetQuantityText().ToString();
set => Component->SetQuantityText(value);
}
public DragDropType AcceptedType {
get => Component->AcceptedType;
set => Component->AcceptedType = value;
}
public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression {
get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression;
set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value;
}
public bool IsDraggable {
get => !Component->Flags.HasFlag(DragDropFlag.Locked);
set {
if (value) {
Component->Flags &= ~DragDropFlag.Locked;
}
private bool IsDragDropEndRegistered { get; set; }
public Action<DragDropNode>? OnBegin { get; set; }
public Action<DragDropNode>? OnEnd { get; set; }
public Action<DragDropNode, DragDropPayload>? OnPayloadAccepted { get; set; }
public Action<DragDropNode>? OnDiscard { get; set; }
public Action<DragDropNode>? OnClicked { get; set; }
public Action<DragDropNode>? OnRollOver { get; set; }
public Action<DragDropNode>? OnRollOut { get; set; }
public DragDropPayload Payload { get; set; }
public uint IconId {
get => IconNode.IconId;
set {
IconNode.IconId = value;
IconNode.IsVisible = value != 0;
}
}
public bool IsIconDisabled {
get => IconNode.IsIconDisabled;
set => IconNode.IsIconDisabled = value;
}
public int Quantity {
get => int.Parse(Component->GetQuantityText().ToString());
set => Component->SetQuantity(value);
}
public string QuantityString {
get => Component->GetQuantityText().ToString();
set => Component->SetQuantityText(value);
}
public DragDropType AcceptedType {
get => Component->AcceptedType;
set => Component->AcceptedType = value;
}
public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression {
get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression;
set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value;
}
public bool IsDraggable {
get => !Component->Flags.HasFlag(DragDropFlag.Locked);
set {
if (value) {
Component->Flags &= ~DragDropFlag.Locked;
}
else {
Component->Flags |= DragDropFlag.Locked;
}
}
}
public bool IsClickable {
get => Component->Flags.HasFlag(DragDropFlag.Clickable);
set {
if (value) {
Component->Flags |= DragDropFlag.Clickable;
}
else {
Component->Flags &= ~DragDropFlag.Clickable;
}
}
}
private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
// FIX: Use extension method to write payload using fixed VTable
Payload.ToFixedInterface(atkEventData->DragDropData.DragDropInterface);
OnBegin?.Invoke(this);
if (!IsDragDropEndRegistered) {
AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
IsDragDropEndRegistered = true;
}
}
private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
// FIX: Use extension method to read payload using fixed VTable
var payload = DragDropPayloadExtensions.FromFixedInterface(atkEventData->DragDropData.DragDropInterface);
Payload.Clear();
IconId = 0;
OnPayloadAccepted?.Invoke(this, payload);
}
private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
OnDiscard?.Invoke(this);
}
private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
// FIX: Cast to shadow struct to call the correct GetPayloadContainer (Index 12)
var fixedInterface = (AtkDragDropInterfaceFixed*)atkEventData->DragDropData.DragDropInterface;
fixedInterface->GetPayloadContainer()->Clear();
OnEnd?.Invoke(this);
if (IsDragDropEndRegistered) {
RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
IsDragDropEndRegistered = false;
}
}
private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
OnClicked?.Invoke(this);
}
private void DragDropRollOverHandler()
=> OnRollOver?.Invoke(this);
private void DragDropRollOutHandler()
=> OnRollOut?.Invoke(this);
public void Clear() {
Payload.Clear();
IconId = 0;
}
public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) {
if (AtkStage.Instance()->DragDropManager.IsDragging) return;
// FIX: Explicitly use 'this.ResNode' and cast to (AtkResNode*) to avoid ambiguity with the class name
var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)this);
if (addon is null) return;
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
tooltipArgs.Ctor();
tooltipArgs.ActionArgs.Id = Payload.Int2;
tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind;
AtkStage.Instance()->TooltipManager.ShowTooltip(
AtkTooltipManager.AtkTooltipType.Action,
addon->Id,
(AtkResNode*)this, // FIX: Explicit cast here as well
&tooltipArgs);
}
private void LoadTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabelPair(1, 10, 1)
.AddLabelPair(11, 19, 2)
.AddLabelPair(20, 29, 3)
.AddLabelPair(30, 39, 7)
.AddLabelPair(40, 49, 6)
.AddLabelPair(50, 59, 4)
.EndFrameSet()
.Build());
else {
Component->Flags |= DragDropFlag.Locked;
}
}
}
public bool IsClickable {
get => Component->Flags.HasFlag(DragDropFlag.Clickable);
set {
if (value) {
Component->Flags |= DragDropFlag.Clickable;
}
else {
Component->Flags &= ~DragDropFlag.Clickable;
}
}
}
private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
// FIX: Use extension method to write payload using fixed VTable
Payload.ToFixedInterface(atkEventData->DragDropData.DragDropInterface);
OnBegin?.Invoke(this);
if (!IsDragDropEndRegistered) {
AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
IsDragDropEndRegistered = true;
}
}
private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
// FIX: Use extension method to read payload using fixed VTable
var payload = DragDropPayloadExtensions.FromFixedInterface(atkEventData->DragDropData.DragDropInterface);
Payload.Clear();
IconId = 0;
OnPayloadAccepted?.Invoke(this, payload);
}
private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
OnDiscard?.Invoke(this);
}
private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
// FIX: Cast to shadow struct to call the correct GetPayloadContainer (Index 12)
var fixedInterface = (AtkDragDropInterfaceFixed*)atkEventData->DragDropData.DragDropInterface;
fixedInterface->GetPayloadContainer()->Clear();
OnEnd?.Invoke(this);
if (IsDragDropEndRegistered) {
RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
IsDragDropEndRegistered = false;
}
}
private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
atkEvent->State.ReturnFlags = 1;
OnClicked?.Invoke(this);
}
private void DragDropRollOverHandler()
=> OnRollOver?.Invoke(this);
private void DragDropRollOutHandler()
=> OnRollOut?.Invoke(this);
public void Clear() {
Payload.Clear();
IconId = 0;
}
public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) {
if (AtkStage.Instance()->DragDropManager.IsDragging) return;
// FIX: Explicitly use 'this.ResNode' and cast to (AtkResNode*) to avoid ambiguity with the class name
var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)this);
if (addon is null) return;
var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
tooltipArgs.Ctor();
tooltipArgs.ActionArgs.Id = Payload.Int2;
tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind;
AtkStage.Instance()->TooltipManager.ShowTooltip(
AtkTooltipManager.AtkTooltipType.Action,
addon->Id,
(AtkResNode*)this, // FIX: Explicit cast here as well
&tooltipArgs);
}
private void LoadTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabelPair(1, 10, 1)
.AddLabelPair(11, 19, 2)
.AddLabelPair(20, 29, 3)
.AddLabelPair(30, 39, 7)
.AddLabelPair(40, 49, 6)
.AddLabelPair(50, 59, 4)
.EndFrameSet()
.Build());
}
}
@@ -1,15 +1,9 @@
using System.Collections.Generic;
using System.Numerics;
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.ReadOnly;
namespace AetherBags.Nodes.Inventory;
@@ -75,30 +69,25 @@ public sealed class InventoryNotificationNode : SimpleComponentNode
messageTextNode.Size = Size with { Y = 16 };
}
public InventoryNotificationType NotificationType
public InventoryNotificationInfo NotificationInfo
{
get;
set
{
field = value;
if (value == InventoryNotificationType.None)
titleTextNode.SeString = value.Title;
messageTextNode.SeString = value.Message;
if (value.Title.IsEmpty && value.Message.IsEmpty)
{
titleTextNode.String = string.Empty;
messageTextNode.String = string.Empty;
Timeline?.PlayAnimation(17); // Hide
}
else
{
var info = NotificationState.GetNotificationInfo((uint)value);
if (info != null)
{
titleTextNode.SeString = info.Title;
messageTextNode.SeString = info.Message;
Timeline?.PlayAnimation(101); // Show
}
Timeline?.PlayAnimation(17);
return;
}
Timeline?.PlayAnimation(101);
}
} = InventoryNotificationType.None;
}
// Future Zeff, this always goes on a parent
private Timeline ParentLabels => new TimelineBuilder()
+4
View File
@@ -1,5 +1,6 @@
using System;
using System.Numerics;
using AetherBags.AddonLifecycles;
using AetherBags.Addons;
using AetherBags.Helpers;
using AetherBags.Hooks;
@@ -16,6 +17,7 @@ public unsafe class Plugin : IDalamudPlugin
private static string HelpDescription => "Opens your inventory.";
private readonly InventoryHooks _inventoryHooks;
private readonly InventoryLifecycles _inventoryLifecycles;
public Plugin(IDalamudPluginInterface pluginInterface)
{
@@ -64,6 +66,7 @@ public unsafe class Plugin : IDalamudPlugin
}
_inventoryHooks = new InventoryHooks();
_inventoryLifecycles = new InventoryLifecycles();
}
public void Dispose()
@@ -82,6 +85,7 @@ public unsafe class Plugin : IDalamudPlugin
KamiToolKitLibrary.Dispose();
_inventoryHooks.Dispose();
_inventoryLifecycles.Dispose();
}
private void OnCommand(string command, string args)