├── .gitignore ├── .idea ├── .idea.Mappy │ └── .idea │ │ ├── encodings.xml │ │ ├── indexLayout.xml │ │ ├── vcs.xml │ │ └── .gitignore └── .idea.SortaKinda │ └── .idea │ ├── encodings.xml │ ├── indexLayout.xml │ ├── vcs.xml │ └── .gitignore ├── .gitmodules ├── SortaKinda ├── ViewComponents │ ├── IRuleConfigurationTab.cs │ ├── IOneColumnRuleConfigurationTab.cs │ ├── IInventoryConfigurationTab.cs │ ├── ITwoColumnRuleConfigurationTab.cs │ ├── QuadInventoryView.cs │ ├── InventoryGridView.cs │ ├── InventorySlotView.cs │ ├── SortingRuleTooltipView.cs │ ├── ArmoryInventoryGridView.cs │ ├── SortControllerView.cs │ ├── SortingRuleListView.cs │ └── SortingRuleView.cs ├── SortaKinda.csproj ├── SortaKinda.json ├── Classes │ ├── SystemConfig.cs │ ├── InventoryGrid.cs │ ├── RangeFilter.cs │ ├── InventorySlot.cs │ ├── ToggleFilter.cs │ └── SortingRule.cs ├── System.cs ├── Service.cs ├── Addons │ ├── AddonControllers.cs │ ├── InventoryLargeController.cs │ ├── InventoryExpansionController.cs │ ├── ArmouryBoardController.cs │ └── InventoryController.cs ├── Modules │ ├── MainInventoryModule.cs │ ├── ArmoryInventoryModule.cs │ └── ModuleBase.cs ├── packages.lock.json ├── Controllers │ ├── SortingThreadController.cs │ ├── SortController.cs │ ├── AreaPaintController.cs │ ├── InventoryController.cs │ ├── ModuleController.cs │ └── InventorySorter.cs ├── Windows │ ├── ConfigurationWindow.cs │ ├── RuleConfigWindow.cs │ └── TutorialWindow.cs └── SortaKindaPlugin.cs ├── LICENSE ├── .github └── FUNDING.yml ├── SortaKinda.sln └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | bin/ 4 | *.user -------------------------------------------------------------------------------- /.idea/.idea.Mappy/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "KamiLib"] 2 | path = KamiLib 3 | url = https://github.com/MidoriKami/KamiLib.git 4 | [submodule "KamiToolKit"] 5 | path = KamiToolKit 6 | url = https://github.com/MidoriKami/KamiToolKit.git 7 | -------------------------------------------------------------------------------- /.idea/.idea.SortaKinda/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.Mappy/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.SortaKinda/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Mappy/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/IRuleConfigurationTab.cs: -------------------------------------------------------------------------------- 1 | using KamiLib.Classes; 2 | 3 | namespace SortaKinda.ViewComponents; 4 | 5 | public interface IRuleConfigurationTab : ITabItem { 6 | void ITabItem.Draw() 7 | => DrawConfigurationTab(); 8 | 9 | void DrawConfigurationTab(); 10 | } -------------------------------------------------------------------------------- /.idea/.idea.SortaKinda/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Mappy/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /modules.xml 6 | /projectSettingsUpdater.xml 7 | /.idea.Mappy.iml 8 | /contentModel.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.SortaKinda/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /projectSettingsUpdater.xml 6 | /modules.xml 7 | /.idea.SortaKinda.iml 8 | /contentModel.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /SortaKinda/SortaKinda.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://github.com/MidoriKami/SortaKinda.git 4 | 2.1.1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SortaKinda/SortaKinda.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "MidoriKami", 3 | "Name": "SortaKinda", 4 | "Punchline": "Total customization of your inventory", 5 | "Description": "Sort your inventory however you want and keep it sorted.", 6 | "Tags": [ 7 | "utility" 8 | ], 9 | "CategoryTags": [ 10 | "Utility", 11 | "Other" 12 | ], 13 | "InternalName": "sortakinda", 14 | "RepoUrl": "https://github.com/MidoriKami/SortaKinda", 15 | "AcceptsFeedback": false 16 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/SystemConfig.cs: -------------------------------------------------------------------------------- 1 | using KamiLib.Configuration; 2 | 3 | namespace SortaKinda.Classes; 4 | 5 | public class SystemConfig : CharacterConfiguration { 6 | public bool SortOnItemAdded = true; 7 | public bool SortOnItemRemoved = true; 8 | public bool SortOnItemChanged; 9 | public bool SortOnItemMoved; 10 | public bool SortOnItemMerged; 11 | public bool SortOnItemSplit; 12 | public bool SortOnZoneChange = true; 13 | public bool SortOnJobChange = true; 14 | public bool SortOnLogin = true; 15 | public bool ReorderUnsortedItems; 16 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/IOneColumnRuleConfigurationTab.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility.Raii; 2 | using ImGuiNET; 3 | 4 | namespace SortaKinda.ViewComponents; 5 | 6 | public interface IOneColumnRuleConfigurationTab : IRuleConfigurationTab { 7 | string FirstLabel { get; } 8 | 9 | void IRuleConfigurationTab.DrawConfigurationTab() { 10 | using var table = ImRaii.Table("##RuleConfigTable", 1, ImGuiTableFlags.SizingStretchSame, ImGui.GetContentRegionAvail()); 11 | 12 | ImGui.TableNextColumn(); 13 | ImGui.TextUnformatted(FirstLabel); 14 | ImGui.Separator(); 15 | 16 | ImGui.TableNextColumn(); 17 | DrawContents(); 18 | } 19 | 20 | void DrawContents(); 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 MidoriKami 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: MidoriKami 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /SortaKinda/System.cs: -------------------------------------------------------------------------------- 1 | using KamiLib.CommandManager; 2 | using KamiLib.Window; 3 | using KamiToolKit; 4 | using SortaKinda.Addons; 5 | using SortaKinda.Classes; 6 | using SortaKinda.Controllers; 7 | using SortaKinda.Windows; 8 | 9 | namespace SortaKinda; 10 | 11 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 12 | public static class System { 13 | public static ModuleController ModuleController { get; set; } 14 | public static SortController SortController { get; set; } 15 | public static SystemConfig SystemConfig { get; set; } 16 | public static SortingThreadController SortingThreadController { get; set; } 17 | public static CommandManager CommandManager { get; set; } 18 | public static WindowManager WindowManager { get; set; } 19 | public static NativeController NativeController { get; set; } 20 | public static AddonControllers AddonControllers { get; set; } 21 | } -------------------------------------------------------------------------------- /SortaKinda/Service.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.IoC; 2 | using Dalamud.Plugin; 3 | using Dalamud.Plugin.Services; 4 | 5 | namespace SortaKinda; 6 | 7 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 8 | public class Service { 9 | [PluginService] public static IDalamudPluginInterface PluginInterface { get; set; } 10 | [PluginService] public static IClientState ClientState { get; set; } 11 | [PluginService] public static IGameGui GameGui { get; set; } 12 | [PluginService] public static IFramework Framework { get; set; } 13 | [PluginService] public static IPluginLog Log { get; set; } 14 | [PluginService] public static ITextureProvider TextureProvider { get; set; } 15 | [PluginService] public static IGameInventory GameInventory { get; set; } 16 | [PluginService] public static IChatGui ChatGui { get; set; } 17 | [PluginService] public static IDataManager DataManager { get; set; } 18 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/IInventoryConfigurationTab.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility.Raii; 2 | using ImGuiNET; 3 | using KamiLib.Classes; 4 | 5 | namespace SortaKinda.ViewComponents; 6 | 7 | public interface IInventoryConfigurationTab : ITabItem { 8 | void ITabItem.Draw() { 9 | using var table = ImRaii.Table("##SortaKindaInventoryConfigTable", 2, ImGuiTableFlags.SizingStretchSame); 10 | if (!table) return; 11 | 12 | ImGui.TableNextColumn(); 13 | using (var configChild = ImRaii.Child("##ConfigChild", ImGui.GetContentRegionAvail() - ImGui.GetStyle().FramePadding)) { 14 | if (configChild) System.SortController.Draw(); 15 | } 16 | 17 | ImGui.TableNextColumn(); 18 | using (var inventoryChild = ImRaii.Child("##InventoryChild", ImGui.GetContentRegionAvail() - ImGui.GetStyle().FramePadding, false, ImGuiWindowFlags.NoMove)) { 19 | if (inventoryChild) DrawInventory(); 20 | } 21 | } 22 | 23 | void DrawInventory(); 24 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/ITwoColumnRuleConfigurationTab.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility.Raii; 2 | using ImGuiNET; 3 | 4 | namespace SortaKinda.ViewComponents; 5 | 6 | public interface ITwoColumnRuleConfigurationTab : IRuleConfigurationTab { 7 | string FirstLabel { get; } 8 | 9 | string SecondLabel { get; } 10 | 11 | void IRuleConfigurationTab.DrawConfigurationTab() { 12 | using var table = ImRaii.Table("##RuleConfigTable", 2, ImGuiTableFlags.SizingStretchSame | ImGuiTableFlags.BordersInnerV, ImGui.GetContentRegionAvail()); 13 | if (!table) return; 14 | 15 | ImGui.TableNextColumn(); 16 | ImGui.TextUnformatted(FirstLabel); 17 | ImGui.Separator(); 18 | 19 | ImGui.TableNextColumn(); 20 | ImGui.TextUnformatted(SecondLabel); 21 | ImGui.Separator(); 22 | 23 | ImGui.TableNextColumn(); 24 | DrawLeftSideContents(); 25 | 26 | ImGui.TableNextColumn(); 27 | DrawRightSideContents(); 28 | } 29 | 30 | void DrawLeftSideContents(); 31 | 32 | void DrawRightSideContents(); 33 | } -------------------------------------------------------------------------------- /SortaKinda/Addons/AddonControllers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SortaKinda.Addons; 4 | 5 | public class AddonControllers : IDisposable { 6 | 7 | private readonly InventoryExpansionController inventoryExpansionController; 8 | private readonly InventoryLargeController inventoryLargeController; 9 | private readonly InventoryController inventoryController; 10 | private readonly ArmouryBoardController armouryBoardController; 11 | 12 | public AddonControllers() { 13 | inventoryExpansionController = new InventoryExpansionController(); 14 | inventoryLargeController = new InventoryLargeController(); 15 | inventoryController = new InventoryController(); 16 | armouryBoardController = new ArmouryBoardController(); 17 | 18 | inventoryExpansionController.Enable(); 19 | inventoryLargeController.Enable(); 20 | inventoryController.Enable(); 21 | armouryBoardController.Enable(); 22 | } 23 | 24 | public void Dispose() { 25 | inventoryExpansionController.Dispose(); 26 | inventoryLargeController.Dispose(); 27 | inventoryController.Dispose(); 28 | armouryBoardController.Dispose(); 29 | } 30 | } -------------------------------------------------------------------------------- /SortaKinda/Addons/InventoryLargeController.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Game.Addon.Events; 3 | using FFXIVClientStructs.FFXIV.Client.UI; 4 | using KamiToolKit; 5 | using KamiToolKit.Classes; 6 | using KamiToolKit.Nodes; 7 | 8 | namespace SortaKinda.Addons; 9 | 10 | public unsafe class InventoryLargeController() : AddonController(Service.PluginInterface, "InventoryLarge") { 11 | 12 | private TextButton? sortButton; 13 | 14 | protected override void AttachNodes(AddonInventoryExpansion* addon) { 15 | var targetNode = addon->RootNode; 16 | if (targetNode is null) return; 17 | 18 | sortButton = new TextButton { 19 | Label = "Sort", 20 | Size = new Vector2(100.0f, 28.0f), 21 | Position = new Vector2(19.0f, 412.0f), 22 | Tooltip = "SortaKinda: Sort all Inventories", 23 | IsVisible = true, 24 | }; 25 | 26 | sortButton.AddEvent(AddonEventType.MouseClick, System.ModuleController.Sort); 27 | 28 | System.NativeController.AttachToAddon(sortButton, addon, targetNode, NodePosition.AsLastChild); 29 | } 30 | 31 | protected override void DetachNodes(AddonInventoryExpansion* addon) { 32 | sortButton?.Dispose(); 33 | sortButton = null; 34 | } 35 | } -------------------------------------------------------------------------------- /SortaKinda/Addons/InventoryExpansionController.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Game.Addon.Events; 3 | using FFXIVClientStructs.FFXIV.Client.UI; 4 | using KamiToolKit; 5 | using KamiToolKit.Classes; 6 | using KamiToolKit.Nodes; 7 | 8 | namespace SortaKinda.Addons; 9 | 10 | public unsafe class InventoryExpansionController() : AddonController(Service.PluginInterface, "InventoryExpansion") { 11 | 12 | private TextButton? sortButton; 13 | 14 | protected override void AttachNodes(AddonInventoryExpansion* addon) { 15 | var targetNode = addon->RootNode; 16 | if (targetNode is null) return; 17 | 18 | sortButton = new TextButton { 19 | Label = "Sort", 20 | Size = new Vector2(100.0f, 28.0f), 21 | Position = new Vector2(19.0f, 742.0f), 22 | Tooltip = "SortaKinda: Sort all Inventories", 23 | IsVisible = true, 24 | }; 25 | 26 | sortButton.AddEvent(AddonEventType.MouseClick, System.ModuleController.Sort); 27 | 28 | System.NativeController.AttachToAddon(sortButton, addon, targetNode, NodePosition.AsLastChild); 29 | } 30 | 31 | protected override void DetachNodes(AddonInventoryExpansion* addon) { 32 | sortButton?.Dispose(); 33 | sortButton = null; 34 | } 35 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/InventoryGrid.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using FFXIVClientStructs.FFXIV.Client.Game; 4 | using SortaKinda.Controllers; 5 | using SortaKinda.Modules; 6 | 7 | namespace SortaKinda.Classes; 8 | 9 | public class InventoryGrid { 10 | public InventoryGrid(InventoryType type, InventoryConfig config) { 11 | Type = type; 12 | Config = config; 13 | Inventory = []; 14 | 15 | foreach (var index in Enumerable.Range(0, InventoryController.GetInventoryPageSize(Type))) { 16 | if (Config.SlotConfigs.Count <= index) { 17 | config.SlotConfigs.Add(new SlotConfig { 18 | RuleId = SortController.DefaultId, 19 | }); 20 | 21 | Service.Log.Info($"Detected inventory inventory size larger than current config for {type}, updating."); 22 | System.SortController.SaveConfig(); 23 | } 24 | 25 | Inventory.Add(new InventorySlot(Type, config.SlotConfigs[index], index)); 26 | } 27 | } 28 | 29 | public InventoryConfig Config { get; init; } 30 | 31 | public InventoryType Type { get; } 32 | 33 | public List Inventory { get; set; } 34 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/QuadInventoryView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using Dalamud.Interface.Utility; 4 | using SortaKinda.Classes; 5 | 6 | namespace SortaKinda.ViewComponents; 7 | 8 | public class QuadInventoryView { 9 | private readonly List views = []; 10 | 11 | public QuadInventoryView(IReadOnlyList inventoryGrids, Vector2 position) { 12 | var grid1 = new InventoryGridView(inventoryGrids[0], position + Vector2.Zero); 13 | var grid2 = new InventoryGridView(inventoryGrids[1], position + new Vector2(grid1.GetGridSize().X + GridSpacing.X, 0.0f)); 14 | var grid3 = new InventoryGridView(inventoryGrids[2], position + new Vector2(0.0f, grid1.GetGridSize().Y + GridSpacing.Y)); 15 | var grid4 = new InventoryGridView(inventoryGrids[3], position + new Vector2(grid1.GetGridSize().X + GridSpacing.X, grid2.GetGridSize().Y + GridSpacing.Y)); 16 | 17 | views.Add(grid1); 18 | views.Add(grid2); 19 | views.Add(grid3); 20 | views.Add(grid4); 21 | } 22 | 23 | private static Vector2 GridSpacing 24 | => ImGuiHelpers.ScaledVector2(8.0f, 8.0f); 25 | 26 | public void Draw() { 27 | foreach (var gridView in views) { 28 | gridView.Draw(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/RangeFilter.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility; 2 | using ImGuiNET; 3 | 4 | namespace SortaKinda.Classes; 5 | 6 | public class RangeFilter(string label, int minValue, int maxValue) { 7 | public bool Enable; 8 | public string Label = label; 9 | public int MaxValue = maxValue; 10 | public int MinValue = minValue; 11 | 12 | public void DrawConfig() { 13 | ImGui.TextUnformatted(Label); 14 | 15 | ImGui.Checkbox($"##Enable{Label}", ref Enable); 16 | ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X * 2.0f); 17 | 18 | ImGui.PushItemWidth(ImGui.GetContentRegionMax().X / 3.0f); 19 | ImGui.InputInt($"##Minimum{Label}", ref MinValue, 0, 0); 20 | 21 | ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); 22 | ImGui.PushItemWidth(ImGui.GetContentRegionMax().X / 3.0f); 23 | ImGui.InputInt($"##Maximum{Label}", ref MaxValue, 0, 0); 24 | 25 | ImGuiHelpers.ScaledDummy(8.0f); 26 | } 27 | 28 | public bool IsItemSlotAllowed(uint? itemSlotValue) 29 | => IsItemSlotAllowed((int?) itemSlotValue); 30 | 31 | private bool IsItemSlotAllowed(int? itemSlotValue) { 32 | if (itemSlotValue is null) return false; 33 | if (itemSlotValue < MinValue) return false; 34 | if (itemSlotValue > MaxValue) return false; 35 | 36 | return true; 37 | } 38 | } -------------------------------------------------------------------------------- /SortaKinda/Addons/ArmouryBoardController.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Game.Addon.Events; 3 | using FFXIVClientStructs.FFXIV.Client.UI; 4 | using KamiToolKit; 5 | using KamiToolKit.Classes; 6 | using KamiToolKit.Nodes; 7 | 8 | namespace SortaKinda.Addons; 9 | 10 | public unsafe class ArmouryBoardController() : AddonController(Service.PluginInterface, "ArmouryBoard") { 11 | 12 | private TextButton? sortButton; 13 | 14 | protected override void AttachNodes(AddonInventoryExpansion* addon) { 15 | var targetNode = addon->RootNode; 16 | if (targetNode is null) return; 17 | 18 | var inventoryButton = addon->GetNodeById(123); 19 | if (inventoryButton is not null) { 20 | inventoryButton->SetXFloat(141.0f); 21 | } 22 | 23 | sortButton = new TextButton { 24 | Label = "Sort", 25 | Size = new Vector2(100.0f, 28.0f), 26 | Position = new Vector2(19.0f, 566.0f), 27 | Tooltip = "SortaKinda: Sort all Inventories", 28 | IsVisible = true, 29 | }; 30 | 31 | sortButton.AddEvent(AddonEventType.MouseClick, System.ModuleController.Sort); 32 | 33 | System.NativeController.AttachToAddon(sortButton, addon, targetNode, NodePosition.AsLastChild); 34 | } 35 | 36 | protected override void DetachNodes(AddonInventoryExpansion* addon) { 37 | var inventoryButton = addon->GetNodeById(17); 38 | if (inventoryButton is not null) { 39 | inventoryButton->SetXFloat(80.0f); 40 | } 41 | 42 | sortButton?.Dispose(); 43 | sortButton = null; 44 | } 45 | } -------------------------------------------------------------------------------- /SortaKinda/Addons/InventoryController.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Game.Addon.Events; 3 | using FFXIVClientStructs.FFXIV.Client.UI; 4 | using KamiToolKit; 5 | using KamiToolKit.Classes; 6 | using KamiToolKit.Nodes; 7 | 8 | namespace SortaKinda.Addons; 9 | 10 | public unsafe class InventoryController() : AddonController(Service.PluginInterface, "Inventory") { 11 | 12 | private CircleButton? sortButton; 13 | 14 | protected override void AttachNodes(AddonInventoryExpansion* addon) { 15 | var targetNode = addon->RootNode; 16 | if (targetNode is null) return; 17 | 18 | var armoryButton = addon->GetNodeById(17); 19 | var saddlebagButton = addon->GetNodeById(16); 20 | if (armoryButton is not null && saddlebagButton is not null) { 21 | armoryButton->SetXFloat(49.0f); 22 | saddlebagButton->SetXFloat(49.0f); 23 | } 24 | 25 | sortButton = new CircleButton { 26 | Size = new Vector2(28.0f, 28.0f), 27 | Position = new Vector2(19.0f, 414.0f), 28 | Tooltip = "SortaKinda: Sort all Inventories", 29 | IsVisible = true, 30 | Icon = ButtonIcon.Sort, 31 | }; 32 | 33 | sortButton.AddEvent(AddonEventType.MouseClick, System.ModuleController.Sort); 34 | 35 | System.NativeController.AttachToAddon(sortButton, addon, targetNode, NodePosition.AsLastChild); 36 | } 37 | 38 | protected override void DetachNodes(AddonInventoryExpansion* addon) { 39 | var armoryButton = addon->GetNodeById(17); 40 | var saddlebagButton = addon->GetNodeById(16); 41 | if (armoryButton is not null && saddlebagButton is not null) { 42 | armoryButton->SetXFloat(33.0f); 43 | saddlebagButton->SetXFloat(33.0f); 44 | } 45 | 46 | sortButton?.Dispose(); 47 | sortButton = null; 48 | } 49 | } -------------------------------------------------------------------------------- /SortaKinda/Modules/MainInventoryModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Numerics; 4 | using FFXIVClientStructs.FFXIV.Client.Game; 5 | using SortaKinda.Classes; 6 | using SortaKinda.Controllers; 7 | using SortaKinda.ViewComponents; 8 | 9 | namespace SortaKinda.Modules; 10 | 11 | public class MainInventoryConfig : IModuleConfig { 12 | public List InventoryConfigs { get; set; } = [ 13 | new(InventoryType.Inventory1), 14 | new(InventoryType.Inventory2), 15 | new(InventoryType.Inventory3), 16 | new(InventoryType.Inventory4), 17 | ]; 18 | } 19 | 20 | public class MainInventoryModule : ModuleBase { 21 | private QuadInventoryView? view; 22 | 23 | public override ModuleName ModuleName => ModuleName.MainInventory; 24 | 25 | protected override List Inventories { get; set; } = []; 26 | 27 | public override MainInventoryConfig ModuleConfig { get; set; } = new(); 28 | 29 | public override void Draw() { 30 | view?.Draw(); 31 | } 32 | 33 | protected override void LoadViews() { 34 | Inventories = []; 35 | foreach (var config in ModuleConfig.InventoryConfigs) { 36 | Inventories.Add(new InventoryGrid(config.Type, config)); 37 | } 38 | 39 | view = new QuadInventoryView(Inventories, Vector2.Zero); 40 | } 41 | 42 | protected override void Sort(params InventoryType[] inventoryTypes) { 43 | if (Inventories.SelectMany(inventory => inventory.Inventory).Any(slot => slot.Rule.Id is not SortController.DefaultId)) { 44 | System.SortingThreadController.AddSortingTask(InventoryType.Inventory1, Inventories.ToArray()); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/InventoryGridView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using Dalamud.Interface.Utility; 4 | using SortaKinda.Classes; 5 | 6 | namespace SortaKinda.ViewComponents; 7 | 8 | public class InventoryGridView { 9 | private const int ItemsPerRow = 5; 10 | 11 | private readonly List inventorySlots = []; 12 | 13 | public InventoryGridView(InventoryGrid grid, Vector2 position) { 14 | foreach (var inventorySlot in grid.Inventory) { 15 | inventorySlots.Add(new InventorySlotView(inventorySlot, position + GetDrawPositionForIndex(inventorySlot.Slot))); 16 | } 17 | } 18 | 19 | private static Vector2 ItemSpacing => ImGuiHelpers.ScaledVector2(5.0f, 5.0f); 20 | 21 | public void Draw() { 22 | foreach (var slot in inventorySlots) { 23 | slot.Draw(); 24 | } 25 | } 26 | 27 | private static Vector2 GetDrawPositionForIndex(int index) { 28 | var xPosition = index % ItemsPerRow; 29 | var yPosition = index / ItemsPerRow; 30 | 31 | var drawPositionX = xPosition * (InventorySlotView.ItemSize.X + ItemSpacing.X); 32 | var drawPositionY = yPosition * (InventorySlotView.ItemSize.Y + ItemSpacing.Y); 33 | 34 | return new Vector2(drawPositionX, drawPositionY); 35 | } 36 | 37 | public Vector2 GetGridSize() { 38 | var rowCount = inventorySlots.Count / 5; 39 | 40 | var xSize = ItemsPerRow * (InventorySlotView.ItemSize.X + ItemSpacing.X); 41 | var ySize = rowCount * (InventorySlotView.ItemSize.Y + ItemSpacing.Y); 42 | 43 | return new Vector2(xSize, ySize); 44 | } 45 | 46 | public static float GetGridWidth() 47 | => InventorySlotView.ItemSize.X * ItemsPerRow + ItemSpacing.X * ( ItemsPerRow - 1 ); 48 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/InventorySlotView.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Numerics; 3 | using Dalamud.Interface.Utility; 4 | using ImGuiNET; 5 | using SortaKinda.Classes; 6 | using SortaKinda.Controllers; 7 | 8 | namespace SortaKinda.ViewComponents; 9 | 10 | public class InventorySlotView(InventorySlot slot, Vector2 position) { 11 | public static Vector2 ItemSize 12 | => ImGuiHelpers.ScaledVector2(35.0f, 35.0f); 13 | 14 | public void Draw() { 15 | DrawItem(); 16 | 17 | if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) slot.OnLeftClick(); 18 | if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) slot.OnRightClick(); 19 | if (ImGui.IsItemHovered()) slot.OnHover(); 20 | if (AreaPaintController.GetDragBounds().IntersectsWith(GetBounds(position, ItemSize))) slot.OnDragCollision(); 21 | 22 | DrawFrame(); 23 | } 24 | 25 | private void DrawItem() { 26 | ImGui.SetCursorPos(position); 27 | ImGui.Image(Service.TextureProvider.GetFromGameIcon((uint) slot.ExdItem.Icon).GetWrapOrEmpty().ImGuiHandle, ItemSize, Vector2.Zero, Vector2.One, Vector4.One with { W = 0.50f }); 28 | } 29 | 30 | private void DrawFrame() { 31 | var start = ImGui.GetWindowPos() + position; 32 | var stop = start + ItemSize; 33 | 34 | ImGui.GetWindowDrawList().AddRect(start, stop, ImGui.GetColorU32(slot.Rule.Color), 5.0f, ImDrawFlags.None, 2.0f); 35 | } 36 | 37 | private static Rectangle GetBounds(Vector2 drawPosition, Vector2 size) { 38 | var start = ImGui.GetWindowPos() + drawPosition; 39 | var stop = start + size; 40 | 41 | var startPoint = new Point((int) start.X, (int) start.Y); 42 | var stopPoint = new Point((int) stop.X, (int) stop.Y); 43 | var rectSize = new Size(stopPoint.X - startPoint.X, stopPoint.Y - startPoint.Y); 44 | 45 | return new Rectangle(startPoint, rectSize); 46 | } 47 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/InventorySlot.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json.Serialization; 3 | using FFXIVClientStructs.FFXIV.Client.Game; 4 | using FFXIVClientStructs.FFXIV.Client.UI.Misc; 5 | using Lumina.Excel.Sheets; 6 | using SortaKinda.Controllers; 7 | 8 | namespace SortaKinda.Classes; 9 | 10 | public class SlotConfig { 11 | [JsonIgnore] public bool Dirty; 12 | 13 | public string RuleId { get; set; } = SortController.DefaultId; 14 | } 15 | 16 | public unsafe class InventorySlot(InventoryType type, SlotConfig config, int index) { 17 | 18 | private InventoryType Type { get; } = type; 19 | 20 | public SlotConfig Config { get; init; } = config; 21 | 22 | [MemberNotNullWhen(true, "ExdItem")] 23 | public bool HasItem => InventoryItem->ItemId is not 0; 24 | 25 | public Item ExdItem => Service.DataManager.GetExcelSheet().GetRow(InventoryItem->ItemId); 26 | 27 | public InventoryItem* InventoryItem => InventoryController.GetItemForSlot(Type, Slot); 28 | 29 | public ItemOrderModuleSorterItemEntry* ItemOrderEntry => InventoryController.GetItemOrderData(Type, Slot); 30 | 31 | public SortingRule Rule { 32 | get { 33 | var sortControllerRule = System.SortController.GetRule(Config.RuleId); 34 | 35 | if (sortControllerRule.Id != Config.RuleId) { 36 | TryApplyRule(sortControllerRule.Id); 37 | } 38 | return sortControllerRule; 39 | } 40 | } 41 | 42 | public int Slot { get; init; } = index; 43 | 44 | public void OnLeftClick() => TryApplyRule(System.SortController.SelectedRule.Id); 45 | 46 | public void OnRightClick() => TryApplyRule(SortController.DefaultId); 47 | 48 | public void OnDragCollision() => TryApplyRule(System.SortController.SelectedRule.Id); 49 | 50 | public void OnHover() => Rule.ShowTooltip(); 51 | 52 | private void TryApplyRule(string id) { 53 | if (Config.RuleId != id) { 54 | Config.RuleId = id; 55 | Config.Dirty = true; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /SortaKinda/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net9.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[12.0.0, )", 8 | "resolved": "12.0.0", 9 | "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" 10 | }, 11 | "DotNet.ReproducibleBuilds": { 12 | "type": "Direct", 13 | "requested": "[1.2.25, )", 14 | "resolved": "1.2.25", 15 | "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" 16 | }, 17 | "HtmlAgilityPack": { 18 | "type": "Transitive", 19 | "resolved": "1.11.46", 20 | "contentHash": "dLMn4EVfJBHWmWK4Uh0XGD76FPLHI0qr2Tm0s1m/xmgiHb1JUb9zB8AzO8HtrkBBlMN6JfCUBYddhqC0hZNR+g==" 21 | }, 22 | "Karashiiro.HtmlAgilityPack.CssSelectors.NetCoreFork": { 23 | "type": "Transitive", 24 | "resolved": "0.0.2", 25 | "contentHash": "+7cqe/+tN7gDW36pgrefSeyAQvhqznM34D0uTwnETgU25iAC6boPtblxbrMluW5rA9o6MzjEg4qZK7WrnT+tSw==", 26 | "dependencies": { 27 | "HtmlAgilityPack": "1.11.46" 28 | } 29 | }, 30 | "NetStone": { 31 | "type": "Transitive", 32 | "resolved": "1.1.1", 33 | "contentHash": "7AWc3j6082Ut3xTbJTWBhsZC2Zd7hYVprQiNiei+FGxSSvGacv+xQMBpBIfAnQnnmsKnEQ47NExNgZpt6UcI4A==", 34 | "dependencies": { 35 | "HtmlAgilityPack": "1.11.46", 36 | "Karashiiro.HtmlAgilityPack.CssSelectors.NetCoreFork": "0.0.2", 37 | "Newtonsoft.Json": "13.0.3" 38 | } 39 | }, 40 | "Newtonsoft.Json": { 41 | "type": "Transitive", 42 | "resolved": "13.0.3", 43 | "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" 44 | }, 45 | "kamilib": { 46 | "type": "Project", 47 | "dependencies": { 48 | "NetStone": "[1.1.1, )" 49 | } 50 | }, 51 | "kamitoolkit": { 52 | "type": "Project" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /SortaKinda/Controllers/SortingThreadController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FFXIVClientStructs.FFXIV.Client.Game; 7 | using FFXIVClientStructs.FFXIV.Client.UI.Misc; 8 | using SortaKinda.Classes; 9 | 10 | namespace SortaKinda.Controllers; 11 | 12 | public unsafe class SortingThreadController : IDisposable { 13 | private readonly List sortingTasks = []; 14 | 15 | private bool SortPending => sortingTasks.Any(task => task.Status is TaskStatus.Created); 16 | 17 | private readonly CancellationTokenSource cancellationTokenSource = new(); 18 | 19 | public void Dispose() { 20 | cancellationTokenSource.Cancel(); 21 | } 22 | 23 | public void AddSortingTask(InventoryType type, params InventoryGrid[] grids) { 24 | var sortingTask = new Task(() => { 25 | InventorySorter.SortInventory(type, grids); 26 | }, cancellationTokenSource.Token); 27 | 28 | sortingTasks.Add(sortingTask); 29 | } 30 | 31 | public void Update() { 32 | if (SortPending) { 33 | Service.Log.Verbose($"Launching sorting tasks. {sortingTasks.Count(task => task.Status is TaskStatus.Created)} Tasks Pending."); 34 | 35 | foreach (var task in sortingTasks.Where(task => task.Status is TaskStatus.Created)) { 36 | Service.Log.Verbose("Starting Task"); 37 | task.Start(); 38 | } 39 | 40 | Service.Log.Verbose("Scheduling Continuation"); 41 | Task.WhenAll(sortingTasks).ContinueWith(_ => OnCompletion(), cancellationTokenSource.Token); 42 | } 43 | } 44 | 45 | private void OnCompletion() { 46 | Service.Log.Verbose("Continuing!"); 47 | 48 | Service.Framework.RunOnTick(() => { 49 | Service.Log.Debug("Marked ItemODR as changed."); 50 | 51 | ItemOrderModule.Instance()->UserFileEvent.HasChanges = true; 52 | }, delayTicks: 5); 53 | 54 | sortingTasks.RemoveAll(task => task.Status is TaskStatus.RanToCompletion or TaskStatus.Canceled or TaskStatus.Faulted); 55 | } 56 | } -------------------------------------------------------------------------------- /SortaKinda/Modules/ArmoryInventoryModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using FFXIVClientStructs.FFXIV.Client.Game; 4 | using SortaKinda.Classes; 5 | using SortaKinda.Controllers; 6 | using SortaKinda.ViewComponents; 7 | 8 | namespace SortaKinda.Modules; 9 | 10 | public class ArmoryConfig : IModuleConfig { 11 | public List InventoryConfigs { get; set; } = [ 12 | new(InventoryType.ArmoryMainHand), 13 | new(InventoryType.ArmoryOffHand), 14 | new(InventoryType.ArmoryHead), 15 | new(InventoryType.ArmoryBody), 16 | new(InventoryType.ArmoryHands), 17 | new(InventoryType.ArmoryLegs), 18 | new(InventoryType.ArmoryFeets), 19 | new(InventoryType.ArmoryEar), 20 | new(InventoryType.ArmoryNeck), 21 | new(InventoryType.ArmoryWrist), 22 | new(InventoryType.ArmoryRings), 23 | new(InventoryType.ArmorySoulCrystal), 24 | ]; 25 | } 26 | 27 | public class ArmoryInventoryModule : ModuleBase { 28 | protected override List Inventories { get; set; } = []; 29 | 30 | private ArmoryInventoryGridView? view; 31 | 32 | public override ModuleName ModuleName => ModuleName.ArmoryInventory; 33 | 34 | public override ArmoryConfig ModuleConfig { get; set; } = new(); 35 | 36 | protected override void LoadViews() { 37 | Inventories = []; 38 | foreach (var config in ModuleConfig.InventoryConfigs) { 39 | Inventories.Add(new InventoryGrid(config.Type, config)); 40 | } 41 | 42 | view = new ArmoryInventoryGridView(Inventories); 43 | } 44 | 45 | public override void Dispose() { 46 | view?.Dispose(); 47 | base.Dispose(); 48 | } 49 | 50 | public override void Draw() { 51 | view?.Draw(); 52 | } 53 | 54 | protected override void Sort(params InventoryType[] inventoryTypes) { 55 | foreach (var type in inventoryTypes) { 56 | if (Inventories.FirstOrDefault(inventory => inventory.Type == type) is { } targetInventory) { 57 | if (targetInventory.Inventory.Any(slot => slot.Rule.Id is not SortController.DefaultId)) { 58 | System.SortingThreadController.AddSortingTask(targetInventory.Type, targetInventory); 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /SortaKinda/Controllers/SortController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | using System.Linq; 4 | using Dalamud.Interface; 5 | using KamiLib.Configuration; 6 | using SortaKinda.Classes; 7 | using SortaKinda.ViewComponents; 8 | 9 | namespace SortaKinda.Controllers; 10 | 11 | public class SortingRuleConfig { 12 | public List Rules { get; set; } = [ 13 | new() { 14 | Color = KnownColor.White.Vector(), 15 | Id = SortController.DefaultId, 16 | Name = "Unsorted", 17 | }, 18 | ]; 19 | } 20 | 21 | public class SortController { 22 | public const string DefaultId = "Default"; 23 | 24 | public int SelectedRuleIndex = 0; 25 | 26 | public SortingRuleConfig RuleConfig { get; set; } = new(); 27 | 28 | public SortingRule SelectedRule 29 | => SelectedRuleIndex < RuleConfig.Rules.Count ? RuleConfig.Rules[SelectedRuleIndex] : DefaultRule; 30 | 31 | public SortControllerView? View { get; set; } 32 | 33 | private static SortingRule DefaultRule => new() { 34 | Id = DefaultId, 35 | Name = "Unsorted", 36 | Index = 0, 37 | Color = KnownColor.White.Vector() 38 | }; 39 | 40 | public List Rules 41 | => RuleConfig.Rules; 42 | 43 | public void SortAllInventories() 44 | => System.ModuleController.Sort(); 45 | 46 | public void Load() { 47 | RuleConfig = LoadConfig(); 48 | 49 | View = new SortControllerView(this); 50 | EnsureDefaultRule(); 51 | } 52 | 53 | public void Draw() => View?.Draw(); 54 | 55 | public SortingRule GetRule(string id) => RuleConfig.Rules.FirstOrDefault(rule => rule.Id == id) ?? RuleConfig.Rules[0]; 56 | 57 | private void EnsureDefaultRule() { 58 | if (RuleConfig.Rules.Count is 0) { 59 | RuleConfig.Rules.Add(DefaultRule); 60 | } 61 | 62 | if (RuleConfig.Rules[0] is not { Id: DefaultId, Name: "Unsorted", Index: 0 }) { 63 | RuleConfig.Rules[0] = DefaultRule; 64 | } 65 | } 66 | 67 | private SortingRuleConfig LoadConfig() 68 | => Service.PluginInterface.LoadCharacterFile(Service.ClientState.LocalContentId, "SortingRules.config.json", () => new SortingRuleConfig()); 69 | 70 | public void SaveConfig() 71 | => Service.PluginInterface.SaveCharacterFile(Service.ClientState.LocalContentId, "SortingRules.config.json", RuleConfig); 72 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/SortingRuleTooltipView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Linq; 4 | using Dalamud.Interface; 5 | using Dalamud.Utility; 6 | using ImGuiNET; 7 | using KamiLib.Extensions; 8 | using Lumina.Excel.Sheets; 9 | using SortaKinda.Classes; 10 | using SortaKinda.Controllers; 11 | 12 | namespace SortaKinda.ViewComponents; 13 | 14 | public class SortingRuleTooltipView(SortingRule sortingRule) { 15 | public void Draw() { 16 | ImGui.BeginTooltip(); 17 | 18 | var imGuiColor = sortingRule.Color; 19 | if (ImGui.ColorEdit4("##ColorTooltip", ref imGuiColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoPicker)) { 20 | sortingRule.Color = imGuiColor; 21 | } 22 | 23 | ImGui.SameLine(); 24 | ImGui.Text(sortingRule.Name); 25 | 26 | if (sortingRule.Id is not SortController.DefaultId) { 27 | var itemFiltersString = GetAllowedItemsString(); 28 | 29 | ImGui.TextColored(KnownColor.Gray.Vector(), itemFiltersString.IsNullOrEmpty() ? "Any Item" : itemFiltersString); 30 | ImGui.TextColored(KnownColor.Gray.Vector(), sortingRule.SortMode.GetDescription()); 31 | } 32 | 33 | ImGui.EndTooltip(); 34 | } 35 | 36 | private string GetAllowedItemsString() { 37 | var strings = new[] { 38 | sortingRule.AllowedItemTypes.Count > 0 ? string.Join(", ", sortingRule.AllowedItemTypes.Select(type => Service.DataManager.GetExcelSheet().GetRow(type).Name.ExtractText())) : string.Empty, 39 | sortingRule.AllowedNameRegexes.Count > 0 ? string.Join(", ", sortingRule.AllowedNameRegexes.Select(regex => @$"""{regex.Text}""")) : string.Empty, 40 | sortingRule.AllowedItemRarities.Count > 0 ? string.Join(", ", sortingRule.AllowedItemRarities.Select(rarity => rarity.GetDescription())) : string.Empty, 41 | sortingRule.ItemLevelFilter.Enable ? $"{sortingRule.ItemLevelFilter.MinValue} ilvl → {sortingRule.ItemLevelFilter.MaxValue} ilvl" : string.Empty, 42 | sortingRule.VendorPriceFilter.Enable ? $"{sortingRule.VendorPriceFilter.MinValue} gil → {sortingRule.VendorPriceFilter.MaxValue} gil" : string.Empty, 43 | sortingRule.LevelFilter.Enable ? $"{sortingRule.LevelFilter.MinValue} lvl → {sortingRule.LevelFilter.MaxValue} lvl" : string.Empty, 44 | }; 45 | 46 | return string.Join("\n", strings 47 | .Where(eachString => !eachString.IsNullOrEmpty()) 48 | .Select(eachString => eachString[..Math.Min(eachString.Length, 55)])); 49 | } 50 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/ToggleFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Dalamud.Interface.Utility; 4 | using Dalamud.Interface.Utility.Raii; 5 | using ImGuiNET; 6 | using KamiLib.Extensions; 7 | using Lumina.Excel.Sheets; 8 | 9 | namespace SortaKinda.Classes; 10 | 11 | public class ToggleFilter(PropertyFilter filter, ToggleFilterState state = ToggleFilterState.Ignored) { 12 | public ToggleFilterState State = state; 13 | public PropertyFilter Filter = filter; 14 | 15 | public void DrawConfig() { 16 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3.0f * ImGuiHelpers.GlobalScale); 17 | ImGui.TextUnformatted(Filter.GetDescription()); 18 | 19 | ImGui.SameLine(ImGui.GetContentRegionMax().X / 2.0f); 20 | 21 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3.0f * ImGuiHelpers.GlobalScale); 22 | ImGui.PushItemWidth(ImGui.GetContentRegionMax().X / 2.0f); 23 | using var combo = ImRaii.Combo($"##{Filter.ToString()}Combo", State.GetDescription()); 24 | if (!combo) return; 25 | 26 | foreach(var value in Enum.GetValues()) { 27 | if (ImGui.Selectable(value.GetDescription(), value == State)) { 28 | State = value; 29 | } 30 | } 31 | } 32 | 33 | public bool IsItemSlotAllowed(InventorySlot slot) => State switch { 34 | ToggleFilterState.Ignored => false, 35 | ToggleFilterState.Allow => ItemHasProperty(slot.ExdItem), 36 | ToggleFilterState.Disallow => !ItemHasProperty(slot.ExdItem), 37 | _ => true, 38 | }; 39 | 40 | private bool ItemHasProperty(Item item) => Filter switch { 41 | PropertyFilter.Collectable when item.IsCollectable => true, 42 | PropertyFilter.Dyeable when item.DyeCount > 0 => true, 43 | PropertyFilter.Unique when item.IsUnique => true, 44 | PropertyFilter.Untradable when item.IsUntradable => true, 45 | PropertyFilter.Repairable when item.ItemRepair.RowId is not 0 => true, 46 | _ => false, 47 | }; 48 | } 49 | 50 | public enum PropertyFilter { 51 | [Description("Untradable")] 52 | Untradable, 53 | 54 | [Description("Dyeable")] 55 | Dyeable, 56 | 57 | [Description("Unique")] 58 | Unique, 59 | 60 | [Description("Collectable")] 61 | Collectable, 62 | 63 | [Description("Repairable")] 64 | Repairable, 65 | } 66 | 67 | public enum ToggleFilterState { 68 | [Description("Ignored")] 69 | Ignored, 70 | 71 | [Description("Allow")] 72 | Allow, 73 | 74 | [Description("Disallow")] 75 | Disallow, 76 | } -------------------------------------------------------------------------------- /SortaKinda.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SortaKinda", "SortaKinda\SortaKinda.csproj", "{ED012F22-F994-4178-809F-7CDC44BFF840}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiLib", "KamiLib\KamiLib.csproj", "{C9A1A350-DB07-409B-9C69-9ECAF63C0EF7}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KamiToolKit", "KamiToolKit\KamiToolKit.csproj", "{92A59F4D-9B70-41C2-83BB-1F22AFC1F329}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|x64 = Debug|x64 15 | Release|x64 = Release|x64 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {ED012F22-F994-4178-809F-7CDC44BFF840}.Debug|x64.ActiveCfg = Debug|x64 19 | {ED012F22-F994-4178-809F-7CDC44BFF840}.Debug|x64.Build.0 = Debug|x64 20 | {ED012F22-F994-4178-809F-7CDC44BFF840}.Release|x64.ActiveCfg = Release|x64 21 | {ED012F22-F994-4178-809F-7CDC44BFF840}.Release|x64.Build.0 = Release|x64 22 | {BDF198BE-0703-408A-B179-B7E00C7B8663}.Debug|x64.ActiveCfg = Debug|x64 23 | {BDF198BE-0703-408A-B179-B7E00C7B8663}.Debug|x64.Build.0 = Debug|x64 24 | {BDF198BE-0703-408A-B179-B7E00C7B8663}.Release|x64.ActiveCfg = Release|x64 25 | {BDF198BE-0703-408A-B179-B7E00C7B8663}.Release|x64.Build.0 = Release|x64 26 | {B1015978-67C0-45C6-955F-018E4B270498}.Debug|x64.ActiveCfg = Debug|x64 27 | {B1015978-67C0-45C6-955F-018E4B270498}.Debug|x64.Build.0 = Debug|x64 28 | {B1015978-67C0-45C6-955F-018E4B270498}.Release|x64.ActiveCfg = Release|x64 29 | {B1015978-67C0-45C6-955F-018E4B270498}.Release|x64.Build.0 = Release|x64 30 | {C9A1A350-DB07-409B-9C69-9ECAF63C0EF7}.Debug|x64.ActiveCfg = Debug|x64 31 | {C9A1A350-DB07-409B-9C69-9ECAF63C0EF7}.Debug|x64.Build.0 = Debug|x64 32 | {C9A1A350-DB07-409B-9C69-9ECAF63C0EF7}.Release|x64.ActiveCfg = Release|x64 33 | {C9A1A350-DB07-409B-9C69-9ECAF63C0EF7}.Release|x64.Build.0 = Release|x64 34 | {92A59F4D-9B70-41C2-83BB-1F22AFC1F329}.Debug|x64.ActiveCfg = Debug|x64 35 | {92A59F4D-9B70-41C2-83BB-1F22AFC1F329}.Debug|x64.Build.0 = Debug|x64 36 | {92A59F4D-9B70-41C2-83BB-1F22AFC1F329}.Release|x64.ActiveCfg = Release|x64 37 | {92A59F4D-9B70-41C2-83BB-1F22AFC1F329}.Release|x64.Build.0 = Release|x64 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /SortaKinda/Controllers/AreaPaintController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Numerics; 4 | using ImGuiNET; 5 | using KamiLib.Classes; 6 | using SortaKinda.Windows; 7 | 8 | namespace SortaKinda.Controllers; 9 | 10 | public class AreaPaintController { 11 | private static bool dragStarted; 12 | private static Vector2 dragStartPosition = Vector2.Zero; 13 | private static Vector2 dragStopPosition = Vector2.Zero; 14 | private Vector2 lastWindowPosition = Vector2.Zero; 15 | 16 | public static Rectangle GetDragBounds() { 17 | if (dragStarted is false) return Rectangle.Empty; 18 | 19 | var startPoint = new Point((int) dragStartPosition.X, (int) dragStartPosition.Y); 20 | var stopPoint = new Point((int) dragStopPosition.X, (int) dragStopPosition.Y); 21 | var size = new Size(Math.Abs(stopPoint.X - startPoint.X), Math.Abs(stopPoint.Y - startPoint.Y)); 22 | 23 | var minX = Math.Min(startPoint.X, stopPoint.X); 24 | var minY = Math.Min(startPoint.Y, stopPoint.Y); 25 | 26 | return new Rectangle(new Point(minX, minY), size); 27 | } 28 | 29 | public void Draw() { 30 | if (!ShouldDraw()) { 31 | dragStarted = false; 32 | return; 33 | } 34 | 35 | if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !dragStarted) { 36 | dragStartPosition = ImGui.GetMousePos(); 37 | dragStopPosition = ImGui.GetMousePos(); 38 | dragStarted = true; 39 | } 40 | 41 | if (ImGui.IsMouseDragging(ImGuiMouseButton.Left) && dragStarted) { 42 | dragStopPosition = ImGui.GetMousePos(); 43 | 44 | if (System.SortController.SelectedRule is not { Color: var ruleColor }) return; 45 | ImGui.GetWindowDrawList().AddRect(dragStartPosition, dragStopPosition, ImGui.GetColorU32(ruleColor), 0.0f, ImDrawFlags.None, 3.0f); 46 | ImGui.GetWindowDrawList().AddRectFilled(dragStartPosition, dragStopPosition, ImGui.GetColorU32(ruleColor with { W = 0.33f })); 47 | } 48 | else if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { 49 | dragStarted = false; 50 | } 51 | } 52 | 53 | private bool ShouldDraw() { 54 | if (System.WindowManager.GetWindow() is not { IsFocused: true }) return false; 55 | if (!WindowBounds.IsCursorInWindow() || WindowBounds.IsCursorInWindowHeader()) return false; 56 | if (IsWindowMoving()) return false; 57 | 58 | return true; 59 | } 60 | 61 | private bool IsWindowMoving() { 62 | var currentPosition = ImGui.GetWindowPos(); 63 | var windowMoved = currentPosition != lastWindowPosition; 64 | 65 | lastWindowPosition = currentPosition; 66 | return windowMoved; 67 | } 68 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SortaKinda 2 | [![Download count](https://img.shields.io/endpoint?url=https://qzysathwfhebdai6xgauhz4q7m0mzmrf.lambda-url.us-east-1.on.aws/SortaKinda)](https://github.com/MidoriKami/SortaKinda) 3 | 4 | SortaKinda is a XivLauncher/Dalamud plugin. 5 | 6 | SortaKinda is an inventory management plugin, worry no more about having to constantly sort your inventory, or lamenting how terrible /isort actually is. 7 | 8 | With SortaKinda you define various sorting rules and item categories that fits your preferences, and then tell the plugin where you want those items to go, and in what order they should appear in. 9 | 10 | Each `Sorting Rule` is made of a `Filter` and a `Ordering`. 11 | 12 | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/0e5dc299-bd9c-41f3-b967-4613c617c8b9) 13 | 14 | ## Filters 15 | Currently you can define two kinds of filters, "Name" or "Type". 16 | 17 | Name filters use regular expressions to match items, `Prism` will match both `Clear Prism` and `Glamour Prism`. 18 | You can use more advanced regex strings to filter items more specifically `Crached (Anth|Dend)` to match `Cracked Anthocluster` and `Cracked Dendrocluster`. 19 | 20 | Type filters use the items type as the filter, all items display their item type below the tooltip icon. 21 | SortaKinda provides a search box for quick lookups, and a complete table for enabling/disabling several item types at once. 22 | 23 | | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/36c8399b-6edc-4cf3-92f6-391b80218daa) | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/3034ec9f-f438-428e-88a4-a8105f39e408) | 24 | |----------------|-----------| 25 | | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/7e161a51-7f4d-4f21-b2ca-9d3647c14bd7) | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/56243fb8-5636-4bc0-a3dd-e10597fd432c) | 26 | 27 | 28 | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/7b314475-3f25-4cf8-bc33-0db678472488) 29 | 30 | ## Ordering 31 | Currently you can select between one of five orderings, "Alphabetical", "Item Level", "Rarity", "Sell Price", or "Item Id" 32 | It's worth noting, that "Rarity" refers to the items color, ie Pink, Green, Blue, Purple. 33 | 34 | You can then choose to order them "Ascending", which puts the lower value first, or decending which puts the higher value first. 35 | Ex. Ascending: `ilvl 200` will be before `ilvl 500` 36 | Ex. Descending: `Butchers Knife` will be before `Augmented Cleaver` 37 | 38 | Finally you can choose the order that SortaKinda fills the selected inventory slots. 39 | 40 | Fill from Top, will place items starting with the first available inventory slot. 41 | Fill from Bottom, will place items starting with the last available inventory slot. 42 | 43 | ![image](https://github.com/MidoriKami/SortaKinda/assets/9083275/96def8a1-8966-443e-9ea0-cff03f4ea8cb) 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SortaKinda/Windows/ConfigurationWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Interface; 3 | using Dalamud.Interface.Style; 4 | using ImGuiNET; 5 | using KamiLib.Classes; 6 | using KamiLib.CommandManager; 7 | using KamiLib.Configuration; 8 | using KamiLib.Window; 9 | using SortaKinda.Controllers; 10 | using SortaKinda.Modules; 11 | using SortaKinda.ViewComponents; 12 | 13 | namespace SortaKinda.Windows; 14 | 15 | public class ConfigurationWindow : Window { 16 | private readonly AreaPaintController areaPaintController = new(); 17 | 18 | private readonly TabBar tabBar = new("SortaKindaConfigTabBar", [ 19 | new MainInventoryTab(), 20 | new ArmoryInventoryTab(), 21 | new GeneralConfigurationTab(), 22 | ]); 23 | 24 | public ConfigurationWindow() : base("SortaKinda - Configuration Window", new Vector2(840.0f, 636.0f), true) { 25 | Flags |= ImGuiWindowFlags.NoScrollbar; 26 | Flags |= ImGuiWindowFlags.NoScrollWithMouse; 27 | 28 | WindowFlags = WindowFlags.IsConfigWindow; 29 | 30 | TitleBarButtons.Add(new TitleBarButton { 31 | Icon = FontAwesomeIcon.Cog, 32 | ShowTooltip = () => ImGui.SetTooltip("Open Configuration Manager"), 33 | Click = _ => System.WindowManager.AddWindow(new ConfigurationManagerWindow(), WindowFlags.OpenImmediately), 34 | IconOffset = new Vector2(2.0f, 2.0f), 35 | }); 36 | 37 | System.CommandManager.RegisterCommand(new CommandHandler { 38 | Delegate = OpenConfigWindow, 39 | ActivationPath = "/", 40 | }); 41 | } 42 | 43 | public override bool DrawConditions() 44 | => Service.ClientState is { IsLoggedIn: true }; 45 | 46 | public override void PreDraw() 47 | => StyleModelV1.DalamudStandard.Push(); 48 | 49 | protected override void DrawContents() { 50 | tabBar.Draw(); 51 | areaPaintController.Draw(); 52 | } 53 | 54 | public override void PostDraw() 55 | => StyleModelV1.DalamudStandard.Pop(); 56 | 57 | private void OpenConfigWindow(params string[] args) { 58 | switch (Service.ClientState) { 59 | case { IsLoggedIn: false }: 60 | return; 61 | 62 | default: 63 | UnCollapseOrToggle(); 64 | break; 65 | } 66 | } 67 | } 68 | 69 | public class MainInventoryTab : IInventoryConfigurationTab { 70 | public string Name => "Main Inventory"; 71 | 72 | public bool Disabled => false; 73 | 74 | public void DrawInventory() 75 | => System.ModuleController.DrawModule(ModuleName.MainInventory); 76 | } 77 | 78 | public class ArmoryInventoryTab : IInventoryConfigurationTab { 79 | public string Name => "Armory Inventory"; 80 | 81 | public bool Disabled => false; 82 | 83 | public void DrawInventory() 84 | => System.ModuleController.DrawModule(ModuleName.ArmoryInventory); 85 | } 86 | 87 | public class GeneralConfigurationTab : ITabItem { 88 | public string Name => "General Settings"; 89 | 90 | public bool Disabled => false; 91 | 92 | public void Draw() 93 | => SortaKindaPlugin.DrawConfig(); 94 | } -------------------------------------------------------------------------------- /SortaKinda/Controllers/InventoryController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FFXIVClientStructs.FFXIV.Client.Game; 3 | using FFXIVClientStructs.FFXIV.Client.UI.Misc; 4 | 5 | namespace SortaKinda.Controllers; 6 | 7 | public static unsafe class InventoryController { 8 | public static int GetInventoryPageSize(InventoryType type) 9 | => GetInventorySorter(type)->ItemsPerPage; 10 | 11 | public static InventoryItem* GetItemForSlot(InventoryType type, int slot) 12 | => InventoryManager.Instance()->GetInventoryContainer(GetAdjustedInventoryType(type) + GetItemOrderData(type, slot)->Page)->GetInventorySlot(GetItemOrderData(type, slot)->Slot); 13 | 14 | public static ItemOrderModuleSorterItemEntry* GetItemOrderData(InventoryType type, int slot) 15 | => GetInventorySorter(type)->Items[slot + GetInventoryStartIndex(type)]; 16 | 17 | private static ItemOrderModuleSorter* GetInventorySorter(InventoryType type) => type switch { 18 | InventoryType.Inventory1 => ItemOrderModule.Instance()->InventorySorter, 19 | InventoryType.Inventory2 => ItemOrderModule.Instance()->InventorySorter, 20 | InventoryType.Inventory3 => ItemOrderModule.Instance()->InventorySorter, 21 | InventoryType.Inventory4 => ItemOrderModule.Instance()->InventorySorter, 22 | InventoryType.ArmoryMainHand => ItemOrderModule.Instance()->ArmouryMainHandSorter, 23 | InventoryType.ArmoryOffHand => ItemOrderModule.Instance()->ArmouryOffHandSorter, 24 | InventoryType.ArmoryHead => ItemOrderModule.Instance()->ArmouryHeadSorter, 25 | InventoryType.ArmoryBody => ItemOrderModule.Instance()->ArmouryBodySorter, 26 | InventoryType.ArmoryHands => ItemOrderModule.Instance()->ArmouryHandsSorter, 27 | InventoryType.ArmoryLegs => ItemOrderModule.Instance()->ArmouryLegsSorter, 28 | InventoryType.ArmoryFeets => ItemOrderModule.Instance()->ArmouryFeetSorter, 29 | InventoryType.ArmoryEar => ItemOrderModule.Instance()->ArmouryEarsSorter, 30 | InventoryType.ArmoryNeck => ItemOrderModule.Instance()->ArmouryNeckSorter, 31 | InventoryType.ArmoryWrist => ItemOrderModule.Instance()->ArmouryWristsSorter, 32 | InventoryType.ArmoryRings => ItemOrderModule.Instance()->ArmouryRingsSorter, 33 | InventoryType.ArmorySoulCrystal => ItemOrderModule.Instance()->ArmourySoulCrystalSorter, 34 | _ => throw new Exception($"Type Not Implemented: {type}"), 35 | }; 36 | 37 | private static InventoryType GetAdjustedInventoryType(InventoryType type) => type switch { 38 | InventoryType.Inventory1 => InventoryType.Inventory1, 39 | InventoryType.Inventory2 => InventoryType.Inventory1, 40 | InventoryType.Inventory3 => InventoryType.Inventory1, 41 | InventoryType.Inventory4 => InventoryType.Inventory1, 42 | _ => type, 43 | }; 44 | 45 | private static int GetInventoryStartIndex(InventoryType type) => type switch { 46 | InventoryType.Inventory2 => GetInventorySorter(type)->ItemsPerPage, 47 | InventoryType.Inventory3 => GetInventorySorter(type)->ItemsPerPage * 2, 48 | InventoryType.Inventory4 => GetInventorySorter(type)->ItemsPerPage * 3, 49 | _ => 0, 50 | }; 51 | } -------------------------------------------------------------------------------- /SortaKinda/Modules/ModuleBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using Dalamud.Interface.Utility; 6 | using FFXIVClientStructs.FFXIV.Client.Game; 7 | using KamiLib.Configuration; 8 | using SortaKinda.Classes; 9 | using SortaKinda.Controllers; 10 | 11 | namespace SortaKinda.Modules; 12 | 13 | public class InventoryConfig { 14 | public InventoryConfig(InventoryType type) { 15 | Type = type; 16 | SlotConfigs = []; 17 | foreach (var _ in Enumerable.Range(0, InventoryController.GetInventoryPageSize(type))) { 18 | SlotConfigs.Add(new SlotConfig { 19 | RuleId = SortController.DefaultId, 20 | }); 21 | } 22 | } 23 | 24 | public List SlotConfigs { get; set; } 25 | 26 | public InventoryType Type { get; set; } 27 | } 28 | 29 | public interface IModule : IDisposable { 30 | ModuleName ModuleName { get; } 31 | 32 | IEnumerable InventoryTypes { get; } 33 | 34 | void LoadModule(); 35 | 36 | void UnloadModule(); 37 | 38 | void UpdateModule(); 39 | 40 | void SortModule(); 41 | 42 | void Draw(); 43 | 44 | void InventoryChanged(params InventoryType[] changedInventories); 45 | 46 | IModuleConfig Config { get; set; } 47 | 48 | void Save(); 49 | } 50 | 51 | public interface IModuleConfig { 52 | List InventoryConfigs { get; set; } 53 | } 54 | 55 | public abstract class ModuleBase : IModule where T : IModuleConfig, new() { 56 | private bool IsLoaded { get; set; } 57 | 58 | private float lastScale; 59 | 60 | protected abstract List Inventories { get; set; } 61 | 62 | public IEnumerable InventoryTypes 63 | => Inventories.Select(inventory => inventory.Type); 64 | 65 | public abstract T ModuleConfig { get; set; } 66 | 67 | public IModuleConfig Config { 68 | get => ModuleConfig; 69 | set => ModuleConfig = (T) value; 70 | } 71 | 72 | public void Save() 73 | => SaveConfig(); 74 | 75 | public abstract ModuleName ModuleName { get; } 76 | 77 | public abstract void Draw(); 78 | 79 | protected abstract void LoadViews(); 80 | 81 | public virtual void Dispose() { } 82 | 83 | public void LoadModule() { 84 | Service.Log.Debug($"[{ModuleName}] Loading Module"); 85 | 86 | ModuleConfig = LoadConfig(); 87 | Load(); 88 | LoadViews(); 89 | IsLoaded = true; 90 | 91 | SaveConfig(); 92 | } 93 | 94 | public void UnloadModule() { 95 | IsLoaded = false; 96 | } 97 | 98 | public void UpdateModule() { 99 | if (!IsLoaded) return; 100 | 101 | var needsSaving = false; 102 | foreach (var inventory in ModuleConfig.InventoryConfigs) { 103 | foreach (var slot in inventory.SlotConfigs) { 104 | if (slot.Dirty) { 105 | needsSaving = true; 106 | slot.Dirty = false; 107 | } 108 | } 109 | } 110 | 111 | if (needsSaving) SaveConfig(); 112 | 113 | if (!ImGuiHelpers.GlobalScale.Equals(lastScale)) { 114 | LoadViews(); 115 | } 116 | lastScale = ImGuiHelpers.GlobalScale; 117 | } 118 | 119 | public void SortModule() { 120 | if (!IsLoaded) return; 121 | 122 | Sort(InventoryTypes.ToArray()); 123 | } 124 | 125 | protected virtual void Load() { } 126 | 127 | public void InventoryChanged(params InventoryType[] changedInventories) 128 | => Sort(changedInventories); 129 | 130 | 131 | protected abstract void Sort(params InventoryType[] inventories); 132 | 133 | private T LoadConfig() 134 | => Service.PluginInterface.LoadCharacterFile(Service.ClientState.LocalContentId, $"{ModuleName}.config.json", () => new T()); 135 | 136 | private void SaveConfig() 137 | => Service.PluginInterface.SaveCharacterFile(Service.ClientState.LocalContentId, $"{ModuleName}.config.json", ModuleConfig); 138 | } 139 | 140 | public enum ModuleName { 141 | [Description("MainInventory")] 142 | MainInventory, 143 | 144 | [Description("ArmoryInventory")] 145 | ArmoryInventory, 146 | } -------------------------------------------------------------------------------- /SortaKinda/Controllers/ModuleController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Dalamud.Game.Inventory; 5 | using Dalamud.Game.Inventory.InventoryEventArgTypes; 6 | using FFXIVClientStructs.FFXIV.Client.Game; 7 | using SortaKinda.Modules; 8 | 9 | namespace SortaKinda.Controllers; 10 | 11 | public class ModuleController : IDisposable { 12 | private readonly List modules = [ 13 | new MainInventoryModule(), 14 | new ArmoryInventoryModule(), 15 | ]; 16 | 17 | public void Dispose() { 18 | Unload(); 19 | 20 | foreach (var module in modules.OfType()) { 21 | module.Dispose(); 22 | } 23 | } 24 | 25 | public void Load() { 26 | foreach (var module in modules) { 27 | module.LoadModule(); 28 | } 29 | } 30 | 31 | public void Unload() { 32 | foreach (var module in modules) { 33 | module.UnloadModule(); 34 | } 35 | } 36 | 37 | public void Update() { 38 | foreach (var module in modules) { 39 | module.UpdateModule(); 40 | } 41 | } 42 | 43 | public void Sort() { 44 | foreach (var module in modules) { 45 | module.SortModule(); 46 | } 47 | } 48 | 49 | public IModule GetModule() where T : IModule 50 | => modules.OfType().First(); 51 | 52 | public void InventoryChanged(IReadOnlyCollection events) { 53 | if (!Service.ClientState.IsLoggedIn) return; 54 | 55 | foreach (var module in modules) { 56 | var inventoryTypes = new HashSet(); 57 | 58 | foreach (var itemEvent in events) { 59 | if (!IsEventAllowed(itemEvent)) continue; 60 | 61 | AddChangedInventories(itemEvent, inventoryTypes); 62 | 63 | inventoryTypes.RemoveWhere(type => !module.InventoryTypes.Contains(type)); 64 | } 65 | 66 | if (inventoryTypes.Count != 0) { 67 | module.InventoryChanged(inventoryTypes.ToArray()); 68 | } 69 | } 70 | } 71 | 72 | private static void AddChangedInventories(InventoryEventArgs itemEvent, ICollection inventoryTypes) { 73 | switch (itemEvent) { 74 | case null: 75 | break; 76 | 77 | case InventoryItemAddedArgs itemAdded: 78 | inventoryTypes.Add((InventoryType) itemAdded.Inventory); 79 | break; 80 | 81 | case InventoryItemRemovedArgs itemRemoved: 82 | inventoryTypes.Add((InventoryType) itemRemoved.Inventory); 83 | break; 84 | 85 | case InventoryItemChangedArgs itemChanged: 86 | inventoryTypes.Add((InventoryType) itemChanged.Inventory); 87 | break; 88 | 89 | case InventoryItemMergedArgs itemMerged: 90 | inventoryTypes.Add((InventoryType) itemMerged.SourceInventory); 91 | inventoryTypes.Add((InventoryType) itemMerged.TargetInventory); 92 | break; 93 | 94 | case InventoryItemMovedArgs itemMoved: 95 | inventoryTypes.Add((InventoryType) itemMoved.SourceInventory); 96 | inventoryTypes.Add((InventoryType) itemMoved.TargetInventory); 97 | break; 98 | 99 | case InventoryItemSplitArgs itemSplit: 100 | inventoryTypes.Add((InventoryType) itemSplit.SourceInventory); 101 | inventoryTypes.Add((InventoryType) itemSplit.TargetInventory); 102 | break; 103 | 104 | default: 105 | throw new ArgumentOutOfRangeException(nameof(itemEvent), itemEvent, null); 106 | } 107 | } 108 | 109 | public void DrawModule(ModuleName module) { 110 | modules.FirstOrDefault(drawableModule => drawableModule.ModuleName == module)?.Draw(); 111 | } 112 | 113 | private static bool IsEventAllowed(InventoryEventArgs argument) => argument.Type switch { 114 | GameInventoryEvent.Added when System.SystemConfig.SortOnItemAdded => true, 115 | GameInventoryEvent.Removed when System.SystemConfig.SortOnItemRemoved => true, 116 | GameInventoryEvent.Changed when System.SystemConfig.SortOnItemChanged => true, 117 | GameInventoryEvent.Moved when System.SystemConfig.SortOnItemMoved => true, 118 | GameInventoryEvent.Split when System.SystemConfig.SortOnItemSplit => true, 119 | GameInventoryEvent.Merged when System.SystemConfig.SortOnItemMerged => true, 120 | _ => false, 121 | }; 122 | } -------------------------------------------------------------------------------- /SortaKinda/Controllers/InventorySorter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using FFXIVClientStructs.FFXIV.Client.Game; 6 | using KamiLib.Classes; 7 | using SortaKinda.Classes; 8 | 9 | namespace SortaKinda.Controllers; 10 | 11 | public static unsafe class InventorySorter { 12 | private static void SwapItems(IReadOnlyList targetSlots, IReadOnlyList sourceSlots) { 13 | foreach (var index in Enumerable.Range(0, Math.Min(targetSlots.Count, sourceSlots.Count))) { 14 | SwapItem(targetSlots[index], sourceSlots[index]); 15 | } 16 | } 17 | 18 | private static void SwapItem(InventorySlot target, InventorySlot source) { 19 | var slotData = target.ItemOrderEntry; 20 | var itemData = source.ItemOrderEntry; 21 | 22 | (*slotData, *itemData) = (*itemData, *slotData); 23 | } 24 | 25 | public static void SortInventory(InventoryType type, params InventoryGrid[] grids) => HookSafety.ExecuteSafe(() => { 26 | var stopwatch = Stopwatch.StartNew(); 27 | Service.Log.Debug($"Sorting Inventory: {type}"); 28 | 29 | // Get all rules for this inventory for priority determinations 30 | var rulesForInventory = grids 31 | .SelectMany(grid => grid.Inventory) 32 | .Select(slots => slots.Rule) 33 | .Distinct() 34 | .ToArray(); 35 | 36 | // Step 1: Put all items that belong into a category into a category 37 | MoveItemsIntoCategories(grids, rulesForInventory); 38 | 39 | // Step 2: Remove items that don't belong in categories 40 | RemoveItemsFromCategories(grids); 41 | 42 | // Step 3: Sort remaining items in categories 43 | SortCategories(grids); 44 | 45 | Service.Log.Debug($"Sorted {type} in {stopwatch.Elapsed.TotalMilliseconds}ms"); 46 | }, Service.Log, $"Exception Caught During Sorting '{type}'"); 47 | 48 | private static void MoveItemsIntoCategories(InventoryGrid[] grids, IReadOnlyCollection rulesForInventory) { 49 | foreach (var rule in System.SortController.Rules) { 50 | if (rule.Id is SortController.DefaultId) continue; 51 | 52 | var higherPriorityRules = rulesForInventory.Where(otherRules => otherRules.Index > rule.Index).ToList(); 53 | 54 | // Get all items this rule applies to, and aren't already in any of the slots for that rule 55 | var itemSlotsForRule = grids 56 | .SelectMany(grid => grid.Inventory) 57 | .Where(slot => slot.HasItem) 58 | .Where(slot => !slot.Rule.Equals(rule)) 59 | .Where(slot => rule.IsItemSlotAllowed(slot)) 60 | .Where(slot => !higherPriorityRules.Any(otherRules => otherRules.IsItemSlotAllowed(slot))) 61 | .Order(rule) 62 | .ToArray(); 63 | 64 | // Get all target slots this rule applies to, that doesn't have an item that's supposed to be there 65 | var targetSlotsForRule = grids 66 | .SelectMany(grid => grid.Inventory) 67 | .Where(slot => slot.Rule.Equals(rule)) 68 | .Where(slot => !rule.IsItemSlotAllowed(slot)) 69 | .ToArray(); 70 | 71 | SwapItems(targetSlotsForRule, itemSlotsForRule); 72 | } 73 | } 74 | 75 | private static void RemoveItemsFromCategories(InventoryGrid[] grids) { 76 | foreach (var rule in System.SortController.Rules) { 77 | if (rule.Id is SortController.DefaultId) continue; 78 | 79 | // Get all IInventorySlot's for this rule, where the item doesn't match the filter 80 | var inventorySlotsForRule = grids 81 | .SelectMany(grid => grid.Inventory) 82 | .Where(slot => slot.Rule.Equals(rule) && slot.HasItem) 83 | .Where(slot => !rule.IsItemSlotAllowed(slot)); 84 | 85 | // Get all empty unsorted InventorySlots 86 | var emptyInventorySlots = grids 87 | .SelectMany(grid => grid.Inventory) 88 | .Where(slot => slot.Rule.Id is SortController.DefaultId && !slot.HasItem); 89 | 90 | // Perform the Sort 91 | SwapItems(emptyInventorySlots.ToList(), inventorySlotsForRule.ToList()); 92 | } 93 | } 94 | 95 | private static void SortCategories(InventoryGrid[] grids) { 96 | foreach (var rule in System.SortController.Rules) { 97 | if (rule.Id is SortController.DefaultId && !System.SystemConfig.ReorderUnsortedItems) continue; 98 | 99 | // Get all target slots this rule applies to 100 | var targetSlotsForRule = grids 101 | .SelectMany(grid => grid.Inventory) 102 | .Where(slot => slot.Rule.Equals(rule)) 103 | .ToList(); 104 | 105 | // Yup, that's a bubble sort. And not even the efficient kind of bubble sort. 106 | foreach (var _ in targetSlotsForRule) { 107 | foreach (var index in Enumerable.Range(0, targetSlotsForRule.Count - 1)) { 108 | if (rule.CompareSlots(targetSlotsForRule[index], targetSlotsForRule[index + 1])) { 109 | SwapItem(targetSlotsForRule[index], targetSlotsForRule[index + 1]); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /SortaKinda/Windows/RuleConfigWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | using System.Numerics; 4 | using Dalamud.Interface; 5 | using Dalamud.Interface.Style; 6 | using Dalamud.Interface.Utility; 7 | using Dalamud.Interface.Utility.Raii; 8 | using ImGuiNET; 9 | using SortaKinda.Classes; 10 | using SortaKinda.ViewComponents; 11 | using Window = KamiLib.Window.Window; 12 | 13 | namespace SortaKinda.Windows; 14 | 15 | public class RuleConfigWindow : Window { 16 | private readonly List ruleList; 17 | private readonly SortingRuleView view; 18 | private readonly SortingRule rule; 19 | private static Vector2 FooterSize => ImGuiHelpers.ScaledVector2(0.0f, 30.0f); 20 | 21 | public RuleConfigWindow(SortingRule sortingRule, List sortingRules) : base($"SortaKinda Rule Configuration - {sortingRule.Name}###RuleConfig{sortingRule.Id}", new Vector2(550.0f, 375.0f)) { 22 | rule = sortingRule; 23 | ruleList = sortingRules; 24 | view = new SortingRuleView(sortingRule); 25 | 26 | TitleBarButtons.Add(new TitleBarButton { 27 | Icon = FontAwesomeIcon.Cog, 28 | ShowTooltip = () => ImGui.SetTooltip("Additional Settings"), 29 | IconOffset = new Vector2(2.0f, 2.0f), 30 | Click = _ => ImGui.OpenPopup("Advanced Options"), 31 | }); 32 | 33 | Position = ImGui.GetMainViewport().Size / 2.0f - new Vector2(500.0f, 400.0f) / 2.0f; 34 | PositionCondition = ImGuiCond.Appearing; 35 | 36 | IsOpen = true; 37 | } 38 | 39 | public override void PreDraw() 40 | => StyleModelV1.DalamudStandard.Push(); 41 | 42 | protected override void DrawContents() { 43 | DrawHeader(); 44 | 45 | using (var child = ImRaii.Child("RuleConfigurationTabChild", ImGui.GetContentRegionAvail() - FooterSize - ImGui.GetStyle().FramePadding)) { 46 | if (child) { 47 | view.Draw(); 48 | } 49 | } 50 | 51 | DrawFooter(); 52 | 53 | DrawPopup(); 54 | } 55 | 56 | private void DrawPopup() { 57 | using var popup = ImRaii.Popup("Advanced Options"); 58 | if (!popup) return; 59 | 60 | if (ImGui.Checkbox("Use Inclusive Logic", ref rule.InclusiveAnd)) { 61 | System.SortController.SaveConfig(); 62 | } 63 | } 64 | 65 | public override void PostDraw() 66 | => StyleModelV1.DalamudStandard.Pop(); 67 | 68 | private void DrawHeader() { 69 | DrawColorEdit(); 70 | ImGui.SameLine(); 71 | DrawNameEdit(); 72 | ImGui.SameLine(); 73 | DrawDeleteButton(); 74 | ImGuiHelpers.ScaledDummy(5.0f); 75 | } 76 | 77 | private void DrawColorEdit() { 78 | ImGui.SetCursorPos(ImGui.GetCursorPos() with { X = ImGui.GetContentRegionMax().X / 4.0f - ImGuiHelpers.GlobalScale * 70.0f + ImGui.GetStyle().ItemSpacing.X / 2.0f }); 79 | var imGuiColor = rule.Color; 80 | if (ImGui.ColorEdit4("##ColorConfig", ref imGuiColor, ImGuiColorEditFlags.NoInputs)) { 81 | rule.Color = imGuiColor; 82 | } 83 | } 84 | 85 | private void DrawNameEdit() { 86 | ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X / 2.0f); 87 | var imGuiName = rule.Name; 88 | if (ImGui.InputText("##NameEdit", ref imGuiName, 1024, ImGuiInputTextFlags.AutoSelectAll)) { 89 | rule.Name = imGuiName; 90 | WindowName = $"SortaKinda Rule Configuration - {rule.Name}###RuleConfig{rule.Id}##SortaKinda"; 91 | } 92 | } 93 | 94 | private void DrawDeleteButton() { 95 | using var disabled = ImRaii.Disabled(!(ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl)); 96 | 97 | if (ImGui.Button("Delete", ImGuiHelpers.ScaledVector2(100.0f, 23.0f))) { 98 | ruleList.Remove(rule); 99 | IsOpen = false; 100 | } 101 | 102 | if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { 103 | ImGui.SetTooltip("Hold Shift + Control while clicking to delete this rule"); 104 | } 105 | } 106 | 107 | private void DrawFooter() { 108 | var buttonSize = ImGuiHelpers.ScaledVector2(100.0f, 23.0f); 109 | 110 | using var child = ImRaii.Child("##RuleConfigurationTabFooter", FooterSize - ImGui.GetStyle().FramePadding); 111 | if (!child) return; 112 | 113 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3.0f * ImGuiHelpers.GlobalScale); 114 | ImGui.TextColored(KnownColor.Gray.Vector(), rule.Id); 115 | 116 | ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonSize.X * 2.0f - ImGui.GetStyle().ItemSpacing.X); 117 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3.0f * ImGuiHelpers.GlobalScale); 118 | if (ImGui.Button("Save", buttonSize)) { 119 | System.ModuleController.Sort(); 120 | System.SortController.SaveConfig(); 121 | } 122 | 123 | ImGui.SameLine(); 124 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3.0f * ImGuiHelpers.GlobalScale); 125 | if (ImGui.Button("Save & Close", buttonSize)) { 126 | System.ModuleController.Sort(); 127 | System.SortController.SaveConfig(); 128 | Close(); 129 | } 130 | } 131 | 132 | public override void OnClose() { 133 | System.SortController.SaveConfig(); 134 | System.WindowManager.RemoveWindow(this); 135 | } 136 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/ArmoryInventoryGridView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Numerics; 5 | using Dalamud.Interface; 6 | using Dalamud.Interface.Textures.TextureWraps; 7 | using Dalamud.Interface.Utility; 8 | using FFXIVClientStructs.FFXIV.Client.Game; 9 | using ImGuiNET; 10 | using SortaKinda.Classes; 11 | 12 | namespace SortaKinda.ViewComponents; 13 | 14 | public class ArmoryInventoryGridView : IDisposable { 15 | private readonly UldWrapper armouryBoard; 16 | private readonly Dictionary leftTabTextures = new(); 17 | private readonly Dictionary rightTabTextures = new(); 18 | 19 | private readonly Dictionary views = new(); 20 | private InventoryType selectedTab = InventoryType.ArmoryMainHand; 21 | 22 | private static Vector2 ButtonSize 23 | => ImGuiHelpers.ScaledVector2(80.0f, 80.0f) * .75f; 24 | 25 | private static Vector2 ButtonPadding 26 | => ImGuiHelpers.ScaledVector2(5.0f, 5.0f); 27 | 28 | public ArmoryInventoryGridView(List armoryInventories) { 29 | var region = ImGuiHelpers.ScaledVector2(404, 565); 30 | var position = new Vector2(region.X / 2.0f - InventoryGridView.GetGridWidth() / 2.0f, region.Y / 8.0f); 31 | 32 | foreach (var inventory in armoryInventories) { 33 | views.Add(inventory.Type, new InventoryGridView(inventory, position)); 34 | } 35 | 36 | armouryBoard = Service.PluginInterface.UiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); 37 | 38 | leftTabTextures.Add(InventoryType.ArmoryMainHand, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); 39 | leftTabTextures.Add(InventoryType.ArmoryHead, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); 40 | leftTabTextures.Add(InventoryType.ArmoryBody, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); 41 | leftTabTextures.Add(InventoryType.ArmoryHands, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); 42 | leftTabTextures.Add(InventoryType.ArmoryLegs, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); 43 | leftTabTextures.Add(InventoryType.ArmoryFeets, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); 44 | 45 | rightTabTextures.Add(InventoryType.ArmoryOffHand, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); 46 | rightTabTextures.Add(InventoryType.ArmoryEar, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); 47 | rightTabTextures.Add(InventoryType.ArmoryNeck, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); 48 | rightTabTextures.Add(InventoryType.ArmoryWrist, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); 49 | rightTabTextures.Add(InventoryType.ArmoryRings, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); 50 | rightTabTextures.Add(InventoryType.ArmorySoulCrystal, armouryBoard.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 12)); 51 | } 52 | 53 | public void Dispose() { 54 | foreach (var loadedTexture in leftTabTextures) loadedTexture.Value?.Dispose(); 55 | foreach (var loadedTexture in rightTabTextures) loadedTexture.Value?.Dispose(); 56 | armouryBoard.Dispose(); 57 | } 58 | 59 | public void Draw() { 60 | views[selectedTab].Draw(); 61 | 62 | var region = ImGuiHelpers.ScaledVector2(404, 565); 63 | var gridWidth = InventoryGridView.GetGridWidth(); 64 | var position = new Vector2(region.X / 2.0f - gridWidth / 2.0f, region.Y / 8.0f); 65 | 66 | var leftOffset = ButtonSize.X + ButtonPadding.X; 67 | var leftBarPosition = position - new Vector2(leftOffset, 0.0f); 68 | DrawTabBar(leftTabTextures, leftBarPosition); 69 | 70 | var rightOffset = ButtonPadding.X; 71 | var rightBarPosition = position + new Vector2(gridWidth + rightOffset, 0.0f); 72 | DrawTabBar(rightTabTextures, rightBarPosition); 73 | } 74 | 75 | private void DrawTabBar(Dictionary textures, Vector2 drawPosition) { 76 | var itemSpacing = new Vector2(0.0f, ButtonSize.Y + ButtonPadding.Y); 77 | 78 | var index = 0; 79 | foreach (var (tab, texture) in textures) { 80 | if (texture is null) continue; 81 | var inactiveColor = Vector4.One with { W = 0.33f }; 82 | var activeColor = Vector4.One; 83 | 84 | if (tab == selectedTab) { 85 | var windowPosition = ImGui.GetWindowPos(); 86 | 87 | var borderColor = ImGui.GetColorU32(KnownColor.White.Vector()); 88 | var backgroundColor = ImGui.GetColorU32(KnownColor.Gray.Vector() with { W = 0.50f }); 89 | 90 | var rectStart = windowPosition + drawPosition + itemSpacing * index; 91 | var rectStop = rectStart + ButtonSize; 92 | 93 | ImGui.GetWindowDrawList().AddRectFilled(rectStart, rectStop, backgroundColor, 5.0f); 94 | ImGui.GetWindowDrawList().AddRect(rectStart, rectStop, borderColor, 5.0f); 95 | } 96 | 97 | ImGui.SetCursorPos(drawPosition + itemSpacing * index++); 98 | ImGui.Image(texture.ImGuiHandle, ButtonSize, Vector2.Zero, Vector2.One, selectedTab == tab ? activeColor : inactiveColor); 99 | 100 | if (ImGui.IsItemClicked()) { 101 | selectedTab = tab; 102 | } 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/SortControllerView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using Dalamud.Interface; 5 | using Dalamud.Interface.Utility; 6 | using Dalamud.Utility; 7 | using ImGuiNET; 8 | using KamiLib.Classes; 9 | using KamiLib.Window; 10 | using SortaKinda.Classes; 11 | using SortaKinda.Controllers; 12 | using SortaKinda.Modules; 13 | using SortaKinda.Windows; 14 | 15 | namespace SortaKinda.ViewComponents; 16 | 17 | public class SortControllerView(SortController sortingController) { 18 | private readonly SortingRuleListView listView = new(sortingController, sortingController.Rules); 19 | 20 | public void Draw() { 21 | DrawHeader(); 22 | DrawRules(); 23 | } 24 | 25 | private void DrawHeader() { 26 | var importExportButtonSize = ImGuiHelpers.ScaledVector2(23.0f, 23.0f); 27 | var sortButtonSize = ImGuiHelpers.ScaledVector2(100.0f, 23.0f); 28 | 29 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3.0f * ImGuiHelpers.GlobalScale); 30 | ImGui.TextUnformatted("Sorting Rules"); 31 | 32 | ImGui.SameLine(ImGui.GetContentRegionAvail().X - importExportButtonSize.X * 3.0f - sortButtonSize.X - ImGui.GetStyle().ItemSpacing.X * 3.0f); 33 | 34 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Question, "HelpButton", importExportButtonSize, "Open Help Window")) { 35 | System.WindowManager.AddWindow(new TutorialWindow(), WindowFlags.OpenImmediately); 36 | } 37 | 38 | ImGui.SameLine(); 39 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Clipboard, "ImportButton", importExportButtonSize, "Import rules from clipboard")) { 40 | ImportRules(); 41 | } 42 | 43 | ImGui.SameLine(); 44 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.ExternalLinkAlt, "ExportButton", importExportButtonSize, "Export rules to clipboard")) { 45 | ExportRules(); 46 | } 47 | 48 | ImGui.SameLine(); 49 | if (ImGui.Button("Sort All", sortButtonSize)) { 50 | sortingController.SortAllInventories(); 51 | } 52 | 53 | ImGui.Separator(); 54 | } 55 | 56 | private record ClipboardRules(SortingRule[] Rules, MainInventoryConfig MainInventory, ArmoryConfig Armory); 57 | 58 | private void ImportRules() { 59 | try { 60 | var decodedString = Convert.FromBase64String(ImGui.GetClipboardText()); 61 | var uncompressed = Util.DecompressString(decodedString); 62 | 63 | if (uncompressed.IsNullOrEmpty()) { 64 | Service.ChatGui.PrintError("Tried to import sorting rules, but got nothing, try copying the code again."); 65 | return; 66 | } 67 | 68 | if (JsonSerializer.Deserialize(uncompressed, SerializerOptions) is { } clipboardData) { 69 | if (clipboardData.Rules.Length is 0) { 70 | Service.ChatGui.PrintError("Tried to import sorting rules, but got nothing, try copying the code again."); 71 | return; 72 | } 73 | 74 | var addedCount = 0; 75 | foreach (var rule in clipboardData.Rules) { 76 | if (sortingController.Rules.All(existingRule => existingRule.Id != rule.Id)) { 77 | rule.Index = sortingController.Rules.Count; 78 | sortingController.Rules.Add(rule); 79 | addedCount++; 80 | } 81 | } 82 | 83 | Service.ChatGui.Print($"Received {clipboardData.Rules.Length} sorting rules from clipboard. ", "Import"); 84 | Service.ChatGui.Print($"Added {addedCount} new sorting rules.", "Import"); 85 | sortingController.SaveConfig(); 86 | 87 | var mainInventoryModule = System.ModuleController.GetModule(); 88 | mainInventoryModule.Config = clipboardData.MainInventory; 89 | mainInventoryModule.Save(); 90 | mainInventoryModule.LoadModule(); 91 | 92 | var armoryInventoryModule = System.ModuleController.GetModule(); 93 | armoryInventoryModule.Config = clipboardData.Armory; 94 | armoryInventoryModule.Save(); 95 | armoryInventoryModule.LoadModule(); 96 | } 97 | } 98 | catch (Exception e) { 99 | Service.ChatGui.PrintError("Something went wrong trying to import rules, check you copied the code correctly."); 100 | Service.Log.Error(e, "Handled exception while importing rules."); 101 | } 102 | } 103 | 104 | private static readonly JsonSerializerOptions SerializerOptions = new() { 105 | IncludeFields = true, 106 | }; 107 | 108 | private void ExportRules() { 109 | var data = new ClipboardRules( 110 | sortingController.Rules.ToArray()[1..], 111 | (MainInventoryConfig) System.ModuleController.GetModule().Config, 112 | (ArmoryConfig) System.ModuleController.GetModule().Config); 113 | 114 | var jsonString = JsonSerializer.Serialize(data, SerializerOptions); 115 | 116 | var compressed = Util.CompressString(jsonString); 117 | ImGui.SetClipboardText(Convert.ToBase64String(compressed)); 118 | 119 | Service.ChatGui.Print($"Exported {data.Rules.Length} rules to clipboard.", "Export"); 120 | } 121 | 122 | private void DrawRules() 123 | => listView.Draw(); 124 | } 125 | -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/SortingRuleListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Numerics; 5 | using Dalamud.Interface; 6 | using Dalamud.Interface.Components; 7 | using Dalamud.Interface.Utility; 8 | using Dalamud.Interface.Utility.Raii; 9 | using ImGuiNET; 10 | using KamiLib.Classes; 11 | using SortaKinda.Classes; 12 | using SortaKinda.Controllers; 13 | using SortaKinda.Windows; 14 | 15 | namespace SortaKinda.ViewComponents; 16 | 17 | public class SortingRuleListView(SortController sortController, List rules) { 18 | private int? deletionRuleId; 19 | 20 | public void Draw() { 21 | var region = ImGui.GetContentRegionAvail(); 22 | var negativeOffset = new Vector2(0.0f, 23.0f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y + 1.0f); 23 | deletionRuleId = null; 24 | 25 | using (var ruleList = ImRaii.Child("RuleListChild", region - negativeOffset)) { 26 | if (ruleList) { 27 | foreach (var index in Enumerable.Range(0, rules.Count)) { 28 | var rule = rules[index]; 29 | if (rule.Index != index) { 30 | rule.Index = index; 31 | sortController.SaveConfig(); 32 | } 33 | 34 | DrawRule(rule, index); 35 | } 36 | } 37 | } 38 | 39 | AddNewRuleButton(); 40 | 41 | if (deletionRuleId is { } ruleToDelete) { 42 | if (ruleToDelete == rules.Count) { 43 | System.SortController.SelectedRuleIndex = rules.Count - 1; 44 | } 45 | rules.RemoveAt(ruleToDelete); 46 | sortController.SaveConfig(); 47 | } 48 | } 49 | 50 | private void AddNewRuleButton() { 51 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Plus, "AddNewRuleButton", ImGui.GetContentRegionAvail() with { Y = 23.0f * ImGuiHelpers.GlobalScale })) { 52 | var newRule = new SortingRule { 53 | Color = GetRandomColor(), 54 | Id = Guid.NewGuid().ToString("N"), 55 | Name = "New Rule", 56 | Index = rules.Count 57 | }; 58 | 59 | rules.Add(newRule); 60 | sortController.SaveConfig(); 61 | 62 | System.WindowManager.AddWindow(new RuleConfigWindow(newRule, rules)); 63 | } 64 | } 65 | 66 | private void DrawRule(SortingRule rule, int index) { 67 | DrawArrows(index); 68 | DrawRadioButton(index); 69 | DrawRuleEntry(rule, index); 70 | } 71 | 72 | private void DrawArrows(int index) { 73 | var rule = rules[index]; 74 | 75 | using (var _ = ImRaii.Disabled(index is 0 || index == rules.Count - 1)) { 76 | if (ImGuiComponents.IconButton($"##DownButton{rule.Id}", FontAwesomeIcon.ArrowDown)) { 77 | if (rules.Count > 1) { 78 | rules.Remove(rule); 79 | rules.Insert(index + 1, rule); 80 | sortController.SaveConfig(); 81 | } 82 | } 83 | } 84 | 85 | ImGui.SameLine(); 86 | 87 | using (var _ = ImRaii.Disabled(index is 1 or 0)) { 88 | if (ImGuiComponents.IconButton($"##UpButton{rule.Id}", FontAwesomeIcon.ArrowUp)) { 89 | if (rules.Count > 1) { 90 | rules.Remove(rule); 91 | rules.Insert(index - 1, rule); 92 | sortController.SaveConfig(); 93 | } 94 | } 95 | } 96 | } 97 | 98 | private static void DrawRadioButton(int index) { 99 | ImGui.SameLine(); 100 | ImGui.RadioButton($"##Selected{index}", ref System.SortController.SelectedRuleIndex, index); 101 | } 102 | 103 | private void DrawRuleEntry(SortingRule rule, int index) { 104 | ImGui.SameLine(); 105 | 106 | var ruleColor = rule.Color; 107 | ImGui.ColorEdit4($"##{rule.Id}ColorEdit", ref ruleColor, ImGuiColorEditFlags.NoPicker | ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoTooltip); 108 | 109 | ImGui.SameLine(); 110 | ImGui.TextUnformatted(rule.Name); 111 | 112 | ImGui.BeginDisabled(rule.Id is SortController.DefaultId); 113 | DrawDeleteButton(index); 114 | ImGui.EndDisabled(); 115 | 116 | ImGui.BeginDisabled(rule.Id is SortController.DefaultId); 117 | DrawConfigButton(rule); 118 | ImGui.EndDisabled(); 119 | } 120 | 121 | private void DrawDeleteButton(int index) { 122 | var buttonSize = ImGuiHelpers.ScaledVector2(23.0f, 23.0f); 123 | 124 | ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonSize.X * 2.0f - ImGui.GetStyle().ItemSpacing.X); 125 | 126 | using (var _ = ImRaii.Disabled(!(ImGui.GetIO().KeyShift && ImGui.GetIO().KeyCtrl))) { 127 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Trash, index.ToString(), buttonSize)) { 128 | deletionRuleId = index; 129 | } 130 | } 131 | 132 | if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { 133 | ImGui.SetTooltip("Hold Shift + Control while clicking to delete this rule"); 134 | } 135 | } 136 | 137 | private void DrawConfigButton(SortingRule rule) { 138 | var buttonSize = ImGuiHelpers.ScaledVector2(23.0f, 23.0f); 139 | ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonSize.X); 140 | 141 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Cog, rule.Id, buttonSize)) { 142 | System.WindowManager.AddWindow(new RuleConfigWindow(rule, rules)); 143 | } 144 | } 145 | 146 | private static Vector4 GetRandomColor() { 147 | var random = new Random(); 148 | 149 | return new Vector4( 150 | random.Next(0, 255) / 255.0f, 151 | random.Next(0, 255) / 255.0f, 152 | random.Next(0, 255) / 255.0f, 153 | 1.0f); 154 | } 155 | } -------------------------------------------------------------------------------- /SortaKinda/SortaKindaPlugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Dalamud.Game.Inventory.InventoryEventArgTypes; 4 | using Dalamud.Plugin; 5 | using Dalamud.Plugin.Services; 6 | using ImGuiNET; 7 | using KamiLib.CommandManager; 8 | using KamiLib.Configuration; 9 | using KamiLib.Window; 10 | using KamiToolKit; 11 | using SortaKinda.Addons; 12 | using SortaKinda.Classes; 13 | using SortaKinda.Controllers; 14 | using SortaKinda.Windows; 15 | 16 | namespace SortaKinda; 17 | 18 | public sealed class SortaKindaPlugin : IDalamudPlugin { 19 | private uint lastJob = uint.MaxValue; 20 | private DateTime lastSortCommand = DateTime.MinValue; 21 | 22 | public SortaKindaPlugin(IDalamudPluginInterface pluginInterface) { 23 | pluginInterface.Create(); 24 | 25 | System.NativeController = new NativeController(Service.PluginInterface); 26 | System.SystemConfig = new SystemConfig(); 27 | System.SortingThreadController = new SortingThreadController(); 28 | System.SortController = new SortController(); 29 | System.ModuleController = new ModuleController(); 30 | System.CommandManager = new CommandManager(Service.PluginInterface, "sortakinda", "sorta"); 31 | System.WindowManager = new WindowManager(Service.PluginInterface); 32 | 33 | System.CommandManager.RegisterCommand(new CommandHandler { 34 | Delegate = SortCommand, 35 | ActivationPath = "/sort", 36 | }); 37 | 38 | System.WindowManager.AddWindow(new ConfigurationWindow(), WindowFlags.IsConfigWindow); 39 | 40 | if (Service.ClientState is { IsLoggedIn: true }) { 41 | Service.Framework.RunOnFrameworkThread(OnLogin); 42 | } 43 | 44 | System.AddonControllers = new AddonControllers(); 45 | 46 | Service.ClientState.Login += OnLogin; 47 | Service.ClientState.Logout += OnLogout; 48 | Service.Framework.Update += OnUpdate; 49 | Service.ClientState.TerritoryChanged += OnZoneChange; 50 | Service.GameInventory.InventoryChanged += OnInventoryChanged; 51 | } 52 | 53 | public void Dispose() { 54 | Service.ClientState.Login -= OnLogin; 55 | Service.ClientState.Logout -= OnLogout; 56 | Service.Framework.Update -= OnUpdate; 57 | Service.ClientState.TerritoryChanged -= OnZoneChange; 58 | Service.GameInventory.InventoryChanged -= OnInventoryChanged; 59 | 60 | System.WindowManager.Dispose(); 61 | System.ModuleController.Dispose(); 62 | System.SortingThreadController.Dispose(); 63 | System.CommandManager.Dispose(); 64 | System.NativeController.Dispose(); 65 | System.AddonControllers.Dispose(); 66 | } 67 | 68 | private void OnLogin() { 69 | System.SystemConfig = LoadConfig(); 70 | System.SystemConfig.UpdateCharacterData(); 71 | SaveConfig(); 72 | 73 | System.SortController.Load(); 74 | System.ModuleController.Load(); 75 | 76 | if (System.SystemConfig.SortOnLogin) System.ModuleController.Sort(); 77 | } 78 | 79 | private void OnLogout(int type, int code) { 80 | System.ModuleController.Unload(); 81 | lastJob = uint.MaxValue; 82 | } 83 | 84 | private void OnUpdate(IFramework framework) { 85 | if (Service.ClientState is { IsLoggedIn: false }) return; 86 | if (Service.ClientState is not { LocalPlayer.ClassJob.RowId: var classJobId }) return; 87 | 88 | // Don't update modules if the Retainer transfer window is open 89 | if (Service.GameGui.GetAddonByName("RetainerItemTransferProgress") != nint.Zero) return; 90 | 91 | System.ModuleController.Update(); 92 | 93 | // Prevent sorting on load, we have a different option for that 94 | if (lastJob is uint.MaxValue) lastJob = classJobId; 95 | 96 | if (System.SystemConfig.SortOnJobChange && lastJob != classJobId) { 97 | System.ModuleController.Sort(); 98 | lastJob = classJobId; 99 | } 100 | 101 | System.SortingThreadController.Update(); 102 | } 103 | 104 | private void OnZoneChange(ushort e) { 105 | if (Service.ClientState is { IsLoggedIn: false }) return; 106 | 107 | if (System.SystemConfig.SortOnZoneChange) System.ModuleController.Sort(); 108 | } 109 | 110 | private void OnInventoryChanged(IReadOnlyCollection events) 111 | => System.ModuleController.InventoryChanged(events); 112 | 113 | private void SortCommand(params string[] args) { 114 | var timeSinceLastSort = DateTime.UtcNow - lastSortCommand; 115 | 116 | if (timeSinceLastSort.TotalSeconds > 10) { 117 | System.ModuleController.Sort(); 118 | lastSortCommand = DateTime.UtcNow; 119 | } 120 | else { 121 | Service.ChatGui.PrintError($"Attempted to sort too soon after last sort. Try again in {10 - timeSinceLastSort.Seconds} seconds."); 122 | } 123 | } 124 | 125 | public static void DrawConfig() { 126 | var settingsChanged = ImGui.Checkbox("Sort on Item Added", ref System.SystemConfig.SortOnItemAdded); 127 | settingsChanged |= ImGui.Checkbox("Sort on Item Removed", ref System.SystemConfig.SortOnItemRemoved); 128 | settingsChanged |= ImGui.Checkbox("Sort on Item Changed", ref System.SystemConfig.SortOnItemChanged); 129 | settingsChanged |= ImGui.Checkbox("Sort on Item Moved", ref System.SystemConfig.SortOnItemMoved); 130 | settingsChanged |= ImGui.Checkbox("Sort on Item Merged", ref System.SystemConfig.SortOnItemMerged); 131 | settingsChanged |= ImGui.Checkbox("Sort on Item Split", ref System.SystemConfig.SortOnItemSplit); 132 | settingsChanged |= ImGui.Checkbox("Sort on Zone Change", ref System.SystemConfig.SortOnZoneChange); 133 | settingsChanged |= ImGui.Checkbox("Sort on Job Change", ref System.SystemConfig.SortOnJobChange); 134 | settingsChanged |= ImGui.Checkbox("Sort on Login", ref System.SystemConfig.SortOnLogin); 135 | settingsChanged |= ImGui.Checkbox("Reorder Unsorted Items", ref System.SystemConfig.ReorderUnsortedItems); 136 | 137 | if (settingsChanged) { 138 | SaveConfig(); 139 | } 140 | } 141 | 142 | private static SystemConfig LoadConfig() 143 | => Service.PluginInterface.LoadCharacterFile(Service.ClientState.LocalContentId, "System.config.json", () => new SystemConfig()); 144 | 145 | private static void SaveConfig() 146 | => Service.PluginInterface.SaveCharacterFile(Service.ClientState.LocalContentId, "System.config.json", System.SystemConfig); 147 | } -------------------------------------------------------------------------------- /SortaKinda/Windows/TutorialWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Interface.Utility; 3 | using ImGuiNET; 4 | using KamiLib.Classes; 5 | using KamiLib.Window; 6 | 7 | namespace SortaKinda.Windows; 8 | 9 | public class TutorialWindow : Window { 10 | private readonly TabBar tabBar; 11 | 12 | public TutorialWindow() : base("SortaKinda - Tutorial", new Vector2(640.0f, 425.0f)) { 13 | 14 | Flags |= ImGuiWindowFlags.NoScrollbar; 15 | Flags |= ImGuiWindowFlags.NoScrollWithMouse; 16 | 17 | tabBar = new TabBar("TutorialTabBar", [ 18 | new TutorialAboutTab(), 19 | new TutorialSortingRules(), 20 | new TutorialConfiguringInventory(), 21 | new TutorialAdvancedSorting() 22 | ]); 23 | } 24 | 25 | protected override void DrawContents() 26 | => tabBar.Draw(); 27 | 28 | public override void OnClose() 29 | => System.WindowManager.RemoveWindow(this); 30 | } 31 | 32 | public class TutorialAboutTab : ITabItem { 33 | public string Name => "About"; 34 | 35 | public bool Disabled => false; 36 | 37 | public void Draw() { 38 | ImGuiHelpers.ScaledDummy(10.0f); 39 | 40 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, 10.0f * ImGuiHelpers.GlobalScale)); 41 | 42 | ImGui.TextWrapped(AboutText); 43 | 44 | ImGui.PopStyleVar(); 45 | } 46 | 47 | private const string AboutText = "Welcome to SortaKinda! A highly customizable inventory management tool.\n" + 48 | "This plugin was designed for you to define precisely what items you want to always be in specific sections of your inventory.\n\n" + 49 | "SortaKinda has no relation to the built in 'isort' function. It does not interact with 'isort' in any way.\n" + 50 | "SortaKinda will override any other sorting systems you attempt to use.\n\n" + 51 | "Automatic sort triggers are available in the general settings tab, these triggers allow SortaKinda to automatically re-sort your inventory as you are playing.\n\n" + 52 | "There may be times where SortaKinda might not catch a change, worry not as triggers should be frequent enough to sort things out on the next change."; 53 | } 54 | 55 | public class TutorialSortingRules : ITabItem { 56 | public string Name => "Sorting Rules"; 57 | 58 | public bool Disabled => false; 59 | 60 | public void Draw() { 61 | ImGuiHelpers.ScaledDummy(10.0f); 62 | 63 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, 10.0f * ImGuiHelpers.GlobalScale)); 64 | 65 | ImGui.TextWrapped(SortingRulesHelp); 66 | 67 | ImGui.PopStyleVar(); 68 | } 69 | 70 | private const string SortingRulesHelp = "Sorting rules are definitions for what items you want to allow into specific inventory slots.\n" + 71 | "Rules are shown on the left side of the configuration window, new rules are added to the bottom of the list.\n\n" + 72 | "Rules can be deleted by pressing and holding Shift and Control at the same time, then clicking the trash icon. The 'Unsorted' rule can not be deleted.\n\n" + 73 | "If multiple types of filters are used in the same rule, then an item will only be allowed if all filter types permit it.\n\n" + 74 | "For example, if you set a rule with ItemType: Miscallany, and ItemRarity: Green, then only items that are both Miscellany AND Green rarity will be allowed in those slots.\n\n" + 75 | "Items that don't match any rules will be moved into the 'Unsorted' sections of your inventory. You must always have some inventory slots marked as 'Unsorted'.\n\n" + 76 | "If SortaKinda is unable to move an item out of a sorted section into a 'Unsorted' section, then it will act as if that item belongs in the sorted section and order it based on that sections rule."; 77 | } 78 | 79 | public class TutorialConfiguringInventory : ITabItem { 80 | public string Name => "Using Rules"; 81 | 82 | public bool Disabled => false; 83 | 84 | public void Draw() { 85 | ImGuiHelpers.ScaledDummy(10.0f); 86 | 87 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, 10.0f * ImGuiHelpers.GlobalScale)); 88 | 89 | ImGui.TextWrapped(UsingRulesHelp); 90 | 91 | ImGui.PopStyleVar(); 92 | } 93 | 94 | private const string UsingRulesHelp = "To use rules, you need to first select a rule on the left side of the configuration window.\n" + 95 | "The currently selected rule will have a filledin dot next to the color/name of the rule.\n\n" + 96 | "Once you have a rule selected you can 'paint' your inventory slots on the right side of the configuration window. Painted slots do not have to be adjacent to each other.\n\n" + 97 | "Whenever a sort is triggered, items that match the rules of a painted inventory slot will try to be moved into those inventory slots, and then re-ordered according to the rule's settings.\n" + 98 | "Items that don't match a rule but are in a rules inventory slots will be moved out into 'Unsorted' slots.\n\n" + 99 | "You must always have some inventory slots marked as 'Unsorted'."; 100 | } 101 | 102 | public class TutorialAdvancedSorting : ITabItem { 103 | public string Name => "Advanced Techniques"; 104 | 105 | public bool Disabled => false; 106 | 107 | public void Draw() { 108 | ImGuiHelpers.ScaledDummy(10.0f); 109 | 110 | ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, 10.0f * ImGuiHelpers.GlobalScale)); 111 | 112 | ImGui.TextWrapped(AdvancedTech); 113 | 114 | ImGui.PopStyleVar(); 115 | } 116 | 117 | private const string AdvancedTech = "SortaKinda evaluates sorting rules in a specific order, " + 118 | "this allows you to define rules in such a way that you can have items that match multiple rules always end up in one specific section of your inventory.\n\n" + 119 | "Rules are evaluated from the top of the list (where 'Unsorted' is), to the bottom of the list (where the add new rule button is), " + 120 | "If an item would be allowed by multiple rules, the rule lowest in the list will get the item in the end.\n\n" + 121 | "You can use this characteristic of SortaKinda to define generalized sorting rules at the top of the list, and more specific sorting rules at the bottom of the list, " + 122 | "any items that match the more specific rules at the bottom will have the items in the end.\n\n" + 123 | "In other words, you can consider the order the rules are in to be a soft-priority system, where the rules on the bottom are more important."; 124 | } -------------------------------------------------------------------------------- /SortaKinda/ViewComponents/SortingRuleView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using Dalamud.Interface; 4 | using Dalamud.Interface.Components; 5 | using Dalamud.Interface.Utility; 6 | using Dalamud.Interface.Utility.Raii; 7 | using Dalamud.Utility; 8 | using ImGuiNET; 9 | using KamiLib.Classes; 10 | using KamiLib.Extensions; 11 | using KamiLib.Window; 12 | using KamiLib.Window.SelectionWindows; 13 | using Lumina.Excel.Sheets; 14 | using SortaKinda.Classes; 15 | 16 | namespace SortaKinda.ViewComponents; 17 | 18 | public class SortingRuleView(SortingRule rule) { 19 | private readonly TabBar tabBar = new("SortingRuleTabBar", [ 20 | new ItemTypeFilterTab.ItemNameFilterTab(rule), 21 | new ItemTypeFilterTab(rule), 22 | new OtherFiltersTab(rule), 23 | new ToggleFiltersTab(rule), 24 | new SortOrderTab(rule), 25 | ], false); 26 | 27 | public void Draw() => tabBar.Draw(); 28 | } 29 | 30 | public class ItemTypeFilterTab(SortingRule rule) : IOneColumnRuleConfigurationTab { 31 | public string Name => "Item Type Filter"; 32 | 33 | public string FirstLabel => "Allowed Item Types"; 34 | 35 | public bool Disabled => false; 36 | 37 | public SortingRule SortingRule { get; } = rule; 38 | 39 | public void DrawContents() { 40 | DrawSelectedTypes(); 41 | 42 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Plus, "openItemTypeSelect", ImGui.GetContentRegionAvail())) { 43 | System.WindowManager.AddWindow(new ItemUiCategorySelectionWindow(Service.PluginInterface) { 44 | MultiSelectionCallback = selections => { 45 | foreach (var selected in selections) { 46 | SortingRule.AllowedItemTypes.Add(selected.RowId); 47 | } 48 | }, 49 | }, WindowFlags.OpenImmediately); 50 | } 51 | } 52 | 53 | public class ItemNameFilterTab(SortingRule rule) : IOneColumnRuleConfigurationTab { 54 | private UserRegex newRegex = new(); 55 | private bool setNameFocus = true; 56 | 57 | public string Name => "Item Name Filter"; 58 | 59 | public bool Disabled => false; 60 | 61 | public string FirstLabel => "Allowed Item Names"; 62 | 63 | public SortingRule SortingRule { get; } = rule; 64 | 65 | public void DrawContents() { 66 | DrawFilteredNames(); 67 | DrawAddItemNameInput(); 68 | } 69 | 70 | private void DrawFilteredNames() { 71 | UserRegex? removalRegex = null; 72 | 73 | using var child = ImRaii.Child("##NameFilterChild", ImGuiHelpers.ScaledVector2(0.0f, -50.0f)); 74 | if (!child) return; 75 | 76 | if (SortingRule.AllowedNameRegexes.Count is 0) { 77 | ImGui.TextColored(KnownColor.Orange.Vector(), "Nothing Filtered"); 78 | } 79 | 80 | foreach (var userRegex in SortingRule.AllowedNameRegexes) { 81 | if (ImGuiComponents.IconButton($"##RemoveNameRegex{userRegex.Text}", FontAwesomeIcon.Trash)) { 82 | removalRegex = userRegex; 83 | } 84 | ImGui.SameLine(); 85 | ImGui.TextUnformatted(userRegex.Text); 86 | } 87 | 88 | if (removalRegex is { } toRemoveRegex) { 89 | SortingRule.AllowedNameRegexes.Remove(toRemoveRegex); 90 | } 91 | } 92 | 93 | private void DrawAddItemNameInput() { 94 | var buttonSize = ImGuiHelpers.ScaledVector2(25.0f, 23.0f); 95 | 96 | if (setNameFocus || ImGui.IsWindowAppearing()) { 97 | ImGui.SetKeyboardFocusHere(); 98 | setNameFocus = false; 99 | } 100 | 101 | ImGui.TextColored(KnownColor.Gray.Vector(), "Supports Regex for item name filtering"); 102 | 103 | if (UserRegex.DrawRegexInput("##NewName", ref newRegex, "Item Name", null, ImGui.GetContentRegionAvail().X - buttonSize.X - ImGui.GetStyle().ItemSpacing.X, ImGui.GetColorU32(KnownColor.OrangeRed.Vector()))) { 104 | if (newRegex.Regex is not null) { 105 | SortingRule.AllowedNameRegexes.Add(newRegex); 106 | newRegex = new UserRegex(); 107 | } 108 | setNameFocus = true; 109 | } 110 | 111 | ImGui.SameLine(); 112 | 113 | using var disabled = ImRaii.Disabled(newRegex.Regex is null || newRegex.Text.IsNullOrEmpty()); 114 | if (ImGuiTweaks.IconButtonWithSize(Service.PluginInterface.UiBuilder.IconFontFixedWidthHandle, FontAwesomeIcon.Plus, "AddNameButton", buttonSize, "Add Name")) { 115 | if (newRegex.Regex is not null) { 116 | SortingRule.AllowedNameRegexes.Add(newRegex); 117 | newRegex = new UserRegex(); 118 | } 119 | } 120 | } 121 | } 122 | 123 | private void DrawSelectedTypes() { 124 | uint? removalEntry = null; 125 | 126 | using var itemFilterChild = ImRaii.Child("##ItemFilterChild", ImGuiHelpers.ScaledVector2(0.0f, -30.0f)); 127 | if (!itemFilterChild) return; 128 | 129 | if (SortingRule.AllowedItemTypes.Count is 0) { 130 | ImGui.TextColored(KnownColor.Orange.Vector(), "Nothing Filtered"); 131 | } 132 | 133 | foreach (var category in SortingRule.AllowedItemTypes) { 134 | if (Service.DataManager.GetExcelSheet().GetRow(category) is not { RowId: not 0, Icon: var iconCategory, Name: var entryName }) continue; 135 | if (Service.TextureProvider.GetFromGameIcon((uint) iconCategory) is not { } iconTexture) continue; 136 | 137 | if (ImGuiComponents.IconButton($"##RemoveButton{category}", FontAwesomeIcon.Trash)) { 138 | removalEntry = category; 139 | } 140 | 141 | ImGui.SameLine(); 142 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 1.0f * ImGuiHelpers.GlobalScale); 143 | ImGui.Image(iconTexture.GetWrapOrEmpty().ImGuiHandle, ImGuiHelpers.ScaledVector2(20.0f, 20.0f)); 144 | 145 | ImGui.SameLine(); 146 | ImGui.TextUnformatted(entryName.ExtractText()); 147 | } 148 | 149 | if (removalEntry is { } toRemove) { 150 | SortingRule.AllowedItemTypes.Remove(toRemove); 151 | } 152 | } 153 | } 154 | 155 | public class OtherFiltersTab(SortingRule rule) : ITwoColumnRuleConfigurationTab { 156 | public string Name => "Other Filters"; 157 | 158 | public bool Disabled => false; 159 | 160 | public SortingRule SortingRule { get; } = rule; 161 | 162 | public string FirstLabel => "Range Filters"; 163 | 164 | public string SecondLabel => "Item Rarity Filter"; 165 | 166 | public void DrawLeftSideContents() { 167 | SortingRule.LevelFilter.DrawConfig(); 168 | SortingRule.ItemLevelFilter.DrawConfig(); 169 | SortingRule.VendorPriceFilter.DrawConfig(); 170 | } 171 | 172 | public void DrawRightSideContents() { 173 | foreach (var enumValue in Enum.GetValues()) { 174 | var enabled = SortingRule.AllowedItemRarities.Contains(enumValue); 175 | if (ImGuiComponents.ToggleButton($"{enumValue.GetDescription()}", ref enabled)) { 176 | if (enabled) SortingRule.AllowedItemRarities.Add(enumValue); 177 | if (!enabled) SortingRule.AllowedItemRarities.Remove(enumValue); 178 | } 179 | 180 | ImGui.SameLine(); 181 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3.0f); 182 | ImGui.TextUnformatted(enumValue.GetDescription()); 183 | } 184 | } 185 | } 186 | 187 | public class ToggleFiltersTab(SortingRule rule) : IOneColumnRuleConfigurationTab { 188 | public string Name => "Property Filters"; 189 | 190 | public bool Disabled => false; 191 | 192 | public SortingRule SortingRule { get; } = rule; 193 | 194 | public string FirstLabel => "Property Filters"; 195 | 196 | public void DrawContents() { 197 | SortingRule.UntradableFilter.DrawConfig(); 198 | SortingRule.UniqueFilter.DrawConfig(); 199 | SortingRule.DyeableFilter.DrawConfig(); 200 | SortingRule.CollectableFilter.DrawConfig(); 201 | SortingRule.RepairableFilter.DrawConfig(); 202 | } 203 | } 204 | 205 | public class SortOrderTab(SortingRule rule) : ITwoColumnRuleConfigurationTab { 206 | public string Name => "Sort Order"; 207 | 208 | public bool Disabled => false; 209 | 210 | public SortingRule SortingRule { get; } = rule; 211 | 212 | public string FirstLabel => "Sort By"; 213 | 214 | public string SecondLabel => "Sort Options"; 215 | 216 | public void DrawLeftSideContents() { 217 | ImGui.Text("Order items using"); 218 | ImGuiComponents.HelpMarker("The primary property of an item to use for ordering"); 219 | var sortMode = SortingRule.SortMode; 220 | DrawRadioEnum(ref sortMode); 221 | 222 | SortingRule.SortMode = sortMode; 223 | } 224 | 225 | public void DrawRightSideContents() { 226 | ImGui.Text("Sort item by"); 227 | ImGuiComponents.HelpMarker("Ascending: A -> Z\nDescending Z -> A"); 228 | var sortDirection = SortingRule.Direction; 229 | DrawRadioEnum(ref sortDirection); 230 | 231 | ImGuiHelpers.ScaledDummy(8.0f); 232 | ImGui.Text("Fill inventory slots from"); 233 | ImGuiComponents.HelpMarker("Top - Items are shifted to the top left-most slots\nBottom - Items are shifted to the bottom right-most slots"); 234 | var fillMode = SortingRule.FillMode; 235 | DrawRadioEnum(ref fillMode); 236 | 237 | SortingRule.Direction = sortDirection; 238 | SortingRule.FillMode = fillMode; 239 | } 240 | 241 | private static void DrawRadioEnum(ref T configValue) where T : Enum { 242 | foreach (Enum value in Enum.GetValues(configValue.GetType())) { 243 | var isSelected = Convert.ToInt32(configValue); 244 | if (ImGui.RadioButton($"{value.GetDescription()}##{configValue.GetType()}", ref isSelected, Convert.ToInt32(value))) { 245 | configValue = (T) value; 246 | } 247 | } 248 | } 249 | } -------------------------------------------------------------------------------- /SortaKinda/Classes/SortingRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Numerics; 6 | using FFXIVClientStructs.FFXIV.Client.Game; 7 | using KamiLib.Classes; 8 | using Lumina.Excel.Sheets; 9 | using SortaKinda.Controllers; 10 | using SortaKinda.ViewComponents; 11 | 12 | namespace SortaKinda.Classes; 13 | 14 | public unsafe class SortingRule : IComparer{ 15 | private readonly SortingRuleTooltipView view; 16 | private readonly List filterRules; 17 | 18 | public SortingRule() { 19 | view = new SortingRuleTooltipView(this); 20 | filterRules = new List { 21 | new() { 22 | Active = () => AllowedNameRegexes.Count != 0, 23 | IsSlotAllowed = slot => { 24 | foreach (var allowedRegex in AllowedNameRegexes) { 25 | if (slot is { ExdItem: { Name: var itemName, RowId: not 0 } }) { 26 | if (allowedRegex.Match(itemName.ExtractText())) return true; 27 | } 28 | } 29 | 30 | return false; 31 | }, 32 | }, 33 | new() { 34 | Active = () => AllowedItemTypes.Count != 0, 35 | IsSlotAllowed = slot => AllowedItemTypes.Any(allowed => slot.ExdItem.ItemUICategory.RowId == allowed), 36 | }, 37 | new() { 38 | Active = () => AllowedItemRarities.Count != 0, 39 | IsSlotAllowed = slot => AllowedItemRarities.Any(allowed => slot.ExdItem.Rarity == (byte) allowed), 40 | }, 41 | new() { 42 | Active = () => LevelFilter.Enable, 43 | IsSlotAllowed = slot => LevelFilter.IsItemSlotAllowed(slot.ExdItem.LevelEquip), 44 | }, 45 | new() { 46 | Active = () => ItemLevelFilter.Enable, 47 | IsSlotAllowed = slot => ItemLevelFilter.IsItemSlotAllowed(slot.ExdItem.LevelItem.RowId), 48 | }, 49 | new() { 50 | Active = () => VendorPriceFilter.Enable, 51 | IsSlotAllowed = slot => VendorPriceFilter.IsItemSlotAllowed(slot.ExdItem.PriceLow), 52 | }, 53 | new() { 54 | Active = () => UntradableFilter.State is not ToggleFilterState.Ignored, 55 | IsSlotAllowed = slot => UntradableFilter.IsItemSlotAllowed(slot), 56 | }, 57 | new() { 58 | Active = () => UniqueFilter.State is not ToggleFilterState.Ignored, 59 | IsSlotAllowed = slot => UniqueFilter.IsItemSlotAllowed(slot), 60 | }, 61 | new() { 62 | Active = () => CollectableFilter.State is not ToggleFilterState.Ignored, 63 | IsSlotAllowed = slot => CollectableFilter.IsItemSlotAllowed(slot), 64 | }, 65 | new() { 66 | Active = () => DyeableFilter.State is not ToggleFilterState.Ignored, 67 | IsSlotAllowed = slot => DyeableFilter.IsItemSlotAllowed(slot), 68 | }, 69 | new() { 70 | Active = () => RepairableFilter.State is not ToggleFilterState.Ignored, 71 | IsSlotAllowed = slot => RepairableFilter.IsItemSlotAllowed(slot), 72 | } 73 | }; 74 | } 75 | 76 | public Vector4 Color { get; set; } 77 | public string Id { get; set; } = SortController.DefaultId; 78 | public string Name { get; set; } = "New Rule"; 79 | public int Index { get; set; } 80 | public HashSet AllowedNameRegexes { get; set; } = []; 81 | public HashSet AllowedItemTypes { get; set; } = []; 82 | public HashSet AllowedItemRarities { get; set; } = []; 83 | public RangeFilter LevelFilter { get; set; } = new("Level Filter", 0, 200); 84 | public RangeFilter ItemLevelFilter { get; set; } = new("Item Level Filter", 0, 1000); 85 | public RangeFilter VendorPriceFilter { get; set; } = new("Vendor Price Filter", 0, 1_000_000); 86 | public ToggleFilter UntradableFilter { get; set; } = new(PropertyFilter.Untradable); 87 | public ToggleFilter UniqueFilter { get; set; } = new(PropertyFilter.Unique); 88 | public ToggleFilter CollectableFilter { get; set; } = new(PropertyFilter.Collectable); 89 | public ToggleFilter DyeableFilter { get; set; } = new(PropertyFilter.Dyeable); 90 | public ToggleFilter RepairableFilter { get; set; } = new(PropertyFilter.Repairable); 91 | public SortOrderDirection Direction { get; set; } = SortOrderDirection.Ascending; 92 | public FillMode FillMode { get; set; } = FillMode.Top; 93 | public SortOrderMode SortMode { get; set; } = SortOrderMode.Alphabetically; 94 | public bool InclusiveAnd = false; 95 | 96 | public void ShowTooltip() { 97 | view.Draw(); 98 | } 99 | 100 | public int Compare(InventorySlot? x, InventorySlot? y) { 101 | if (x is null) return 0; 102 | if (y is null) return 0; 103 | if (x.ExdItem.RowId is 0) return 0; 104 | if (y.ExdItem.RowId is 0) return 0; 105 | if (IsFilterMatch(x.ExdItem, y.ExdItem)) return 0; 106 | if (CompareSlots(x, y)) return 1; 107 | return -1; 108 | } 109 | 110 | public bool IsItemSlotAllowed(InventorySlot slot) => InclusiveAnd ? 111 | filterRules.Any(rule => rule.Active() && rule.IsSlotAllowed(slot)) : 112 | filterRules.All(rule => !rule.Active() || rule.Active() && rule.IsSlotAllowed(slot)); 113 | 114 | public bool CompareSlots(InventorySlot a, InventorySlot b) { 115 | var firstItem = a.ExdItem; 116 | var secondItem = b.ExdItem; 117 | 118 | switch (a.HasItem, b.HasItem) { 119 | // If both items are null, don't swap 120 | case (false, false): return false; 121 | 122 | // first slot empty, second slot full, if Ascending we want to leave justify, move the items left, if Descending right justify, leave the empty slot on the left. 123 | case (false, true): return FillMode is FillMode.Top; 124 | 125 | // first slot full, second slot empty, if Ascending we want to leave justify, and we have that already, if Descending right justify, move the item right 126 | case (true, false): return FillMode is FillMode.Bottom; 127 | 128 | case (true, true) when firstItem.RowId is not 0 && secondItem.RowId is not 0: 129 | 130 | var shouldSwap = false; 131 | 132 | // They are the same item 133 | if (firstItem.RowId == secondItem.RowId) { 134 | // if left is not HQ, and right is HQ, swap 135 | if (!a.InventoryItem->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) && b.InventoryItem->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality)) { 136 | shouldSwap = true; 137 | } 138 | // else if left has lower quantity then right, swap 139 | else if (a.InventoryItem->Quantity < b.InventoryItem->Quantity) { 140 | shouldSwap = true; 141 | } 142 | } 143 | // else if they match according to the default filter, fallback to alphabetical 144 | else if (IsFilterMatch(firstItem, secondItem)) { 145 | shouldSwap = ShouldSwap(firstItem, secondItem, SortOrderMode.Alphabetically); 146 | } 147 | // else they are not the same item, and the filter result doesn't match 148 | else { 149 | shouldSwap = ShouldSwap(firstItem, secondItem, SortMode); 150 | } 151 | 152 | return Direction is SortOrderDirection.Descending ? !shouldSwap : shouldSwap; 153 | 154 | // Something went horribly wrong... best not touch it and walk away. 155 | default: return false; 156 | } 157 | } 158 | 159 | private bool IsFilterMatch(Item firstItem, Item secondItem) => SortMode switch { 160 | SortOrderMode.ItemId => firstItem.RowId == secondItem.RowId, 161 | SortOrderMode.ItemLevel => firstItem.LevelItem.RowId == secondItem.LevelItem.RowId, 162 | SortOrderMode.Alphabetically => string.Equals(firstItem.Name.ExtractText(), secondItem.Name.ExtractText(), StringComparison.OrdinalIgnoreCase), 163 | SortOrderMode.SellPrice => firstItem.PriceLow == secondItem.PriceLow, 164 | SortOrderMode.Rarity => firstItem.Rarity == secondItem.Rarity, 165 | SortOrderMode.ItemType => firstItem.ItemUICategory.RowId == secondItem.ItemUICategory.RowId, 166 | SortOrderMode.Level => firstItem.LevelEquip == secondItem.LevelEquip, 167 | _ => false, 168 | }; 169 | 170 | private static bool ShouldSwap(Item firstItem, Item secondItem, SortOrderMode sortMode) => sortMode switch { 171 | SortOrderMode.ItemId => firstItem.RowId > secondItem.RowId, 172 | SortOrderMode.ItemLevel => firstItem.LevelItem.RowId > secondItem.LevelItem.RowId, 173 | SortOrderMode.Alphabetically => string.Compare(firstItem.Name.ExtractText(), secondItem.Name.ExtractText(), StringComparison.OrdinalIgnoreCase) > 0, 174 | SortOrderMode.SellPrice => firstItem.PriceLow > secondItem.PriceLow, 175 | SortOrderMode.Rarity => firstItem.Rarity > secondItem.Rarity, 176 | SortOrderMode.ItemType => ShouldSwapItemUiCategory(firstItem, secondItem), 177 | SortOrderMode.Level => firstItem.LevelEquip > secondItem.LevelEquip, 178 | _ => false, 179 | }; 180 | 181 | private static bool ShouldSwapItemUiCategory(Item firstItem, Item secondItem) { 182 | // If same category, don't swap, other system handles fallback to alphabetical in this case 183 | if (firstItem.ItemUICategory.RowId == secondItem.ItemUICategory.RowId) return false; 184 | 185 | if (firstItem is { RowId: not 0, ItemUICategory.Value: var first } && secondItem is { RowId: not 0, ItemUICategory.Value: var second }) { 186 | if (first.OrderMajor == second.OrderMajor) { 187 | return first.OrderMinor > second.OrderMinor; 188 | } 189 | 190 | return first.OrderMajor > second.OrderMajor; 191 | } 192 | 193 | return false; 194 | } 195 | } 196 | 197 | public enum FillMode { 198 | [Description("Top")] 199 | Top, 200 | 201 | [Description("Bottom")] 202 | Bottom, 203 | } 204 | 205 | public enum ItemRarity { 206 | [Description("White")] 207 | White = 1, 208 | 209 | [Description("Green")] 210 | Green = 2, 211 | 212 | [Description("Blue")] 213 | Blue = 3, 214 | 215 | [Description("Purple")] 216 | Purple = 4, 217 | 218 | [Description("Pink")] 219 | Pink = 7, 220 | } 221 | 222 | public enum SortOrderDirection { 223 | [Description("Ascending")] 224 | Ascending, 225 | 226 | [Description("Descending")] 227 | Descending, 228 | } 229 | 230 | public enum SortOrderMode { 231 | [Description("Alphabetical")] 232 | Alphabetically, 233 | 234 | [Description("Item Level")] 235 | ItemLevel, 236 | 237 | [Description("Rarity")] 238 | Rarity, 239 | 240 | [Description("Sell Price")] 241 | SellPrice, 242 | 243 | [Description("Item Id")] 244 | ItemId, 245 | 246 | [Description("Item Type")] 247 | ItemType, 248 | 249 | [Description("Level")] 250 | Level, 251 | } 252 | 253 | public class SortingFilter { 254 | public required Func Active { get; init; } 255 | 256 | public required Func IsSlotAllowed { get; init; } 257 | } --------------------------------------------------------------------------------