diff --git a/Glamourer/Gui/MainWindow.cs b/Glamourer/Gui/MainWindow.cs index 6f09d04..cb21a91 100644 --- a/Glamourer/Gui/MainWindow.cs +++ b/Glamourer/Gui/MainWindow.cs @@ -9,6 +9,7 @@ using Glamourer.Gui.Tabs.AutomationTab; using Glamourer.Gui.Tabs.DebugTab; using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Gui.Tabs.NpcTab; +using Glamourer.Gui.Tabs.SettingsTab; using Glamourer.Gui.Tabs.UnlocksTab; using ImGuiNET; using OtterGui.Custom; diff --git a/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs new file mode 100644 index 0000000..2ddabda --- /dev/null +++ b/Glamourer/Gui/Tabs/SettingsTab/CollectionOverrideDrawer.cs @@ -0,0 +1,122 @@ +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Style; +using Glamourer.Interop; +using Glamourer.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.GameData.Actors; + +namespace Glamourer.Gui.Tabs.SettingsTab; + +public class CollectionOverrideDrawer( + CollectionOverrideService collectionOverrides, + Configuration config, + ObjectManager objects, + ActorManager actors) : IService +{ + private string _newIdentifier = string.Empty; + private ActorIdentifier[] _identifiers = []; + private int _dragDropIndex = -1; + private Exception? _exception; + private string _collection = string.Empty; + + public void Draw() + { + using var header = ImRaii.CollapsingHeader("Collection Overrides"); + ImGuiUtil.HoverTooltip( + "Here you can set up overrides for Penumbra collections that should have their settings changed when automatically applying mod settings from a design.\n" + + "Instead of the collection associated with the overridden character, the overridden collection will be manipulated."); + if (!header) + return; + + using var table = ImRaii.Table("table", 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + var buttonSize = new Vector2(ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("buttons", ImGuiTableColumnFlags.WidthFixed, buttonSize.X); + ImGui.TableSetupColumn("identifiers", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("collections", ImGuiTableColumnFlags.WidthStretch, 0.4f); + + for (var i = 0; i < collectionOverrides.Overrides.Count; ++i) + { + var (identifier, collection) = collectionOverrides.Overrides[i]; + using var id = ImRaii.PushId(i); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, "Delete this override.", false, true)) + collectionOverrides.DeleteOverride(i--); + + ImGui.TableNextColumn(); + ImGui.Selectable(config.Ephemeral.IncognitoMode ? identifier.Incognito(null) : identifier.ToString()); + + using (var target = ImRaii.DragDropTarget()) + { + if (target.Success && ImGuiUtil.IsDropping("DraggingOverride")) + { + collectionOverrides.MoveOverride(_dragDropIndex, i); + _dragDropIndex = -1; + } + } + + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("DraggingOverride", nint.Zero, 0); + ImGui.TextUnformatted($"Reordering Override #{i + 1}..."); + _dragDropIndex = i; + } + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText("##input", ref collection, 64) && collection.Length > 0) + collectionOverrides.ChangeOverride(i, collection); + } + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PersonCirclePlus.ToIconString(), buttonSize, "Add override for current player.", + !objects.Player.Valid, true)) + collectionOverrides.AddOverride([objects.PlayerData.Identifier], _collection.Length > 0 ? _collection : "TempCollection"); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - buttonSize.X * 2); + if (ImGui.InputTextWithHint("##newActor", "New Identifier...", ref _newIdentifier, 80)) + try + { + _identifiers = actors.FromUserString(_newIdentifier, false); + } + catch (ActorIdentifierFactory.IdentifierParseError e) + { + _exception = e; + _identifiers = []; + } + + var tt = _identifiers.Any(i => i.IsValid) + ? $"Add a new override for {_identifiers.First(i => i.IsValid)}." + : _newIdentifier.Length == 0 + ? "Please enter an identifier string first." + : $"The identifier string {_newIdentifier} does not result in a valid identifier{(_exception == null ? "." : $":\n\n{_exception?.Message}")}"; + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, tt[0] is 'T', true)) + collectionOverrides.AddOverride(_identifiers, _collection.Length > 0 ? _collection : "TempCollection"); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + ActorIdentifierFactory.WriteUserStringTooltip(false); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##collection", "Enter Collection...", ref _collection, 80); + } +} diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs similarity index 95% rename from Glamourer/Gui/Tabs/SettingsTab.cs rename to Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs index 11490df..5d0fcca 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab/SettingsTab.cs @@ -15,7 +15,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; -namespace Glamourer.Gui.Tabs; +namespace Glamourer.Gui.Tabs.SettingsTab; public class SettingsTab( Configuration config, @@ -29,7 +29,8 @@ public class SettingsTab( IKeyState keys, DesignColorUi designColorUi, PaletteImport paletteImport, - PalettePlusChecker paletteChecker) + PalettePlusChecker paletteChecker, + CollectionOverrideDrawer overrides) : ITab { private readonly VirtualKey[] _validKeys = keys.GetValidVirtualKeys().Prepend(VirtualKey.NO_KEY).ToArray(); @@ -57,6 +58,7 @@ public class SettingsTab( DrawBehaviorSettings(); DrawInterfaceSettings(); DrawColorSettings(); + overrides.Draw(); DrawCodes(); } @@ -90,6 +92,9 @@ public class SettingsTab( "Enable the display and editing of advanced customization options like arbitrary colors.", config.UseAdvancedParameters, paletteChecker.SetAdvancedParameters); PaletteImportButton(); + Checkbox("Enable Advanced Dye Options", + "Enable the display and editing of advanced dyes (color sets) for all equipment", + config.UseAdvancedDyes, v => config.UseAdvancedDyes = v); Checkbox("Always Apply Associated Mods", "Whenever a design is applied to a character (including via automation), Glamourer will try to apply its associated mod settings to the collection currently associated with that character, if it is available.\n\n" + "Glamourer will NOT revert these applied settings automatically. This may mess up your collection and configuration.\n\n" @@ -189,6 +194,7 @@ public class SettingsTab( ImGui.NewLine(); } + private void PaletteImportButton() { if (!config.UseAdvancedParameters || !config.ShowPalettePlusImport) diff --git a/Glamourer/Interop/Penumbra/ModSettingApplier.cs b/Glamourer/Interop/Penumbra/ModSettingApplier.cs index 01ea4d7..198c6ad 100644 --- a/Glamourer/Interop/Penumbra/ModSettingApplier.cs +++ b/Glamourer/Interop/Penumbra/ModSettingApplier.cs @@ -1,11 +1,13 @@ using Glamourer.Designs.Links; using Glamourer.Interop.Structs; +using Glamourer.Services; using Glamourer.State; using OtterGui.Services; namespace Glamourer.Interop.Penumbra; -public class ModSettingApplier(PenumbraService penumbra, Configuration config, ObjectManager objects) : IService +public class ModSettingApplier(PenumbraService penumbra, Configuration config, ObjectManager objects, CollectionOverrideService overrides) + : IService { public void HandleStateApplication(ActorState state, MergedDesign design) { @@ -24,7 +26,7 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O foreach (var actor in data.Objects) { - var collection = penumbra.GetActorCollection(actor); + var (collection, overridden) = overrides.GetCollection(actor, state.Identifier); if (collection.Length == 0) { Glamourer.Log.Verbose($"[Mod Applier] Could not obtain associated collection for {actor.Utf8Name}."); @@ -40,16 +42,17 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O if (message.Length > 0) Glamourer.Log.Verbose($"[Mod Applier] Error applying mod settings: {message}"); else - Glamourer.Log.Verbose($"[Mod Applier] Set mod settings for {mod.DirectoryName} in {collection}."); + Glamourer.Log.Verbose( + $"[Mod Applier] Set mod settings for {mod.DirectoryName} in {collection}{(overridden ? " (overridden by settings)" : string.Empty)}."); } } } - public (List Messages, int Applied, string Collection) ApplyModSettings(IReadOnlyDictionary settings, Actor actor) + public (List Messages, int Applied, string Collection, bool Overridden) ApplyModSettings(IReadOnlyDictionary settings, Actor actor) { - var collection = penumbra.GetActorCollection(actor); + var (collection, overridden) = overrides.GetCollection(actor); if (collection.Length <= 0) - return ([$"Could not obtain associated collection for {actor.Utf8Name}."], 0, string.Empty); + return ([$"Could not obtain associated collection for {actor.Utf8Name}."], 0, string.Empty, false); var messages = new List(); var appliedMods = 0; @@ -62,6 +65,6 @@ public class ModSettingApplier(PenumbraService penumbra, Configuration config, O ++appliedMods; } - return (messages, appliedMods, collection); + return (messages, appliedMods, collection, overridden); } } diff --git a/Glamourer/Services/CollectionOverrideService.cs b/Glamourer/Services/CollectionOverrideService.cs new file mode 100644 index 0000000..7cdbc47 --- /dev/null +++ b/Glamourer/Services/CollectionOverrideService.cs @@ -0,0 +1,160 @@ +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.GameData.Actors; + +namespace Glamourer.Services; + +public sealed class CollectionOverrideService : IService, ISavable +{ + public const int Version = 1; + private readonly SaveService _saveService; + private readonly ActorManager _actors; + private readonly PenumbraService _penumbra; + + public CollectionOverrideService(SaveService saveService, ActorManager actors, PenumbraService penumbra) + { + _saveService = saveService; + _actors = actors; + _penumbra = penumbra; + Load(); + } + + public unsafe (string Collection, bool Overriden) GetCollection(Actor actor, ActorIdentifier identifier = default) + { + if (!identifier.IsValid) + identifier = _actors.FromObject(actor.AsObject, out _, true, true, true); + + return _overrides.FindFirst(p => p.Actor.Matches(identifier), out var ret) + ? (ret.Collection, true) + : (_penumbra.GetActorCollection(actor), false); + } + + private readonly List<(ActorIdentifier Actor, string Collection)> _overrides = []; + + public IReadOnlyList<(ActorIdentifier Actor, string Collection)> Overrides + => _overrides; + + public string ToFilename(FilenameService fileNames) + => fileNames.CollectionOverrideFile; + + public void AddOverride(IEnumerable identifiers, string collection) + { + if (collection.Length == 0) + return; + + foreach (var id in identifiers.Where(i => i.IsValid)) + { + _overrides.Add((id, collection)); + Glamourer.Log.Debug($"Added collection override {id.Incognito(null)} -> {collection}."); + _saveService.QueueSave(this); + } + } + + public void ChangeOverride(int idx, string newCollection) + { + if (idx < 0 || idx >= _overrides.Count || newCollection.Length == 0) + return; + + var current = _overrides[idx]; + if (current.Collection == newCollection) + return; + + _overrides[idx] = current with { Collection = newCollection }; + Glamourer.Log.Debug($"Changed collection override {idx + 1} from {current.Collection} to {newCollection}."); + _saveService.QueueSave(this); + } + + public void DeleteOverride(int idx) + { + if (idx < 0 || idx >= _overrides.Count) + return; + + _overrides.RemoveAt(idx); + Glamourer.Log.Debug($"Removed collection override {idx + 1}."); + _saveService.QueueSave(this); + } + + public void MoveOverride(int idxFrom, int idxTo) + { + if (!_overrides.Move(idxFrom, idxTo)) + return; + + Glamourer.Log.Debug($"Moved collection override {idxFrom + 1} to {idxTo + 1}."); + _saveService.QueueSave(this); + } + + private void Load() + { + var file = _saveService.FileNames.CollectionOverrideFile; + if (!File.Exists(file)) + return; + + try + { + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) + { + case 1: + if (jObj["Overrides"] is not JArray array) + { + Glamourer.Log.Error($"Invalid format of collection override file, ignored."); + return; + } + + foreach (var token in array.OfType()) + { + var collection = token["Collection"]?.ToObject() ?? string.Empty; + var identifier = _actors.FromJson(token); + if (!identifier.IsValid) + Glamourer.Log.Warning($"Invalid identifier for collection override with collection [{collection}], skipped."); + else if (collection.Length == 0) + Glamourer.Log.Warning($"Empty collection override for identifier {identifier.Incognito(null)}, skipped."); + else + _overrides.Add((identifier, collection)); + } + + break; + default: + Glamourer.Log.Error($"Invalid version {version} of collection override file, ignored."); + return; + } + } + catch (Exception ex) + { + Glamourer.Log.Error($"Error loading collection override file:\n{ex}"); + } + } + + public void Save(StreamWriter writer) + { + var jObj = new JObject() + { + ["Version"] = Version, + ["Overrides"] = SerializeOverrides(), + }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObj.WriteTo(j); + return; + + JArray SerializeOverrides() + { + var jArray = new JArray(); + foreach (var (actor, collection) in _overrides) + { + var obj = actor.ToJson(); + obj["Collection"] = collection; + jArray.Add(obj); + } + + return jArray; + } + } +} diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 20cd1e6..74364e2 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -440,13 +440,13 @@ public class CommandService : IDisposable if (!applyMods || design is not Design d) return; - var (messages, appliedMods, collection) = _modApplier.ApplyModSettings(d.AssociatedMods, actor); + var (messages, appliedMods, collection, overridden) = _modApplier.ApplyModSettings(d.AssociatedMods, actor); foreach (var message in messages) Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); if (appliedMods > 0) - Glamourer.Messager.Chat.Print($"Applied {appliedMods} mod settings to {collection}."); + Glamourer.Messager.Chat.Print($"Applied {appliedMods} mod settings to {collection}{(overridden ? " (overridden by settings)" : string.Empty)}."); } private bool Delete(string argument) diff --git a/Glamourer/Services/FilenameService.cs b/Glamourer/Services/FilenameService.cs index 244c972..e19e289 100644 --- a/Glamourer/Services/FilenameService.cs +++ b/Glamourer/Services/FilenameService.cs @@ -17,21 +17,23 @@ public class FilenameService public readonly string DesignColorFile; public readonly string EphemeralConfigFile; public readonly string NpcAppearanceFile; + public readonly string CollectionOverrideFile; public FilenameService(DalamudPluginInterface pi) { - ConfigDirectory = pi.ConfigDirectory.FullName; - ConfigFile = pi.ConfigFile.FullName; - AutomationFile = Path.Combine(ConfigDirectory, "automation.json"); - DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); - MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); - UnlockFileCustomize = Path.Combine(ConfigDirectory, "unlocks_customize.json"); - UnlockFileItems = Path.Combine(ConfigDirectory, "unlocks_items.json"); - DesignDirectory = Path.Combine(ConfigDirectory, "designs"); - FavoriteFile = Path.Combine(ConfigDirectory, "favorites.json"); - DesignColorFile = Path.Combine(ConfigDirectory, "design_colors.json"); - EphemeralConfigFile = Path.Combine(ConfigDirectory, "ephemeral_config.json"); - NpcAppearanceFile = Path.Combine(ConfigDirectory, "npc_appearance_data.json"); + ConfigDirectory = pi.ConfigDirectory.FullName; + ConfigFile = pi.ConfigFile.FullName; + AutomationFile = Path.Combine(ConfigDirectory, "automation.json"); + DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json"); + MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json"); + UnlockFileCustomize = Path.Combine(ConfigDirectory, "unlocks_customize.json"); + UnlockFileItems = Path.Combine(ConfigDirectory, "unlocks_items.json"); + DesignDirectory = Path.Combine(ConfigDirectory, "designs"); + FavoriteFile = Path.Combine(ConfigDirectory, "favorites.json"); + DesignColorFile = Path.Combine(ConfigDirectory, "design_colors.json"); + EphemeralConfigFile = Path.Combine(ConfigDirectory, "ephemeral_config.json"); + NpcAppearanceFile = Path.Combine(ConfigDirectory, "npc_appearance_data.json"); + CollectionOverrideFile = Path.Combine(ConfigDirectory, "collection_overrides.json"); } public IEnumerable Designs() diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index a5cc0ee..aca333e 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -12,6 +12,7 @@ using Glamourer.Gui.Tabs.AutomationTab; using Glamourer.Gui.Tabs.DebugTab; using Glamourer.Gui.Tabs.DesignTab; using Glamourer.Gui.Tabs.NpcTab; +using Glamourer.Gui.Tabs.SettingsTab; using Glamourer.Gui.Tabs.UnlocksTab; using Glamourer.Interop; using Glamourer.Interop.Penumbra; diff --git a/Glamourer/Services/TextureService.cs b/Glamourer/Services/TextureService.cs index 6539c7b..0619279 100644 --- a/Glamourer/Services/TextureService.cs +++ b/Glamourer/Services/TextureService.cs @@ -7,13 +7,10 @@ using Penumbra.GameData.Structs; namespace Glamourer.Services; -public sealed class TextureService : TextureCache, IDisposable +public sealed class TextureService(UiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) + : TextureCache(dataManager, textureProvider), IDisposable { - public TextureService(UiBuilder uiBuilder, IDataManager dataManager, ITextureProvider textureProvider) - : base(dataManager, textureProvider) - => _slotIcons = CreateSlotIcons(uiBuilder); - - private readonly IDalamudTextureWrap?[] _slotIcons; + private readonly IDalamudTextureWrap?[] _slotIcons = CreateSlotIcons(uiBuilder); public (nint, Vector2, bool) GetIcon(EquipItem item, EquipSlot slot) { diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a7f6d8..5825baf 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a7f6d86c9975a4892f58be3c629b7664e6c3733 +Subproject commit 5825bafb0602cf8a252581f21291c0d27e5561f0