diff --git a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs index 916b39c..3f0ac09 100644 --- a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs +++ b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using AetherBags.Configuration; using AetherBags.Nodes.Configuration.Category; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit; @@ -17,27 +19,32 @@ public class AddonCategoryConfigurationWindow : NativeAddon private CategoryConfigurationNode? _configNode; private TextNode? _nothingSelectedTextNode; + private List _categoryWrappers = new(); + protected override unsafe void OnSetup(AtkUnitBase* addon) { - List categoryDefinitionsWrappers = System.Config.Categories.UserCategories - .Select(categoryDefinition => new CategoryWrapper(categoryDefinition)) - .ToList(); + _categoryWrappers = CreateCategoryWrappers(); - _selectionListNode = new ModifyListNode { + _selectionListNode = new ModifyListNode + { Position = ContentStartPosition, Size = new Vector2(250.0f, ContentSize.Y), - SelectionOptions = categoryDefinitionsWrappers, + SelectionOptions = _categoryWrappers, OnOptionChanged = OnOptionChanged, + AddNewEntry = OnAddNewCategory, + RemoveEntry = OnRemoveCategory, }; _selectionListNode.AttachNode(this); - _separatorLine = new VerticalLineNode { + _separatorLine = new VerticalLineNode + { Position = ContentStartPosition + new Vector2(250.0f + 8.0f, 0.0f), Size = new Vector2(4.0f, ContentSize.Y), }; _separatorLine.AttachNode(this); - _nothingSelectedTextNode = new TextNode { + _nothingSelectedTextNode = new TextNode + { Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f), Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f), AlignmentType = AlignmentType.Center, @@ -50,21 +57,60 @@ public class AddonCategoryConfigurationWindow : NativeAddon }; _nothingSelectedTextNode.AttachNode(this); - _configNode = new CategoryConfigurationNode { + _configNode = new CategoryConfigurationNode + { Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f), Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f), IsVisible = false, + OnCategoryChanged = RefreshSelectionList, }; _configNode.AttachNode(this); } - private void OnOptionChanged(CategoryWrapper? newOption) { + private List CreateCategoryWrappers() + { + return System.Config.Categories.UserCategories + .Select(categoryDefinition => new CategoryWrapper(categoryDefinition)) + .ToList(); + } + + private void OnOptionChanged(CategoryWrapper? newOption) + { if (_configNode is null) return; _configNode.IsVisible = newOption is not null; - _nothingSelectedTextNode?.IsVisible = newOption is null; + if (_nothingSelectedTextNode is not null) + _nothingSelectedTextNode.IsVisible = newOption is null; _configNode.ConfigurationOption = newOption; } + + private void OnAddNewCategory(ModifyListNode listNode) + { + var newCategory = new UserCategoryDefinition + { + Name = $"New Category {System.Config.Categories.UserCategories.Count + 1}", + Order = System.Config.Categories.UserCategories.Count, + }; + + System.Config.Categories.UserCategories.Add(newCategory); + + var newWrapper = new CategoryWrapper(newCategory); + _categoryWrappers.Add(newWrapper); + listNode.AddOption(newWrapper); + } + + private void OnRemoveCategory(CategoryWrapper categoryWrapper) + { + if (categoryWrapper.CategoryDefinition is null) return; + + System.Config.Categories.UserCategories.Remove(categoryWrapper.CategoryDefinition); + _categoryWrappers.Remove(categoryWrapper); + } + + private void RefreshSelectionList() + { + _selectionListNode?.UpdateList(); + } } \ No newline at end of file diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index f732071..2cf5597 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -122,12 +122,14 @@ public class AddonInventoryWindow : NativeAddon public void ManualInventoryRefresh() { + if (!Services.ClientState.IsLoggedIn) return; InventoryState.RefreshFromGame(); RefreshCategoriesCore(true); } public void ManualCurrencyRefresh() { + if (!Services.ClientState.IsLoggedIn) return; _footerNode.RefreshCurrencies(); } diff --git a/AetherBags/Currency/CurrencyState.cs b/AetherBags/Currency/CurrencyState.cs index c0a852a..a357bab 100644 --- a/AetherBags/Currency/CurrencyState.cs +++ b/AetherBags/Currency/CurrencyState.cs @@ -71,12 +71,12 @@ public static unsafe class CurrencyState return currencyInfoList; } - private static uint? GetLimitedTomestoneItemIdCached() + private static uint? GetLimitedTomestoneItemIdCached() { if (_cachedLimitedTomestoneItemId.HasValue) return _cachedLimitedTomestoneItemId.Value; - uint? itemId = Services.DataManager.GetExcelSheet() + uint? itemId = Services.DataManager.GetExcelSheet() .FirstOrDefault(t => t.Tomestones.RowId == 3) .Item.RowId; @@ -84,7 +84,7 @@ public static unsafe class CurrencyState return itemId; } - private static uint? GetNonLimitedTomestoneItemIdCached() + private static uint? GetNonLimitedTomestoneItemIdCached() { if (_cachedNonLimitedTomestoneItemId.HasValue) return _cachedNonLimitedTomestoneItemId.Value; diff --git a/AetherBags/Extensions/InventoryTypeExtensions.cs b/AetherBags/Extensions/InventoryTypeExtensions.cs index 628462b..4c28d8f 100644 --- a/AetherBags/Extensions/InventoryTypeExtensions.cs +++ b/AetherBags/Extensions/InventoryTypeExtensions.cs @@ -158,7 +158,7 @@ public static unsafe class InventoryTypeExtensions { _ when inventoryType.IsMainInventory => InventoryType.Inventory1, _ when inventoryType.IsSaddleBag => inventoryType is InventoryType. SaddleBag1 or InventoryType.SaddleBag2 - ? InventoryType. SaddleBag1 + ? InventoryType. SaddleBag1 : InventoryType.PremiumSaddleBag1, _ => inventoryType, }; diff --git a/AetherBags/Hooks/InventoryHook.cs b/AetherBags/Hooks/InventoryHook.cs index 1ed98de..5e821fc 100644 --- a/AetherBags/Hooks/InventoryHook.cs +++ b/AetherBags/Hooks/InventoryHook.cs @@ -17,14 +17,14 @@ public sealed unsafe class InventoryHooks : IDisposable ushort dstSlot, bool unk); - private readonly Hook? _moveItemSlotHook; + private readonly Hook? _moveItemSlotHook; public InventoryHooks() { try { _moveItemSlotHook = Services.GameInteropProvider.HookFromSignature( - "E8 ?? ?? ?? ?? 48 8B 03 66 FF C5", + "E8 ?? ?? ?? ?? 48 8B 03 66 FF C5", MoveItemSlotDetour); _moveItemSlotHook.Enable(); @@ -36,8 +36,7 @@ public sealed unsafe class InventoryHooks : IDisposable } } - private int MoveItemSlotDetour( - InventoryManager* manager, + private int MoveItemSlotDetour(InventoryManager* manager, InventoryType srcType, ushort srcSlot, InventoryType dstType, @@ -47,8 +46,7 @@ public sealed unsafe class InventoryHooks : IDisposable InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); - Services.Logger.Info( - $"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}"); + Services.Logger.Debug($"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}"); return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk); } diff --git a/AetherBags/Inventory/CategoryBucketManager.cs b/AetherBags/Inventory/CategoryBucketManager.cs index f87ead0..6d67130 100644 --- a/AetherBags/Inventory/CategoryBucketManager.cs +++ b/AetherBags/Inventory/CategoryBucketManager.cs @@ -56,7 +56,7 @@ public static class CategoryBucketManager UserCategoryDefinition category = sortedScratch[i]; uint bucketKey = MakeUserCategoryKey(category.Order); - if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket)) + if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket)) { bucket = new CategoryBucket { diff --git a/AetherBags/Inventory/InventoryScanner.cs b/AetherBags/Inventory/InventoryScanner.cs index 0bb921b..00e9d2b 100644 --- a/AetherBags/Inventory/InventoryScanner.cs +++ b/AetherBags/Inventory/InventoryScanner.cs @@ -85,7 +85,7 @@ public static unsafe class InventoryScanner bool isHq = (item.Flags & InventoryItem.ItemFlags.HighQuality) != 0; ulong key = stackMode == InventoryStackMode.AggregateByItemId - ? MakeAggregatedItemKey(id, isHq) + ? MakeAggregatedItemKey(id, isHq) : MakeNaturalSlotKey(inventoryType, slot); Services.Logger.DebugOnly($"Slot {inventoryType}[{slot}] ItemId={id} Qty={quantity} Key=0x{key: X16}"); @@ -120,7 +120,7 @@ public static unsafe class InventoryScanner ulong key = kvp.Key; AggregatedItem agg = kvp.Value; - if (!itemInfoByKey.TryGetValue(key, out ItemInfo? info)) + if (!itemInfoByKey.TryGetValue(key, out ItemInfo? info)) { info = new ItemInfo { diff --git a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs index e69e209..8a8eb5a 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs @@ -1,19 +1,23 @@ -using System.Collections.Generic; +using System; using System.Numerics; using AetherBags.Addons; using AetherBags.Configuration; using KamiToolKit.Nodes; using KamiToolKit.Premade.Nodes; -using Lumina.Excel.Sheets; namespace AetherBags.Nodes.Configuration.Category; -public class CategoryConfigurationNode : ConfigNode { +public class CategoryConfigurationNode : ConfigNode +{ private readonly ScrollingAreaNode _categoryList; private CategoryDefinitionConfigurationNode? _activeNode; - public CategoryConfigurationNode() { - _categoryList = new ScrollingAreaNode { + public Action? OnCategoryChanged { get; set; } + + public CategoryConfigurationNode() + { + _categoryList = new ScrollingAreaNode + { ContentHeight = 100.0f, AutoHideScrollBar = true, }; @@ -21,35 +25,51 @@ public class CategoryConfigurationNode : ConfigNode { _categoryList.AttachNode(this); } - - protected override void OptionChanged(CategoryWrapper? option) { - if (option?.CategoryDefinition is null) { + protected override void OptionChanged(CategoryWrapper? option) + { + if (option?.CategoryDefinition is null) + { _categoryList.IsVisible = false; return; } _categoryList.IsVisible = true; - if (_activeNode is null) { - _activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) { - Size = new Vector2(_categoryList.ContentNode.Width, 0f), + if (_activeNode is null) + { + _activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) + { + Size = _categoryList.ContentNode.Size, + OnLayoutChanged = UpdateScrollHeight, + OnCategoryPropertyChanged = OnCategoryChanged, }; _categoryList.ContentNode.AddNode(_activeNode); - } else { + } + else + { _activeNode.SetCategory(option.CategoryDefinition); } + UpdateScrollHeight(); + } + + private void UpdateScrollHeight() + { _categoryList.ContentNode.RecalculateLayout(); _categoryList.ContentHeight = _categoryList.ContentNode.Height; } - protected override void OnSizeChanged() { + protected override void OnSizeChanged() + { base.OnSizeChanged(); _categoryList.Size = Size; _categoryList.ContentNode.Width = Width; - foreach (var node in _categoryList.ContentNode.GetNodes()) { + foreach (var node in _categoryList.ContentNode.GetNodes()) + { node.Width = Width; } + + UpdateScrollHeight(); } -} +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index af525f9..d5d261f 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -1,69 +1,467 @@ +using System; using System.Numerics; using AetherBags.Configuration; using AetherBags.Nodes.Color; -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Utility; +using KamiToolKit.Classes; using KamiToolKit.Nodes; -using Action = Lumina.Excel.Sheets.Action; +using Lumina.Excel; +using Lumina.Excel.Sheets; +using Action = System.Action; namespace AetherBags.Nodes.Configuration.Category; -public sealed class CategoryDefinitionConfigurationNode : VerticalListNode { - private readonly CheckboxNode enabledCheckbox; - private readonly TextInputNode nameInputNode; - private readonly TextInputNode descriptionInputNode; - private readonly ColorInputRow colorInputNode; +public sealed class CategoryDefinitionConfigurationNode : VerticalListNode +{ + private readonly CheckboxNode _enabledCheckbox; + private readonly TextInputNode _nameInputNode; + private readonly TextInputNode _descriptionInputNode; + private readonly ColorInputRow _colorInputNode; + private readonly NumericInputNode _priorityInputNode; + private readonly NumericInputNode _orderInputNode; - public UserCategoryDefinition CategoryDefinition { get; private set; } + private readonly CheckboxNode _levelEnabledCheckbox; + private readonly NumericInputNode _levelMinNode; + private readonly NumericInputNode _levelMaxNode; - public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition) { + private readonly CheckboxNode _itemLevelEnabledCheckbox; + private readonly NumericInputNode _itemLevelMinNode; + private readonly NumericInputNode _itemLevelMaxNode; + + private readonly CheckboxNode _vendorPriceEnabledCheckbox; + private readonly NumericInputNode _vendorPriceMinNode; + private readonly NumericInputNode _vendorPriceMaxNode; + + private readonly StateFilterRowNode _untradableFilter; + private readonly StateFilterRowNode _uniqueFilter; + private readonly StateFilterRowNode _collectableFilter; + private readonly StateFilterRowNode _dyeableFilter; + private readonly StateFilterRowNode _repairableFilter; + + private readonly UintListEditorNode _allowedItemIdsEditor; + private readonly StringListEditorNode _allowedNamePatternsEditor; + private readonly UintListEditorNode _allowedUiCategoriesEditor; + private readonly RarityEditorNode _allowedRaritiesEditor; + + private bool _isInitialized; + + private static ExcelSheet? _sItemSheet; + private static ExcelSheet? _sUICategorySheet; + + public Action? OnLayoutChanged { get; set; } + + public Action? OnCategoryPropertyChanged { get; set; } + + private UserCategoryDefinition CategoryDefinition { get; set; } + + public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition) + { CategoryDefinition = categoryDefinition; - FirstItemSpacing = 35.0f; - ItemSpacing = 5.0f; + _sItemSheet ??= Services.DataManager.GetExcelSheet(); + _sUICategorySheet ??= Services.DataManager.GetExcelSheet(); - enabledCheckbox = new CheckboxNode { + FitContents = true; + ItemSpacing = 4.0f; + + AddNode(CreateSectionHeader("Basic Settings")); + + _enabledCheckbox = new CheckboxNode + { + Size = new Vector2(200, 20), + String = "Enabled", IsChecked = CategoryDefinition.Enabled, - OnClick = isChecked => CategoryDefinition.Enabled = isChecked, + OnClick = isChecked => + { + CategoryDefinition.Enabled = isChecked; + NotifyChanged(); + NotifyCategoryPropertyChanged(); + }, + }; + AddNode(_enabledCheckbox); + + AddNode(new LabelTextNode { Size = new Vector2(80, 20), String = "Name:" }); + _nameInputNode = new TextInputNode + { + Size = new Vector2(250, 28), + String = CategoryDefinition.Name, + PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "", + OnInputReceived = name => + { + CategoryDefinition.Name = name.ExtractText(); + NotifyChanged(); + NotifyCategoryPropertyChanged(); + }, + }; + AddNode(_nameInputNode); + + AddNode(new LabelTextNode { Size = new Vector2(80, 20), String = "Description:" }); + _descriptionInputNode = new TextInputNode + { + Size = new Vector2(250, 28), + String = CategoryDefinition.Description, + PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "", + OnInputReceived = desc => + { + CategoryDefinition.Description = desc.ExtractText(); + NotifyChanged(); + }, + }; + AddNode(_descriptionInputNode); + + _colorInputNode = new ColorInputRow + { + Label = "Color", + Size = new Vector2(300, 28), + CurrentColor = CategoryDefinition.Color, + DefaultColor = new UserCategoryDefinition().Color, + OnColorConfirmed = color => + { + CategoryDefinition.Color = color; + NotifyChanged(); + }, + OnColorCanceled = color => + { + CategoryDefinition.Color = color; + NotifyChanged(); + }, + }; + AddNode(_colorInputNode); + + AddNode(new LabelTextNode { Size = new Vector2(80, 20), String = "Priority:" }); + _priorityInputNode = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = 0, + Max = 1000, + Step = 1, + Value = CategoryDefinition.Priority, + OnValueUpdate = val => + { + CategoryDefinition.Priority = val; + NotifyChanged(); + }, + }; + AddNode(_priorityInputNode); + + AddNode(new LabelTextNode { Size = new Vector2(80, 20), String = "Order:" }); + _orderInputNode = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = 0, + Max = 9999, + Step = 1, + Value = CategoryDefinition.Order, + OnValueUpdate = val => + { + CategoryDefinition.Order = val; + NotifyChanged(); + NotifyCategoryPropertyChanged(); + }, + }; + AddNode(_orderInputNode); + + AddNode(CreateSectionHeader("Range Filters")); + + (_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter( + "Level", + CategoryDefinition.Rules.Level, + 0, 200, + (enabled, min, max) => + { + CategoryDefinition.Rules.Level.Enabled = enabled; + CategoryDefinition.Rules.Level.Min = min; + CategoryDefinition.Rules.Level.Max = max; + NotifyChanged(); + } + ); + + (_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter( + "Item Level", + CategoryDefinition.Rules.ItemLevel, + 0, 2000, + (enabled, min, max) => + { + CategoryDefinition.Rules.ItemLevel.Enabled = enabled; + CategoryDefinition.Rules.ItemLevel.Min = min; + CategoryDefinition.Rules.ItemLevel.Max = max; + NotifyChanged(); + } + ); + + (_vendorPriceEnabledCheckbox, _vendorPriceMinNode, _vendorPriceMaxNode) = CreateRangeFilterUint( + "Vendor Price", + CategoryDefinition.Rules.VendorPrice, + 0, 9_999_999 + ); + + AddNode(CreateSectionHeader("State Filters")); + + _untradableFilter = new StateFilterRowNode("Untradable", CategoryDefinition.Rules.Untradable, NotifyChanged); + AddNode(_untradableFilter); + + _uniqueFilter = new StateFilterRowNode("Unique", CategoryDefinition.Rules.Unique, NotifyChanged); + AddNode(_uniqueFilter); + + _collectableFilter = new StateFilterRowNode("Collectable", CategoryDefinition.Rules.Collectable, NotifyChanged); + AddNode(_collectableFilter); + + _dyeableFilter = new StateFilterRowNode("Dyeable", CategoryDefinition.Rules.Dyeable, NotifyChanged); + AddNode(_dyeableFilter); + + _repairableFilter = new StateFilterRowNode("Repairable", CategoryDefinition.Rules.Repairable, NotifyChanged); + AddNode(_repairableFilter); + + AddNode(CreateSectionHeader("List Filters")); + + _allowedItemIdsEditor = new UintListEditorNode( + "Allowed Item IDs:", + CategoryDefinition.Rules.AllowedItemIds, + OnListChanged, + ResolveItemName + ); + AddNode(_allowedItemIdsEditor); + + _allowedNamePatternsEditor = new StringListEditorNode( + "Name Patterns (Regex):", + CategoryDefinition.Rules.AllowedItemNamePatterns, + OnListChanged + ); + AddNode(_allowedNamePatternsEditor); + + _allowedUiCategoriesEditor = new UintListEditorNode( + "UI Categories:", + CategoryDefinition.Rules.AllowedUiCategoryIds, + OnListChanged, + ResolveUiCategoryName + ); + AddNode(_allowedUiCategoriesEditor); + + _allowedRaritiesEditor = new RarityEditorNode( + CategoryDefinition.Rules.AllowedRarities, + NotifyChanged + ); + AddNode(_allowedRaritiesEditor); + + _isInitialized = true; + } + + private void OnListChanged() + { + NotifyChanged(); + RecalculateLayout(); + OnLayoutChanged?.Invoke(); + } + + private static string ResolveItemName(uint itemId) + { + try + { + var item = _sItemSheet?.GetRow(itemId); + return item?.Name.ToString() ?? "Unknown"; + } + catch + { + return "Unknown"; + } + } + + private static string ResolveUiCategoryName(uint categoryId) + { + try + { + var category = _sUICategorySheet?.GetRow(categoryId); + return category?.Name.ToString() ?? "Unknown"; + } + catch + { + return "Unknown"; + } + } + + private static void NotifyChanged() + { + System.AddonInventoryWindow?.ManualInventoryRefresh(); + } + + private void NotifyCategoryPropertyChanged() + { + OnCategoryPropertyChanged?.Invoke(); + } + + private static LabelTextNode CreateSectionHeader(string text) + { + return new LabelTextNode + { + Size = new Vector2(300, 22), + String = text, + TextColor = ColorHelper.GetColor(2), + TextOutlineColor = ColorHelper.GetColor(0), + }; + } + + private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter( + string label, + RangeFilter filter, + int minBound, + int maxBound, + Action onUpdate) + { + var enabledCheckbox = new CheckboxNode + { + Size = new Vector2(200, 20), + String = $"{label} Filter", + IsChecked = filter.Enabled, }; AddNode(enabledCheckbox); - colorInputNode = new ColorInputRow + var minNode = new NumericInputNode { - Label = "Color", - CurrentColor = CategoryDefinition.Color, - DefaultColor = new UserCategoryDefinition().Color, - OnColorConfirmed = color => CategoryDefinition.Color = color, - // OnColorChange = color => CategoryDefinition.Color = color, - OnColorCanceled = color => CategoryDefinition.Color = color, + Size = new Vector2(120, 28), + Min = minBound, + Max = maxBound, + Value = filter.Min, + IsEnabled = filter.Enabled, }; - AddNode(colorInputNode); - nameInputNode = new TextInputNode + var maxNode = new NumericInputNode { - String = CategoryDefinition.Name, - OnInputComplete = name => CategoryDefinition.Name = name.ExtractText() + Size = new Vector2(120, 28), + Min = minBound, + Max = maxBound, + Value = filter.Max, + IsEnabled = filter.Enabled, }; - AddNode(nameInputNode); - descriptionInputNode = new TextInputNode + var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; + rangeRow.AddNode(new LabelTextNode { Size = new Vector2(30, 28), String = "Min:" }); + rangeRow.AddNode(minNode); + rangeRow.AddNode(new LabelTextNode { Size = new Vector2(30, 28), String = "Max:" }); + rangeRow.AddNode(maxNode); + AddNode(rangeRow); + + enabledCheckbox.OnClick = isChecked => { - String = CategoryDefinition.Description, - OnInputComplete = name => CategoryDefinition.Description = name.ExtractText() + minNode.IsEnabled = isChecked; + maxNode.IsEnabled = isChecked; + onUpdate(isChecked, minNode.Value, maxNode.Value); }; - AddNode(descriptionInputNode); - // TODO: Add Rules + minNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, val, maxNode.Value); + maxNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, minNode.Value, val); + + return (enabledCheckbox, minNode, maxNode); } - public void SetCategory(UserCategoryDefinition newCategory) { + private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilterUint( + string label, + RangeFilter filter, + int minBound, + int maxBound) + { + var enabledCheckbox = new CheckboxNode + { + Size = new Vector2(200, 20), + String = $"{label} Filter", + IsChecked = filter.Enabled, + }; + AddNode(enabledCheckbox); + + var minNode = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = minBound, + Max = maxBound, + Value = (int)filter.Min, + IsEnabled = filter.Enabled, + }; + + var maxNode = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = minBound, + Max = maxBound, + Value = (int)Math.Min(filter.Max, maxBound), + IsEnabled = filter.Enabled, + }; + + var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; + rangeRow.AddNode(new LabelTextNode { Size = new Vector2(30, 28), String = "Min:" }); + rangeRow.AddNode(minNode); + rangeRow.AddNode(new LabelTextNode { Size = new Vector2(30, 28), String = "Max:" }); + rangeRow.AddNode(maxNode); + AddNode(rangeRow); + + enabledCheckbox.OnClick = isChecked => + { + minNode.IsEnabled = isChecked; + maxNode.IsEnabled = isChecked; + CategoryDefinition.Rules.VendorPrice.Enabled = isChecked; + NotifyChanged(); + }; + + minNode.OnValueUpdate = value => + { + CategoryDefinition.Rules.VendorPrice.Min = (uint)value; + NotifyChanged(); + }; + + maxNode.OnValueUpdate = value => + { + CategoryDefinition.Rules.VendorPrice.Max = (uint)value; + NotifyChanged(); + }; + + return (enabledCheckbox, minNode, maxNode); + } + + public void SetCategory(UserCategoryDefinition newCategory) + { CategoryDefinition = newCategory; RefreshValues(); } private void RefreshValues() { - enabledCheckbox.IsChecked = CategoryDefinition.Enabled; - colorInputNode.CurrentColor = CategoryDefinition.Color; - nameInputNode.String = CategoryDefinition.Name; + if (! _isInitialized) return; + + _enabledCheckbox.IsChecked = CategoryDefinition.Enabled; + _colorInputNode.CurrentColor = CategoryDefinition.Color; + _nameInputNode.String = CategoryDefinition.Name; + _descriptionInputNode.String = CategoryDefinition.Description; + _priorityInputNode.Value = CategoryDefinition.Priority; + _orderInputNode.Value = CategoryDefinition.Order; + + RefreshRangeFilter(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode, CategoryDefinition.Rules.Level); + RefreshRangeFilter(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode, CategoryDefinition.Rules.ItemLevel); + + _vendorPriceEnabledCheckbox.IsChecked = CategoryDefinition.Rules.VendorPrice.Enabled; + _vendorPriceMinNode.Value = (int)CategoryDefinition.Rules.VendorPrice.Min; + _vendorPriceMaxNode.Value = (int)Math.Min(CategoryDefinition.Rules.VendorPrice.Max, int.MaxValue); + _vendorPriceMinNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled; + _vendorPriceMaxNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled; + + _untradableFilter.SetState(CategoryDefinition.Rules.Untradable); + _uniqueFilter.SetState(CategoryDefinition.Rules.Unique); + _collectableFilter.SetState(CategoryDefinition.Rules.Collectable); + _dyeableFilter.SetState(CategoryDefinition.Rules.Dyeable); + _repairableFilter.SetState(CategoryDefinition.Rules.Repairable); + + _allowedItemIdsEditor.SetList(CategoryDefinition.Rules.AllowedItemIds); + _allowedNamePatternsEditor.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); + _allowedUiCategoriesEditor.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); + _allowedRaritiesEditor.SetList(CategoryDefinition.Rules.AllowedRarities); + + RecalculateLayout(); + OnLayoutChanged?.Invoke(); } -} + + private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter filter) + { + enabled.IsChecked = filter.Enabled; + min.Value = filter.Min; + max.Value = filter.Max; + min.IsEnabled = filter.Enabled; + max.IsEnabled = filter.Enabled; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs new file mode 100644 index 0000000..3182704 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class RarityEditorNode : VerticalListNode +{ + private static readonly string[] RarityNames = { "Common (White)", "Uncommon (Green)", "Rare (Blue)", "Relic (Purple)", "Aetherial (Pink)" }; + + private List _list; + private readonly List _checkboxes = new(); + private readonly Action? _onChanged; + + public RarityEditorNode(List list, Action? onChanged = null) + { + _list = list; + _onChanged = onChanged; + + FitContents = true; + ItemSpacing = 2.0f; + + var headerLabel = new LabelTextNode + { + Size = new Vector2(280, 18), + String = "Allowed Rarities:", + TextColor = ColorHelper.GetColor(8), + }; + AddNode(headerLabel); + + for (int i = 0; i < RarityNames.Length; i++) + { + var rarity = i; + var checkbox = new CheckboxNode + { + Size = new Vector2(200, 20), + String = RarityNames[i], + IsChecked = _list.Contains(i), + OnClick = isChecked => + { + if (isChecked && !_list.Contains(rarity)) + { + _list.Add(rarity); + _list.Sort(); + } + else if (!isChecked && _list.Contains(rarity)) + { + _list.Remove(rarity); + } + _onChanged?.Invoke(); + }, + }; + _checkboxes.Add(checkbox); + AddNode(checkbox); + } + } + + public void SetList(List newList) + { + _list = newList; + Refresh(); + } + + public void Refresh() + { + for (int i = 0; i < _checkboxes.Count; i++) + { + _checkboxes[i].IsChecked = _list.Contains(i); + } + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs new file mode 100644 index 0000000..f96a972 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs @@ -0,0 +1,54 @@ +using System; +using System.Numerics; +using AetherBags.Configuration; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class StateFilterRowNode : HorizontalListNode +{ + private readonly LabelTextNode _labelNode; + private readonly TextButtonNode _stateButton; + private readonly Action? _onChanged; + private StateFilter _filter; + + private static readonly string[] StateLabels = { "Ignored", "Allow", "Disallow" }; + + public StateFilterRowNode(string label, StateFilter filter, Action? onChanged = null) + { + _filter = filter; + _onChanged = onChanged; + Size = new Vector2(280, 24); + ItemSpacing = 8.0f; + + _labelNode = new LabelTextNode + { + Size = new Vector2(100, 24), + String = $"{label}:", + TextColor = ColorHelper.GetColor(8), + }; + AddNode(_labelNode); + + _stateButton = new TextButtonNode + { + Size = new Vector2(100, 24), + String = StateLabels[_filter.State], + OnClick = CycleState, + }; + AddNode(_stateButton); + } + + private void CycleState() + { + _filter.State = (_filter.State + 1) % 3; + _stateButton.String = StateLabels[_filter.State]; + _onChanged?.Invoke(); + } + + public void SetState(StateFilter newFilter) + { + _filter = newFilter; + _stateButton.String = StateLabels[_filter.State]; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs new file mode 100644 index 0000000..e8378b2 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class StringListEditorNode : VerticalListNode +{ + private List _list; + private readonly TextInputNode _addInput; + private readonly VerticalListNode _itemsContainer; + private readonly Action? _onChanged; + + public StringListEditorNode(string label, List list, Action? onChanged = null) + { + _list = list; + _onChanged = onChanged; + + FitContents = true; + ItemSpacing = 4.0f; + + var headerLabel = new LabelTextNode + { + Size = new Vector2(280, 18), + String = label, + TextColor = ColorHelper.GetColor(8), + }; + AddNode(headerLabel); + + _itemsContainer = new VerticalListNode + { + FitContents = true, + ItemSpacing = 2.0f, + }; + AddNode(_itemsContainer); + + var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; + + _addInput = new TextInputNode + { + Size = new Vector2(200, 28), + PlaceholderString = "Add new...", + OnInputComplete = text => + { + var value = text.ExtractText(); + if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value)) + { + _list.Add(value); + _addInput.String = ""; + RefreshItems(); + _onChanged?.Invoke(); + } + }, + }; + addRow.AddNode(_addInput); + + var addButton = new TextButtonNode + { + Size = new Vector2(60, 28), + String = "Add", + OnClick = () => + { + var value = _addInput.String; + if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value)) + { + _list.Add(value); + _addInput.String = ""; + RefreshItems(); + _onChanged?.Invoke(); + } + }, + }; + addRow.AddNode(addButton); + + AddNode(addRow); + + RefreshItems(); + } + + public void SetList(List newList) + { + _list = newList; + RefreshItems(); + } + + private void RefreshItems() + { + _itemsContainer.SyncWithListData( + _list, + node => node.Value, + value => new StringListItemNode(value) + { + Size = new Vector2(280, 22), + OnRemove = () => + { + _list.Remove(value); + RefreshItems(); + _onChanged?.Invoke(); + }, + } + ); + + _itemsContainer.RecalculateLayout(); + RecalculateLayout(); + } + + public void Refresh() + { + RefreshItems(); + } +} + +public sealed class StringListItemNode : HorizontalListNode +{ + public string Value { get; } + public Action? OnRemove { get; init; } + + public StringListItemNode(string value) + { + Value = value; + ItemSpacing = 4.0f; + + var itemLabel = new LabelTextNode + { + Size = new Vector2(220, 22), + String = value, + TextColor = ColorHelper.GetColor(3), + }; + AddNode(itemLabel); + + var removeButton = new TextButtonNode + { + Size = new Vector2(50, 22), + String = "X", + OnClick = () => OnRemove?.Invoke(), + }; + AddNode(removeButton); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs new file mode 100644 index 0000000..6aae6fb --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class UintListEditorNode : VerticalListNode +{ + private List _list; + private readonly NumericInputNode _addInput; + private readonly VerticalListNode _itemsContainer; + private readonly Action? _onChanged; + private readonly Func? _labelResolver; + + public UintListEditorNode(string label, List list, Action? onChanged = null, Func? labelResolver = null) + { + _list = list; + _onChanged = onChanged; + _labelResolver = labelResolver; + + FitContents = true; + ItemSpacing = 4.0f; + + var headerLabel = new LabelTextNode + { + Size = new Vector2(280, 18), + String = label, + TextColor = ColorHelper.GetColor(8), + }; + AddNode(headerLabel); + + _itemsContainer = new VerticalListNode + { + FitContents = true, + ItemSpacing = 2.0f, + }; + AddNode(_itemsContainer); + + var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; + + _addInput = new NumericInputNode + { + Size = new Vector2(120, 28), + Min = 0, + Max = int.MaxValue, + Value = 0, + }; + addRow.AddNode(_addInput); + + var addButton = new TextButtonNode + { + Size = new Vector2(60, 28), + String = "Add", + OnClick = () => + { + var value = (uint)_addInput.Value; + if (! _list.Contains(value)) + { + _list.Add(value); + RefreshItems(); + _onChanged?.Invoke(); + } + }, + }; + addRow.AddNode(addButton); + + AddNode(addRow); + + RefreshItems(); + } + + public void SetList(List newList) + { + _list = newList; + RefreshItems(); + } + + private void RefreshItems() + { + _itemsContainer.SyncWithListData( + _list, + node => node.Value, + value => new UintListItemNode(value, _labelResolver) + { + Size = new Vector2(280, 22), + OnRemove = () => + { + _list.Remove(value); + RefreshItems(); + _onChanged?.Invoke(); + }, + } + ); + + _itemsContainer.RecalculateLayout(); + RecalculateLayout(); + } + + public void Refresh() + { + RefreshItems(); + } +} + +public sealed class UintListItemNode : HorizontalListNode +{ + public uint Value { get; } + public Action? OnRemove { get; init; } + + public UintListItemNode(uint value, Func? labelResolver = null) + { + Value = value; + ItemSpacing = 4.0f; + + var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString(); + var itemLabel = new LabelTextNode + { + Size = new Vector2(220, 22), + String = displayText, + TextColor = ColorHelper.GetColor(3), + }; + AddNode(itemLabel); + + var removeButton = new TextButtonNode + { + Size = new Vector2(50, 22), + String = "X", + OnClick = () => OnRemove?.Invoke(), + }; + AddNode(removeButton); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs index 3098e00..f744cb6 100644 --- a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs @@ -28,8 +28,10 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode { if (Enum.TryParse(selected, out var parsed)) + { config.StackMode = parsed; - RefreshInventory(); + System.AddonInventoryWindow.ManualInventoryRefresh(); + } } }; ContentNode.AddNode(_stackDropDown); diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 3702545..f8fbd36 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -23,7 +23,7 @@ public class InventoryCategoryNode : SimpleComponentNode private const float HeaderHeight = 16; private const float MinWidth = 40; - private float? _fixedWidth; + private float? _fixedWidth; private int _hoverRefs; private bool _headerSuppressed; private bool _headerExpanded; @@ -92,7 +92,7 @@ public class InventoryCategoryNode : SimpleComponentNode } } - public float? FixedWidth + public float? FixedWidth { get => _fixedWidth; set @@ -184,8 +184,8 @@ public class InventoryCategoryNode : SimpleComponentNode int rows = (itemCount + itemsPerLine - 1) / itemsPerLine; int actualColumns = Math.Min(itemCount, itemsPerLine); - float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; - float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; + float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; + float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; float hPad = _itemGridNode.HorizontalPadding; float vPad = _itemGridNode.VerticalPadding; @@ -259,7 +259,7 @@ public class InventoryCategoryNode : SimpleComponentNode InventoryType targetContainer = targetItemInfo.Item.Container; ushort targetSlot = (ushort)targetItemInfo.Item.Slot; - Services.Logger.Info($"[OnPayload] Moving {sourceContainer}@{sourceSlot} -> {targetContainer}@{targetSlot}"); + Services.Logger.Debug($"[OnPayload] Moving {sourceContainer}@{sourceSlot} -> {targetContainer}@{targetSlot}"); InventoryMoveHelper.MoveItem(sourceContainer, sourceSlot, targetContainer, targetSlot); }