Heavily improve changed item display.

This commit is contained in:
Ottermandias 2025-03-01 00:33:56 +01:00
parent 1ebe4099d6
commit deba8ac910
30 changed files with 360 additions and 126 deletions

@ -1 +1 @@
Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf

@ -1 +1 @@
Subproject commit bc339208d1d453582eb146533c572823146a4592 Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4

View file

@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
public void CloseMainWindow() public void CloseMainWindow()
=> _configWindow.IsOpen = false; => _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data)
{ {
if (ChangedItemClicked == null) if (ChangedItemClicked == null)
return; return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); var (type, id) = data.ToApiObject();
ChangedItemClicked.Invoke(button, type, id); ChangedItemClicked.Invoke(button, type, id);
} }
private void OnChangedItemHover(IIdentifiedObjectData? data) private void OnChangedItemHover(IIdentifiedObjectData data)
{ {
if (ChangedItemTooltip == null) if (ChangedItemTooltip == null)
return; return;
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); var (type, id) = data.ToApiObject();
ChangedItemTooltip.Invoke(type, id); ChangedItemTooltip.Invoke(type, id);
} }
} }

View file

@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
: throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
} }
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData?> data) : IReadOnlyDictionary<string, object?> private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
{ {
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); => data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();

View file

@ -23,7 +23,7 @@ public sealed class CollectionCache : IDisposable
private readonly CollectionCacheManager _manager; private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection; private readonly ModCollection _collection;
public readonly CollectionModData ModData = new(); public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = []; private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new(); public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources; public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta; public readonly MetaCache Meta;
@ -43,7 +43,7 @@ public sealed class CollectionCache : IDisposable
private int _changedItemsSaveCounter = -1; private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before. // Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
{ {
get get
{ {
@ -441,7 +441,7 @@ public sealed class CollectionCache : IDisposable
// Skip IMCs because they would result in far too many false-positive items, // Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant. // since they are per set instead of per item-slot/item/variant.
var identifier = _manager.MetaFileManager.Identifier; var identifier = _manager.MetaFileManager.Identifier;
var items = new SortedList<string, IIdentifiedObjectData?>(512); var items = new SortedList<string, IIdentifiedObjectData>(512);
void AddItems(IMod mod) void AddItems(IMod mod)
{ {

View file

@ -46,8 +46,8 @@ public partial class ModCollection
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>(); => _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>(); => _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>(); => _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();

View file

@ -12,7 +12,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item> /// <item>Parameter is the clicked object data if any. </item>
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick)) public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{ {
public enum Priority public enum Priority
{ {

View file

@ -10,7 +10,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item> /// <item>Parameter is the hovered object data if any. </item>
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover)) public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{ {
public enum Priority public enum Priority
{ {

View file

@ -31,7 +31,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac
public override string ToString() public override string ToString()
=> $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
// Nothing specific // Nothing specific
} }

View file

@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
public Gender Gender public Gender Gender
=> GenderRace.Split().Item1; => GenderRace.Split().Item1;
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot));
public MetaIndex FileIndex() public MetaIndex FileIndex()

View file

@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable<EqpIdentifier> public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable<EqpIdentifier>
{ {
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot));
public MetaIndex FileIndex() public MetaIndex FileIndex()

View file

@ -24,17 +24,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
public Gender Gender public Gender Gender
=> GenderRace.Split().Item1; => GenderRace.Split().Item1;
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
switch (Slot) switch (Slot)
{ {
case EstType.Hair: case EstType.Hair:
changedItems.TryAdd( changedItems.UpdateCountOrSet(
$"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName());
break; break;
case EstType.Face: case EstType.Face:
changedItems.TryAdd( changedItems.UpdateCountOrSet(
$"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName());
break; break;
case EstType.Body: case EstType.Body:
identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body));

View file

@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
public override string ToString() public override string ToString()
=> $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
var path = Type switch var path = Type switch
{ {
@ -86,9 +86,9 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
if (path.Length > 0) if (path.Length > 0)
identifier.Identify(changedItems, path); identifier.Identify(changedItems, path);
else if (Type is GlobalEqpType.DoNotHideVieraHats) else if (Type is GlobalEqpType.DoNotHideVieraHats)
changedItems["All Hats for Viera"] = null; changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName());
else if (Type is GlobalEqpType.DoNotHideHrothgarHats) else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
changedItems["All Hats for Hrothgar"] = null; changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName());
} }
public MetaIndex FileIndex() public MetaIndex FileIndex()

View file

@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable<GmpIdentifier> public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable<GmpIdentifier>
{ {
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head));
public MetaIndex FileIndex() public MetaIndex FileIndex()

View file

@ -19,7 +19,7 @@ public enum MetaManipulationType : byte
public interface IMetaIdentifier public interface IMetaIdentifier
{ {
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems); public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems);
public MetaIndex FileIndex(); public MetaIndex FileIndex();

View file

@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier(
: this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown)
{ } { }
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> AddChangedItems(identifier, changedItems, false); => AddChangedItems(identifier, changedItems, false);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems, bool allVariants) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems, bool allVariants)
{ {
var path = ObjectType switch var path = ObjectType switch
{ {

View file

@ -8,8 +8,8 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier
{ {
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); => changedItems.UpdateCountOrSet($"{SubRace.ToName()} {Attribute.ToFullString()}", () => new IdentifiedName());
public MetaIndex FileIndex() public MetaIndex FileIndex()
=> MetaIndex.HumanCmp; => MetaIndex.HumanCmp;

View file

@ -136,7 +136,7 @@ public sealed class CombiningModGroup : IModGroup
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
=> Data[setting.AsIndex].AddDataTo(redirections, manipulations); => Data[setting.AsIndex].AddDataTo(redirections, manipulations);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
foreach (var container in DataContainers) foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems); identifier.AddChangedItems(container, changedItems);

View file

@ -53,7 +53,7 @@ public interface IModGroup
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations); public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems); public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems);
/// <summary> Ensure that a value is valid for a group. </summary> /// <summary> Ensure that a value is valid for a group. </summary>
public Setting FixSetting(Setting setting); public Setting FixSetting(Setting setting);

View file

@ -131,7 +131,7 @@ public class ImcModGroup(Mod mod) : IModGroup
} }
} }
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> Identifier.AddChangedItems(identifier, changedItems, AllVariants); => Identifier.AddChangedItems(identifier, changedItems, AllVariants);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)

View file

@ -122,7 +122,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
} }
} }
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
foreach (var container in DataContainers) foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems); identifier.AddChangedItems(container, changedItems);

View file

@ -107,7 +107,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
} }
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems) public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
foreach (var container in DataContainers) foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems); identifier.AddChangedItems(container, changedItems);

View file

@ -139,6 +139,7 @@ public class ModCacheManager : IDisposable, IService
mod.ChangedItems.RemoveMachinistOffhands(); mod.ChangedItems.RemoveMachinistOffhands();
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
++mod.LastChangedItemsUpdate;
} }
private static void UpdateCounts(Mod mod) private static void UpdateCounts(Mod mod)

View file

@ -101,13 +101,14 @@ public sealed class Mod : IMod
} }
// Cache // Cache
public readonly SortedList<string, IIdentifiedObjectData?> ChangedItems = new(); public readonly SortedList<string, IIdentifiedObjectData> ChangedItems = new();
public string LowerChangedItemsString { get; internal set; } = string.Empty; public string LowerChangedItemsString { get; internal set; } = string.Empty;
public string AllTagsLower { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty;
public int TotalFileCount { get; internal set; } public int TotalFileCount { get; internal set; }
public int TotalSwapCount { get; internal set; } public int TotalSwapCount { get; internal set; }
public int TotalManipulations { 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; }
} }

View file

@ -25,7 +25,6 @@ public class ResourceTreeViewer(
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
private readonly CommunicatorService _communicator = communicator;
private readonly HashSet<nint> _unfolded = []; private readonly HashSet<nint> _unfolded = [];
private readonly Dictionary<nint, NodeVisibility> _filterCache = []; private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
@ -278,7 +277,7 @@ public class ResourceTreeViewer(
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); ImGui.SetClipboardText(resourceNode.FullPath.ToPath());
if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl)
_communicator.SelectTab.Invoke(TabType.Mods, mod); communicator.SelectTab.Invoke(TabType.Mods, mod);
ImGuiUtil.HoverTooltip( ImGuiUtil.HoverTooltip(
$"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");

View file

@ -9,6 +9,7 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Services; using Penumbra.Services;
@ -86,18 +87,20 @@ public class ChangedItemDrawer : IDisposable, IUiService
} }
/// <summary> Check if a changed item should be drawn based on its category. </summary> /// <summary> Check if a changed item should be drawn based on its category. </summary>
public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) public bool FilterChangedItem(string name, IIdentifiedObjectData data, LowerString filter)
=> (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags
|| _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag()))
&& (filter.IsEmpty || !data.IsFilteredOut(name, filter)); && (filter.IsEmpty || !data.IsFilteredOut(name, filter));
/// <summary> Draw the icon corresponding to the category of a changed item. </summary> /// <summary> Draw the icon corresponding to the category of a changed item. </summary>
public void DrawCategoryIcon(IIdentifiedObjectData? data) public void DrawCategoryIcon(IIdentifiedObjectData data, float height)
=> DrawCategoryIcon(data.GetIcon().ToFlag()); => DrawCategoryIcon(data.GetIcon().ToFlag(), height);
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType)
=> DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight());
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height)
{ {
var height = ImGui.GetFrameHeight();
if (!_icons.TryGetValue(iconFlagType, out var icon)) if (!_icons.TryGetValue(iconFlagType, out var icon))
{ {
ImGui.Dummy(new Vector2(height)); ImGui.Dummy(new Vector2(height));
@ -114,50 +117,50 @@ public class ChangedItemDrawer : IDisposable, IUiService
} }
} }
/// <summary> public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked)
/// Draw a changed item, invoking the Api-Events for clicks and tooltips.
/// Also draw the item ID in grey if requested.
/// </summary>
public void DrawChangedItem(string name, IIdentifiedObjectData? data)
{ {
name = data?.ToName(name) ?? name; var ret = leftClicked ? MouseButton.Left : MouseButton.None;
using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
.Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
{ if (ret != MouseButton.None)
var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) _communicator.ChangedItemClick.Invoke(ret, data);
? MouseButton.Left if (!ImGui.IsItemHovered())
: MouseButton.None; return;
ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
if (ret != MouseButton.None)
_communicator.ChangedItemClick.Invoke(ret, data);
}
if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) using var tt = ImUtf8.Tooltip();
{ if (data.Count == 1)
// We can not be sure that any subscriber actually prints something in any case. ImUtf8.Text("This item is changed through a single effective change.\n");
// Circumvent ugly blank tooltip with less-ugly useless tooltip. else
using var tt = ImRaii.Tooltip(); ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n");
using (ImRaii.Group()) ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale);
{ ImGui.Separator();
_communicator.ChangedItemHover.Invoke(data); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale);
} _communicator.ChangedItemHover.Invoke(data);
if (ImGui.GetItemRectSize() == Vector2.Zero)
ImGui.TextUnformatted("No actions available.");
}
} }
/// <summary> Draw the model information, right-justified. </summary> /// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(IIdentifiedObjectData? data) public static void DrawModelData(IIdentifiedObjectData data, float height)
{ {
var additionalData = data?.AdditionalData ?? string.Empty; var additionalData = data.AdditionalData;
if (additionalData.Length == 0) if (additionalData.Length == 0)
return; return;
ImGui.SameLine(ImGui.GetContentRegionAvail().X); ImGui.SameLine();
ImGui.AlignTextToFramePadding(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2);
ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X);
}
/// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(ReadOnlySpan<byte> text, float height)
{
if (text.Length == 0)
return;
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2);
ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X);
} }
/// <summary> Draw a header line with the different icon types to filter them. </summary> /// <summary> Draw a header line with the different icon types to filter them. </summary>
@ -276,7 +279,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
return true; return true;
} }
private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider)
{ {
var unk = gameData.GetFile<TexFile>("ui/uld/levelup2_hr1.tex"); var unk = gameData.GetFile<TexFile>("ui/uld/levelup2_hr1.tex");
if (unk == null) if (unk == null)

View file

@ -1,47 +1,268 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Mods;
using Penumbra.String;
namespace Penumbra.UI.ModsTab; namespace Penumbra.UI.ModsTab;
public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) : ITab, IUiService public class ModPanelChangedItemsTab(
ModFileSystemSelector selector,
ChangedItemDrawer drawer,
ImGuiCacheService cacheService,
EphemeralConfig config)
: ITab, IUiService
{ {
private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId();
private class ChangedItemsCache
{
private Mod? _lastSelected;
private ushort _lastUpdate;
private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags;
private bool _reset;
public readonly List<Container> Data = [];
public bool AnyExpandable { get; private set; }
public record struct Container
{
public IIdentifiedObjectData Data;
public ByteString Text;
public ByteString ModelData;
public uint Id;
public int Children;
public ChangedItemIconFlag Icon;
public bool Expandable;
public bool Expanded;
public bool Child;
public static Container Single(string text, IIdentifiedObjectData data)
=> new()
{
Child = false,
Text = ByteString.FromStringUnsafe(data.ToName(text), false),
ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false),
Icon = data.GetIcon().ToFlag(),
Expandable = false,
Expanded = false,
Data = data,
Id = 0,
Children = 0,
};
public static Container Parent(string text, IIdentifiedObjectData data, uint id, int children, bool expanded)
=> new()
{
Child = false,
Text = ByteString.FromStringUnsafe(data.ToName(text), false),
ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false),
Icon = data.GetIcon().ToFlag(),
Expandable = true,
Expanded = expanded,
Data = data,
Id = id,
Children = children,
};
public static Container Indent(string text, IIdentifiedObjectData data)
=> new()
{
Child = true,
Text = ByteString.FromStringUnsafe(data.ToName(text), false),
ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false),
Icon = data.GetIcon().ToFlag(),
Expandable = false,
Expanded = false,
Data = data,
Id = 0,
Children = 0,
};
}
public void Reset()
=> _reset = true;
public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter)
{
if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset)
return;
_reset = false;
Data.Clear();
AnyExpandable = false;
_lastSelected = mod;
_filter = filter;
if (_lastSelected == null)
return;
_lastUpdate = _lastSelected.LastChangedItemsUpdate;
var tmp = new Dictionary<(PrimaryId, FullEquipType), List<IdentifiedItem>>();
foreach (var (s, i) in _lastSelected.ChangedItems)
{
if (i is not IdentifiedItem item)
continue;
if (!drawer.FilterChangedItem(s, item, LowerString.Empty))
continue;
if (tmp.TryGetValue((item.Item.PrimaryId, item.Item.Type), out var p))
p.Add(item);
else
tmp[(item.Item.PrimaryId, item.Item.Type)] = [item];
}
foreach (var list in tmp.Values)
{
list.Sort((i1, i2) =>
{
// reversed
var count = i2.Count.CompareTo(i1.Count);
if (count != 0)
return count;
return string.Compare(i1.Item.Name, i2.Item.Name, StringComparison.Ordinal);
});
}
var sortedTmp = tmp.Values.OrderBy(s => s[0].Item.Name).ToArray();
var sortedTmpIdx = 0;
foreach (var (s, i) in _lastSelected.ChangedItems)
{
if (i is IdentifiedItem)
continue;
if (!drawer.FilterChangedItem(s, i, LowerString.Empty))
continue;
while (sortedTmpIdx < sortedTmp.Length
&& string.Compare(sortedTmp[sortedTmpIdx][0].Item.Name, s, StringComparison.Ordinal) <= 0)
AddList(sortedTmp[sortedTmpIdx++]);
Data.Add(Container.Single(s, i));
}
for (; sortedTmpIdx < sortedTmp.Length; ++sortedTmpIdx)
AddList(sortedTmp[sortedTmpIdx]);
return;
void AddList(List<IdentifiedItem> list)
{
var mainItem = list[0];
if (list.Count == 1)
{
Data.Add(Container.Single(mainItem.Item.Name, mainItem));
}
else
{
var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}");
var expanded = ImGui.GetStateStorage().GetBool(id, false);
Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded));
AnyExpandable = true;
if (!expanded)
return;
foreach (var item in list.Skip(1))
Data.Add(Container.Indent(item.Item.Name, item));
}
}
}
}
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Changed Items"u8; => "Changed Items"u8;
public bool IsVisible public bool IsVisible
=> selector.Selected!.ChangedItems.Count > 0; => selector.Selected!.ChangedItems.Count > 0;
private ImGuiStoragePtr _stateStorage;
private Vector2 _buttonSize;
public void DrawContent() public void DrawContent()
{ {
if (cacheService.Cache(_cacheId, () => (new ChangedItemsCache(), "ModPanelChangedItemsCache")) is not { } cache)
return;
drawer.DrawTypeFilter(); drawer.DrawTypeFilter();
_stateStorage = ImGui.GetStateStorage();
cache.Update(selector.Selected, drawer, config.ChangedItemFilter);
ImGui.Separator(); ImGui.Separator();
using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight());
using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero)
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FramePadding, Vector2.Zero)
.Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f));
using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY,
new Vector2(ImGui.GetContentRegionAvail().X, -1)); new Vector2(ImGui.GetContentRegionAvail().X, -1));
if (!table) if (!table)
return; return;
var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); if (cache.AnyExpandable)
var height = ImGui.GetFrameHeightWithSpacing(); {
ImGui.TableNextColumn(); ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y);
var skips = ImGuiClip.GetNecessarySkips(height); ImUtf8.TableSetupColumn("##text"u8, ImGuiTableColumnFlags.WidthStretch);
var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); ImGuiClip.ClippedDraw(cache.Data, DrawContainerExpandable, _buttonSize.Y);
ImGuiClip.DrawEndDummy(remainder, height); }
else
{
ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing());
}
} }
private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) private void DrawContainerExpandable(ChangedItemsCache.Container obj, int idx)
=> drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); {
using var id = ImUtf8.PushId(idx);
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." :
"Show one other item using the same model.",
_buttonSize))
{
_stateStorage.SetBool(obj.Id, !obj.Expanded);
if (cacheService.TryGetCache<ChangedItemsCache>(_cacheId, out var cache))
cache.Reset();
}
}
else
{
ImGui.Dummy(_buttonSize);
}
private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) DrawBaseContainer(obj, idx);
}
private void DrawContainer(ChangedItemsCache.Container obj, int idx)
{
using var id = ImUtf8.PushId(idx);
DrawBaseContainer(obj, idx);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
drawer.DrawCategoryIcon(kvp.Data); using var indent = ImRaii.PushIndent(1, obj.Child);
ImGui.SameLine(); drawer.DrawCategoryIcon(obj.Icon, _buttonSize.Y);
drawer.DrawChangedItem(kvp.Name, kvp.Data); ImGui.SameLine(0, 0);
ChangedItemDrawer.DrawModelData(kvp.Data); var clicked = ImUtf8.Selectable(obj.Text.Span, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 });
drawer.ChangedItemHandling(obj.Data, clicked);
ChangedItemDrawer.DrawModelData(obj.ModelData.Span, _buttonSize.Y);
} }
} }

View file

@ -3,6 +3,7 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
@ -26,30 +27,36 @@ public class ChangedItemsTab(
private LowerString _changedItemFilter = LowerString.Empty; private LowerString _changedItemFilter = LowerString.Empty;
private LowerString _changedItemModFilter = LowerString.Empty; private LowerString _changedItemModFilter = LowerString.Empty;
private Vector2 _buttonSize;
public void DrawContent() public void DrawContent()
{ {
collectionHeader.Draw(true); collectionHeader.Draw(true);
drawer.DrawTypeFilter(); drawer.DrawTypeFilter();
var varWidth = DrawFilters(); var varWidth = DrawFilters();
using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); using var child = ImUtf8.Child("##changedItemsChild"u8, -Vector2.One);
if (!child) if (!child)
return; return;
var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight());
var skips = ImGuiClip.GetNecessarySkips(height); using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero)
using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FramePadding, Vector2.Zero)
.Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f));
var skips = ImGuiClip.GetNecessarySkips(_buttonSize.Y);
using var list = ImUtf8.Table("##changedItems"u8, 3, ImGuiTableFlags.RowBg, -Vector2.One);
if (!list) if (!list)
return; return;
const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed;
ImGui.TableSetupColumn("items", flags, 450 * UiHelpers.Scale); ImUtf8.TableSetupColumn("items"u8, flags, 450 * UiHelpers.Scale);
ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); ImUtf8.TableSetupColumn("mods"u8, flags, varWidth - 140 * UiHelpers.Scale);
ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); ImUtf8.TableSetupColumn("id"u8, flags, 140 * UiHelpers.Scale);
var items = collectionManager.Active.Current.ChangedItems; var items = collectionManager.Active.Current.ChangedItems;
var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
ImGuiClip.DrawEndDummy(rest, height); ImGuiClip.DrawEndDummy(rest, _buttonSize.Y);
} }
/// <summary> Draw a pair of filters and return the variable width of the flexible column. </summary> /// <summary> Draw a pair of filters and return the variable width of the flexible column. </summary>
@ -67,22 +74,25 @@ public class ChangedItemsTab(
} }
/// <summary> Apply the current filters. </summary> /// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item) private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData)> item)
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary> /// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item) private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData)> item)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
drawer.DrawCategoryIcon(item.Value.Item2); drawer.DrawCategoryIcon(item.Value.Item2, _buttonSize.Y);
ImGui.SameLine(); ImGui.SameLine(0, 0);
drawer.DrawChangedItem(item.Key, item.Value.Item2); var name = item.Value.Item2.ToName(item.Key);
var clicked = ImUtf8.Selectable(name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 });
drawer.ChangedItemHandling(item.Value.Item2, clicked);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawModColumn(item.Value.Item1); DrawModColumn(item.Value.Item1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ChangedItemDrawer.DrawModelData(item.Value.Item2); ChangedItemDrawer.DrawModelData(item.Value.Item2, _buttonSize.Y);
} }
private void DrawModColumn(SingleArray<IMod> mods) private void DrawModColumn(SingleArray<IMod> mods)
@ -90,19 +100,18 @@ public class ChangedItemsTab(
if (mods.Count <= 0) if (mods.Count <= 0)
return; return;
var first = mods[0]; var first = mods[0];
using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); if (ImUtf8.Selectable(first.Name.Text, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 })
if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
&& ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyCtrl
&& first is Mod mod) && first is Mod mod)
communicator.SelectTab.Invoke(TabType.Mods, mod); communicator.SelectTab.Invoke(TabType.Mods, mod);
if (ImGui.IsItemHovered()) if (!ImGui.IsItemHovered())
{ return;
using var _ = ImRaii.Tooltip();
ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); using var _ = ImRaii.Tooltip();
if (mods.Count > 1) ImUtf8.Text("Hold Control and click to jump to mod.\n"u8);
ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); if (mods.Count > 1)
} ImUtf8.Text("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name)));
} }
} }

View file

@ -748,7 +748,7 @@ public class DebugTab : Window, ITab, IUiService
} }
private string _changedItemPath = string.Empty; private string _changedItemPath = string.Empty;
private readonly Dictionary<string, IIdentifiedObjectData?> _changedItems = []; private readonly Dictionary<string, IIdentifiedObjectData> _changedItems = [];
private void DrawChangedItemTest() private void DrawChangedItemTest()
{ {

View file

@ -10,7 +10,7 @@ namespace Penumbra.Util;
public static class IdentifierExtensions public static class IdentifierExtensions
{ {
public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
IDictionary<string, IIdentifiedObjectData?> changedItems) IDictionary<string, IIdentifiedObjectData> changedItems)
{ {
foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys))
identifier.Identify(changedItems, gamePath.ToString()); identifier.Identify(changedItems, gamePath.ToString());
@ -19,7 +19,7 @@ public static class IdentifierExtensions
manip.AddChangedItems(identifier, changedItems); manip.AddChangedItems(identifier, changedItems);
} }
public static void RemoveMachinistOffhands(this SortedList<string, IIdentifiedObjectData?> changedItems) public static void RemoveMachinistOffhands(this SortedList<string, IIdentifiedObjectData> changedItems)
{ {
for (var i = 0; i < changedItems.Count; i++) for (var i = 0; i < changedItems.Count; i++)
{ {
@ -31,7 +31,7 @@ public static class IdentifierExtensions
} }
} }
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> changedItems) public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> changedItems)
{ {
for (var i = 0; i < changedItems.Count; i++) for (var i = 0; i < changedItems.Count; i++)
{ {