From 509f11561aee52acd23a56f7c35d6f4494572384 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 22:21:36 +0100 Subject: [PATCH] Add preferred changed items to mods. --- Penumbra/Mods/Manager/ModDataEditor.cs | 158 ++++++++++++++++-- Penumbra/Mods/Mod.cs | 28 ++-- Penumbra/Mods/ModLocalData.cs | 27 ++- Penumbra/Mods/ModMeta.cs | 12 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 71 +++++++- schemas/local_mod_data-v3.json | 9 + schemas/mod_meta-v3.json | 9 + 7 files changed, 273 insertions(+), 41 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7c48205a..1349b525 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,7 +1,8 @@ using Dalamud.Utility; -using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -9,23 +10,25 @@ namespace Penumbra.Mods.Manager; [Flags] public enum ModDataChangeType : ushort { - None = 0x0000, - Name = 0x0001, - Author = 0x0002, - Description = 0x0004, - Version = 0x0008, - Website = 0x0010, - Deletion = 0x0020, - Migration = 0x0040, - ModTags = 0x0080, - ImportDate = 0x0100, - Favorite = 0x0200, - LocalTags = 0x0400, - Note = 0x0800, - Image = 0x1000, + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, + Image = 0x1000, + DefaultChangedItems = 0x2000, + PreferredChangedItems = 0x4000, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService { public SaveService SaveService => saveService; @@ -35,7 +38,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic string? website) { var mod = new Mod(directory); - mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); mod.Author = author != null ? new LowerString(author) : mod.Author; mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; @@ -175,4 +178,125 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}"); } } + + public void AddPreferredItem(Mod mod, CustomItemId id, bool toDefault, bool cleanExisting) + { + if (CleanExisting(mod.PreferredChangedItems)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (toDefault && CleanExisting(mod.DefaultPreferredItems)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + bool CleanExisting(HashSet items) + { + if (!items.Add(id)) + return false; + + if (!cleanExisting) + return true; + + var it1Exists = itemData.Primary.TryGetValue(id, out var it1); + var it2Exists = itemData.Secondary.TryGetValue(id, out var it2); + var it3Exists = itemData.Tertiary.TryGetValue(id, out var it3); + + foreach (var item in items.ToArray()) + { + if (item == id) + continue; + + if (it1Exists + && itemData.Primary.TryGetValue(item, out var oldItem1) + && oldItem1.PrimaryId == it1.PrimaryId + && oldItem1.Type == it1.Type) + items.Remove(item); + + else if (it2Exists + && itemData.Primary.TryGetValue(item, out var oldItem2) + && oldItem2.PrimaryId == it2.PrimaryId + && oldItem2.Type == it2.Type) + items.Remove(item); + + else if (it3Exists + && itemData.Primary.TryGetValue(item, out var oldItem3) + && oldItem3.PrimaryId == it3.PrimaryId + && oldItem3.Type == it3.Type) + items.Remove(item); + } + + return true; + } + } + + public void RemovePreferredItem(Mod mod, CustomItemId id, bool fromDefault) + { + if (!fromDefault && mod.PreferredChangedItems.Remove(id)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (fromDefault && mod.DefaultPreferredItems.Remove(id)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + } + + public void ClearInvalidPreferredItems(Mod mod) + { + var currentChangedItems = mod.ChangedItems.Values.OfType().Select(i => i.Item.Id).Distinct().ToHashSet(); + var newSet = new HashSet(mod.PreferredChangedItems.Count); + + if (CheckItems(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = newSet; + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + newSet = new HashSet(mod.DefaultPreferredItems.Count); + if (CheckItems(mod.DefaultPreferredItems)) + { + mod.DefaultPreferredItems = newSet; + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + return; + + bool CheckItems(HashSet set) + { + var changes = false; + foreach (var item in set) + { + if (currentChangedItems.Contains(item)) + newSet.Add(item); + else + changes = true; + } + + return changes; + } + } + + public void ResetPreferredItems(Mod mod) + { + if (mod.PreferredChangedItems.SetEquals(mod.DefaultPreferredItems)) + return; + + mod.PreferredChangedItems.Clear(); + mod.PreferredChangedItems.UnionWith(mod.DefaultPreferredItems); + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 9829d5a0..efd92631 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,6 +1,7 @@ using OtterGui; using OtterGui.Classes; using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -47,21 +48,22 @@ public sealed class Mod : IMod => Name.Text; // Meta Data - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public string Image { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = []; + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public string Image { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = []; + public HashSet DefaultPreferredItems { get; internal set; } = []; // Local Data - public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public IReadOnlyList LocalTags { get; internal set; } = []; - public string Note { get; internal set; } = string.Empty; - public bool Favorite { get; internal set; } = false; - + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; internal set; } = []; + public string Note { get; internal set; } = string.Empty; + public HashSet PreferredChangedItems { get; internal set; } = []; + public bool Favorite { get; internal set; } = false; // Options public readonly DefaultSubMod Default; @@ -110,5 +112,5 @@ public sealed class Mod : IMod public int TotalSwapCount { get; internal set; } public int TotalManipulations { get; internal set; } public ushort LastChangedItemsUpdate { get; internal set; } - public bool HasOptions { get; internal set; } + public bool HasOptions { get; internal set; } } diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index d3534391..cc20fad6 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -21,6 +22,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable { nameof(Mod.LocalTags), JToken.FromObject(mod.LocalTags) }, { nameof(Mod.Note), JToken.FromObject(mod.Note) }, { nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) }, + { nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -36,6 +38,8 @@ public readonly struct ModLocalData(Mod mod) : ISavable var favorite = false; var note = string.Empty; + HashSet preferredChangedItems = []; + var save = true; if (File.Exists(dataFile)) try @@ -43,16 +47,21 @@ public readonly struct ModLocalData(Mod mod) : ISavable var text = File.ReadAllText(dataFile); var json = JObject.Parse(text); - importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; - save = false; + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; + preferredChangedItems = (json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values().Select(i => (CustomItemId) i).ToHashSet() ?? mod.DefaultPreferredItems; + save = false; } catch (Exception e) { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } + else + { + preferredChangedItems = mod.DefaultPreferredItems; + } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -64,7 +73,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.ImportDate; } - changes |= ModLocalData.UpdateTags(mod, null, localTags); + changes |= UpdateTags(mod, null, localTags); if (mod.Favorite != favorite) { @@ -78,6 +87,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.Note; } + if (!preferredChangedItems.SetEquals(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = preferredChangedItems; + changes |= ModDataChangeType.PreferredChangedItems; + } + if (save) editor.SaveService.QueueSave(new ModLocalData(mod)); diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 0cebcf81..1b104af4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -25,6 +26,7 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.Version), JToken.FromObject(mod.Version) }, { nameof(Mod.Website), JToken.FromObject(mod.Website) }, { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, + { nameof(Mod.DefaultPreferredItems), JToken.FromObject(mod.DefaultPreferredItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -48,7 +50,7 @@ public readonly struct ModMeta(Mod mod) : ISavable var newFileVersion = json[nameof(FileVersion)]?.Value() ?? 0; // Empty name gets checked after loading and is not allowed. - var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; @@ -56,6 +58,8 @@ public readonly struct ModMeta(Mod mod) : ISavable var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values().Select(i => (CustomItemId)i).ToHashSet() + ?? []; ModDataChangeType changes = 0; if (mod.Name != newName) @@ -94,6 +98,12 @@ public readonly struct ModMeta(Mod mod) : ISavable mod.Website = newWebsite; } + if (!mod.DefaultPreferredItems.SetEquals(defaultItems)) + { + changes |= ModDataChangeType.DefaultChangedItems; + mod.DefaultPreferredItems = defaultItems; + } + if (newFileVersion != FileVersion) if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion)) { diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index f97e4d51..700f1d66 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.String; namespace Penumbra.UI.ModsTab; @@ -18,7 +19,8 @@ public class ModPanelChangedItemsTab( ModFileSystemSelector selector, ChangedItemDrawer drawer, ImGuiCacheService cacheService, - Configuration config) + Configuration config, + ModDataEditor dataEditor) : ITab, IUiService { private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); @@ -77,7 +79,7 @@ public class ModPanelChangedItemsTab( => new() { Child = true, - Text = ByteString.FromStringUnsafe(data.ToName(text), false), + Text = ByteString.FromStringUnsafe(data.ToName(text), false), ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), Icon = data.GetIcon().ToFlag(), Expandable = false, @@ -93,7 +95,11 @@ public class ModPanelChangedItemsTab( public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter, ChangedItemMode mode) { - if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset && _lastMode == mode) + if (mod == _lastSelected + && _lastSelected!.LastChangedItemsUpdate == _lastUpdate + && _filter == filter + && !_reset + && _lastMode == mode) return; _reset = false; @@ -138,6 +144,12 @@ public class ModPanelChangedItemsTab( { list.Sort((i1, i2) => { + // reversed + var preferred = _lastSelected.PreferredChangedItems.Contains(i2.Item.Id) + .CompareTo(_lastSelected.PreferredChangedItems.Contains(i1.Item.Id)); + if (preferred != 0) + return preferred; + // reversed var count = i2.Count.CompareTo(i1.Count); if (count != 0) @@ -217,6 +229,7 @@ public class ModPanelChangedItemsTab( .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); @@ -241,7 +254,6 @@ public class ModPanelChangedItemsTab( ImGui.TableNextColumn(); if (obj.Expandable) { - using var color = ImRaii.PushColor(ImGuiCol.Button, 0); if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, obj.Expanded ? "Hide the other items using the same model." : obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : @@ -253,6 +265,10 @@ public class ModPanelChangedItemsTab( cache.Reset(); } } + else if (obj is { Child: true, Data: IdentifiedItem item }) + { + DrawPreferredButton(item, idx); + } else { ImGui.Dummy(_buttonSize); @@ -267,6 +283,53 @@ public class ModPanelChangedItemsTab( DrawBaseContainer(obj, idx); } + private void DrawPreferredButton(IdentifiedItem item, int idx) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Star, "Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8, _buttonSize, + false, ImGui.GetColorU32(ImGuiCol.TextDisabled, 0.1f))) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true); + using var context = ImUtf8.PopupContextItem("StarContext"u8, ImGuiPopupFlags.MouseButtonRight); + if (!context) + return; + + if (cacheService.TryGetCache(_cacheId, out var cache)) + for (--idx; idx >= 0; --idx) + { + if (!cache.Data[idx].Expanded) + continue; + + if (cache.Data[idx].Data is IdentifiedItem it) + { + if (selector.Selected!.PreferredChangedItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Local Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, false); + if (selector.Selected!.DefaultPreferredItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Default Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, true); + } + + break; + } + + var enabled = !selector.Selected!.DefaultPreferredItems.Contains(item.Item.Id); + if (enabled) + { + if (ImUtf8.MenuItem("Add to Local and Default Preferred Changed Items"u8)) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, true, true); + } + else + { + if (ImUtf8.MenuItem("Remove from Default Preferred Changed Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, item.Item.Id, true); + } + + if (ImUtf8.MenuItem("Reset Local Preferred Items to Default"u8)) + dataEditor.ResetPreferredItems(selector.Selected!); + + if (ImUtf8.MenuItem("Clear Local and Default Preferred Items not Changed by the Mod"u8)) + dataEditor.ClearInvalidPreferredItems(selector.Selected!); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json index bf5d1311..c50e130e 100644 --- a/schemas/local_mod_data-v3.json +++ b/schemas/local_mod_data-v3.json @@ -26,6 +26,15 @@ "Favorite": { "description": "Whether the mod is favourited by the user.", "type": "boolean" + }, + "PreferredChangedItems": { + "description": "Preferred items to list as the main item of a group.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [ "FileVersion" ] diff --git a/schemas/mod_meta-v3.json b/schemas/mod_meta-v3.json index a926b49e..ed63a228 100644 --- a/schemas/mod_meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -43,6 +43,15 @@ "minLength": 1 }, "uniqueItems": true + }, + "DefaultPreferredItems": { + "description": "Default preferred items to list as the main item of a group managed by the mod creator.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [