Start rework of identified objects.

This commit is contained in:
Ottermandias 2024-08-03 22:45:38 +02:00
parent 4454ac48da
commit da3f3b8df3
41 changed files with 342 additions and 389 deletions

@ -1 +1 @@
Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1
Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0

@ -1 +1 @@
Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c
Subproject commit 44427ad0149059ab5ccb4e4a2f42a1a43423e4c5

View file

@ -36,7 +36,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) :
collection = ModCollection.Empty;
if (collection.HasCache)
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject());
Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded.");
return [];

View file

@ -134,6 +134,6 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
public Dictionary<string, object?> GetChangedItems(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject())
: [];
}

View file

@ -1,7 +1,7 @@
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable
public void CloseMainWindow()
=> _configWindow.IsOpen = false;
private void OnChangedItemClick(MouseButton button, object? data)
private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data)
{
if (ChangedItemClicked == null)
return;
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
ChangedItemClicked.Invoke(button, type, id);
}
private void OnChangedItemHover(object? data)
private void OnChangedItemHover(IIdentifiedObjectData? data)
{
if (ChangedItemTooltip == null)
return;
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data);
var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0);
ChangedItemTooltip.Invoke(type, id);
}
}

View file

@ -8,7 +8,7 @@ using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Data;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Penumbra.Api.IpcTester;
@ -17,10 +17,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
{
private int _objectIdx;
private string _collectionIdString = string.Empty;
private Guid? _collectionId = null;
private bool _allowCreation = true;
private bool _allowDeletion = true;
private ApiCollectionType _type = ApiCollectionType.Yourself;
private Guid? _collectionId;
private bool _allowCreation = true;
private bool _allowDeletion = true;
private ApiCollectionType _type = ApiCollectionType.Yourself;
private Dictionary<Guid, string> _collections = [];
private (string, ChangedItemType, uint)[] _changedItems = [];
@ -116,7 +116,7 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty));
_changedItems = items.Select(kvp =>
{
var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value);
var (type, id) = kvp.Value.ToApiObject();
return (kvp.Key, type, id);
}).ToArray();
ImGui.OpenPopup("Changed Item List");
@ -130,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
if (!p)
return;
using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit))
{
if (t)
if (table)
ImGuiClip.ClippedDraw(_changedItems, t =>
{
ImGuiUtil.DrawTableColumn(t.Item1);

View file

@ -6,6 +6,7 @@ using Penumbra.Communication;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.Util;
using Penumbra.GameData.Data;
namespace Penumbra.Collections.Cache;
@ -18,14 +19,14 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
/// </summary>
public sealed class CollectionCache : IDisposable
{
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection;
public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
public int Calculating = -1;
@ -41,7 +42,7 @@ public sealed class CollectionCache : IDisposable
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
{
get
{
@ -412,7 +413,7 @@ public sealed class CollectionCache : IDisposable
// 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.
var identifier = _manager.MetaFileManager.Identifier;
var items = new SortedList<string, object?>(512);
var items = new SortedList<string, IIdentifiedObjectData?>(512);
void AddItems(IMod mod)
{
@ -421,8 +422,8 @@ public sealed class CollectionCache : IDisposable
if (!_changedItems.TryGetValue(name, out var data))
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
else if (!data.Item1.Contains(mod))
_changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj);
else if (obj is int x && data.Item2 is int y)
_changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj);
else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y)
_changedItems[name] = (data.Item1, x + y);
}

View file

@ -1,8 +1,8 @@
using OtterGui.Classes;
using Penumbra.Mods;
using Penumbra.Meta.Files;
using Penumbra.String.Classes;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Data;
using Penumbra.Mods.Editor;
namespace Penumbra.Collections;
@ -46,8 +46,8 @@ public partial class ModCollection
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, object?)>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();

View file

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

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api.Api;
using Penumbra.GameData.Data;
namespace Penumbra.Communication;
@ -9,7 +10,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{

View file

@ -23,24 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable, IService
[JsonIgnore]
private readonly ModPathChanged _modPathChanged;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public bool AdvancedEditingOpen { get; set; } = false;
public bool ForceRedrawOnFileChange { get; set; } = false;
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
public bool DebugSeparateWindow { get; set; } = false;
public int TutorialStep { get; set; } = 0;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool EnableResourceWatcher { get; set; } = false;
public bool OnlyAddMatchingResources { get; set; } = true;
public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes;
public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories;
public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords;
public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment;
public TabType SelectedTab { get; set; } = TabType.Settings;
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty;
public bool AdvancedEditingOpen { get; set; } = false;
public bool ForceRedrawOnFileChange { get; set; } = false;
/// <summary>
/// Load the current configuration.

View file

@ -345,7 +345,7 @@ internal unsafe partial record ResolveContext(
_ => string.Empty,
}
+ item.Name;
return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item));
return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag());
}
var dataFromPath = GuessUiDataFromPath(gamePath);
@ -353,8 +353,8 @@ internal unsafe partial record ResolveContext(
return dataFromPath;
return isEquipment
? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot()))
: new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown);
? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag())
: new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
}
internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath)
@ -362,13 +362,13 @@ internal unsafe partial record ResolveContext(
foreach (var obj in Global.Identifier.Identify(gamePath.ToString()))
{
var name = obj.Key;
if (name.StartsWith("Customization:"))
if (obj.Value is IdentifiedCustomization)
name = name[14..].Trim();
if (name != "Unknown")
return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value));
return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag());
}
return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown);
return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown);
}
private static string? SafeGet(ReadOnlySpan<string> array, Index index)

View file

@ -1,7 +1,7 @@
using Penumbra.Api.Enums;
using Penumbra.String;
using Penumbra.String.Classes;
using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon;
using Penumbra.UI;
namespace Penumbra.Interop.ResourceTree;
@ -9,7 +9,7 @@ public class ResourceNode : ICloneable
{
public string? Name;
public string? FallbackName;
public ChangedItemIcon Icon;
public ChangedItemIconFlag IconFlag;
public readonly ResourceType Type;
public readonly nint ObjectAddress;
public readonly nint ResourceHandle;
@ -51,7 +51,7 @@ public class ResourceNode : ICloneable
{
Name = other.Name;
FallbackName = other.FallbackName;
Icon = other.Icon;
IconFlag = other.IconFlag;
Type = other.Type;
ObjectAddress = other.ObjectAddress;
ResourceHandle = other.ResourceHandle;
@ -79,7 +79,7 @@ public class ResourceNode : ICloneable
public void SetUiData(UiData uiData)
{
Name = uiData.Name;
Icon = uiData.Icon;
IconFlag = uiData.IconFlag;
}
public void PrependName(string prefix)
@ -88,9 +88,9 @@ public class ResourceNode : ICloneable
Name = prefix + Name;
}
public readonly record struct UiData(string? Name, ChangedItemIcon Icon)
public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag)
{
public UiData PrependName(string prefix)
=> Name == null ? this : new UiData(prefix + Name, Icon);
=> Name == null ? this : new UiData(prefix + Name, IconFlag);
}
}

View file

@ -174,7 +174,7 @@ public class ResourceTree
{
pbdNode = pbdNode.Clone();
pbdNode.FallbackName = "Racial Deformer";
pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
pbdNode.IconFlag = ChangedItemIconFlag.Customization;
}
Nodes.Add(pbdNode);
@ -192,7 +192,7 @@ public class ResourceTree
{
decalNode = decalNode.Clone();
decalNode.FallbackName = "Face Decal";
decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
decalNode.IconFlag = ChangedItemIconFlag.Customization;
}
Nodes.Add(decalNode);
@ -209,7 +209,7 @@ public class ResourceTree
{
legacyDecalNode = legacyDecalNode.Clone();
legacyDecalNode.FallbackName = "Legacy Body Decal";
legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization;
legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization;
}
Nodes.Add(legacyDecalNode);

View file

@ -67,7 +67,7 @@ internal static class ResourceTreeApiHelper
continue;
var fullPath = node.FullPath.ToPath();
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon)));
resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)node.IconFlag.ToApiIcon()));
}
}
@ -106,7 +106,7 @@ internal static class ResourceTreeApiHelper
var ret = new JObject
{
[nameof(ResourceNodeDto.Type)] = new JValue(node.Type),
[nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)),
[nameof(ResourceNodeDto.Icon)] = new JValue(node.IconFlag.ToApiIcon()),
[nameof(ResourceNodeDto.Name)] = node.Name,
[nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(),
[nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(),

View file

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

View file

@ -24,7 +24,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
public Gender Gender
=> GenderRace.Split().Item1;
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
{
switch (Slot)
{

View file

@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
public override string ToString()
=> $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
{
var path = Type switch
{

View file

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

View file

@ -18,7 +18,7 @@ public enum MetaManipulationType : byte
public interface IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems);
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)
{ }
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
=> AddChangedItems(identifier, changedItems, false);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems, bool allVariants)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems, bool allVariants)
{
var path = ObjectType switch
{

View file

@ -1,4 +1,3 @@
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
@ -9,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, object?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
=> changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null);
public MetaIndex FileIndex()

View file

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

View file

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

View file

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

View file

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

View file

@ -186,7 +186,7 @@ public static class EquipmentSwap
PrimaryId idTo, byte mtrlTo)
{
var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr);
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr);
var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom);
var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo);
var meta = new MetaSwap<EqdpIdentifier, EqdpEntryInternal>(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier,
@ -255,7 +255,8 @@ public static class EquipmentSwap
{
items = identifier.Identify(slotFrom.IsEquipment()
? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)
: GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType<EquipItem>()
: GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom))
.Select(kvp => kvp.Value).OfType<IdentifiedItem>().Select(i => i.Item)
.ToArray();
variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray();
}

View file

@ -1,5 +1,6 @@
using OtterGui;
using OtterGui.Classes;
using Penumbra.GameData.Data;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
@ -100,7 +101,7 @@ public sealed class Mod : IMod
}
// Cache
public readonly SortedList<string, object?> ChangedItems = new();
public readonly SortedList<string, IIdentifiedObjectData?> ChangedItems = new();
public string LowerChangedItemsString { get; internal set; } = string.Empty;
public string AllTagsLower { get; internal set; } = string.Empty;

View file

@ -21,6 +21,8 @@ using Penumbra.GameData.Enums;
using Penumbra.UI;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using System.Xml.Linq;
using Dalamud.Plugin.Services;
using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks;
using Penumbra.Interop.Hooks.ResourceLoading;
@ -109,16 +111,17 @@ public class Penumbra : IDalamudPlugin
private void SetupApi()
{
_services.GetService<IpcProviders>();
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
_communicatorService.ChangedItemHover.Subscribe(it =>
{
if (it is (Item, FullEquipType))
if (it is IdentifiedItem)
ImGui.TextUnformatted("Left Click to create an item link in chat.");
}, ChangedItemHover.Priority.Link);
_communicatorService.ChangedItemClick.Subscribe((button, it) =>
{
if (button == MouseButton.Left && it is (Item item, FullEquipType type))
Messager.LinkItem(item);
if (button == MouseButton.Left && it is IdentifiedItem item && itemSheet.GetRow(item.Item.ItemId.Id) is { } i)
Messager.LinkItem(i);
}, ChangedItemClick.Priority.Link);
}

View file

@ -115,7 +115,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu
_data["ResourceWatcherRecordTypes"]?.ToObject<RecordType>() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
_config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject<CollectionsTab.PanelMode>() ?? _config.Ephemeral.CollectionPanel;
_config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject<TabType>() ?? _config.Ephemeral.SelectedTab;
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemDrawer.ChangedItemIcon>()
_config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject<ChangedItemIconFlag>()
?? _config.Ephemeral.ChangedItemFilter;
_config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject<bool>() ?? _config.Ephemeral.FixMainWindow;
_config.Ephemeral.Save();

View file

@ -135,7 +135,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService
: FilterComboCache<(EquipItem Item, bool InMod)>(() =>
{
var list = data.ByType[type];
if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type))
if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type))
return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList();
return list.Select(i => (i, false)).ToList();

View file

@ -27,7 +27,7 @@ public class ResourceTreeViewer
private readonly Dictionary<nint, NodeVisibility> _filterCache;
private TreeCategory _categoryFilter;
private ChangedItemDrawer.ChangedItemIcon _typeFilter;
private ChangedItemIconFlag _typeFilter;
private string _nameFilter;
private string _nodeFilter;
@ -48,7 +48,7 @@ public class ResourceTreeViewer
_filterCache = [];
_categoryFilter = AllCategories;
_typeFilter = ChangedItemDrawer.AllFlags;
_typeFilter = ChangedItemFlagExtensions.AllFlags;
_nameFilter = string.Empty;
_nodeFilter = string.Empty;
}
@ -185,13 +185,13 @@ public class ResourceTreeViewer
});
private void DrawNodes(IEnumerable<ResourceNode> resourceNodes, int level, nint pathHash,
ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
ChangedItemIconFlag parentFilterIconFlag)
{
var debugMode = _config.DebugMode;
var frameHeight = ImGui.GetFrameHeight();
var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f;
bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon)
bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon)
{
if (!_typeFilter.HasFlag(filterIcon))
return false;
@ -205,12 +205,12 @@ public class ResourceTreeViewer
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
}
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
{
if (node.Internal && !debugMode)
return NodeVisibility.Hidden;
var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon;
var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon;
if (MatchesFilter(node, filterIcon))
return NodeVisibility.Visible;
@ -223,7 +223,7 @@ public class ResourceTreeViewer
return NodeVisibility.Hidden;
}
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon)
NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon)
{
if (!_filterCache.TryGetValue(nodePathHash, out var visibility))
{
@ -241,7 +241,7 @@ public class ResourceTreeViewer
{
var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle);
var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon);
var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIconFlag);
if (visibility == NodeVisibility.Hidden)
continue;
@ -250,7 +250,7 @@ public class ResourceTreeViewer
using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal);
var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon;
var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag;
using var id = ImRaii.PushId(index);
ImGui.TableNextColumn();
@ -281,7 +281,7 @@ public class ResourceTreeViewer
ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X);
}
_changedItemDrawer.DrawCategoryIcon(resourceNode.Icon);
_changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag);
ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.TableHeader(resourceNode.Name);
if (ImGui.IsItemClicked() && unfoldable)

View file

@ -3,72 +3,24 @@ using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using ImGuiNET;
using Lumina.Data.Files;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Data;
using Penumbra.Services;
using Penumbra.UI.Classes;
using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon;
namespace Penumbra.UI;
public class ChangedItemDrawer : IDisposable, IUiService
{
[Flags]
public enum ChangedItemIcon : uint
{
Head = 0x00_00_01,
Body = 0x00_00_02,
Hands = 0x00_00_04,
Legs = 0x00_00_08,
Feet = 0x00_00_10,
Ears = 0x00_00_20,
Neck = 0x00_00_40,
Wrists = 0x00_00_80,
Finger = 0x00_01_00,
Monster = 0x00_02_00,
Demihuman = 0x00_04_00,
Customization = 0x00_08_00,
Action = 0x00_10_00,
Mainhand = 0x00_20_00,
Offhand = 0x00_40_00,
Unknown = 0x00_80_00,
Emote = 0x01_00_00,
}
private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray();
private static readonly ChangedItemIcon[] Order =
[
ChangedItemIcon.Head,
ChangedItemIcon.Body,
ChangedItemIcon.Hands,
ChangedItemIcon.Legs,
ChangedItemIcon.Feet,
ChangedItemIcon.Ears,
ChangedItemIcon.Neck,
ChangedItemIcon.Wrists,
ChangedItemIcon.Finger,
ChangedItemIcon.Mainhand,
ChangedItemIcon.Offhand,
ChangedItemIcon.Customization,
ChangedItemIcon.Action,
ChangedItemIcon.Emote,
ChangedItemIcon.Monster,
ChangedItemIcon.Demihuman,
ChangedItemIcon.Unknown,
];
private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray();
public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIcon slot)
public static bool TryParseIndex(ReadOnlySpan<char> input, out ChangedItemIconFlag slot)
{
// Handle numeric cases before TryParse because numbers
// are not logical otherwise.
@ -77,15 +29,15 @@ public class ChangedItemDrawer : IDisposable, IUiService
// We assume users will use 1-based index, but if they enter 0, just use the first.
if (idx == 0)
{
slot = Order[0];
slot = ChangedItemFlagExtensions.Order[0];
return true;
}
// Use 1-based index.
--idx;
if (idx >= 0 && idx < Order.Length)
if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count)
{
slot = Order[idx];
slot = ChangedItemFlagExtensions.Order[idx];
return true;
}
}
@ -94,13 +46,13 @@ public class ChangedItemDrawer : IDisposable, IUiService
return false;
}
public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot)
public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot)
{
if (TryParseIndex(lowerInput, out slot))
return true;
slot = 0;
foreach (var (item, flag) in LowerNames.Zip(Order))
foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order))
{
if (item.Contains(lowerInput, StringComparison.Ordinal))
slot |= flag;
@ -109,15 +61,11 @@ public class ChangedItemDrawer : IDisposable, IUiService
return slot != 0;
}
public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF;
public static readonly int NumCategories = Order.Length;
public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand;
private readonly Configuration _config;
private readonly ExcelSheet<Item> _items;
private readonly CommunicatorService _communicator;
private readonly Dictionary<ChangedItemIcon, IDalamudTextureWrap> _icons = new(16);
private float _smallestIconWidth;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly Dictionary<ChangedItemIconFlag, IDalamudTextureWrap> _icons = new(16);
private float _smallestIconWidth;
public static Vector2 TypeFilterIconSize
=> new(2 * ImGui.GetTextLineHeight());
@ -125,7 +73,6 @@ public class ChangedItemDrawer : IDisposable, IUiService
public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator,
Configuration config)
{
_items = gameData.GetExcelSheet<Item>()!;
uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true);
_communicator = communicator;
_config = config;
@ -139,18 +86,19 @@ public class ChangedItemDrawer : IDisposable, IUiService
}
/// <summary> Check if a changed item should be drawn based on its category. </summary>
public bool FilterChangedItem(string name, object? data, LowerString filter)
=> (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data)))
&& (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data)));
public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter)
=> (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags
|| _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag()))
&& (filter.IsEmpty || !data.IsFilteredOut(name, filter));
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
public void DrawCategoryIcon(string name, object? data)
=> DrawCategoryIcon(GetCategoryIcon(name, data));
public void DrawCategoryIcon(IIdentifiedObjectData? data)
=> DrawCategoryIcon(data.GetIcon().ToFlag());
public void DrawCategoryIcon(ChangedItemIcon iconType)
public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType)
{
var height = ImGui.GetFrameHeight();
if (!_icons.TryGetValue(iconType, out var icon))
if (!_icons.TryGetValue(iconFlagType, out var icon))
{
ImGui.Dummy(new Vector2(height));
return;
@ -162,18 +110,18 @@ public class ChangedItemDrawer : IDisposable, IUiService
using var tt = ImRaii.Tooltip();
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
ImGui.SameLine();
ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0);
ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
}
}
/// <summary>
/// Draw a changed item, invoking the Api-Events for clicks and tooltips.
/// Also draw the item Id in grey if requested.
/// Also draw the item ID in grey if requested.
/// </summary>
public void DrawChangedItem(string name, object? data)
public void DrawChangedItem(string name, IIdentifiedObjectData? data)
{
name = ChangedItemName(name, data);
using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f))
name = data?.ToName(name) ?? name;
using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f))
.Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2)))
{
var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
@ -182,31 +130,34 @@ public class ChangedItemDrawer : IDisposable, IUiService
ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret;
ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret;
if (ret != MouseButton.None)
_communicator.ChangedItemClick.Invoke(ret, Convert(data));
_communicator.ChangedItemClick.Invoke(ret, data);
}
if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered())
{
// We can not be sure that any subscriber actually prints something in any case.
// Circumvent ugly blank tooltip with less-ugly useless tooltip.
using var tt = ImRaii.Tooltip();
using var group = ImRaii.Group();
_communicator.ChangedItemHover.Invoke(Convert(data));
group.Dispose();
using var tt = ImRaii.Tooltip();
using (ImRaii.Group())
{
_communicator.ChangedItemHover.Invoke(data);
}
if (ImGui.GetItemRectSize() == Vector2.Zero)
ImGui.TextUnformatted("No actions available.");
}
}
/// <summary> Draw the model information, right-justified. </summary>
public void DrawModelData(object? data)
public static void DrawModelData(IIdentifiedObjectData? data)
{
if (!GetChangedItemObject(data, out var text))
var additionalData = data?.AdditionalData ?? string.Empty;
if (additionalData.Length == 0)
return;
ImGui.SameLine(ImGui.GetContentRegionAvail().X);
ImGui.AlignTextToFramePadding();
ImGuiUtil.RightJustify(text, ColorId.ItemId.Value());
ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value());
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
@ -224,7 +175,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
public bool DrawTypeFilter(ref ChangedItemIcon typeFilter)
public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter)
{
var ret = false;
using var _ = ImRaii.PushId("ChangedItemIconFilter");
@ -232,16 +183,38 @@ public class ChangedItemDrawer : IDisposable, IUiService
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter)
foreach (var iconType in ChangedItemFlagExtensions.Order)
{
var ret = false;
var icon = _icons[type];
var flag = typeFilter.HasFlag(type);
ret |= DrawIcon(iconType, ref typeFilter);
ImGui.SameLine();
}
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
typeFilter switch
{
0 => new Vector4(0.6f, 0.3f, 0.3f, 1f),
ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f),
_ => new Vector4(0.5f, 0.5f, 1f, 1f),
});
if (ImGui.IsItemClicked())
{
typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags;
ret = true;
}
return ret;
bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter)
{
var localRet = false;
var icon = _icons[type];
var flag = typeFilter.HasFlag(type);
ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f));
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
typeFilter = flag ? typeFilter & ~type : typeFilter | type;
ret = true;
localRet = true;
}
using var popup = ImRaii.ContextPopupItem(type.ToString());
@ -249,7 +222,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
if (ImGui.MenuItem("Enable Only This"))
{
typeFilter = type;
ret = true;
localRet = true;
ImGui.CloseCurrentPopup();
}
@ -258,165 +231,13 @@ public class ChangedItemDrawer : IDisposable, IUiService
using var tt = ImRaii.Tooltip();
ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth));
ImGui.SameLine();
ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0);
ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0);
}
return ret;
}
foreach (var iconType in Order)
{
ret |= DrawIcon(iconType, ref typeFilter);
ImGui.SameLine();
}
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X);
ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One,
typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) :
typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f));
if (ImGui.IsItemClicked())
{
typeFilter = typeFilter == AllFlags ? 0 : AllFlags;
ret = true;
}
return ret;
}
/// <summary> Obtain the icon category corresponding to a changed item. </summary>
internal static ChangedItemIcon GetCategoryIcon(string name, object? obj)
{
var iconType = ChangedItemIcon.Unknown;
switch (obj)
{
case EquipItem it:
iconType = GetCategoryIcon(it.Type.ToSlot());
break;
case ModelChara m:
iconType = (CharacterBase.ModelType)m.Type switch
{
CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman,
CharacterBase.ModelType.Monster => ChangedItemIcon.Monster,
_ => ChangedItemIcon.Unknown,
};
break;
default:
{
if (name.StartsWith("Action: "))
iconType = ChangedItemIcon.Action;
else if (name.StartsWith("Emote: "))
iconType = ChangedItemIcon.Emote;
else if (name.StartsWith("Customization: "))
iconType = ChangedItemIcon.Customization;
break;
}
}
return iconType;
}
internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => ChangedItemIcon.Mainhand,
EquipSlot.OffHand => ChangedItemIcon.Offhand,
EquipSlot.Head => ChangedItemIcon.Head,
EquipSlot.Body => ChangedItemIcon.Body,
EquipSlot.Hands => ChangedItemIcon.Hands,
EquipSlot.Legs => ChangedItemIcon.Legs,
EquipSlot.Feet => ChangedItemIcon.Feet,
EquipSlot.Ears => ChangedItemIcon.Ears,
EquipSlot.Neck => ChangedItemIcon.Neck,
EquipSlot.Wrists => ChangedItemIcon.Wrists,
EquipSlot.RFinger => ChangedItemIcon.Finger,
_ => ChangedItemIcon.Unknown,
};
/// <summary> Return more detailed object information in text, if it exists. </summary>
private static bool GetChangedItemObject(object? obj, out string text)
{
switch (obj)
{
case EquipItem it:
text = it.ModelString;
return true;
case ModelChara m:
text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})";
return true;
default:
text = string.Empty;
return false;
return localRet;
}
}
/// <summary> We need to transform the internal EquipItem type to the Lumina Item type for API-events. </summary>
private object? Convert(object? data)
{
if (data is EquipItem it)
return (_items.GetRow(it.ItemId.Id), it.Type);
return data;
}
private static string ToDescription(ChangedItemIcon icon)
=> icon switch
{
ChangedItemIcon.Head => EquipSlot.Head.ToName(),
ChangedItemIcon.Body => EquipSlot.Body.ToName(),
ChangedItemIcon.Hands => EquipSlot.Hands.ToName(),
ChangedItemIcon.Legs => EquipSlot.Legs.ToName(),
ChangedItemIcon.Feet => EquipSlot.Feet.ToName(),
ChangedItemIcon.Ears => EquipSlot.Ears.ToName(),
ChangedItemIcon.Neck => EquipSlot.Neck.ToName(),
ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(),
ChangedItemIcon.Finger => "Ring",
ChangedItemIcon.Monster => "Monster",
ChangedItemIcon.Demihuman => "Demi-Human",
ChangedItemIcon.Customization => "Customization",
ChangedItemIcon.Action => "Action",
ChangedItemIcon.Emote => "Emote",
ChangedItemIcon.Mainhand => "Weapon (Mainhand)",
ChangedItemIcon.Offhand => "Weapon (Offhand)",
_ => "Other",
};
internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon)
=> icon switch
{
ChangedItemIcon.Head => ApiChangedItemIcon.Head,
ChangedItemIcon.Body => ApiChangedItemIcon.Body,
ChangedItemIcon.Hands => ApiChangedItemIcon.Hands,
ChangedItemIcon.Legs => ApiChangedItemIcon.Legs,
ChangedItemIcon.Feet => ApiChangedItemIcon.Feet,
ChangedItemIcon.Ears => ApiChangedItemIcon.Ears,
ChangedItemIcon.Neck => ApiChangedItemIcon.Neck,
ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists,
ChangedItemIcon.Finger => ApiChangedItemIcon.Finger,
ChangedItemIcon.Monster => ApiChangedItemIcon.Monster,
ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman,
ChangedItemIcon.Customization => ApiChangedItemIcon.Customization,
ChangedItemIcon.Action => ApiChangedItemIcon.Action,
ChangedItemIcon.Emote => ApiChangedItemIcon.Emote,
ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand,
ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand,
ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown,
_ => ApiChangedItemIcon.None,
};
/// <summary> Apply Changed Item Counters to the Name if necessary. </summary>
private static string ChangedItemName(string name, object? data)
=> data is int counter ? $"{counter} Files Manipulating {name}s" : name;
/// <summary> Add filterable information to the string. </summary>
private static string ChangedItemFilterName(string name, object? data)
=> data switch
{
int counter => $"{counter} Files Manipulating {name}s",
EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}",
ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}",
_ => name,
};
/// <summary> Initialize the icons. </summary>
private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider)
{
@ -425,30 +246,30 @@ public class ChangedItemDrawer : IDisposable, IUiService
if (!equipTypeIcons.Valid)
return false;
void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex)
void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex)
{
if (tex != null)
_icons.Add(icon, tex);
}
Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062042_hr1.tex")!));
Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062041_hr1.tex")!));
Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider));
Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider));
Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));
Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0));
Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1));
Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2));
Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3));
Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5));
Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6));
Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7));
Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8));
Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9));
Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10));
Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11));
Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062044_hr1.tex")!));
Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062043_hr1.tex")!));
Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062045_hr1.tex")!));
Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/062000/062001_hr1.tex")!));
Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider));
Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider));
Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile<TexFile>("ui/icon/114000/114052_hr1.tex")!));
_smallestIconWidth = _icons.Values.Min(i => i.Width);
@ -487,6 +308,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
}
}
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon");
return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2,
"Penumbra.EmoteItemIcon");
}
}

View file

@ -0,0 +1,122 @@
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
namespace Penumbra.UI;
[Flags]
public enum ChangedItemIconFlag : uint
{
Head = 0x00_00_01,
Body = 0x00_00_02,
Hands = 0x00_00_04,
Legs = 0x00_00_08,
Feet = 0x00_00_10,
Ears = 0x00_00_20,
Neck = 0x00_00_40,
Wrists = 0x00_00_80,
Finger = 0x00_01_00,
Monster = 0x00_02_00,
Demihuman = 0x00_04_00,
Customization = 0x00_08_00,
Action = 0x00_10_00,
Mainhand = 0x00_20_00,
Offhand = 0x00_40_00,
Unknown = 0x00_80_00,
Emote = 0x01_00_00,
}
public static class ChangedItemFlagExtensions
{
public static readonly IReadOnlyList<ChangedItemIconFlag> Order =
[
ChangedItemIconFlag.Head,
ChangedItemIconFlag.Body,
ChangedItemIconFlag.Hands,
ChangedItemIconFlag.Legs,
ChangedItemIconFlag.Feet,
ChangedItemIconFlag.Ears,
ChangedItemIconFlag.Neck,
ChangedItemIconFlag.Wrists,
ChangedItemIconFlag.Finger,
ChangedItemIconFlag.Mainhand,
ChangedItemIconFlag.Offhand,
ChangedItemIconFlag.Customization,
ChangedItemIconFlag.Action,
ChangedItemIconFlag.Emote,
ChangedItemIconFlag.Monster,
ChangedItemIconFlag.Demihuman,
ChangedItemIconFlag.Unknown,
];
public const ChangedItemIconFlag AllFlags = (ChangedItemIconFlag)0x01FFFF;
public static readonly int NumCategories = Order.Count;
public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand;
public static string ToDescription(this ChangedItemIconFlag iconFlag)
=> iconFlag switch
{
ChangedItemIconFlag.Head => EquipSlot.Head.ToName(),
ChangedItemIconFlag.Body => EquipSlot.Body.ToName(),
ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(),
ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(),
ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(),
ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(),
ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(),
ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(),
ChangedItemIconFlag.Finger => "Ring",
ChangedItemIconFlag.Monster => "Monster",
ChangedItemIconFlag.Demihuman => "Demi-Human",
ChangedItemIconFlag.Customization => "Customization",
ChangedItemIconFlag.Action => "Action",
ChangedItemIconFlag.Emote => "Emote",
ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)",
ChangedItemIconFlag.Offhand => "Weapon (Offhand)",
_ => "Other",
};
public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag)
=> iconFlag switch
{
ChangedItemIconFlag.Head => ChangedItemIcon.Head,
ChangedItemIconFlag.Body => ChangedItemIcon.Body,
ChangedItemIconFlag.Hands => ChangedItemIcon.Hands,
ChangedItemIconFlag.Legs => ChangedItemIcon.Legs,
ChangedItemIconFlag.Feet => ChangedItemIcon.Feet,
ChangedItemIconFlag.Ears => ChangedItemIcon.Ears,
ChangedItemIconFlag.Neck => ChangedItemIcon.Neck,
ChangedItemIconFlag.Wrists => ChangedItemIcon.Wrists,
ChangedItemIconFlag.Finger => ChangedItemIcon.Finger,
ChangedItemIconFlag.Monster => ChangedItemIcon.Monster,
ChangedItemIconFlag.Demihuman => ChangedItemIcon.Demihuman,
ChangedItemIconFlag.Customization => ChangedItemIcon.Customization,
ChangedItemIconFlag.Action => ChangedItemIcon.Action,
ChangedItemIconFlag.Emote => ChangedItemIcon.Emote,
ChangedItemIconFlag.Mainhand => ChangedItemIcon.Mainhand,
ChangedItemIconFlag.Offhand => ChangedItemIcon.Offhand,
ChangedItemIconFlag.Unknown => ChangedItemIcon.Unknown,
_ => ChangedItemIcon.None,
};
public static ChangedItemIconFlag ToFlag(this ChangedItemIcon icon)
=> icon switch
{
ChangedItemIcon.Unknown => ChangedItemIconFlag.Unknown,
ChangedItemIcon.Head => ChangedItemIconFlag.Head,
ChangedItemIcon.Body => ChangedItemIconFlag.Body,
ChangedItemIcon.Hands => ChangedItemIconFlag.Hands,
ChangedItemIcon.Legs => ChangedItemIconFlag.Legs,
ChangedItemIcon.Feet => ChangedItemIconFlag.Feet,
ChangedItemIcon.Ears => ChangedItemIconFlag.Ears,
ChangedItemIcon.Neck => ChangedItemIconFlag.Neck,
ChangedItemIcon.Wrists => ChangedItemIconFlag.Wrists,
ChangedItemIcon.Finger => ChangedItemIconFlag.Finger,
ChangedItemIcon.Mainhand => ChangedItemIconFlag.Mainhand,
ChangedItemIcon.Offhand => ChangedItemIconFlag.Offhand,
ChangedItemIcon.Customization => ChangedItemIconFlag.Customization,
ChangedItemIcon.Monster => ChangedItemIconFlag.Monster,
ChangedItemIcon.Demihuman => ChangedItemIconFlag.Demihuman,
ChangedItemIcon.Action => ChangedItemIconFlag.Action,
ChangedItemIcon.Emote => ChangedItemIconFlag.Emote,
_ => ChangedItemIconFlag.Unknown,
};
}

View file

@ -550,7 +550,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
+ "Enter t:[string] to filter for mods set to specific tags.\n"
+ "Enter n:[string] to filter only for mod names and no paths.\n"
+ "Enter a:[string] to filter for mods by specific authors.\n"
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories + 1} or partial category name).\n\n"
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemFlagExtensions.NumCategories + 1} or partial category name).\n\n"
+ "Use None as a placeholder value that only matches empty lists or names.\n"
+ "Regularly, a mod has to match all supplied criteria separately.\n"
+ "Put a - in front of a search token to search only for mods not matching the criterion.\n"

View file

@ -4,6 +4,7 @@ using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
namespace Penumbra.UI.ModsTab;
@ -24,7 +25,7 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem
if (!table)
return;
var zipList = ZipList.FromSortedList((SortedList<string, object?>)selector.Selected!.ChangedItems);
var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems);
var height = ImGui.GetFrameHeightWithSpacing();
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
@ -32,15 +33,15 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem
ImGuiClip.DrawEndDummy(remainder, height);
}
private bool CheckFilter((string Name, object? Data) kvp)
private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp)
=> drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty);
private void DrawChangedItem((string Name, object? Data) kvp)
private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp)
{
ImGui.TableNextColumn();
drawer.DrawCategoryIcon(kvp.Name, kvp.Data);
drawer.DrawCategoryIcon(kvp.Data);
ImGui.SameLine();
drawer.DrawChangedItem(kvp.Name, kvp.Data);
drawer.DrawModelData(kvp.Data);
ChangedItemDrawer.DrawModelData(kvp.Data);
}
}

View file

@ -19,16 +19,16 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
{
public readonly struct Entry : ISplitterEntry<ModSearchType, Entry>
{
public string Needle { get; init; }
public ModSearchType Type { get; init; }
public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; }
public string Needle { get; init; }
public ModSearchType Type { get; init; }
public ChangedItemIconFlag IconFlagFilter { get; init; }
public bool Contains(Entry other)
{
if (Type != other.Type)
return false;
if (Type is ModSearchType.Category)
return IconFilter == other.IconFilter;
return IconFlagFilter == other.IconFlagFilter;
return Needle.Contains(other.Needle);
}
@ -77,7 +77,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon))
list[i] = entry with
{
IconFilter = icon,
IconFlagFilter = icon,
Needle = string.Empty,
};
else
@ -110,7 +110,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType
ModSearchType.Name => leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Category => leaf.Value.ChangedItems.Any(p
=> (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0),
=> ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0),
_ => true,
};

View file

@ -6,6 +6,7 @@ using OtterGui.Services;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Data;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Services;
@ -66,22 +67,22 @@ public class ChangedItemsTab(
}
/// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
{
ImGui.TableNextColumn();
drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
drawer.DrawCategoryIcon(item.Value.Item2);
ImGui.SameLine();
drawer.DrawChangedItem(item.Key, item.Value.Item2);
ImGui.TableNextColumn();
DrawModColumn(item.Value.Item1);
ImGui.TableNextColumn();
drawer.DrawModelData(item.Value.Item2);
ChangedItemDrawer.DrawModelData(item.Value.Item2);
}
private void DrawModColumn(SingleArray<IMod> mods)

View file

@ -429,7 +429,7 @@ public class SettingsTab : ITab, IUiService
_config.HideChangedItemFilters = v;
if (v)
{
_config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags;
_config.Ephemeral.ChangedItemFilter = ChangedItemFlagExtensions.AllFlags;
_config.Ephemeral.Save();
}
});

View file

@ -10,7 +10,7 @@ namespace Penumbra.Util;
public static class IdentifierExtensions
{
public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
IDictionary<string, object?> changedItems)
IDictionary<string, IIdentifiedObjectData?> changedItems)
{
foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys))
identifier.Identify(changedItems, gamePath.ToString());
@ -19,25 +19,25 @@ public static class IdentifierExtensions
manip.AddChangedItems(identifier, changedItems);
}
public static void RemoveMachinistOffhands(this SortedList<string, object?> changedItems)
public static void RemoveMachinistOffhands(this SortedList<string, IIdentifiedObjectData?> changedItems)
{
for (var i = 0; i < changedItems.Count; i++)
{
{
var value = changedItems.Values[i];
if (value is EquipItem { Type: FullEquipType.GunOff })
if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff })
changedItems.RemoveAt(i--);
}
}
}
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, object?)> changedItems)
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> changedItems)
{
for (var i = 0; i < changedItems.Count; i++)
{
{
var value = changedItems.Values[i].Item2;
if (value is EquipItem { Type: FullEquipType.GunOff })
if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff })
changedItems.RemoveAt(i--);
}
}