Initial project

This commit is contained in:
Zeffuro
2025-12-20 04:04:31 +01:00
parent e59da8ab0b
commit 659c295c16
26 changed files with 923 additions and 14 deletions
+1
View File
@@ -1,5 +1,6 @@
bin/ bin/
obj/ obj/
.idea/
/packages/ /packages/
riderModule.iml riderModule.iml
/_ReSharper.Caches/ /_ReSharper.Caches/
+3
View File
@@ -0,0 +1,3 @@
[submodule "KamiToolKit"]
path = KamiToolKit
url = https://github.com/MidoriKami/KamiToolKit
+6
View File
@@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AetherBags", "AetherBags\AetherBags.csproj", "{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AetherBags", "AetherBags\AetherBags.csproj", "{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiToolKit", "KamiToolKit\KamiToolKit.csproj", "{0907374F-93F8-427F-AD0A-49DB4B0A3DD4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64 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}.Debug|x64.Build.0 = Debug|x64
{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Release|x64.ActiveCfg = Release|x64 {5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Release|x64.ActiveCfg = Release|x64
{5BBE4215-8189-4A8A-AFD0-C5C6074DB47E}.Release|x64.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal
+2
View File
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
+1
View File
@@ -0,0 +1 @@
/.idea/
+86
View File
@@ -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);
}
}
+20 -1
View File
@@ -1,5 +1,24 @@
<Project Sdk="Dalamud.NET.Sdk/12.0.2"> <Project Sdk="Dalamud.NET.Sdk/14.0.1">
<PropertyGroup> <PropertyGroup>
<Version>1.0.0.0</Version> <Version>1.0.0.0</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<Author>Zeffuro</Author>
<Name>AetherBags</Name>
<InternalName>AetherBags</InternalName>
<Punchline>Never think too hard about your bags again!</Punchline>
<Description>This plugin replaces your inventory with it's own categorified inventory addon.</Description>
<RepoUrl>https://github.com/Zeffuro/AetherBags</RepoUrl>
<Tags>ui</Tags>
<AcceptsFeedback>true</AcceptsFeedback>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\KamiToolKit\KamiToolKit.csproj"/>
</ItemGroup>
<ItemGroup>
<Content Include=".gitignore" />
</ItemGroup>
</Project> </Project>
-6
View File
@@ -1,6 +0,0 @@
{
"Name": "AetherBags",
"Author": "",
"Description": "",
"Punchline": ""
}
@@ -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);
}
}
}
}
@@ -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
);
}
}
}
@@ -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<Item>().TryGetRow(baseItemId, out var baseItem)) {
return baseItem;
}
return null;
}
private EventItem? GetEventItem() {
var baseItemId = item.GetBaseItemId();
if (ItemUtil.IsEventItem(baseItemId) &&
Services.DataManager.GetExcelSheet<EventItem>().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<EventItem>().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<Item>().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);
}
}
}
+18
View File
@@ -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,
};
}
}
@@ -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);
}
}
+1
View File
@@ -0,0 +1 @@
global using KamiToolKit.Extensions;
+124
View File
@@ -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<byte>(), 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);
}
}
+11
View File
@@ -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;
}
+63
View File
@@ -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<InventoryType> 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<InventoryType> inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type);
public static List<ItemInfo> GetInventoryItems() {
List<InventoryType> inventories = [ InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 ];
List<InventoryItem> 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<ItemInfo> 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;
}
}
+60
View File
@@ -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<ItemInfo> {
public required InventoryItem Item { get; set; }
public required int ItemCount { get; set; }
private Item ItemData => Services.DataManager.GetExcelSheet<Item>().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
}
@@ -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<NodeBase> { }
public class HybridDirectionalFlexNode<T> : 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++;
}
}
}
}
+90
View File
@@ -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<DragDropNode> _itemGridNode;
public InventoryCategoryNode()
{
_categoryNameTextNode = new TextNode
{
Size = new Vector2(100, 14),
AlignmentType = AlignmentType.Left
};
_categoryNameTextNode.AttachNode(this);
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
{
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<ItemInfo> 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;
}
}
+54
View File
@@ -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();
}
}
+55 -5
View File
@@ -1,27 +1,77 @@
using System.Numerics;
using AetherBags.Addons;
using AetherBags.Helpers;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using KamiToolKit;
namespace AetherBags; namespace AetherBags;
public class Plugin : IDalamudPlugin public class Plugin : IDalamudPlugin
{ {
public const string CommandName = "/aetherbags"; private static string HelpDescription => "Opens your inventory.";
public Plugin(IDalamudPluginInterface pluginInterface) public Plugin(IDalamudPluginInterface pluginInterface)
{ {
pluginInterface.Create<Services>(); pluginInterface.Create<Services>();
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() 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) private void OnCommand(string command, string args)
{ {
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
}
}
+6 -1
View File
@@ -6,6 +6,11 @@ namespace AetherBags;
public class Services 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 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!;
} }
+8
View File
@@ -0,0 +1,8 @@
using AetherBags.Addons;
namespace AetherBags;
public static class System
{
public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
}
+30
View File
@@ -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, )"
}
}
}
}
}
Submodule
+1
Submodule KamiToolKit added at 44ac1e0c3a