diff --git a/.gitignore b/.gitignore
index add57be..b285a22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
bin/
obj/
+.idea/
/packages/
riderModule.iml
/_ReSharper.Caches/
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..69a9f30
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "KamiToolKit"]
+ path = KamiToolKit
+ url = https://github.com/MidoriKami/KamiToolKit
diff --git a/AetherBags.sln b/AetherBags.sln
index 2ec8073..3e7404a 100644
--- a/AetherBags.sln
+++ b/AetherBags.sln
@@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AetherBags", "AetherBags\AetherBags.csproj", "{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiToolKit", "KamiToolKit\KamiToolKit.csproj", "{0907374F-93F8-427F-AD0A-49DB4B0A3DD4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -12,5 +14,9 @@ Global
{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Debug|x64.Build.0 = Debug|x64
{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Release|x64.ActiveCfg = Release|x64
{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Release|x64.Build.0 = Release|x64
+ {0907374F-93F8-427F-AD0A-49DB4B0A3DD4}.Debug|x64.ActiveCfg = Debug|x64
+ {0907374F-93F8-427F-AD0A-49DB4B0A3DD4}.Debug|x64.Build.0 = Debug|x64
+ {0907374F-93F8-427F-AD0A-49DB4B0A3DD4}.Release|x64.ActiveCfg = Release|x64
+ {0907374F-93F8-427F-AD0A-49DB4B0A3DD4}.Release|x64.Build.0 = Release|x64
EndGlobalSection
EndGlobal
diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user
new file mode 100644
index 0000000..a250ab0
--- /dev/null
+++ b/AetherBags.sln.DotSettings.user
@@ -0,0 +1,2 @@
+
+ ForceIncluded
\ No newline at end of file
diff --git a/AetherBags/.gitignore b/AetherBags/.gitignore
new file mode 100644
index 0000000..57f1cb2
--- /dev/null
+++ b/AetherBags/.gitignore
@@ -0,0 +1 @@
+/.idea/
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs
new file mode 100644
index 0000000..4787062
--- /dev/null
+++ b/AetherBags/Addons/AddonInventoryWindow.cs
@@ -0,0 +1,86 @@
+using System.Linq;
+using System.Numerics;
+using AetherBags.Extensions;
+using AetherBags.Inventory;
+using AetherBags.Nodes;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit;
+using KamiToolKit.Classes;
+
+namespace AetherBags.Addons;
+
+public class AddonInventoryWindow : NativeAddon
+{
+ private InventoryCategoryNode _categoryNode;
+ private InventoryDragDropNode _dragDropNode;
+ protected override unsafe void OnSetup(AtkUnitBase* addon)
+ {
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
+ _categoryNode = new InventoryCategoryNode
+ {
+ Position = ContentStartPosition,
+ Size = ContentSize,
+ Category = new CategoryInfo
+ {
+ Name = "AetherBags",
+ },
+ Items = InventoryState.GetInventoryItems()
+ };
+ _categoryNode.AttachNode(this);
+ /*
+ var data = InventoryState.GetInventoryItems().Find(item => item.Name.Contains("Cookie"));
+
+
+ if (data != null)
+ {
+ var item = data.Item;
+ _dragDropNode = new InventoryDragDropNode
+ {
+ Size = new Vector2(48),
+ IsVisible = true,
+ IconId = data.IconId,
+ AcceptedType = DragDropType.Nothing,
+ IsDraggable = false,
+ Payload = new DragDropPayload
+ {
+ Type = DragDropType.Item,
+ Int1 = (int)data.Item.Container,
+ Int2 = (int)data.Item.ItemId,
+ },
+ IsClickable = true,
+ OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot),
+ OnRollOut = node => node.HideTooltip(),
+ OnClicked = _ =>
+ {
+
+ AgentInventoryContext* context = AgentInventoryContext.Instance();
+ context->OpenForItemSlot(data.Item.Container, data.Item.Slot, 0, context->AddonId);
+ //item.UseItem();
+ },
+ ItemInfo = data
+ };
+ _dragDropNode.AttachNode(this);
+ }
+ */
+
+ }
+
+ protected override unsafe void OnUpdate(AtkUnitBase* addon)
+ {
+
+ }
+
+ private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
+ {
+
+ }
+
+ protected override unsafe void OnFinalize(AtkUnitBase* addon)
+ {
+ Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/AetherBags.csproj b/AetherBags/AetherBags.csproj
index 81bf000..34817a8 100644
--- a/AetherBags/AetherBags.csproj
+++ b/AetherBags/AetherBags.csproj
@@ -1,5 +1,24 @@
-
+
1.0.0.0
+
+
+ Zeffuro
+ AetherBags
+ AetherBags
+ Never think too hard about your bags again!
+ This plugin replaces your inventory with it's own categorified inventory addon.
+ https://github.com/Zeffuro/AetherBags
+ ui
+ true
+
+
+
+
+
+
+
+
+
diff --git a/AetherBags/AetherBags.json b/AetherBags/AetherBags.json
deleted file mode 100644
index 0e9adda..0000000
--- a/AetherBags/AetherBags.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "Name": "AetherBags",
- "Author": "",
- "Description": "",
- "Punchline": ""
-}
diff --git a/AetherBags/Extensions/AtkResNodeExtensions.cs b/AetherBags/Extensions/AtkResNodeExtensions.cs
new file mode 100644
index 0000000..f5da790
--- /dev/null
+++ b/AetherBags/Extensions/AtkResNodeExtensions.cs
@@ -0,0 +1,16 @@
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace AetherBags.Extensions;
+
+public static unsafe class AtkResNodeExtensions
+{
+ extension(ref AtkResNode node)
+ {
+ public void ShowInventoryItemTooltip(InventoryType container, short slot) {
+ fixed (AtkResNode* nodePointer = &node) {
+ AtkStage.Instance()->ShowInventoryItemTooltip(nodePointer, container, slot);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Extensions/AtkStageExtensions.cs b/AetherBags/Extensions/AtkStageExtensions.cs
new file mode 100644
index 0000000..63e7aea
--- /dev/null
+++ b/AetherBags/Extensions/AtkStageExtensions.cs
@@ -0,0 +1,34 @@
+using FFXIVClientStructs.FFXIV.Client.Enums;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace AetherBags.Extensions;
+
+
+public static unsafe class AtkStageExtensions
+{
+ extension(ref AtkStage stage)
+ {
+ public void ShowInventoryItemTooltip(AtkResNode* node, InventoryType container, short slot)
+ {
+ var tooltipArgs = stackalloc AtkTooltipManager.AtkTooltipArgs[1];
+ tooltipArgs->Ctor();
+ tooltipArgs->ItemArgs.Kind = DetailKind.InventoryItem;
+ tooltipArgs->ItemArgs.InventoryType = container;
+ tooltipArgs->ItemArgs.Slot = slot;
+ tooltipArgs->ItemArgs.BuyQuantity = -1;
+ tooltipArgs->ItemArgs.Flag1 = 0;
+
+ var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode(node);
+ if (addon is null) return;
+
+ stage.TooltipManager.ShowTooltip(
+ AtkTooltipManager.AtkTooltipType.Item,
+ addon->Id,
+ node,
+ tooltipArgs
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Extensions/InventoryItemExtensions.cs b/AetherBags/Extensions/InventoryItemExtensions.cs
new file mode 100644
index 0000000..634fc69
--- /dev/null
+++ b/AetherBags/Extensions/InventoryItemExtensions.cs
@@ -0,0 +1,113 @@
+using System.Text.RegularExpressions;
+using Dalamud.Utility;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using Lumina.Excel.Sheets;
+using Lumina.Text.ReadOnly;
+
+namespace AetherBags.Extensions;
+
+public static unsafe class InventoryItemExtensions {
+ extension(ref InventoryItem item) {
+ public uint IconId => item.GetIconId();
+ public ReadOnlySeString Name => item.GetItemName();
+
+ private uint GetIconId() {
+ uint iconId = 0;
+
+ if (item.GetEventItem() is { } eventItem) {
+ iconId = eventItem.Icon;
+ }
+ else if (item.GetItem() is { } regularItem) {
+ iconId = regularItem.Icon;
+
+ if (item.IsHighQuality()) {
+ iconId += 1_000_000;
+ }
+ }
+
+ return iconId;
+ }
+
+ private ReadOnlySeString GetItemName() {
+ var itemId = item.GetItemId();
+ var itemName = ItemUtil.GetItemName(itemId);
+
+ return new Lumina.Text.SeStringBuilder()
+ .PushColorType(ItemUtil.GetItemRarityColorType(itemId))
+ .Append(itemName)
+ .PopColorType()
+ .ToReadOnlySeString();
+ }
+
+ private Item? GetItem() {
+ var baseItemId = item.GetBaseItemId();
+
+ if (ItemUtil.IsNormalItem(baseItemId) &&
+ Services.DataManager.GetExcelSheet- ().TryGetRow(baseItemId, out var baseItem)) {
+ return baseItem;
+ }
+
+ return null;
+ }
+
+ private EventItem? GetEventItem() {
+ var baseItemId = item.GetBaseItemId();
+
+ if (ItemUtil.IsEventItem(baseItemId) &&
+ Services.DataManager.GetExcelSheet().TryGetRow(baseItemId, out var eventItem)) {
+ return eventItem;
+ }
+
+ return null;
+ }
+
+ public bool IsRegexMatch(string searchString) {
+ // Skip any data access if string is empty
+ if (searchString.IsNullOrEmpty()) return true;
+
+ var isDescriptionSearch = searchString.StartsWith('$');
+
+ if (isDescriptionSearch) {
+ searchString = searchString[1..];
+ }
+
+ try {
+ var regex = new Regex(searchString,RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+
+ if (ItemUtil.IsEventItem(item.GetBaseItemId())) {
+ if (!Services.DataManager.GetExcelSheet().TryGetRow(item.GetBaseItemId(), out var itemData)) return false;
+
+ if (regex.IsMatch(item.ItemId.ToString())) return true;
+ if (regex.IsMatch(itemData.Name.ToString())) return true;
+ }
+
+ else if (ItemUtil.IsNormalItem(item.GetBaseItemId())) {
+ if (!Services.DataManager.GetExcelSheet
- ().TryGetRow(item.GetBaseItemId(), out var itemData)) return false;
+
+ if (regex.IsMatch(item.ItemId.ToString())) return true;
+ if (regex.IsMatch(itemData.Name.ToString())) return true;
+ if (regex.IsMatch(itemData.Description.ToString()) && isDescriptionSearch) return true;
+ if (regex.IsMatch(itemData.LevelEquip.ToString())) return true;
+ if (regex.IsMatch(itemData.LevelItem.RowId.ToString())) return true;
+ }
+ }
+ catch (RegexParseException) { }
+
+ return false;
+ }
+
+ public void UseItem()
+ {
+ uint itemId = item.ItemId;
+ InventoryType type = item.GetInventoryType() == InventoryType.KeyItems
+ ? InventoryType.KeyItems
+ : InventoryType.Invalid;
+
+ if (InventoryManager.Instance()->GetInventoryItemCount(itemId, true) > 0)
+ itemId += 1_000_000;
+
+ AgentInventoryContext.Instance()->UseItem(itemId, type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Extensions/ItemExtensions.cs b/AetherBags/Extensions/ItemExtensions.cs
new file mode 100644
index 0000000..1de9a4e
--- /dev/null
+++ b/AetherBags/Extensions/ItemExtensions.cs
@@ -0,0 +1,18 @@
+using System.Numerics;
+using KamiToolKit.Classes;
+using Lumina.Excel.Sheets;
+
+namespace AetherBags.Extensions;
+
+public static class ItemExtensions {
+ extension(Item item) {
+ public Vector4 RarityColor => item.Rarity switch {
+ 7 => ColorHelper.GetColor(561),
+ 4 => ColorHelper.GetColor(555),
+ 3 => ColorHelper.GetColor(553),
+ 2 => ColorHelper.GetColor(551),
+ 1 => ColorHelper.GetColor(549),
+ _ => Vector4.One,
+ };
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Extensions/NodeBaseExtensions.cs b/AetherBags/Extensions/NodeBaseExtensions.cs
new file mode 100644
index 0000000..f43e2e1
--- /dev/null
+++ b/AetherBags/Extensions/NodeBaseExtensions.cs
@@ -0,0 +1,12 @@
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit;
+
+namespace AetherBags.Extensions;
+
+public static unsafe class NodeBaseExtensions {
+ extension(NodeBase node) {
+ public void ShowInventoryItemTooltip(InventoryType container, short slot)
+ => AtkStage.Instance()->ShowInventoryItemTooltip(node, container, slot);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/GlobalUsing.cs b/AetherBags/GlobalUsing.cs
new file mode 100644
index 0000000..8cbabd6
--- /dev/null
+++ b/AetherBags/GlobalUsing.cs
@@ -0,0 +1 @@
+global using KamiToolKit.Extensions;
\ No newline at end of file
diff --git a/AetherBags/Helpers/BackupHelper.cs b/AetherBags/Helpers/BackupHelper.cs
new file mode 100644
index 0000000..273e84a
--- /dev/null
+++ b/AetherBags/Helpers/BackupHelper.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Security.Cryptography;
+using Dalamud.Plugin;
+
+namespace AetherBags.Helpers;
+
+// Taken and adapted for StatusTimers using zips from https://github.com/Caraxi/SimpleHeels/blob/0a0fe3c02a0a2c5a7c96b3304952d5078cd338aa/Plugin.cs#L392
+// Thanks Caraxi
+public static class BackupHelper {
+ private const int MaxBackups = 10;
+ private const string Name = "StatusTimers";
+ public static void DoConfigBackup(IDalamudPluginInterface pluginInterface) {
+ Services.Logger.Debug("Backup configuration start.");
+ try {
+ var configDirectory = pluginInterface.ConfigDirectory;
+ if (!configDirectory.Exists) {
+ return;
+ }
+
+ var backupDir = Path.Join(configDirectory.Parent!.Parent!.FullName, "backups", Name);
+ var dir = new DirectoryInfo(backupDir);
+ if (!dir.Exists) {
+ dir.Create();
+ }
+
+ if (!dir.Exists) {
+ throw new Exception("Backup Directory does not exist");
+ }
+
+ var latestFile = new FileInfo(Path.Join(backupDir, $"{Name}.latest.zip"));
+ var tempFile = Path.Join(backupDir, $"{Name}.tmp.zip");
+
+ var needsBackup = false;
+
+ if (latestFile.Exists) {
+ string lastBackupHash = ZipJsonHash(latestFile.FullName);
+ string currentConfigDirHash = DirJsonHash(configDirectory.FullName);
+ if (currentConfigDirHash != lastBackupHash) {
+ needsBackup = true;
+ }
+ } else {
+ needsBackup = true;
+ }
+
+ if (!needsBackup) {
+ return;
+ }
+
+ ZipFile.CreateFromDirectory(configDirectory.FullName, tempFile);
+ if (latestFile.Exists) {
+ var t = latestFile.LastWriteTime;
+ string archiveName = $"{Name}.{t.Year}{t.Month:00}{t.Day:00}{t.Hour:00}{t.Minute:00}{t.Second:00}.zip";
+ string archivePath = Path.Join(backupDir, archiveName);
+
+ bool moved = false;
+ for (int i = 0; i < 5 && !moved; i++) {
+ try {
+ File.Move(latestFile.FullName, archivePath);
+ moved = true;
+ } catch (IOException ioEx) when (i < 4) {
+ Services.Logger.Debug($"Move failed, retrying in 100ms: {ioEx.Message}");
+ global::System.Threading.Thread.Sleep(100);
+ }
+ }
+ if (!moved) {
+ throw new IOException($"Could not move {latestFile.FullName} after several retries.");
+ }
+ }
+
+ if (File.Exists(latestFile.FullName)) {
+ File.Delete(latestFile.FullName);
+ }
+ File.Move(tempFile, latestFile.FullName);
+
+ var allBackups = dir.GetFiles().Where(f => f.Name.StartsWith($"{Name}.2") && f.Name.EndsWith(".zip"))
+ .OrderBy(f => f.LastWriteTime.Ticks).ToList();
+ if (allBackups.Count > MaxBackups) {
+ Services.Logger.Debug($"Removing Oldest Backup: {allBackups[0].FullName}");
+ File.Delete(allBackups[0].FullName);
+ }
+ } catch (Exception exception) {
+ Services.Logger.Warning(exception, "Backup Skipped");
+ }
+ }
+
+ private static string ComputeCombinedJsonHash(IEnumerable<(string name, byte[] contents)> files) {
+ using var sha256 = SHA256.Create();
+ foreach (var file in files.OrderBy(f => f.name, StringComparer.OrdinalIgnoreCase)) {
+ sha256.TransformBlock(file.contents, 0, file.contents.Length, null, 0);
+ }
+ sha256.TransformFinalBlock(Array.Empty(), 0, 0);
+ return sha256.Hash != null ? BitConverter.ToString(sha256.Hash).Replace("-", "") : string.Empty;
+ }
+
+ private static string DirJsonHash(string dirPath) =>
+ ComputeCombinedJsonHash(
+ new DirectoryInfo(dirPath)
+ .GetFiles("*.json", SearchOption.TopDirectoryOnly)
+ .Where(f => !f.Name.EndsWith(".addon.json", StringComparison.OrdinalIgnoreCase))
+ .Select(f => (f.Name, File.ReadAllBytes(f.FullName)))
+ );
+
+ private static string ZipJsonHash(string zipPath) {
+ byte[] zipBytes = File.ReadAllBytes(zipPath);
+ using var msZip = new MemoryStream(zipBytes);
+ using var zip = new ZipArchive(msZip, ZipArchiveMode.Read);
+ var files = zip.Entries
+ .Where(e => e.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
+ && !e.FullName.EndsWith(".addon.json", StringComparison.OrdinalIgnoreCase)
+ && !e.FullName.Contains("/"))
+ .Select(e => {
+ using var ms = new MemoryStream();
+ using (var s = e.Open()) {
+ s.CopyTo(ms);
+ }
+ return (e.FullName, ms.ToArray());
+ });
+ return ComputeCombinedJsonHash(files);
+ }
+}
diff --git a/AetherBags/Inventory/CategoryInfo.cs b/AetherBags/Inventory/CategoryInfo.cs
new file mode 100644
index 0000000..542e5ea
--- /dev/null
+++ b/AetherBags/Inventory/CategoryInfo.cs
@@ -0,0 +1,11 @@
+using System.Numerics;
+using KamiToolKit.Classes;
+
+namespace AetherBags.Inventory;
+
+public class CategoryInfo
+{
+ public required string Name { get; set; }
+ public Vector4 Color { get; set; } = ColorHelper.GetColor(50);
+ public string Description { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/InventoryState.cs
new file mode 100644
index 0000000..f82b6c8
--- /dev/null
+++ b/AetherBags/Inventory/InventoryState.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Game.Inventory;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+
+namespace AetherBags.Inventory;
+
+public static unsafe class InventoryState
+{
+ public static List StandardInventories => [
+ InventoryType.Inventory1,
+ InventoryType.Inventory2,
+ InventoryType.Inventory3,
+ InventoryType.Inventory4,
+ InventoryType.EquippedItems,
+ InventoryType.ArmoryMainHand,
+ InventoryType.ArmoryHead,
+ InventoryType.ArmoryBody,
+ InventoryType.ArmoryHands,
+ InventoryType.ArmoryWaist,
+ InventoryType.ArmoryLegs,
+ InventoryType.ArmoryFeets,
+ InventoryType.ArmoryOffHand,
+ InventoryType.ArmoryEar,
+ InventoryType.ArmoryNeck,
+ InventoryType.ArmoryWrist,
+ InventoryType.ArmoryRings,
+ InventoryType.Currency,
+ InventoryType.Crystals,
+ InventoryType.ArmorySoulCrystal,
+ ];
+
+ public static bool Contains(this List inventoryTypes, GameInventoryType type)
+ => inventoryTypes.Contains((InventoryType)type);
+
+ public static List GetInventoryItems() {
+ List inventories = [ InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 ];
+ List items = [];
+
+ foreach (var inventory in inventories) {
+ var container = InventoryManager.Instance()->GetInventoryContainer(inventory);
+
+ for (var index = 0; index < container->Size; ++index) {
+ ref var item = ref container->Items[index];
+ if (item.ItemId is 0) continue;
+
+ items.Add(item);
+ }
+ }
+
+ List itemInfos = [];
+ itemInfos.AddRange(from itemGroups in items.GroupBy(item => item.ItemId)
+ where itemGroups.Key is not 0
+ let item = itemGroups.First()
+ let itemCount = itemGroups.Sum(duplicateItem => duplicateItem.Quantity)
+ select new ItemInfo {
+ Item = item, ItemCount = itemCount,
+ });
+
+ return itemInfos;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/ItemInfo.cs b/AetherBags/Inventory/ItemInfo.cs
new file mode 100644
index 0000000..41a7e1b
--- /dev/null
+++ b/AetherBags/Inventory/ItemInfo.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Numerics;
+using System.Text.RegularExpressions;
+using AetherBags.Extensions;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Lumina.Excel.Sheets;
+
+namespace AetherBags.Inventory;
+
+public class ItemInfo : IEquatable {
+ public required InventoryItem Item { get; set; }
+ public required int ItemCount { get; set; }
+
+ private Item ItemData => Services.DataManager.GetExcelSheet
- ().GetRow(Item.ItemId);
+
+ public Vector4 RarityColor => ItemData.RarityColor;
+
+ public uint IconId => ItemData.Icon;
+
+ public string Name => ItemData.Name.ToString();
+
+ public int Level => ItemData.LevelEquip;
+
+ public int ItemLevel => (int) ItemData.LevelItem.RowId;
+
+ public int Rarity => ItemData.Rarity;
+
+ public int UiCategory => (int) ItemData.ItemUICategory.RowId;
+
+ private string Description => ItemData.Description.ToString();
+
+ public bool IsRegexMatch(string searchTerms) {
+ const RegexOptions regexOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
+
+ if (Regex.IsMatch(Name, searchTerms, regexOptions)) return true;
+ if (Regex.IsMatch(Description, searchTerms, regexOptions)) return true;
+ if (Regex.IsMatch(Level.ToString(), searchTerms, regexOptions)) return true;
+ if (Regex.IsMatch(ItemLevel.ToString(), searchTerms, regexOptions)) return true;
+
+ return false;
+ }
+
+ public bool Equals(ItemInfo? other) {
+ if (other is null) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Item.ItemId.Equals(other.Item.ItemId) && ItemCount == other.ItemCount;
+ }
+
+ public override bool Equals(object? obj) {
+ if (obj is null) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((ItemInfo) obj);
+ }
+
+ public override int GetHashCode()
+ // ReSharper disable NonReadonlyMemberInGetHashCode
+ => HashCode.Combine(Item.ItemId, ItemCount);
+ // ReSharper restore NonReadonlyMemberInGetHashCode
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/HybridDirectionalFlexNode.cs b/AetherBags/Nodes/HybridDirectionalFlexNode.cs
new file mode 100644
index 0000000..89e636b
--- /dev/null
+++ b/AetherBags/Nodes/HybridDirectionalFlexNode.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using KamiToolKit;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes
+{
+ public enum FlexGrowDirection
+ {
+ DownRight,
+ DownLeft,
+ UpRight,
+ UpLeft
+ }
+
+ public class HybridDirectionalFlexNode : HybridDirectionalFlexNode { }
+
+ public class HybridDirectionalFlexNode : LayoutListNode where T : NodeBase
+ {
+
+ public FlexGrowDirection GrowDirection { get;
+ set {
+ field = value;
+ RecalculateLayout();
+ }
+ } = FlexGrowDirection.DownRight;
+
+ public int ItemsPerLine {
+ get;
+ set {
+ field = value;
+ RecalculateLayout();
+ }
+ } = 1;
+
+ public bool FillRowsFirst {
+ get;
+ set {
+ field = value;
+ RecalculateLayout();
+ }
+ } = true;
+
+ public float HorizontalPadding {
+ get;
+ set {
+ field = value;
+ RecalculateLayout();
+ }
+ } = 1;
+
+ public float VerticalPadding {
+ get;
+ set {
+ field = value;
+ RecalculateLayout();
+ }
+ } = 1;
+
+ protected override void InternalRecalculateLayout()
+ {
+ if (NodeList.Count == 0) {
+ return;
+ }
+
+ int itemsPerLine = Math.Max(1, ItemsPerLine);
+
+ float nodeWidth = NodeList.First().Width;
+ float nodeHeight = NodeList.First().Height;
+
+ bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
+ bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
+
+ float startX = alignRight ? Width : 0f;
+ float startY = alignBottom ? Height : 0f;
+
+ int idx = 0;
+ foreach (var node in NodeList)
+ {
+ int row, col;
+ if (FillRowsFirst)
+ {
+ row = idx / itemsPerLine;
+ col = idx % itemsPerLine;
+ }
+ else
+ {
+ col = idx / itemsPerLine;
+ row = idx % itemsPerLine;
+ }
+
+ float x = alignRight
+ ? startX - (col + 1) * nodeWidth - col * HorizontalPadding
+ : startX + col * (nodeWidth + HorizontalPadding);
+
+ float y = alignBottom
+ ? startY - (row + 1) * nodeHeight - row * VerticalPadding
+ : startY + row * (nodeHeight + VerticalPadding);
+
+ node.X = x;
+ node.Y = y;
+ AdjustNode(node);
+ idx++;
+ }
+ }
+ }
+}
diff --git a/AetherBags/Nodes/InventoryCategoryNode.cs b/AetherBags/Nodes/InventoryCategoryNode.cs
new file mode 100644
index 0000000..b27c630
--- /dev/null
+++ b/AetherBags/Nodes/InventoryCategoryNode.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using AetherBags.Extensions;
+using AetherBags.Inventory;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes;
+
+public class InventoryCategoryNode : SimpleComponentNode
+{
+ private readonly TextNode _categoryNameTextNode;
+ private readonly HybridDirectionalFlexNode _itemGridNode;
+ public InventoryCategoryNode()
+ {
+ _categoryNameTextNode = new TextNode
+ {
+ Size = new Vector2(100, 14),
+ AlignmentType = AlignmentType.Left
+ };
+ _categoryNameTextNode.AttachNode(this);
+
+ _itemGridNode = new HybridDirectionalFlexNode
+ {
+ Position = new Vector2(0, 16),
+ Size = new Vector2(240, 100),
+ FillRowsFirst = true,
+ ItemsPerLine = 10,
+ HorizontalPadding = 6,
+ VerticalPadding = 6,
+ };
+ _itemGridNode.AttachNode(this);
+ }
+
+ public required CategoryInfo Category
+ {
+ get;
+ set
+ {
+ field = value;
+
+ _categoryNameTextNode.String = value.Name;
+ _categoryNameTextNode.TextColor = value.Color;
+ _categoryNameTextNode.TooltipString = value.Description;
+ }
+ }
+
+ public required List Items
+ {
+ get;
+ set
+ {
+ field = value;
+
+ UpdateItemGrid();
+ }
+ }
+
+ private bool UpdateItemGrid()
+ {
+ var listUpdated = _itemGridNode.SyncWithListData(Items, node => node.ItemInfo, data => CreateInventoryDragDropNode(data));
+ return listUpdated;
+ }
+
+ private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
+ {
+ InventoryDragDropNode node = new InventoryDragDropNode
+ {
+ Size = new Vector2(40),
+ IsVisible = true,
+ IconId = data.IconId,
+ AcceptedType = DragDropType.Nothing,
+ IsDraggable = false,
+ Payload = new DragDropPayload
+ {
+ Type = DragDropType.Item,
+ Int1 = (int)data.Item.Container,
+ Int2 = (int)data.Item.ItemId,
+ },
+ IsClickable = true,
+ OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot),
+ OnRollOut = node => node.HideTooltip(),
+ ItemInfo = data
+ };
+ return node;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/InventoryDragDropNode.cs b/AetherBags/Nodes/InventoryDragDropNode.cs
new file mode 100644
index 0000000..9df8e69
--- /dev/null
+++ b/AetherBags/Nodes/InventoryDragDropNode.cs
@@ -0,0 +1,54 @@
+using System.Numerics;
+using AetherBags.Extensions;
+using AetherBags.Inventory;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes;
+
+public class InventoryDragDropNode : DragDropNode
+{
+ private readonly TextNode _quantityTextNode;
+ public unsafe InventoryDragDropNode()
+ {
+ _quantityTextNode = new TextNode {
+ Size = new Vector2(40.0f, 12.0f),
+ Position = new Vector2(4.0f, 34.0f),
+ NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents,
+ Color = ColorHelper.GetColor(50),
+ TextOutlineColor = ColorHelper.GetColor(51),
+ TextFlags = TextFlags.Edge,
+ AlignmentType = AlignmentType.Right,
+ };
+ _quantityTextNode.AttachNode(this);
+ CollisionNode.AddEvent(AtkEventType.MouseDown, OnItemMouseDown);
+ CollisionNode.AddEvent(AtkEventType.MouseClick, OnItemClicked);
+ }
+
+ public required ItemInfo ItemInfo
+ {
+ get;
+ set
+ {
+ field = value;
+ _quantityTextNode.String = value.ItemCount.ToString();
+ }
+ }
+
+ private unsafe void OnItemMouseDown(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
+ InventoryItem item = ItemInfo.Item;
+ if (!atkEventData->IsRightClick) return;
+
+ AgentInventoryContext* context = AgentInventoryContext.Instance();
+ context->OpenForItemSlot(item.Container, item.Slot, 0, context->AddonId);
+ }
+
+ private unsafe void OnItemClicked(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
+ InventoryItem item = ItemInfo.Item;
+ if (!atkEventData->IsLeftClick) return;
+ item.UseItem();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs
index 21ac106..9617931 100644
--- a/AetherBags/Plugin.cs
+++ b/AetherBags/Plugin.cs
@@ -1,27 +1,77 @@
+using System.Numerics;
+using AetherBags.Addons;
+using AetherBags.Helpers;
+using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Plugin;
using Dalamud.Game.Command;
+using KamiToolKit;
namespace AetherBags;
public class Plugin : IDalamudPlugin
{
- public const string CommandName = "/aetherbags";
-
+ private static string HelpDescription => "Opens your inventory.";
public Plugin(IDalamudPluginInterface pluginInterface)
{
pluginInterface.Create();
- Services.CommandManager.AddHandler(CommandName, new CommandInfo(this.OnCommand)
+ BackupHelper.DoConfigBackup(pluginInterface);
+
+ KamiToolKitLibrary.Initialize(pluginInterface);
+
+ System.AddonInventoryWindow = new AddonInventoryWindow
{
- HelpMessage = ""
+ InternalName = "AetherBags",
+ Title = "AetherBags",
+ Size = new Vector2(750, 750),
+ };
+
+ Services.CommandManager.AddHandler("/aetherbags", new CommandInfo(OnCommand)
+ {
+ DisplayOrder = 1,
+ ShowInHelp = true,
+ HelpMessage = HelpDescription
});
+ Services.CommandManager.AddHandler("/ab", new CommandInfo(OnCommand)
+ {
+ DisplayOrder = 2,
+ ShowInHelp = true,
+ HelpMessage = HelpDescription
+ });
+ Services.ClientState.Login += OnLogin;
+
+ if (Services.ClientState.IsLoggedIn) {
+ Services.Framework.RunOnFrameworkThread(OnLogin);
+ }
}
public void Dispose()
{
- Services.CommandManager.RemoveHandler(CommandName);
+ Services.ClientState.Login -= OnLogin;
+
+ Services.CommandManager.RemoveHandler("/aetherbags");
+ Services.CommandManager.RemoveHandler("/ab");
+
+ KamiToolKitLibrary.Dispose();
}
private void OnCommand(string command, string args)
{
- }
\ No newline at end of file
+ switch (command)
+ {
+ case "/aetherbags":
+ case "/ab":
+ if(args.Length == 0)
+ System.AddonInventoryWindow.Toggle();
+ if(args == "config")
+ System.AddonInventoryWindow.Toggle();
+ break;
+ }
+ }
+
+ private void OnLogin() {
+ #if DEBUG
+ System.AddonInventoryWindow.Toggle();
+ #endif
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Services.cs b/AetherBags/Services.cs
index 759afeb..e31584c 100644
--- a/AetherBags/Services.cs
+++ b/AetherBags/Services.cs
@@ -6,6 +6,11 @@ namespace AetherBags;
public class Services
{
- [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
+ [PluginService] public static IAddonLifecycle AddonLifecycle { get; set; } = null!;
+ [PluginService] public static IClientState ClientState { get; private set; } = null!;
[PluginService] public static ICommandManager CommandManager { get; private set; } = null!;
+ [PluginService] public static IDataManager DataManager { get; set; } = null!;
+ [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
+ [PluginService] public static IFramework Framework { get; private set; } = null!;
+ [PluginService] public static IPluginLog Logger { get; private set; } = null!;
}
\ No newline at end of file
diff --git a/AetherBags/System.cs b/AetherBags/System.cs
new file mode 100644
index 0000000..2a267dd
--- /dev/null
+++ b/AetherBags/System.cs
@@ -0,0 +1,8 @@
+using AetherBags.Addons;
+
+namespace AetherBags;
+
+public static class System
+{
+ public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/AetherBags/packages.lock.json b/AetherBags/packages.lock.json
new file mode 100644
index 0000000..7dce951
--- /dev/null
+++ b/AetherBags/packages.lock.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net10.0-windows7.0": {
+ "DalamudPackager": {
+ "type": "Direct",
+ "requested": "[14.0.1, )",
+ "resolved": "14.0.1",
+ "contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA=="
+ },
+ "DotNet.ReproducibleBuilds": {
+ "type": "Direct",
+ "requested": "[1.2.39, )",
+ "resolved": "1.2.39",
+ "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
+ },
+ "SixLabors.ImageSharp": {
+ "type": "Transitive",
+ "resolved": "3.1.12",
+ "contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
+ },
+ "kamitoolkit": {
+ "type": "Project",
+ "dependencies": {
+ "SixLabors.ImageSharp": "[3.1.12, )"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/KamiToolKit b/KamiToolKit
new file mode 160000
index 0000000..44ac1e0
--- /dev/null
+++ b/KamiToolKit
@@ -0,0 +1 @@
+Subproject commit 44ac1e0c3ae2bf6fb81870ced7c52dd7fb4e38c1