diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 92d9ef3d..487cd568 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; using Penumbra.GameData.Enums; @@ -43,5 +44,8 @@ namespace Penumbra.Api // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile( string gamePath, string characterName ) where T : FileResource; + + // Gets a dictionary of effected items from a collection + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection(string collectionName); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 51360940..5190bc07 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Mods; @@ -131,5 +135,32 @@ namespace Penumbra.Api public T? GetFile< T >( string gamePath, string characterName ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath, characterName ) ); + + public IReadOnlyDictionary< string, object? > GetChangedItemsForCollection( string collectionName ) + { + CheckInitialized(); + try + { + var modManager = Service< ModManager >.Get(); + if( !modManager.Collections.Collections.TryGetValue( collectionName, out var collection ) ) + { + collection = ModCollection.Empty; + } + + if( collection.Cache != null ) + { + return collection.Cache.ChangedItems; + } + + PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); + return new Dictionary< string, object? >(); + + } + catch( Exception e ) + { + PluginLog.Error( $"Could not obtain Changed Items for {collectionName}:\n{e}" ); + throw; + } + } } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index fba72015..2f77cc61 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Dalamud.Plugin; @@ -18,15 +19,17 @@ namespace Penumbra.Api public const string LabelProviderChangedItemTooltip = "Penumbra.ChangedItemTooltip"; public const string LabelProviderChangedItemClick = "Penumbra.ChangedItemClick"; + public const string LabelProviderGetChangedItems = "Penumbra.GetChangedItems"; - internal ICallGateProvider< int >? ProviderApiVersion; - internal ICallGateProvider< string, int, object >? ProviderRedrawName; - internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; - internal ICallGateProvider< int, object >? ProviderRedrawAll; - internal ICallGateProvider< string, string >? ProviderResolveDefault; - internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; - internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; - internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< int >? ProviderApiVersion; + internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; + internal ICallGateProvider< int, object >? ProviderRedrawAll; + internal ICallGateProvider< string, string >? ProviderResolveDefault; + internal ICallGateProvider< string, string, string >? ProviderResolveCharacter; + internal ICallGateProvider< ChangedItemType, uint, object >? ProviderChangedItemTooltip; + internal ICallGateProvider< MouseButton, ChangedItemType, uint, object >? ProviderChangedItemClick; + internal ICallGateProvider< string, IReadOnlyDictionary< string, object? > >? ProviderGetChangedItems; internal readonly IPenumbraApi Api; @@ -137,6 +140,16 @@ namespace Penumbra.Api { PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); } + + try + { + ProviderGetChangedItems = pi.GetIpcProvider< string, IReadOnlyDictionary< string, object? > >( LabelProviderGetChangedItems ); + ProviderGetChangedItems.RegisterFunc( api.GetChangedItemsForCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderChangedItemClick}:\n{e}" ); + } } public void Dispose() @@ -147,6 +160,7 @@ namespace Penumbra.Api ProviderRedrawAll?.UnregisterAction(); ProviderResolveDefault?.UnregisterFunc(); ProviderResolveCharacter?.UnregisterFunc(); + ProviderGetChangedItems?.UnregisterFunc(); Api.ChangedItemClicked -= OnClick; Api.ChangedItemTooltip -= OnTooltip; } diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 1e4582c6..17fe47b3 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -24,10 +25,20 @@ namespace Penumbra.Mods public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); - public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FileInfo > MissingFiles = new(); - public readonly MetaManager MetaManipulations; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); + public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public readonly HashSet< FileInfo > MissingFiles = new(); + public readonly MetaManager MetaManipulations; + + public IReadOnlyDictionary< string, object? > ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); @@ -52,6 +63,7 @@ namespace Penumbra.Mods SwappedFiles.Clear(); MissingFiles.Clear(); RegisteredFiles.Clear(); + _changedItems.Clear(); foreach( var mod in AvailableMods.Values .Where( m => m.Settings.Enabled ) @@ -65,6 +77,35 @@ namespace Penumbra.Mods AddMetaFiles(); } + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip meta files because IMCs would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var metaFiles = MetaManipulations.Files.Select( p => p.Item1 ).ToHashSet(); + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) + { + identifier.Identify( _changedItems, resolved ); + } + + foreach( var swapped in SwappedFiles.Keys ) + { + identifier.Identify( _changedItems, swapped ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + private void AddFiles( Mod.Mod mod ) { @@ -97,7 +138,7 @@ namespace Penumbra.Mods else { mod.Cache.AddConflict( oldMod, gamePath ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority) + if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) { oldMod.Cache.AddConflict( mod, gamePath ); } diff --git a/Penumbra/UI/MenuTabs/TabChangedItems.cs b/Penumbra/UI/MenuTabs/TabChangedItems.cs new file mode 100644 index 00000000..c161b6d9 --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabChangedItems.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using Penumbra.Mods; +using Penumbra.UI.Custom; +using Penumbra.Util; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabChangedItems + { + private const string LabelTab = "Changed Items"; + private readonly ModManager _modManager; + private readonly SettingsInterface _base; + + private string _filter = string.Empty; + private string _filterLower = string.Empty; + + public TabChangedItems( SettingsInterface ui ) + { + _base = ui; + _modManager = Service< ModManager >.Get(); + } + + public void Draw() + { + var items = _modManager.Collections.ActiveCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var forced = _modManager.Collections.ForcedCollection.Cache?.ChangedItems ?? new Dictionary< string, object? >(); + var count = items.Count + forced.Count; + if( count > 0 && !ImGui.BeginTabItem( LabelTab ) ) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputTextWithHint( "##ChangedItemsFilter", "Filter...", ref _filter, 64 ) ) + { + _filterLower = _filter.ToLowerInvariant(); + } + + if( !ImGui.BeginTable( "##ChangedItemsTable", 1, ImGuiTableFlags.RowBg, AutoFillSize ) ) + { + return; + } + + raii.Push( ImGui.EndTable ); + + var list = items.AsEnumerable(); + if( forced.Count > 0 ) + { + list = list.Concat( forced ).OrderBy( kvp => kvp.Key ); + } + + if( _filter.Any() ) + { + list = list.Where( kvp => kvp.Key.ToLowerInvariant().Contains( _filterLower ) ); + } + + foreach( var (name, data) in list ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _base.DrawChangedItem( name, data ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 22aea5c7..b95e0f6f 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -185,30 +185,7 @@ namespace Penumbra.UI raii.Push( ImGui.EndListBox ); foreach( var (name, data) in Mod.Data.ChangedItems ) { - var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; - - if( ret != MouseButton.None ) - { - _base._penumbra.Api.InvokeClick( ret, data ); - } - - if( _base._penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) - { - ImGui.BeginTooltip(); - raii.Push( ImGui.EndTooltip ); - _base._penumbra.Api.InvokeTooltip( data ); - raii.Pop(); - } - - if( data is Item it ) - { - var modelId = $"({( ( Quad )it.ModelMain ).A})"; - var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; - ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - offset); - ImGui.TextColored( new Vector4(0.5f, 0.5f, 0.5f, 1 ), modelId ); - } + _base.DrawChangedItem( name, data ); } } diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index f5a34cd1..8db86756 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -7,7 +7,6 @@ using System.Text.RegularExpressions; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; -using Penumbra.GameData.Enums; using Penumbra.Interop; using Penumbra.Mods; using Penumbra.UI.Custom; diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index e46034e8..7a3ec8ab 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -19,9 +19,10 @@ namespace Penumbra.UI private readonly TabSettings _settingsTab; private readonly TabImport _importTab; private readonly TabBrowser _browserTab; + private readonly TabEffective _effectiveTab; + private readonly TabChangedItems _changedItems; internal readonly TabCollections CollectionsTab; internal readonly TabInstalled InstalledTab; - private readonly TabEffective _effectiveTab; public SettingsMenu( SettingsInterface ui ) { @@ -32,6 +33,7 @@ namespace Penumbra.UI InstalledTab = new TabInstalled( _base ); CollectionsTab = new TabCollections( InstalledTab.Selector ); _effectiveTab = new TabEffective(); + _changedItems = new TabChangedItems( _base ); } #if DEBUG @@ -72,7 +74,7 @@ namespace Penumbra.UI { _browserTab.Draw(); InstalledTab.Draw(); - + _changedItems.Draw(); if( Penumbra.Config.ShowAdvanced ) { _effectiveTab.Draw(); diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs new file mode 100644 index 00000000..47c0e59b --- /dev/null +++ b/Penumbra/UI/UiHelpers.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Enums; +using Penumbra.UI.Custom; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + internal void DrawChangedItem( string name, object? data ) + { + var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; + + if( ret != MouseButton.None ) + { + _penumbra.Api.InvokeClick( ret, data ); + } + + if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) + { + ImGui.BeginTooltip(); + using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); + _penumbra.Api.InvokeTooltip( data ); + } + + if( data is Item it ) + { + var modelId = $"({( ( Quad )it.ModelMain ).A})"; + var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X; + ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); + ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId ); + } + } + } +} \ No newline at end of file