Add preferred changed items to mods.

This commit is contained in:
Ottermandias 2025-03-01 22:21:36 +01:00
parent 13adbd5466
commit 509f11561a
7 changed files with 273 additions and 41 deletions

View file

@ -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;
@ -23,9 +24,11 @@ public enum ModDataChangeType : ushort
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<CustomItemId> 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<IdentifiedItem>().Select(i => i.Item.Id).Distinct().ToHashSet();
var newSet = new HashSet<CustomItemId>(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<CustomItemId>(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<CustomItemId> 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);
}
}

View file

@ -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;
@ -54,15 +55,16 @@ public sealed class Mod : IMod
public string Website { get; internal set; } = string.Empty;
public string Image { get; internal set; } = string.Empty;
public IReadOnlyList<string> ModTags { get; internal set; } = [];
public HashSet<CustomItemId> DefaultPreferredItems { get; internal set; } = [];
// Local Data
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public IReadOnlyList<string> LocalTags { get; internal set; } = [];
public string Note { get; internal set; } = string.Empty;
public HashSet<CustomItemId> PreferredChangedItems { get; internal set; } = [];
public bool Favorite { get; internal set; } = false;
// Options
public readonly DefaultSubMod Default;
public readonly List<IModGroup> Groups = [];

View file

@ -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<CustomItemId> preferredChangedItems = [];
var save = true;
if (File.Exists(dataFile))
try
@ -47,12 +51,17 @@ public readonly struct ModLocalData(Mod mod) : ISavable
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
preferredChangedItems = (json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values<ulong>().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));

View file

@ -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;
@ -56,6 +58,8 @@ public readonly struct ModMeta(Mod mod) : ISavable
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values<string>().OfType<string>();
var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values<ulong>().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))
{

View file

@ -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();
@ -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<ChangedItemsCache>(_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)

View file

@ -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" ]

View file

@ -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": [