Add new configuration options

This commit is contained in:
Zeffuro
2025-12-25 13:23:57 +01:00
parent c3e3f8b2bf
commit 0eb1adc9c0
15 changed files with 947 additions and 79 deletions
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration;
using AetherBags.Nodes.Configuration.Category; using AetherBags.Nodes.Configuration.Category;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit; using KamiToolKit;
@@ -17,27 +19,32 @@ public class AddonCategoryConfigurationWindow : NativeAddon
private CategoryConfigurationNode? _configNode; private CategoryConfigurationNode? _configNode;
private TextNode? _nothingSelectedTextNode; private TextNode? _nothingSelectedTextNode;
private List<CategoryWrapper> _categoryWrappers = new();
protected override unsafe void OnSetup(AtkUnitBase* addon) protected override unsafe void OnSetup(AtkUnitBase* addon)
{ {
List<CategoryWrapper> categoryDefinitionsWrappers = System.Config.Categories.UserCategories _categoryWrappers = CreateCategoryWrappers();
.Select(categoryDefinition => new CategoryWrapper(categoryDefinition))
.ToList();
_selectionListNode = new ModifyListNode<CategoryWrapper> { _selectionListNode = new ModifyListNode<CategoryWrapper>
{
Position = ContentStartPosition, Position = ContentStartPosition,
Size = new Vector2(250.0f, ContentSize.Y), Size = new Vector2(250.0f, ContentSize.Y),
SelectionOptions = categoryDefinitionsWrappers, SelectionOptions = _categoryWrappers,
OnOptionChanged = OnOptionChanged, OnOptionChanged = OnOptionChanged,
AddNewEntry = OnAddNewCategory,
RemoveEntry = OnRemoveCategory,
}; };
_selectionListNode.AttachNode(this); _selectionListNode.AttachNode(this);
_separatorLine = new VerticalLineNode { _separatorLine = new VerticalLineNode
{
Position = ContentStartPosition + new Vector2(250.0f + 8.0f, 0.0f), Position = ContentStartPosition + new Vector2(250.0f + 8.0f, 0.0f),
Size = new Vector2(4.0f, ContentSize.Y), Size = new Vector2(4.0f, ContentSize.Y),
}; };
_separatorLine.AttachNode(this); _separatorLine.AttachNode(this);
_nothingSelectedTextNode = new TextNode { _nothingSelectedTextNode = new TextNode
{
Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f), Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f),
Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f), Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f),
AlignmentType = AlignmentType.Center, AlignmentType = AlignmentType.Center,
@@ -50,21 +57,60 @@ public class AddonCategoryConfigurationWindow : NativeAddon
}; };
_nothingSelectedTextNode.AttachNode(this); _nothingSelectedTextNode.AttachNode(this);
_configNode = new CategoryConfigurationNode { _configNode = new CategoryConfigurationNode
{
Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f), Position = ContentStartPosition + new Vector2(250.0f + 16.0f, 0.0f),
Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f), Size = ContentSize - new Vector2(250.0f + 16.0f, 0.0f),
IsVisible = false, IsVisible = false,
OnCategoryChanged = RefreshSelectionList,
}; };
_configNode.AttachNode(this); _configNode.AttachNode(this);
} }
private void OnOptionChanged(CategoryWrapper? newOption) { private List<CategoryWrapper> CreateCategoryWrappers()
{
return System.Config.Categories.UserCategories
.Select(categoryDefinition => new CategoryWrapper(categoryDefinition))
.ToList();
}
private void OnOptionChanged(CategoryWrapper? newOption)
{
if (_configNode is null) return; if (_configNode is null) return;
_configNode.IsVisible = newOption is not null; _configNode.IsVisible = newOption is not null;
_nothingSelectedTextNode?.IsVisible = newOption is null; if (_nothingSelectedTextNode is not null)
_nothingSelectedTextNode.IsVisible = newOption is null;
_configNode.ConfigurationOption = newOption; _configNode.ConfigurationOption = newOption;
} }
private void OnAddNewCategory(ModifyListNode<CategoryWrapper> 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();
}
} }
@@ -122,12 +122,14 @@ public class AddonInventoryWindow : NativeAddon
public void ManualInventoryRefresh() public void ManualInventoryRefresh()
{ {
if (!Services.ClientState.IsLoggedIn) return;
InventoryState.RefreshFromGame(); InventoryState.RefreshFromGame();
RefreshCategoriesCore(true); RefreshCategoriesCore(true);
} }
public void ManualCurrencyRefresh() public void ManualCurrencyRefresh()
{ {
if (!Services.ClientState.IsLoggedIn) return;
_footerNode.RefreshCurrencies(); _footerNode.RefreshCurrencies();
} }
+3 -3
View File
@@ -71,12 +71,12 @@ public static unsafe class CurrencyState
return currencyInfoList; return currencyInfoList;
} }
private static uint? GetLimitedTomestoneItemIdCached() private static uint? GetLimitedTomestoneItemIdCached()
{ {
if (_cachedLimitedTomestoneItemId.HasValue) if (_cachedLimitedTomestoneItemId.HasValue)
return _cachedLimitedTomestoneItemId.Value; return _cachedLimitedTomestoneItemId.Value;
uint? itemId = Services.DataManager.GetExcelSheet<TomestonesItem>() uint? itemId = Services.DataManager.GetExcelSheet<TomestonesItem>()
.FirstOrDefault(t => t.Tomestones.RowId == 3) .FirstOrDefault(t => t.Tomestones.RowId == 3)
.Item.RowId; .Item.RowId;
@@ -84,7 +84,7 @@ public static unsafe class CurrencyState
return itemId; return itemId;
} }
private static uint? GetNonLimitedTomestoneItemIdCached() private static uint? GetNonLimitedTomestoneItemIdCached()
{ {
if (_cachedNonLimitedTomestoneItemId.HasValue) if (_cachedNonLimitedTomestoneItemId.HasValue)
return _cachedNonLimitedTomestoneItemId.Value; return _cachedNonLimitedTomestoneItemId.Value;
@@ -158,7 +158,7 @@ public static unsafe class InventoryTypeExtensions
{ {
_ when inventoryType.IsMainInventory => InventoryType.Inventory1, _ when inventoryType.IsMainInventory => InventoryType.Inventory1,
_ when inventoryType.IsSaddleBag => inventoryType is InventoryType. SaddleBag1 or InventoryType.SaddleBag2 _ when inventoryType.IsSaddleBag => inventoryType is InventoryType. SaddleBag1 or InventoryType.SaddleBag2
? InventoryType. SaddleBag1 ? InventoryType. SaddleBag1
: InventoryType.PremiumSaddleBag1, : InventoryType.PremiumSaddleBag1,
_ => inventoryType, _ => inventoryType,
}; };
+4 -6
View File
@@ -17,14 +17,14 @@ public sealed unsafe class InventoryHooks : IDisposable
ushort dstSlot, ushort dstSlot,
bool unk); bool unk);
private readonly Hook<MoveItemSlotDelegate>? _moveItemSlotHook; private readonly Hook<MoveItemSlotDelegate>? _moveItemSlotHook;
public InventoryHooks() public InventoryHooks()
{ {
try try
{ {
_moveItemSlotHook = Services.GameInteropProvider.HookFromSignature<MoveItemSlotDelegate>( _moveItemSlotHook = Services.GameInteropProvider.HookFromSignature<MoveItemSlotDelegate>(
"E8 ?? ?? ?? ?? 48 8B 03 66 FF C5", "E8 ?? ?? ?? ?? 48 8B 03 66 FF C5",
MoveItemSlotDetour); MoveItemSlotDetour);
_moveItemSlotHook.Enable(); _moveItemSlotHook.Enable();
@@ -36,8 +36,7 @@ public sealed unsafe class InventoryHooks : IDisposable
} }
} }
private int MoveItemSlotDetour( private int MoveItemSlotDetour(InventoryManager* manager,
InventoryManager* manager,
InventoryType srcType, InventoryType srcType,
ushort srcSlot, ushort srcSlot,
InventoryType dstType, InventoryType dstType,
@@ -47,8 +46,7 @@ public sealed unsafe class InventoryHooks : IDisposable
InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
Services.Logger.Info( Services.Logger.Debug($"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
$"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk); return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
} }
@@ -56,7 +56,7 @@ public static class CategoryBucketManager
UserCategoryDefinition category = sortedScratch[i]; UserCategoryDefinition category = sortedScratch[i];
uint bucketKey = MakeUserCategoryKey(category.Order); uint bucketKey = MakeUserCategoryKey(category.Order);
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket)) if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
{ {
bucket = new CategoryBucket bucket = new CategoryBucket
{ {
+2 -2
View File
@@ -85,7 +85,7 @@ public static unsafe class InventoryScanner
bool isHq = (item.Flags & InventoryItem.ItemFlags.HighQuality) != 0; bool isHq = (item.Flags & InventoryItem.ItemFlags.HighQuality) != 0;
ulong key = stackMode == InventoryStackMode.AggregateByItemId ulong key = stackMode == InventoryStackMode.AggregateByItemId
? MakeAggregatedItemKey(id, isHq) ? MakeAggregatedItemKey(id, isHq)
: MakeNaturalSlotKey(inventoryType, slot); : MakeNaturalSlotKey(inventoryType, slot);
Services.Logger.DebugOnly($"Slot {inventoryType}[{slot}] ItemId={id} Qty={quantity} Key=0x{key: X16}"); 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; ulong key = kvp.Key;
AggregatedItem agg = kvp.Value; AggregatedItem agg = kvp.Value;
if (!itemInfoByKey.TryGetValue(key, out ItemInfo? info)) if (!itemInfoByKey.TryGetValue(key, out ItemInfo? info))
{ {
info = new ItemInfo info = new ItemInfo
{ {
@@ -1,19 +1,23 @@
using System.Collections.Generic; using System;
using System.Numerics; using System.Numerics;
using AetherBags.Addons; using AetherBags.Addons;
using AetherBags.Configuration; using AetherBags.Configuration;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes; using KamiToolKit.Premade.Nodes;
using Lumina.Excel.Sheets;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public class CategoryConfigurationNode : ConfigNode<CategoryWrapper> { public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
{
private readonly ScrollingAreaNode<VerticalListNode> _categoryList; private readonly ScrollingAreaNode<VerticalListNode> _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode; private CategoryDefinitionConfigurationNode? _activeNode;
public CategoryConfigurationNode() { public Action? OnCategoryChanged { get; set; }
_categoryList = new ScrollingAreaNode<VerticalListNode> {
public CategoryConfigurationNode()
{
_categoryList = new ScrollingAreaNode<VerticalListNode>
{
ContentHeight = 100.0f, ContentHeight = 100.0f,
AutoHideScrollBar = true, AutoHideScrollBar = true,
}; };
@@ -21,35 +25,51 @@ public class CategoryConfigurationNode : ConfigNode<CategoryWrapper> {
_categoryList.AttachNode(this); _categoryList.AttachNode(this);
} }
protected override void OptionChanged(CategoryWrapper? option)
protected override void OptionChanged(CategoryWrapper? option) { {
if (option?.CategoryDefinition is null) { if (option?.CategoryDefinition is null)
{
_categoryList.IsVisible = false; _categoryList.IsVisible = false;
return; return;
} }
_categoryList.IsVisible = true; _categoryList.IsVisible = true;
if (_activeNode is null) { if (_activeNode is null)
_activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) { {
Size = new Vector2(_categoryList.ContentNode.Width, 0f), _activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition)
{
Size = _categoryList.ContentNode.Size,
OnLayoutChanged = UpdateScrollHeight,
OnCategoryPropertyChanged = OnCategoryChanged,
}; };
_categoryList.ContentNode.AddNode(_activeNode); _categoryList.ContentNode.AddNode(_activeNode);
} else { }
else
{
_activeNode.SetCategory(option.CategoryDefinition); _activeNode.SetCategory(option.CategoryDefinition);
} }
UpdateScrollHeight();
}
private void UpdateScrollHeight()
{
_categoryList.ContentNode.RecalculateLayout(); _categoryList.ContentNode.RecalculateLayout();
_categoryList.ContentHeight = _categoryList.ContentNode.Height; _categoryList.ContentHeight = _categoryList.ContentNode.Height;
} }
protected override void OnSizeChanged() { protected override void OnSizeChanged()
{
base.OnSizeChanged(); base.OnSizeChanged();
_categoryList.Size = Size; _categoryList.Size = Size;
_categoryList.ContentNode.Width = Width; _categoryList.ContentNode.Width = Width;
foreach (var node in _categoryList.ContentNode.GetNodes<CategoryDefinitionConfigurationNode>()) { foreach (var node in _categoryList.ContentNode.GetNodes<CategoryDefinitionConfigurationNode>())
{
node.Width = Width; node.Width = Width;
} }
UpdateScrollHeight();
} }
} }
@@ -1,69 +1,467 @@
using System;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Nodes.Color; using AetherBags.Nodes.Color;
using FFXIVClientStructs.FFXIV.Component.GUI; using Dalamud.Utility;
using KamiToolKit.Classes;
using KamiToolKit.Nodes; 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; namespace AetherBags.Nodes.Configuration.Category;
public sealed class CategoryDefinitionConfigurationNode : VerticalListNode { public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
private readonly CheckboxNode enabledCheckbox; {
private readonly TextInputNode nameInputNode; private readonly CheckboxNode _enabledCheckbox;
private readonly TextInputNode descriptionInputNode; private readonly TextInputNode _nameInputNode;
private readonly ColorInputRow colorInputNode; 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<Item>? _sItemSheet;
private static ExcelSheet<ItemUICategory>? _sUICategorySheet;
public Action? OnLayoutChanged { get; set; }
public Action? OnCategoryPropertyChanged { get; set; }
private UserCategoryDefinition CategoryDefinition { get; set; }
public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition)
{
CategoryDefinition = categoryDefinition; CategoryDefinition = categoryDefinition;
FirstItemSpacing = 35.0f; _sItemSheet ??= Services.DataManager.GetExcelSheet<Item>();
ItemSpacing = 5.0f; _sUICategorySheet ??= Services.DataManager.GetExcelSheet<ItemUICategory>();
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, 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<int> filter,
int minBound,
int maxBound,
Action<bool, int, int> onUpdate)
{
var enabledCheckbox = new CheckboxNode
{
Size = new Vector2(200, 20),
String = $"{label} Filter",
IsChecked = filter.Enabled,
}; };
AddNode(enabledCheckbox); AddNode(enabledCheckbox);
colorInputNode = new ColorInputRow var minNode = new NumericInputNode
{ {
Label = "Color", Size = new Vector2(120, 28),
CurrentColor = CategoryDefinition.Color, Min = minBound,
DefaultColor = new UserCategoryDefinition().Color, Max = maxBound,
OnColorConfirmed = color => CategoryDefinition.Color = color, Value = filter.Min,
// OnColorChange = color => CategoryDefinition.Color = color, IsEnabled = filter.Enabled,
OnColorCanceled = color => CategoryDefinition.Color = color,
}; };
AddNode(colorInputNode);
nameInputNode = new TextInputNode var maxNode = new NumericInputNode
{ {
String = CategoryDefinition.Name, Size = new Vector2(120, 28),
OnInputComplete = name => CategoryDefinition.Name = name.ExtractText() 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, minNode.IsEnabled = isChecked;
OnInputComplete = name => CategoryDefinition.Description = name.ExtractText() 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<uint> 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; CategoryDefinition = newCategory;
RefreshValues(); RefreshValues();
} }
private void RefreshValues() private void RefreshValues()
{ {
enabledCheckbox.IsChecked = CategoryDefinition.Enabled; if (! _isInitialized) return;
colorInputNode.CurrentColor = CategoryDefinition.Color;
nameInputNode.String = CategoryDefinition.Name; _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<int> filter)
{
enabled.IsChecked = filter.Enabled;
min.Value = filter.Min;
max.Value = filter.Max;
min.IsEnabled = filter.Enabled;
max.IsEnabled = filter.Enabled;
} }
} }
@@ -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<int> _list;
private readonly List<CheckboxNode> _checkboxes = new();
private readonly Action? _onChanged;
public RarityEditorNode(List<int> 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<int> newList)
{
_list = newList;
Refresh();
}
public void Refresh()
{
for (int i = 0; i < _checkboxes.Count; i++)
{
_checkboxes[i].IsChecked = _list.Contains(i);
}
}
}
@@ -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];
}
}
@@ -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<string> _list;
private readonly TextInputNode _addInput;
private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged;
public StringListEditorNode(string label, List<string> 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<string> 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);
}
}
@@ -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<uint> _list;
private readonly NumericInputNode _addInput;
private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged;
private readonly Func<uint, string>? _labelResolver;
public UintListEditorNode(string label, List<uint> list, Action? onChanged = null, Func<uint, string>? 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<uint> 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<uint, string>? 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);
}
}
@@ -28,8 +28,10 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNod
OnOptionSelected = selected => OnOptionSelected = selected =>
{ {
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed)) if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
{
config.StackMode = parsed; config.StackMode = parsed;
RefreshInventory(); System.AddonInventoryWindow.ManualInventoryRefresh();
}
} }
}; };
ContentNode.AddNode(_stackDropDown); ContentNode.AddNode(_stackDropDown);
@@ -23,7 +23,7 @@ public class InventoryCategoryNode : SimpleComponentNode
private const float HeaderHeight = 16; private const float HeaderHeight = 16;
private const float MinWidth = 40; private const float MinWidth = 40;
private float? _fixedWidth; private float? _fixedWidth;
private int _hoverRefs; private int _hoverRefs;
private bool _headerSuppressed; private bool _headerSuppressed;
private bool _headerExpanded; private bool _headerExpanded;
@@ -92,7 +92,7 @@ public class InventoryCategoryNode : SimpleComponentNode
} }
} }
public float? FixedWidth public float? FixedWidth
{ {
get => _fixedWidth; get => _fixedWidth;
set set
@@ -184,8 +184,8 @@ public class InventoryCategoryNode : SimpleComponentNode
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine; int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
int actualColumns = Math.Min(itemCount, itemsPerLine); int actualColumns = Math.Min(itemCount, itemsPerLine);
float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize;
float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize;
float hPad = _itemGridNode.HorizontalPadding; float hPad = _itemGridNode.HorizontalPadding;
float vPad = _itemGridNode.VerticalPadding; float vPad = _itemGridNode.VerticalPadding;
@@ -259,7 +259,7 @@ public class InventoryCategoryNode : SimpleComponentNode
InventoryType targetContainer = targetItemInfo.Item.Container; InventoryType targetContainer = targetItemInfo.Item.Container;
ushort targetSlot = (ushort)targetItemInfo.Item.Slot; 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); InventoryMoveHelper.MoveItem(sourceContainer, sourceSlot, targetContainer, targetSlot);
} }