From d3bdb8d3d90666b2dcc7d0ff5a8a536e57d00cff Mon Sep 17 00:00:00 2001 From: y2_ss <68405449+y2-ss@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:53:53 -0500 Subject: [PATCH] Add associated collections. --- Glamourer/Automation/AutoDesignApplier.cs | 33 +++++++++++- Glamourer/Designs/Design.cs | 3 ++ Glamourer/Designs/DesignManager.cs | 20 +++++++ Glamourer/Events/DesignChanged.cs | 3 ++ .../DesignTab/CollectionAssociationTab.cs | 45 ++++++++++++++++ .../Gui/Tabs/DesignTab/CollectionCombo.cs | 32 +++++++++++ Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 35 +++++++----- Glamourer/Interop/Penumbra/PenumbraService.cs | 53 +++++++++++++++++++ Glamourer/Services/ServiceManager.cs | 1 + 9 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 Glamourer/Gui/Tabs/DesignTab/CollectionAssociationTab.cs create mode 100644 Glamourer/Gui/Tabs/DesignTab/CollectionCombo.cs diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 3013c4a..00a990d 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -4,6 +4,7 @@ using Glamourer.Customization; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.State; @@ -29,6 +30,7 @@ public class AutoDesignApplier : IDisposable private readonly AutomationChanged _event; private readonly ObjectManager _objects; private readonly WeaponLoading _weapons; + private readonly PenumbraService _penumbra; private ActorState? _jobChangeState; private EquipItem _jobChangeMainhand; @@ -36,7 +38,7 @@ public class AutoDesignApplier : IDisposable public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, - AutomationChanged @event, ObjectManager objects, WeaponLoading weapons) + AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, PenumbraService penumbra) { _config = config; _manager = manager; @@ -49,6 +51,7 @@ public class AutoDesignApplier : IDisposable _event = @event; _objects = objects; _weapons = weapons; + _penumbra = penumbra; _jobs.JobChanged += OnJobChange; _event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier); _weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier); @@ -126,7 +129,10 @@ public class AutoDesignApplier : IDisposable { Reduce(data.Objects[0], state, newSet, false, false); foreach (var actor in data.Objects) + { + _penumbra.SetCollection(actor, ReduceCollections(actor, set)); _state.ReapplyState(actor); + } } } else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) @@ -136,6 +142,7 @@ public class AutoDesignApplier : IDisposable var specificId = actor.GetIdentifier(_actors.AwaitedService); if (_state.GetOrCreate(specificId, actor, out var state)) { + _penumbra.SetCollection(actor, ReduceCollections(actor, set)); Reduce(actor, state, newSet, false, false); _state.ReapplyState(actor); } @@ -194,6 +201,7 @@ public class AutoDesignApplier : IDisposable var respectManual = state.LastJob == newJob.Id; state.LastJob = actor.Job; Reduce(actor, state, set, respectManual, true); + _penumbra.SetCollection(actor, ReduceCollections(actor, set)); _state.ReapplyState(actor); } @@ -206,6 +214,29 @@ public class AutoDesignApplier : IDisposable return; Reduce(actor, state, set, false, false); + _penumbra.SetCollection(actor, ReduceCollections(actor, set)); + } + + public unsafe Collection ReduceCollections(Actor actor, AutoDesignSet set) + { + Collection collection = new Collection(); + foreach (var design in set.Designs) + { + if (!design.IsActive(actor)) + continue; + + if (design.ApplicationType is 0) + continue; + + if (actor.AsCharacter->CharacterData.ModelCharaId != design?.Design?.DesignData.ModelId) + continue; + + if (design.Design.AssociatedCollection.IsAssociable()) + { + collection = design.Design.AssociatedCollection; + } + } + return collection; } public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state) diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 9870394..05124d2 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -41,6 +41,7 @@ public sealed class Design : DesignBase, ISavable public string[] Tags { get; internal set; } = Array.Empty(); public int Index { get; internal set; } public SortedList AssociatedMods { get; private set; } = new(); + public Collection AssociatedCollection { get; internal set; } = new(); public string Incognito => Identifier.ToString()[..8]; @@ -64,6 +65,7 @@ public sealed class Design : DesignBase, ISavable ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), ["Mods"] = SerializeMods(), + ["Collection"] = AssociatedCollection.Name, } ; return ret; @@ -124,6 +126,7 @@ public sealed class Design : DesignBase, ISavable Description = json["Description"]?.ToObject() ?? string.Empty, Tags = ParseTags(json), LastEdit = json["LastEdit"]?.ToObject() ?? creationDate, + AssociatedCollection = new Collection(json["Collection"]?.ToObject() ?? string.Empty), }; if (design.LastEdit < creationDate) design.LastEdit = creationDate; diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs index 13d9706..d28ecc7 100644 --- a/Glamourer/Designs/DesignManager.cs +++ b/Glamourer/Designs/DesignManager.cs @@ -251,6 +251,26 @@ public class DesignManager _event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings)); } + /// Change an associated collection to a design. + public void ChangeAssociatedCollection(Design design, Collection collection) + { + var oldAssociatedCollection = design.AssociatedCollection; + if (oldAssociatedCollection == collection) + return; + + design.AssociatedCollection = collection; + design.LastEdit = DateTimeOffset.UtcNow; + _saveService.QueueSave(design); + if (collection.IsAssociable()) + { + Glamourer.Log.Debug($"Removed associated collection from design {design.Identifier}."); + } else + { + Glamourer.Log.Debug($"Set associated collection {collection.Name} to design {design.Identifier}."); + } + _event.Invoke(DesignChanged.Type.ChangedAssociatedCollection, design, oldAssociatedCollection); + } + /// Set the write protection status of a design. public void SetWriteProtection(Design design, bool value) { diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs index 154880a..b0c5ee2 100644 --- a/Glamourer/Events/DesignChanged.cs +++ b/Glamourer/Events/DesignChanged.cs @@ -46,6 +46,9 @@ public sealed class DesignChanged : EventWrapper An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. RemovedMod, + /// An existing design had an associated collection changed. Data is the prior collection [string]. + ChangedAssociatedCollection, + /// An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. Customize, diff --git a/Glamourer/Gui/Tabs/DesignTab/CollectionAssociationTab.cs b/Glamourer/Gui/Tabs/DesignTab/CollectionAssociationTab.cs new file mode 100644 index 0000000..0eb47e4 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/CollectionAssociationTab.cs @@ -0,0 +1,45 @@ +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Logging; +using Dalamud.Utility; +using Glamourer.Designs; +using Glamourer.Interop.Penumbra; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public class CollectionAssociationTab +{ + private readonly DesignFileSystemSelector _selector; + private readonly DesignManager _manager; + private readonly CollectionCombo _collectionCombo; + + public CollectionAssociationTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager) + { + _selector = selector; + _manager = manager; + _collectionCombo = new CollectionCombo(penumbra); + } + + public void Draw() + { + if (!ImGui.CollapsingHeader("Collection Association")) + return; + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + var changed = _collectionCombo.Draw("##new", !_selector.Selected!.AssociatedCollection.IsAssociable() ? "Select Collection" : _selector.Selected!.AssociatedCollection.Name, string.Empty, + width.X, ImGui.GetTextLineHeight()); + var currentCollection = _collectionCombo.CurrentSelection; + if (changed) + { + if (!currentCollection.IsAssociable()) return; + _manager.ChangeAssociatedCollection(_selector.Selected!, currentCollection); + } + if (ImGui.Button($"Remove Associated Collection")) + { + _manager.ChangeAssociatedCollection(_selector.Selected!, new Collection()); + } + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/CollectionCombo.cs b/Glamourer/Gui/Tabs/DesignTab/CollectionCombo.cs new file mode 100644 index 0000000..f8256d6 --- /dev/null +++ b/Glamourer/Gui/Tabs/DesignTab/CollectionCombo.cs @@ -0,0 +1,32 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using Glamourer.Interop.Penumbra; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; + +namespace Glamourer.Gui.Tabs.DesignTab; + +public sealed class CollectionCombo : FilterComboCache +{ + public CollectionCombo(PenumbraService penumbra) + : base(penumbra.GetAllCollections) + { + SearchByParts = false; + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + using var id = ImRaii.PushId(globalIdx); + var collection = Items[globalIdx]; + bool ret; + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.Text))) + { + ret = ImGui.Selectable(collection.Name, selected); + } + return ret; + } +} diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 880489b..9e3be71 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -13,6 +13,7 @@ using Glamourer.Events; using Glamourer.Gui.Customization; using Glamourer.Gui.Equipment; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; using Glamourer.Services; using Glamourer.State; using Glamourer.Structs; @@ -37,22 +38,26 @@ public class DesignPanel private readonly DesignConverter _converter; private readonly DatFileService _datFileService; private readonly FileDialogManager _fileDialog = new(); + private readonly PenumbraService _penumbra; + private readonly CollectionAssociationTab _collectionAssociation; public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects, - StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, ModAssociationsTab modAssociations, - DesignDetailTab designDetails, DesignConverter converter, DatFileService datFileService) + StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, ModAssociationsTab modAssociations, CollectionAssociationTab collectionAssociation, + DesignDetailTab designDetails, DesignConverter converter, DatFileService datFileService, PenumbraService penumbra) { - _selector = selector; - _customizationDrawer = customizationDrawer; - _manager = manager; - _objects = objects; - _state = state; - _equipmentDrawer = equipmentDrawer; - _customizationService = customizationService; - _modAssociations = modAssociations; - _designDetails = designDetails; - _converter = converter; - _datFileService = datFileService; + _selector = selector; + _customizationDrawer = customizationDrawer; + _manager = manager; + _objects = objects; + _state = state; + _equipmentDrawer = equipmentDrawer; + _customizationService = customizationService; + _modAssociations = modAssociations; + _collectionAssociation = collectionAssociation; + _designDetails = designDetails; + _converter = converter; + _datFileService = datFileService; + _penumbra = penumbra; } private HeaderDrawer.Button LockButton() @@ -326,6 +331,7 @@ public class DesignPanel _designDetails.Draw(); DrawApplicationRules(); _modAssociations.Draw(); + _collectionAssociation.Draw(); } private void DrawButtonRow() @@ -374,7 +380,10 @@ public class DesignPanel return; if (_state.GetOrCreate(id, data.Objects[0], out var state)) + { _state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual); + _penumbra.SetCollection(data.Objects[0], _selector.Selected!.AssociatedCollection); + } } private void DrawApplyToTarget() diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index b00aefd..ca21648 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using Dalamud.Logging; using Dalamud.Plugin; +using Dalamud.Utility; using Glamourer.Events; using Glamourer.Interop.Structs; using Penumbra.Api; @@ -36,6 +37,17 @@ public readonly record struct ModSettings(IDictionary> Set public static ModSettings Empty => new(); } +public readonly record struct Collection(string Name) : IComparable +{ + public bool IsAssociable() + { + return !Name.IsNullOrEmpty(); + } + public int CompareTo(Collection other) + { + return string.Compare(Name, other.Name, StringComparison.Ordinal); + } +} public unsafe class PenumbraService : IDisposable { @@ -54,7 +66,9 @@ public unsafe class PenumbraService : IDisposable private FuncSubscriber _objectCollection; private FuncSubscriber> _getMods; private FuncSubscriber _currentCollection; + private FuncSubscriber> _getAllCollections; private FuncSubscriber _getCurrentSettings; + private FuncSubscriber _setCurrentCollection; private FuncSubscriber _setMod; private FuncSubscriber _setModPriority; private FuncSubscriber _setModSetting; @@ -141,6 +155,25 @@ public unsafe class PenumbraService : IDisposable } } + public IReadOnlyList GetAllCollections() + { + if (!Available) + return Array.Empty(); + + try + { + var allCollections = _getAllCollections.Invoke(); + return allCollections + .Select(c => new Collection(c)) + .ToList(); + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error fetching collections from Penumbra:\n{ex}"); + return Array.Empty(); + } + } + public string CurrentCollection => Available ? _currentCollection.Invoke(ApiCollectionType.Current) : ""; @@ -195,6 +228,24 @@ public unsafe class PenumbraService : IDisposable return sb.AppendLine(ex.Message).ToString(); } } + public string SetCollection(Actor actor, Collection collection) + { + if (!Available) + return "Penumbra is not available."; + + var sb = new StringBuilder(); + try + { + var ec = _setCurrentCollection.Invoke(actor.AsObject -> ObjectIndex, collection.Name, true, false); + if (ec.Item1 is PenumbraApiEc.CollectionMissing) + sb.AppendLine($"The collection {collection.Name}] could not be found."); + return sb.ToString(); + } + catch (Exception ex) + { + return sb.AppendLine(ex.Message).ToString(); + } + } /// Obtain the name of the collection currently assigned to the player. public string GetCurrentPlayerCollection() @@ -254,6 +305,8 @@ public unsafe class PenumbraService : IDisposable _getMods = Ipc.GetMods.Subscriber(_pluginInterface); _currentCollection = Ipc.GetCollectionForType.Subscriber(_pluginInterface); _getCurrentSettings = Ipc.GetCurrentModSettings.Subscriber(_pluginInterface); + _getAllCollections = Ipc.GetCollections.Subscriber(_pluginInterface); + _setCurrentCollection = Ipc.SetCollectionForObject.Subscriber( _pluginInterface); _setMod = Ipc.TrySetMod.Subscriber(_pluginInterface); _setModPriority = Ipc.TrySetModPriority.Subscriber(_pluginInterface); _setModSetting = Ipc.TrySetModSetting.Subscriber(_pluginInterface); diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 4a02824..f8770b9 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -128,6 +128,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton()