From 27650708f02ff77b1758b52b9878b548d9025b7e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 12:45:04 +0200 Subject: [PATCH 01/13] Fix collection in use not updating on first load. --- Penumbra/Collections/CollectionManager.Active.cs | 7 +++++-- Penumbra/Collections/CollectionManager.cs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index f06a574f..c75b7e44 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -81,11 +81,14 @@ public partial class ModCollection break; } - CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); - + + UpdateCurrentCollectionInUse(); CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); + public void SetCollection( ModCollection collection, Type type, string? characterName = null ) => SetCollection( collection.Index, type, characterName ); diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 76515f0c..411e7de5 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -72,6 +72,7 @@ public partial class ModCollection CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); + UpdateCurrentCollectionInUse(); } public void Dispose() From 58e46accae34a87215b726c3fc2b72135add4959 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 12:46:06 +0200 Subject: [PATCH 02/13] Add errors when loading from elsewhere than installedPlugins or devPlugins/Penumbra existing and containing dlls in Release mode --- Penumbra/Penumbra.cs | 37 +++++++++++++++++- Penumbra/UI/ConfigWindow.Misc.cs | 13 ++++--- Penumbra/UI/ConfigWindow.cs | 66 +++++++++++++++++++++----------- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e4beecf0..215261b0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,10 +29,14 @@ public class MainClass : IDalamudPlugin { private Penumbra? _penumbra; private readonly CharacterUtility _characterUtility; + public static bool DevPenumbraExists; + public static bool IsNotInstalledPenumbra; public MainClass( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); + DevPenumbraExists = CheckDevPluginPenumbra(); + IsNotInstalledPenumbra = CheckIsNotInstalled(); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); _characterUtility = new CharacterUtility(); _characterUtility.LoadingFinished += () @@ -47,6 +51,38 @@ public class MainClass : IDalamudPlugin public string Name => Penumbra.Name; + + // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. + private static bool CheckDevPluginPenumbra() + { +#if !DEBUG + var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); + var dir = new DirectoryInfo( path ); + + try + { + return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" ); + return true; + } +#else + return false; +#endif + } + + // Check if the loaded version of penumbra itself is in devPlugins. + private static bool CheckIsNotInstalled() + { +#if !DEBUG + return !Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Name.Equals( "installedPlugins", + StringComparison.InvariantCultureIgnoreCase ) ?? true; +#else + return false; +#endif + } } public class Penumbra : IDisposable @@ -71,7 +107,6 @@ public class Penumbra : IDisposable public static FrameworkManager Framework { get; private set; } = null!; public static int ImcExceptions = 0; - public readonly ResourceLogger ResourceLogger; public readonly PathResolver PathResolver; public readonly MusicManager MusicManager; diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 6f70126a..92689787 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -104,7 +104,7 @@ public partial class ConfigWindow _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), }; - using var combo = ImRaii.Combo( label, current.Name ); + using var combo = ImRaii.Combo( label, current.Name ); if( combo ) { foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) @@ -128,20 +128,23 @@ public partial class ConfigWindow if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) { - fileManager.CustomSideBarItems.Add( ("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1) ); + fileManager.CustomSideBarItems.Add( ( "Downloads", downloadsFolder, FontAwesomeIcon.Download, -1 ) ); } if( Functions.GetQuickAccessFolders( out var folders ) ) { foreach( var ((name, path), idx) in folders.WithIndex() ) { - fileManager.CustomSideBarItems.Add( ($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1) ); + fileManager.CustomSideBarItems.Add( ( $"{name}##{idx}", path, FontAwesomeIcon.Folder, -1 ) ); } } + // Add Penumbra Root. This is not updated if the root changes right now. + fileManager.CustomSideBarItems.Add( ("Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0) ); + // Remove Videos and Music. - fileManager.CustomSideBarItems.Add( ("Videos", string.Empty, 0, -1) ); - fileManager.CustomSideBarItems.Add( ("Music", string.Empty, 0, -1) ); + fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); + fileManager.CustomSideBarItems.Add( ( "Music", string.Empty, 0, -1 ) ); return fileManager; } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ccfac393..edaaddcf 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -55,32 +55,39 @@ public sealed partial class ConfigWindow : Window, IDisposable { if( Penumbra.ImcExceptions > 0 ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); - ImGui.NewLine(); - ImGui.NewLine(); - ImGui.TextWrapped( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "Please use the Launcher's Repair Game Files function to repair your client installation." ); - color.Pop(); - - ImGui.NewLine(); - ImGui.NewLine(); - SettingsTab.DrawDiscordButton( 0 ); - ImGui.SameLine(); - SettingsTab.DrawSupportButton(); - return; } - - using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); - SetupSizes(); - _settingsTab.Draw(); - DrawModsTab(); - _collectionsTab.Draw(); - DrawChangedItemTab(); - _effectiveTab.Draw(); - _debugTab.Draw(); - _resourceTab.Draw(); + else if( MainClass.IsNotInstalledPenumbra ) + { + DrawProblemWindow( + $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." ); + } + else if( MainClass.DevPenumbraExists ) + { + DrawProblemWindow( + $"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode." ); + } + else + { + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); + SetupSizes(); + _settingsTab.Draw(); + DrawModsTab(); + _collectionsTab.Draw(); + DrawChangedItemTab(); + _effectiveTab.Draw(); + _debugTab.Draw(); + _resourceTab.Draw(); + } } catch( Exception e ) { @@ -88,6 +95,21 @@ public sealed partial class ConfigWindow : Window, IDisposable } } + private static void DrawProblemWindow( string text ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextWrapped( text ); + color.Pop(); + + ImGui.NewLine(); + ImGui.NewLine(); + SettingsTab.DrawDiscordButton( 0 ); + ImGui.SameLine(); + SettingsTab.DrawSupportButton(); + } + public void Dispose() { _selector.Dispose(); From df1a75b58a994306155c2e93fed9669203905211 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 14:23:28 +0200 Subject: [PATCH 03/13] Add another hook for animations in character collections. --- .../Resolver/PathResolver.Animation.cs | 21 +++++++++++++++++++ .../Interop/Resolver/PathResolver.Data.cs | 9 +++++++- Penumbra/Interop/Resolver/PathResolver.cs | 4 ++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 363ce1e2..53cb7be5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -72,4 +72,25 @@ public unsafe partial class PathResolver _animationLoadCollection = last; return ret; } + + public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); + + [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )] + public Hook< LoadSomePap >? LoadSomePapHook; + + private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 ) + { + var timelinePtr = a1 + 0x50; + var last = _animationLoadCollection; + if( timelinePtr != IntPtr.Zero ) + { + var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); + if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length ) + { + _animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) ); + } + } + LoadSomePapHook!.Original( a1, a2, a3, a4 ); + _animationLoadCollection = last; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 71cda6c5..28241e9c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -93,6 +93,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Enable(); CharacterBaseLoadAnimationHook?.Enable(); LoadSomeAvfxHook?.Enable(); + LoadSomePapHook?.Enable(); } private void DisableDataHooks() @@ -105,6 +106,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Disable(); CharacterBaseLoadAnimationHook?.Disable(); LoadSomeAvfxHook?.Disable(); + LoadSomePapHook?.Disable(); } private void DisposeDataHooks() @@ -116,6 +118,7 @@ public unsafe partial class PathResolver LoadTimelineResourcesHook?.Dispose(); CharacterBaseLoadAnimationHook?.Dispose(); LoadSomeAvfxHook?.Dispose(); + LoadSomePapHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. @@ -272,8 +275,12 @@ public unsafe partial class PathResolver } // Housing Retainers - if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == (byte) ObjectKind.EventNpc && gameObject->DataID == 1011832 ) + if( Penumbra.Config.UseDefaultCollectionForRetainers + && gameObject->ObjectKind == ( byte )ObjectKind.EventNpc + && gameObject->DataID == 1011832 ) + { return Penumbra.CollectionManager.Default; + } string? actorName = null; if( Penumbra.Config.PreferNamedCollectionsOverOwners ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index d12e6d00..ab4d208b 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -76,6 +76,9 @@ public partial class PathResolver : IDisposable private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { + if( type == ResourceType.Pap && _.Path.EndsWith( '0', '1', '0', '.', 'p', 'a', 'p' ) ) + PluginLog.Information( $"PAPPITY PAP {_}" ); + if( _animationLoadCollection != null ) { switch( type ) @@ -84,6 +87,7 @@ public partial class PathResolver : IDisposable case ResourceType.Pap: case ResourceType.Avfx: case ResourceType.Atex: + case ResourceType.Scd: collection = _animationLoadCollection; return true; } From c578bd3a495fe639f418a9d0d0529e304ebf8a2c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 15:18:35 +0200 Subject: [PATCH 04/13] Add mod setting API functions. --- Penumbra/Api/IPenumbraApi.cs | 11 +- Penumbra/Api/PenumbraApi.cs | 173 ++++++++++++++++-- Penumbra/Collections/ModCollection.Changes.cs | 44 +++-- Penumbra/Mods/Manager/Mod.Manager.cs | 24 +++ Penumbra/Mods/Subclasses/ModSettings.cs | 32 +++- 5 files changed, 245 insertions(+), 39 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 25c828e1..5107562c 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -29,13 +29,14 @@ public enum PenumbraApiEc CollectionMissing = 2, ModMissing = 3, OptionGroupMissing = 4, - SettingMissing = 5, + OptionMissing = 5, CharacterCollectionExists = 6, LowerPriority = 7, InvalidGamePath = 8, FileMissing = 9, InvalidManipulation = 10, + InvalidArgument = 11, } public interface IPenumbraApi : IPenumbraApiBase @@ -75,7 +76,7 @@ public interface IPenumbraApi : IPenumbraApiBase public string ResolvePath( string gamePath, string characterName ); // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character - public IList ReverseResolvePath( string moddedPath, string characterName ); + public IList< string > ReverseResolvePath( string moddedPath, string characterName ); // Try to load a given gamePath with the resolved path from Penumbra. public T? GetFile< T >( string gamePath ) where T : FileResource; @@ -109,12 +110,12 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain the potential settings of a mod specified by its directory name first or mod name second. // Returns null if the mod could not be found. - public IDictionary< string, (IList, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); + public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); // Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second, // and whether these settings are inherited, or null if the collection does not set them at all. // If allowInheritance is false, only the collection itself will be checked. - public (PenumbraApiEc, (bool, int, IDictionary< string, IList >, bool)?) GetCurrentModSettings( string collectionName, + public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, bool allowInheritance ); // Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second. @@ -136,7 +137,7 @@ public interface IPenumbraApi : IPenumbraApiBase public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ); public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList options ); + IReadOnlyList< string > options ); // Create a temporary collection without actual settings but with a cache. diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 33b4dedf..19af38d8 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -12,12 +13,15 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Api; public class PenumbraApi : IDisposable, IPenumbraApi { - public int ApiVersion { get; } = 4; + public int ApiVersion + => 4; + private Penumbra? _penumbra; private Lumina.GameData? _lumina; public event GameObjectRedrawn? GameObjectRedrawn; @@ -231,28 +235,173 @@ public class PenumbraApi : IDisposable, IPenumbraApi } public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) - => throw new NotImplementedException(); + { + CheckInitialized(); + return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) + ? mod.Groups.ToDictionary( g => g.Name, g => ( ( IList< string > )g.Select( o => o.Name ).ToList(), g.Type ) ) + : null; + } public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, - string modDirectory, string modName, - bool allowInheritance ) - => throw new NotImplementedException(); + string modDirectory, string modName, bool allowInheritance ) + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return ( PenumbraApiEc.CollectionMissing, null ); + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return ( PenumbraApiEc.ModMissing, null ); + } + + var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; + if( settings == null ) + { + return ( PenumbraApiEc.Okay, null ); + } + + var shareSettings = settings.ConvertToShareable( mod ); + return ( PenumbraApiEc.Okay, + ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); + } public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + + return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) - => throw new NotImplementedException(); + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ) - => throw new NotImplementedException(); + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList< string > options ) - => throw new NotImplementedException(); + string optionName ) + { + CheckInitialized(); + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); + if( groupIdx < 0 ) + { + return PenumbraApiEc.OptionGroupMissing; + } + + var optionIdx = mod.Groups[ groupIdx ].IndexOf( o => o.Name == optionName ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx; + + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } + + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + IReadOnlyList< string > optionNames ) + { + CheckInitialized(); + if( optionNames.Count == 0 ) + { + return PenumbraApiEc.InvalidArgument; + } + + if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) ) + { + return PenumbraApiEc.ModMissing; + } + + var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName ); + if( groupIdx < 0 ) + { + return PenumbraApiEc.OptionGroupMissing; + } + + var group = mod.Groups[ groupIdx ]; + + uint setting = 0; + if( group.Type == SelectType.Single ) + { + var name = optionNames[ ^1 ]; + var optionIdx = group.IndexOf( o => o.Name == name ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + setting = ( uint )optionIdx; + } + else + { + foreach( var name in optionNames ) + { + var optionIdx = group.IndexOf( o => o.Name == name ); + if( optionIdx < 0 ) + { + return PenumbraApiEc.OptionMissing; + } + + setting |= 1u << optionIdx; + } + } + + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + } public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) => throw new NotImplementedException(); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 8009dc14..b2c9ffd5 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -24,17 +24,20 @@ public partial class ModCollection public event ModSettingChangeDelegate ModSettingChanged; // Enable or disable the mod inheritance of mod idx. - public void SetModInheritance( int idx, bool inherit ) + public bool SetModInheritance( int idx, bool inherit ) { if( FixInheritance( idx, inherit ) ) { ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); + return true; } + + return false; } // Set the enabled state mod idx to newValue if it differs from the current enabled state. // If mod idx is currently inherited, stop the inheritance. - public void SetModState( int idx, bool newValue ) + public bool SetModState( int idx, bool newValue ) { var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; if( newValue != oldValue ) @@ -42,7 +45,10 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Enabled = newValue; ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); + return true; } + + return false; } // Enable or disable the mod inheritance of every mod in mods. @@ -78,7 +84,7 @@ public partial class ModCollection // Set the priority of mod idx to newValue if it differs from the current priority. // If mod idx is currently inherited, stop the inheritance. - public void SetModPriority( int idx, int newValue ) + public bool SetModPriority( int idx, int newValue ) { var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; if( newValue != oldValue ) @@ -86,12 +92,15 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Priority = newValue; ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); + return true; } + + return false; } // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. // If mod idx is currently inherited, stop the inheritance. - public void SetModSetting( int idx, int groupIdx, uint newValue ) + public bool SetModSetting( int idx, int groupIdx, uint newValue ) { var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; var oldValue = settings?[ groupIdx ] ?? 0; @@ -100,31 +109,26 @@ public partial class ModCollection var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); + return true; } + + return false; } // Change one of the available mod settings for mod idx discerned by type. // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. // The setting will also be automatically fixed if it is invalid for that setting group. // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - public void ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) + public bool ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) { - switch( type ) + return type switch { - case ModSettingChange.Inheritance: - SetModInheritance( idx, newValue != 0 ); - break; - case ModSettingChange.EnableState: - SetModState( idx, newValue != 0 ); - break; - case ModSettingChange.Priority: - SetModPriority( idx, newValue ); - break; - case ModSettingChange.Setting: - SetModSetting( idx, groupIdx, ( uint )newValue ); - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } + ModSettingChange.Inheritance => SetModInheritance( idx, newValue != 0 ), + ModSettingChange.EnableState => SetModState( idx, newValue != 0 ), + ModSettingChange.Priority => SetModPriority( idx, newValue ), + ModSettingChange.Setting => SetModSetting( idx, groupIdx, ( uint )newValue ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; } // Set inheritance of a mod without saving, diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 7a21a3a8..1306be5f 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Penumbra.Mods; @@ -37,5 +38,28 @@ public sealed partial class Mod ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; } + + + // Try to obtain a mod by its directory name (unique identifier, preferred), + // or the first mod of the given name if no directory fits. + public bool TryGetMod( string modDirectory, string modName, [NotNullWhen( true )] out Mod? mod ) + { + mod = null; + foreach( var m in _mods ) + { + if( m.ModPath.Name == modDirectory ) + { + mod = m; + return true; + } + + if( m.Name == modName ) + { + mod ??= m; + } + } + + return mod != null; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index a6bd27de..98922fdd 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using OtterGui.Filesystem; +using Penumbra.Util; namespace Penumbra.Mods; @@ -10,7 +11,7 @@ namespace Penumbra.Mods; public class ModSettings { public static readonly ModSettings Empty = new(); - public List< uint > Settings { get; init; } = new(); + public List< uint > Settings { get; private init; } = new(); public int Priority { get; set; } public bool Enabled { get; set; } @@ -100,7 +101,7 @@ public class ModSettings private static uint FixSetting( IModGroup group, uint value ) => group.Type switch { - SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), + SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), _ => value, }; @@ -208,4 +209,31 @@ public class ModSettings return changes; } } + + // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. + // Does not repair settings but ignores settings not fitting to the given mod. + public (bool Enabled, int Priority, Dictionary< string, IList< string > > Settings) ConvertToShareable( Mod mod ) + { + var dict = new Dictionary< string, IList< string > >( Settings.Count ); + foreach( var (setting, idx) in Settings.WithIndex() ) + { + if( idx >= mod.Groups.Count ) + { + break; + } + + var group = mod.Groups[ idx ]; + if( group.Type == SelectType.Single && setting < group.Count ) + { + dict.Add( group.Name, new[] { group[ ( int )setting ].Name } ); + } + else + { + var list = group.Where( ( _, optionIdx ) => ( setting & ( 1 << optionIdx ) ) != 0 ).Select( o => o.Name ).ToList(); + dict.Add( group.Name, list ); + } + } + + return ( Enabled, Priority, dict ); + } } \ No newline at end of file From fc767589a21af0b4babe389538d818af63f2fa6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 16:00:20 +0200 Subject: [PATCH 05/13] Change everything in collection caches to use IMod and introduce TemporaryMod. --- Penumbra/Api/PenumbraApi.cs | 2 +- .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Collections/ModCollection.Cache.cs | 81 ++++++++++--------- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 4 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 4 +- Penumbra/Meta/Manager/MetaManager.cs | 4 +- Penumbra/Mods/Editor/IMod.cs | 19 +++++ Penumbra/Mods/Editor/Mod.Editor.cs | 7 +- Penumbra/Mods/Mod.BasePath.cs | 12 ++- Penumbra/Mods/Mod.Meta.cs | 7 +- Penumbra/Mods/Mod.TemporaryMod.cs | 26 ++++++ Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 4 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 4 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 9 ++- 18 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 Penumbra/Mods/Editor/IMod.cs create mode 100644 Penumbra/Mods/Mod.TemporaryMod.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 19af38d8..895d6af9 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -378,7 +378,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( group.Type == SelectType.Single ) { var name = optionNames[ ^1 ]; - var optionIdx = group.IndexOf( o => o.Name == name ); + var optionIdx = group.IndexOf( o => o.Name == optionNames[^1] ); if( optionIdx < 0 ) { return PenumbraApiEc.OptionMissing; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 5cf71fc2..24324553 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -65,8 +65,8 @@ public partial class ModCollection internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); - internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >(); + internal IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< IMod >, object?) >(); internal IEnumerable< SingleArray< ModConflicts > > AllConflicts => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index d4e75a2c..7ffdfb61 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -11,8 +11,8 @@ using Penumbra.Util; namespace Penumbra.Collections; -public record struct ModPath( Mod Mod, FullPath Path ); -public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); +public record struct ModPath( IMod Mod, FullPath Path ); +public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public partial class ModCollection { @@ -21,15 +21,15 @@ public partial class ModCollection private class Cache : IDisposable { private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); public readonly MetaManager MetaManipulations; - private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); + private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); public IEnumerable< SingleArray< ModConflicts > > AllConflicts => _conflicts.Values; - public SingleArray< ModConflicts > Conflicts( Mod mod ) + public SingleArray< ModConflicts > Conflicts( IMod mod ) => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); // Count the number of changes of the effective file list. @@ -38,7 +38,7 @@ public partial class ModCollection private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems { get { @@ -178,13 +178,13 @@ public partial class ModCollection } } - public void ReloadMod( Mod mod, bool addMetaChanges ) + public void ReloadMod( IMod mod, bool addMetaChanges ) { RemoveMod( mod, addMetaChanges ); AddMod( mod, addMetaChanges ); } - public void RemoveMod( Mod mod, bool addMetaChanges ) + public void RemoveMod( IMod mod, bool addMetaChanges ) { var conflicts = Conflicts( mod ); @@ -243,37 +243,40 @@ public partial class ModCollection // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod( Mod mod, bool addMetaChanges ) + public void AddMod( IMod mod, bool addMetaChanges ) { - var settings = _collection[ mod.Index ].Settings; - if( settings is not { Enabled: true } ) + if( mod.Index >= 0 ) { - return; - } - - foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) - { - if( group.Count == 0 ) + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) { - continue; + return; } - var config = settings.Settings[ groupIndex ]; - switch( group.Type ) + foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) { - case SelectType.Single: - AddSubMod( group[ ( int )config ], mod ); - break; - case SelectType.Multi: + if( group.Count == 0 ) { - foreach( var (option, _) in group.WithIndex() - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) - { - AddSubMod( option, mod ); - } + continue; + } - break; + var config = settings.Settings[ groupIndex ]; + switch( group.Type ) + { + case SelectType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } + + break; + } } } } @@ -297,7 +300,7 @@ public partial class ModCollection } // Add all files and possibly manipulations of a specific submod - private void AddSubMod( ISubMod subMod, Mod parentMod ) + private void AddSubMod( ISubMod subMod, IMod parentMod ) { foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) { @@ -320,7 +323,7 @@ public partial class ModCollection // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, Mod mod ) + private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) { if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) { @@ -343,7 +346,7 @@ public partial class ModCollection // Remove all empty conflict sets for a given mod with the given conflicts. // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) { var changedConflicts = oldConflicts.Remove( c => { @@ -372,10 +375,10 @@ public partial class ModCollection // Add a new conflict between the added mod and the existing mod. // Update all other existing conflicts between the existing mod and other mods if necessary. // Returns if the added mod takes priority before the existing mod. - private bool AddConflict( object data, Mod addedMod, Mod existingMod ) + private bool AddConflict( object data, IMod addedMod, IMod existingMod ) { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; if( existingPriority < addedPriority ) { @@ -417,7 +420,7 @@ public partial class ModCollection // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, Mod mod ) + private void AddManipulation( MetaManipulation manip, IMod mod ) { if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) { @@ -463,7 +466,7 @@ public partial class ModCollection { if( !_changedItems.TryGetValue( name, out var data ) ) { - _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + _changedItems.Add( name, ( new SingleArray< IMod >( modPath.Mod ), obj ) ); } else if( !data.Item1.Contains( modPath.Mod ) ) { diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index eae00788..d9371c41 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerCmp : IDisposable { public CmpFile? File = null; - public readonly Dictionary< RspManipulation, Mod > Manipulations = new(); + public readonly Dictionary< RspManipulation, IMod > Manipulations = new(); public MetaManagerCmp() { } @@ -39,7 +39,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, Mod mod ) + public bool ApplyMod( RspManipulation m, IMod mod ) { #if USE_CMP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index bb5ec9d0..92a60470 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -17,7 +17,7 @@ public partial class MetaManager { public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, IMod > Manipulations = new(); public MetaManagerEqdp() { } @@ -51,7 +51,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, Mod mod ) + public bool ApplyMod( EqdpManipulation m, IMod mod ) { #if USE_EQDP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 9c980d67..831e26d9 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerEqp : IDisposable { public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EqpManipulation, IMod > Manipulations = new(); public MetaManagerEqp() { } @@ -39,7 +39,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, Mod mod ) + public bool ApplyMod( EqpManipulation m, IMod mod ) { #if USE_EQP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index ccc5f926..7af2609c 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -18,7 +18,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, Mod > Manipulations = new(); + public readonly Dictionary< EstManipulation, IMod > Manipulations = new(); public MetaManagerEst() { } @@ -51,7 +51,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, Mod mod ) + public bool ApplyMod( EstManipulation m, IMod mod ) { #if USE_EST Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 49c29076..82833017 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -14,7 +14,7 @@ public partial class MetaManager public struct MetaManagerGmp : IDisposable { public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, Mod > Manipulations = new(); + public readonly Dictionary< GmpManipulation, IMod > Manipulations = new(); public MetaManagerGmp() { } @@ -38,7 +38,7 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, Mod mod ) + public bool ApplyMod( GmpManipulation m, IMod mod ) { #if USE_GMP Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 23bdf125..35e7434f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -18,7 +18,7 @@ public partial class MetaManager public readonly struct MetaManagerImc : IDisposable { public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, Mod > Manipulations = new(); + public readonly Dictionary< ImcManipulation, IMod > Manipulations = new(); private readonly ModCollection _collection; private static int _imcManagerCount; @@ -65,7 +65,7 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, Mod mod ) + public bool ApplyMod( ImcManipulation m, IMod mod ) { #if USE_IMC Manipulations[ m ] = mod; diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 5d6c0a5b..a71e37b1 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -30,7 +30,7 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod ) + public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out IMod? mod ) { mod = manip.ManipulationType switch { @@ -86,7 +86,7 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, Mod mod ) + public bool ApplyMod( MetaManipulation m, IMod mod ) { return m.ManipulationType switch { diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs new file mode 100644 index 00000000..5b7b0f20 --- /dev/null +++ b/Penumbra/Mods/Editor/IMod.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public interface IMod +{ + LowerString Name { get; } + + public int Index { get; } + public int Priority { get; } + + public int TotalManipulations { get; } + + public ISubMod Default { get; } + public IReadOnlyList< IModGroup > Groups { get; } + + public IEnumerable< ISubMod > AllSubMods { get; } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index 74158a4e..455dcf8e 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; using Penumbra.Util; namespace Penumbra.Mods; -public partial class Mod +public partial class Mod : IMod { public partial class Editor : IDisposable { @@ -15,7 +12,7 @@ public partial class Mod public Editor( Mod mod, int groupIdx, int optionIdx ) { - _mod = mod; + _mod = mod; SetSubMod( groupIdx, optionIdx ); GroupIdx = groupIdx; _subMod = _mod._default; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 43581735..0add9356 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -17,6 +17,10 @@ public partial class Mod public DirectoryInfo ModPath { get; private set; } public int Index { get; private set; } = -1; + // Unused if Index < 0 but used for special temporary mods. + public int Priority + => 0; + private Mod( DirectoryInfo modPath ) => ModPath = modPath; @@ -30,7 +34,7 @@ public partial class Mod } var mod = new Mod( modPath ); - if( !mod.Reload(out _) ) + if( !mod.Reload( out _ ) ) { // Can not be base path not existing because that is checked before. PluginLog.Error( $"Mod at {modPath} without name is not supported." ); @@ -40,15 +44,17 @@ public partial class Mod return mod; } - private bool Reload(out MetaChangeType metaChange) + private bool Reload( out MetaChangeType metaChange ) { metaChange = MetaChangeType.Deletion; ModPath.Refresh(); if( !ModPath.Exists ) + { return false; + } metaChange = LoadMeta(); - if( metaChange.HasFlag(MetaChangeType.Deletion) || Name.Length == 0 ) + if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 ) { return false; } diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index 3e052b3c..615e0dc1 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -24,10 +24,11 @@ public enum MetaChangeType : ushort public sealed partial class Mod { - public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." )) + public static readonly TemporaryMod ForcedFiles = new() { - Name = "Forced Files", - Index = -1, + Name = "Forced Files", + Index = -1, + Priority = int.MaxValue, }; public const uint CurrentFileVersion = 1; diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs new file mode 100644 index 00000000..6662f826 --- /dev/null +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public sealed partial class Mod +{ + public class TemporaryMod : IMod + { + public LowerString Name { get; init; } = LowerString.Empty; + public int Index { get; init; } = -2; + public int Priority { get; init; } = int.MaxValue; + + public int TotalManipulations + => Default.Manipulations.Count; + + public ISubMod Default { get; } = new SubMod(); + + public IReadOnlyList< IModGroup > Groups + => Array.Empty< IModGroup >(); + + public IEnumerable< ISubMod > AllSubMods + => new[] { Default }; + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 6eb6c5ca..b9105bda 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -23,13 +23,13 @@ public partial class ConfigWindow private void DrawChangedItemTab() { // Functions in here for less pollution. - bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) => ( _changedItemFilter.IsEmpty || ChangedItemName( item.Key, item.Value.Item2 ) .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) { ImGui.TableNextColumn(); DrawChangedItem( item.Key, item.Value.Item2, false ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 38c37485..b9a2213f 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -112,7 +112,7 @@ public partial class ConfigWindow { // We can treat all meta manipulations the same, // we are only really interested in their ToString function here. - static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp ) + static (object, IMod) Convert< T >( KeyValuePair< T, IMod > kvp ) => ( kvp.Key!, kvp.Value ); var it = m.Cmp.Manipulations.Select( Convert ) @@ -183,7 +183,7 @@ public partial class ConfigWindow } // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, Mod) pair ) + private static void DrawLine( (object, IMod) pair ) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index d1c4a838..a7af5ae4 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -128,16 +128,19 @@ public partial class ConfigWindow foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) { - if( ImGui.Selectable( conflict.Mod2.Name ) ) + if( ImGui.Selectable( conflict.Mod2.Name ) && conflict.Mod2 is Mod mod ) { - _window._selector.SelectByValue( conflict.Mod2 ); + _window._selector.SelectByValue( mod ); } ImGui.SameLine(); using( var color = ImRaii.PushColor( ImGuiCol.Text, conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) { - ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" ); + var priority = conflict.Mod2.Index < 0 + ? conflict.Mod2.Priority + : Penumbra.CollectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + ImGui.TextUnformatted( $"(Priority {priority})" ); } using var indent = ImRaii.PushIndent( 30f ); From 8b7dc8fa5b4a8e8b7412954faf89dd4310320b69 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 18 Jun 2022 21:44:26 +0000 Subject: [PATCH 06/13] [CI] Updating repo.json for refs/tags/0.5.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 3fca4ca9..fa95b882 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.1.0", - "TestingAssemblyVersion": "0.5.1.0", + "AssemblyVersion": "0.5.1.1", + "TestingAssemblyVersion": "0.5.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c6e6c0098c6934a496b8f4e3264b71b5295264dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Jun 2022 23:54:52 +0200 Subject: [PATCH 07/13] Emergency Fix --- Penumbra/Penumbra.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 215261b0..4470d29a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,8 +77,11 @@ public class MainClass : IDalamudPlugin private static bool CheckIsNotInstalled() { #if !DEBUG - return !Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Name.Equals( "installedPlugins", - StringComparison.InvariantCultureIgnoreCase ) ?? true; + var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; + if (!ret) + PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); + return !ret; #else return false; #endif From 37dcd0ba55784ffae8eeb55f0d647272f7865248 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 18 Jun 2022 21:57:17 +0000 Subject: [PATCH 08/13] [CI] Updating repo.json for refs/tags/0.5.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index fa95b882..08da37ac 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.1.1", - "TestingAssemblyVersion": "0.5.1.1", + "AssemblyVersion": "0.5.1.2", + "TestingAssemblyVersion": "0.5.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9d43895f388fdff344e47aae6167c415ca1f6f6c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 02:43:25 +0200 Subject: [PATCH 09/13] Destroy clippers to stop leaking. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index d97a2692..729f12c5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d97a26923981db2a27d0172367c9e2841767f9b1 +Subproject commit 729f12c5560536fe77f2a09f3b76c1c3c4cdb1ff From c330859abc9b235f65332d662da0ab1390fd205c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 11:42:38 +0200 Subject: [PATCH 10/13] Add customizable hotkeys for mod deletion. --- OtterGui | 2 +- Penumbra/Configuration.cs | 2 ++ Penumbra/UI/Classes/ModFileSystemSelector.cs | 5 +++-- Penumbra/UI/ConfigWindow.SettingsTab.General.cs | 10 ++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 729f12c5..6ce8ca81 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 729f12c5560536fe77f2a09f3b76c1c3c4cdb1ff +Subproject commit 6ce8ca816678e7a363f9f4a6f43f009f8d79c070 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index dfa22fd2..3d38a069 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration; using Dalamud.Logging; +using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Import; using Penumbra.UI.Classes; @@ -46,6 +47,7 @@ public partial class Configuration : IPluginConfiguration public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; public string DefaultImportFolder { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 51490733..a062ff89 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -14,6 +14,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Numerics; +using OtterGui.Classes; namespace Penumbra.UI.Classes; @@ -286,14 +287,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod private void DeleteModButton( Vector2 size ) { - var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; + var keys = Penumbra.Config.DeleteModModifier.IsActive(); var tt = SelectedLeaf == null ? "No mod selected." : "Delete the currently selected mod entirely from your drive.\n" + "This can not be undone."; if( !keys ) { - tt += "\nHold Control and Shift while clicking to delete the mod."; + tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; } if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs index b545b1eb..22319bfc 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs @@ -6,6 +6,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Filesystem; using OtterGui.Raii; +using OtterGui.Widgets; namespace Penumbra.UI; @@ -89,6 +90,15 @@ public partial class ConfigWindow Penumbra.Config.OpenFoldersByDefault = v; _window._selector.SetFilterDirty(); } ); + + Widget.DoubleModifierSelector( "Mod Deletion Modifier", + "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", _window._inputTextWidth.X, + Penumbra.Config.DeleteModModifier, + v => + { + Penumbra.Config.DeleteModModifier = v; + Penumbra.Config.Save(); + } ); ImGui.Dummy( _window._defaultSpace ); Checkbox( "Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", From c64743ee98446d9e028c75b3a9e6df25db3cebcc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 11:47:14 +0200 Subject: [PATCH 11/13] actually reset test branch to master release, not before master release. --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fbad3c82..3b1efa01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,10 +75,10 @@ jobs: git config --global user.name "Actions User" git config --global user.email "actions@github.com" - git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout master + git fetch origin master && git fetch origin test && git checkout master git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true git push origin master || true - git checkout test + git branch -f test origin/master && git checkout test git push origin test -f || true From 8422d36e4e927e5476c7ac2f24508a4a8e1bb36e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 15:00:24 +0200 Subject: [PATCH 12/13] Add another another animation hook. I hate animations. --- Penumbra/Interop/Resolver/PathResolver.Animation.cs | 13 +++++++++++++ Penumbra/Interop/Resolver/PathResolver.Data.cs | 3 +++ Penumbra/Interop/Resolver/PathResolver.cs | 3 --- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 53cb7be5..1d51b8df 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -73,6 +73,7 @@ public unsafe partial class PathResolver return ret; } + // Unknown what exactly this is but it seems to load a bunch of paps. public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 ); [Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )] @@ -93,4 +94,16 @@ public unsafe partial class PathResolver LoadSomePapHook!.Original( a1, a2, a3, a4 ); _animationLoadCollection = last; } + + // Seems to load character actions when zoning or changing class, maybe. + [Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )] + public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook; + + private void SomeActionLoadDetour( IntPtr gameObject ) + { + var last = _animationLoadCollection; + _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + SomeActionLoadHook!.Original( gameObject ); + _animationLoadCollection = last; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 28241e9c..7f12e600 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -94,6 +94,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Enable(); LoadSomeAvfxHook?.Enable(); LoadSomePapHook?.Enable(); + SomeActionLoadHook?.Enable(); } private void DisableDataHooks() @@ -107,6 +108,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Disable(); LoadSomeAvfxHook?.Disable(); LoadSomePapHook?.Disable(); + SomeActionLoadHook?.Disable(); } private void DisposeDataHooks() @@ -119,6 +121,7 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook?.Dispose(); LoadSomeAvfxHook?.Dispose(); LoadSomePapHook?.Dispose(); + SomeActionLoadHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index ab4d208b..9c8111b7 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -76,9 +76,6 @@ public partial class PathResolver : IDisposable private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { - if( type == ResourceType.Pap && _.Path.EndsWith( '0', '1', '0', '.', 'p', 'a', 'p' ) ) - PluginLog.Information( $"PAPPITY PAP {_}" ); - if( _animationLoadCollection != null ) { switch( type ) From d6d13594e0d61b10e1e698f07125266aec683c30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 19 Jun 2022 19:20:02 +0200 Subject: [PATCH 13/13] Add Mare Synchronos and MUI API/IPC functions for testing. Not tested myself because how. --- Penumbra/Api/IPenumbraApi.cs | 45 +-- Penumbra/Api/ModsController.cs | 3 +- Penumbra/Api/PenumbraApi.cs | 287 +++++++++++++----- Penumbra/Api/PenumbraIpc.cs | 217 +++++++++++++ Penumbra/Api/SimpleRedirectManager.cs | 125 -------- Penumbra/Api/TempModManager.cs | 261 ++++++++++++++++ .../Collections/ModCollection.Cache.Access.cs | 18 ++ Penumbra/Collections/ModCollection.Cache.cs | 12 +- Penumbra/Collections/ModCollection.cs | 11 + .../Interop/Resolver/PathResolver.Data.cs | 3 +- .../Meta/Manipulations/MetaManipulation.cs | 2 + Penumbra/Mods/Mod.TemporaryMod.cs | 19 +- Penumbra/Penumbra.cs | 8 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 8 +- Penumbra/UI/ConfigWindow.DebugTab.cs | 78 ++++- 15 files changed, 857 insertions(+), 240 deletions(-) delete mode 100644 Penumbra/Api/SimpleRedirectManager.cs create mode 100644 Penumbra/Api/TempModManager.cs diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 5107562c..9ba916d4 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,13 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel.Design; using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; -using OtterGui.Classes; -using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods; namespace Penumbra.Api; @@ -24,7 +20,7 @@ public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex ) public enum PenumbraApiEc { - Okay = 0, + Success = 0, NothingChanged = 1, CollectionMissing = 2, ModMissing = 3, @@ -37,6 +33,7 @@ public enum PenumbraApiEc FileMissing = 9, InvalidManipulation = 10, InvalidArgument = 11, + UnknownError = 255, } public interface IPenumbraApi : IPenumbraApiBase @@ -136,32 +133,36 @@ public interface IPenumbraApi : IPenumbraApiBase // If any setting can not be found, it will not change anything. public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ); - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList< string > options ); // Create a temporary collection without actual settings but with a cache. - // If character is non-zero and either no character collection for this character exists or forceOverwriteCharacter is true, + // If no character collection for this character exists or forceOverwriteCharacter is true, // associate this collection to a specific character. - // Can return Okay, CharacterCollectionExists or NothingChanged. - public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ); + // Can return Okay, CharacterCollectionExists or NothingChanged, as well as the name of the new temporary collection on success. + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ); - // Remove a temporary collection if it exists. + // Remove the temporary collection associated with characterName if it exists. // Can return Okay or NothingChanged. - public PenumbraApiEc RemoveTemporaryCollection( string collectionName ); + public PenumbraApiEc RemoveTemporaryCollection( string characterName ); + // Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections. + // Can return Okay, InvalidGamePath, or InvalidManipulation. + public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, + int priority ); - // Set or remove a specific file redirection or meta manipulation under the name of Tag and with a given priority - // for a given collection, which may be temporary. - // Can return Okay, CollectionMissing, InvalidPath, FileMissing, LowerPriority, or NothingChanged. - public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ); + // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary. + // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation. + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, + IReadOnlySet< string > manipCodes, + int priority ); - // Can return Okay, CollectionMissing, InvalidManipulation, LowerPriority, or NothingChanged. - public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ); + // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. + // Can return Okay or NothingDone. + public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ); - // Can return Okay, CollectionMissing, InvalidPath, or NothingChanged. - public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ); - - // Can return Okay, CollectionMissing, InvalidManipulation, or NothingChanged. - public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ); + // Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary. + // Can return Okay or NothingDone. + public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ); } \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index a909aab4..082bbb25 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -37,7 +37,6 @@ public class ModsController : WebApiController return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), o => o.Value.Path.FullName - ) - ?? new Dictionary< string, string >(); + ); } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 895d6af9..99898ba5 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -9,9 +9,11 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; using Newtonsoft.Json; +using OtterGui; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Util; @@ -61,24 +63,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public event ChangedItemHover? ChangedItemTooltip; - internal bool HasTooltip - => ChangedItemTooltip != null; - - internal void InvokeTooltip( object? it ) - => ChangedItemTooltip?.Invoke( it ); - - internal void InvokeClick( MouseButton button, object? it ) - => ChangedItemClicked?.Invoke( button, it ); - - - private void CheckInitialized() - { - if( !Valid ) - { - throw new Exception( "PluginShare is not initialized." ); - } - } - public void RedrawObject( int tableIndex, RedrawType setting ) { CheckInitialized(); @@ -97,29 +81,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); } - private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) - { - GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); - } - public void RedrawAll( RedrawType setting ) { CheckInitialized(); _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) - { - if( !Penumbra.Config.EnableMods ) - { - return path; - } - - var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath( gamePath ); - return ret?.ToString() ?? path; - } - public string ResolvePath( string path ) { CheckInitialized(); @@ -145,25 +112,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi return ret.Select( r => r.ToString() ).ToList(); } - private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource - { - CheckInitialized(); - try - { - if( Path.IsPathRooted( resolvedPath ) ) - { - return _lumina?.GetFileFromDisk< T >( resolvedPath ); - } - - return Dalamud.GameData.GetFile< T >( resolvedPath ); - } - catch( Exception e ) - { - PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); - return null; - } - } - public T? GetFile< T >( string gamePath ) where T : FileResource => GetFileIntern< T >( ResolvePath( gamePath ) ); @@ -259,11 +207,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings; if( settings == null ) { - return ( PenumbraApiEc.Okay, null ); + return ( PenumbraApiEc.Success, null ); } var shareSettings = settings.ConvertToShareable( mod ); - return ( PenumbraApiEc.Okay, + return ( PenumbraApiEc.Success, ( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) ); } @@ -281,7 +229,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } - return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) @@ -297,7 +245,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; } - return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) @@ -313,7 +261,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.ModMissing; } - return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, @@ -344,10 +292,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx; - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName, IReadOnlyList< string > optionNames ) { CheckInitialized(); @@ -378,7 +326,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( group.Type == SelectType.Single ) { var name = optionNames[ ^1 ]; - var optionIdx = group.IndexOf( o => o.Name == optionNames[^1] ); + var optionIdx = group.IndexOf( o => o.Name == optionNames[ ^1 ] ); if( optionIdx < 0 ) { return PenumbraApiEc.OptionMissing; @@ -400,24 +348,213 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Okay : PenumbraApiEc.NothingChanged; + return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } - public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) - => throw new NotImplementedException(); + public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter ) + { + CheckInitialized(); + if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) + || Penumbra.TempMods.Collections.ContainsKey( character ) ) + { + return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); + } - public PenumbraApiEc RemoveTemporaryCollection( string collectionName ) - => throw new NotImplementedException(); + var name = Penumbra.TempMods.SetTemporaryCollection( tag, character ); + return ( PenumbraApiEc.Success, name ); + } - public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ) - => throw new NotImplementedException(); + public PenumbraApiEc RemoveTemporaryCollection( string character ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.ContainsKey( character ) ) + { + return PenumbraApiEc.NothingChanged; + } - public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ) - => throw new NotImplementedException(); + Penumbra.TempMods.RemoveTemporaryCollection( character ); + return PenumbraApiEc.Success; + } - public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ) - => throw new NotImplementedException(); + public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes, + int priority ) + { + CheckInitialized(); + if( !ConvertPaths( paths, out var p ) ) + { + return PenumbraApiEc.InvalidGamePath; + } - public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ) - => throw new NotImplementedException(); + if( !ConvertManips( manipCodes, out var m ) ) + { + return PenumbraApiEc.InvalidManipulation; + } + + return Penumbra.TempMods.Register( tag, null, p, m, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths, + IReadOnlySet< string > manipCodes, int priority ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + if( !ConvertPaths( paths, out var p ) ) + { + return PenumbraApiEc.InvalidGamePath; + } + + if( !ConvertManips( manipCodes, out var m ) ) + { + return PenumbraApiEc.InvalidManipulation; + } + + return Penumbra.TempMods.Register( tag, collection, p, m, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority ) + { + CheckInitialized(); + return Penumbra.TempMods.Unregister( tag, null, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + } + + public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority ) + { + CheckInitialized(); + if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection ) + && !Penumbra.CollectionManager.ByName( collectionName, out collection ) ) + { + return PenumbraApiEc.CollectionMissing; + } + + return Penumbra.TempMods.Unregister( tag, collection, priority ) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + } + + internal bool HasTooltip + => ChangedItemTooltip != null; + + internal void InvokeTooltip( object? it ) + => ChangedItemTooltip?.Invoke( it ); + + internal void InvokeClick( MouseButton button, object? it ) + => ChangedItemClicked?.Invoke( button, it ); + + + private void CheckInitialized() + { + if( !Valid ) + { + throw new Exception( "PluginShare is not initialized." ); + } + } + + private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex ) + { + GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex ); + } + + // Resolve a path given by string for a specific collection. + private static string ResolvePath( string path, Mod.Manager _, ModCollection collection ) + { + if( !Penumbra.Config.EnableMods ) + { + return path; + } + + var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath( gamePath ); + return ret?.ToString() ?? path; + } + + // Get a file for a resolved path. + private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource + { + CheckInitialized(); + try + { + if( Path.IsPathRooted( resolvedPath ) ) + { + return _lumina?.GetFileFromDisk< T >( resolvedPath ); + } + + return Dalamud.GameData.GetFile< T >( resolvedPath ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" ); + return null; + } + } + + + // Convert a dictionary of strings to a dictionary of gamepaths to full paths. + // Only returns true if all paths can successfully be converted and added. + private static bool ConvertPaths( IReadOnlyDictionary< string, string > redirections, + [NotNullWhen( true )] out Dictionary< Utf8GamePath, FullPath >? paths ) + { + paths = new Dictionary< Utf8GamePath, FullPath >( redirections.Count ); + foreach( var (gString, fString) in redirections ) + { + if( !Utf8GamePath.FromString( gString, out var path, false ) ) + { + paths = null; + return false; + } + + var fullPath = new FullPath( fString ); + if( !paths.TryAdd( path, fullPath ) ) + { + paths = null; + return false; + } + } + + return true; + } + + // Convert manipulations from transmitted base64 strings to actual manipulations. + // Only returns true if all conversions are successful and distinct. + private static bool ConvertManips( IReadOnlyCollection< string > manipStrings, + [NotNullWhen( true )] out HashSet< MetaManipulation >? manips ) + { + manips = new HashSet< MetaManipulation >( manipStrings.Count ); + foreach( var m in manipStrings ) + { + if( Functions.FromCompressedBase64< MetaManipulation >( m, out var manip ) != MetaManipulation.CurrentVersion ) + { + manips = null; + return false; + } + + if( !manips.Add( manip ) ) + { + manips = null; + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index 14f3ea11..1aa4ce24 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -22,6 +22,8 @@ public partial class PenumbraIpc : IDisposable InitializeRedrawProviders( pi ); InitializeChangedItemProviders( pi ); InitializeDataProviders( pi ); + InitializeSettingProviders( pi ); + InitializeTempProviders( pi ); ProviderInitialized?.SendMessage(); } @@ -32,6 +34,8 @@ public partial class PenumbraIpc : IDisposable DisposeRedrawProviders(); DisposeResolveProviders(); DisposeGeneralProviders(); + DisposeSettingProviders(); + DisposeTempProviders(); ProviderDisposed?.SendMessage(); } } @@ -402,4 +406,217 @@ public partial class PenumbraIpc ProviderDefaultCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc(); } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderGetAvailableModSettings = "Penumbra.GetAvailableModSettings"; + public const string LabelProviderGetCurrentModSettings = "Penumbra.GetCurrentModSettings"; + public const string LabelProviderTryInheritMod = "Penumbra.TryInheritMod"; + public const string LabelProviderTrySetMod = "Penumbra.TrySetMod"; + public const string LabelProviderTrySetModPriority = "Penumbra.TrySetModPriority"; + public const string LabelProviderTrySetModSetting = "Penumbra.TrySetModSetting"; + public const string LabelProviderTrySetModSettings = "Penumbra.TrySetModSettings"; + + internal ICallGateProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >? ProviderGetAvailableModSettings; + + internal ICallGateProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >? + ProviderGetCurrentModSettings; + + internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTryInheritMod; + internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTrySetMod; + internal ICallGateProvider< string, string, string, int, PenumbraApiEc >? ProviderTrySetModPriority; + internal ICallGateProvider< string, string, string, string, string, PenumbraApiEc >? ProviderTrySetModSetting; + internal ICallGateProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >? ProviderTrySetModSettings; + + private void InitializeSettingProviders( DalamudPluginInterface pi ) + { + try + { + ProviderGetAvailableModSettings = + pi.GetIpcProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >( + LabelProviderGetAvailableModSettings ); + ProviderGetAvailableModSettings.RegisterFunc( Api.GetAvailableModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" ); + } + + try + { + ProviderGetCurrentModSettings = + pi.GetIpcProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >( + LabelProviderGetCurrentModSettings ); + ProviderGetCurrentModSettings.RegisterFunc( Api.GetCurrentModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" ); + } + + try + { + ProviderTryInheritMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTryInheritMod ); + ProviderTryInheritMod.RegisterFunc( Api.TryInheritMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" ); + } + + try + { + ProviderTrySetMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTrySetMod ); + ProviderTrySetMod.RegisterFunc( Api.TrySetMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" ); + } + + try + { + ProviderTrySetModPriority = pi.GetIpcProvider< string, string, string, int, PenumbraApiEc >( LabelProviderTrySetModPriority ); + ProviderTrySetModPriority.RegisterFunc( Api.TrySetModPriority ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" ); + } + + try + { + ProviderTrySetModSetting = + pi.GetIpcProvider< string, string, string, string, string, PenumbraApiEc >( LabelProviderTrySetModSetting ); + ProviderTrySetModSetting.RegisterFunc( Api.TrySetModSetting ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" ); + } + + try + { + ProviderTrySetModSettings = + pi.GetIpcProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >( LabelProviderTrySetModSettings ); + ProviderTrySetModSettings.RegisterFunc( Api.TrySetModSettings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" ); + } + } + + private void DisposeSettingProviders() + { + ProviderGetAvailableModSettings?.UnregisterFunc(); + ProviderGetCurrentModSettings?.UnregisterFunc(); + ProviderTryInheritMod?.UnregisterFunc(); + ProviderTrySetMod?.UnregisterFunc(); + ProviderTrySetModPriority?.UnregisterFunc(); + ProviderTrySetModSetting?.UnregisterFunc(); + ProviderTrySetModSettings?.UnregisterFunc(); + } +} + +public partial class PenumbraIpc +{ + public const string LabelProviderCreateTemporaryCollection = "Penumbra.CreateTemporaryCollection"; + public const string LabelProviderRemoveTemporaryCollection = "Penumbra.RemoveTemporaryCollection"; + public const string LabelProviderAddTemporaryModAll = "Penumbra.AddTemporaryModAll"; + public const string LabelProviderAddTemporaryMod = "Penumbra.AddTemporaryMod"; + public const string LabelProviderRemoveTemporaryModAll = "Penumbra.RemoveTemporaryModAll"; + public const string LabelProviderRemoveTemporaryMod = "Penumbra.RemoveTemporaryMod"; + + internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection; + internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection; + + internal ICallGateProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + ProviderAddTemporaryModAll; + + internal ICallGateProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >? + ProviderAddTemporaryMod; + + internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll; + internal ICallGateProvider< string, string, int, PenumbraApiEc >? ProviderRemoveTemporaryMod; + + private void InitializeTempProviders( DalamudPluginInterface pi ) + { + try + { + ProviderCreateTemporaryCollection = + pi.GetIpcProvider< string, string, bool, (PenumbraApiEc, string) >( LabelProviderCreateTemporaryCollection ); + ProviderCreateTemporaryCollection.RegisterFunc( Api.CreateTemporaryCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryCollection = + pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderRemoveTemporaryCollection ); + ProviderRemoveTemporaryCollection.RegisterFunc( Api.RemoveTemporaryCollection ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" ); + } + + try + { + ProviderAddTemporaryModAll = + pi.GetIpcProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + LabelProviderAddTemporaryModAll ); + ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" ); + } + + try + { + ProviderAddTemporaryMod = + pi.GetIpcProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >( + LabelProviderAddTemporaryMod ); + ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryModAll = pi.GetIpcProvider< string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryModAll ); + ProviderRemoveTemporaryModAll.RegisterFunc( Api.RemoveTemporaryModAll ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" ); + } + + try + { + ProviderRemoveTemporaryMod = pi.GetIpcProvider< string, string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryMod ); + ProviderRemoveTemporaryMod.RegisterFunc( Api.RemoveTemporaryMod ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" ); + } + } + + private void DisposeTempProviders() + { + ProviderCreateTemporaryCollection?.UnregisterFunc(); + ProviderRemoveTemporaryCollection?.UnregisterFunc(); + ProviderAddTemporaryModAll?.UnregisterFunc(); + ProviderAddTemporaryMod?.UnregisterFunc(); + ProviderRemoveTemporaryModAll?.UnregisterFunc(); + ProviderRemoveTemporaryMod?.UnregisterFunc(); + } } \ No newline at end of file diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs deleted file mode 100644 index 2f3e219f..00000000 --- a/Penumbra/Api/SimpleRedirectManager.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Logging; -using Penumbra.Collections; -using Penumbra.GameData.ByteString; -using Penumbra.Mods; - -namespace Penumbra.Api; - -public enum RedirectResult -{ - Registered = 0, - Success = 0, - IdenticalFileRegistered = 1, - InvalidGamePath = 2, - OtherOwner = 3, - NotRegistered = 4, - NoPermission = 5, - FilteredGamePath = 6, - UnknownError = 7, -} - -public class SimpleRedirectManager -{ - internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); - public readonly HashSet< string > AllowedTags = new(); - - public void Apply( IDictionary< Utf8GamePath, ModPath > dict ) - { - foreach( var (gamePath, (file, _)) in Replacements ) - { - dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) ); - } - } - - private RedirectResult? CheckPermission( string tag ) - => AllowedTags.Contains( tag ) ? null : RedirectResult.NoPermission; - - public RedirectResult IsRegistered( Utf8GamePath path, string tag ) - => CheckPermission( tag ) - ?? ( Replacements.TryGetValue( path, out var pair ) - ? pair.Tag == tag ? RedirectResult.Registered : RedirectResult.OtherOwner - : RedirectResult.NotRegistered ); - - public RedirectResult Register( Utf8GamePath path, FullPath file, string tag ) - { - if( CheckPermission( tag ) != null ) - { - return RedirectResult.NoPermission; - } - - if( Mod.FilterFile( path ) ) - { - return RedirectResult.FilteredGamePath; - } - - try - { - if( Replacements.TryGetValue( path, out var pair ) ) - { - if( file.Equals( pair.File ) ) - { - return RedirectResult.IdenticalFileRegistered; - } - - if( tag != pair.Tag ) - { - return RedirectResult.OtherOwner; - } - } - - Replacements[ path ] = ( file, tag ); - return RedirectResult.Success; - } - catch( Exception e ) - { - PluginLog.Error( $"[{tag}] Unknown Error registering simple redirect {path} -> {file}:\n{e}" ); - return RedirectResult.UnknownError; - } - } - - public RedirectResult Unregister( Utf8GamePath path, string tag ) - { - if( CheckPermission( tag ) != null ) - { - return RedirectResult.NoPermission; - } - - try - { - if( !Replacements.TryGetValue( path, out var pair ) ) - { - return RedirectResult.NotRegistered; - } - - if( tag != pair.Tag ) - { - return RedirectResult.OtherOwner; - } - - Replacements.Remove( path ); - return RedirectResult.Success; - } - catch( Exception e ) - { - PluginLog.Error( $"[{tag}] Unknown Error unregistering simple redirect {path}:\n{e}" ); - return RedirectResult.UnknownError; - } - } - - public RedirectResult Register( string path, string file, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? Register( gamePath, new FullPath( file ), tag ) - : RedirectResult.InvalidGamePath; - - public RedirectResult Unregister( string path, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? Unregister( gamePath, tag ) - : RedirectResult.InvalidGamePath; - - public RedirectResult IsRegistered( string path, string tag ) - => Utf8GamePath.FromString( path, out var gamePath, true ) - ? IsRegistered( gamePath, tag ) - : RedirectResult.InvalidGamePath; -} \ No newline at end of file diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs new file mode 100644 index 00000000..b3be6577 --- /dev/null +++ b/Penumbra/Api/TempModManager.cs @@ -0,0 +1,261 @@ +using System.Collections.Generic; +using Penumbra.Collections; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; + +namespace Penumbra.Api; + +public enum RedirectResult +{ + Success = 0, + IdenticalFileRegistered = 1, + NotRegistered = 2, + FilteredGamePath = 3, +} + +public class TempModManager +{ + private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new(); + private readonly List< Mod.TemporaryMod > _modsForAllCollections = new(); + private readonly Dictionary< string, ModCollection > _collections = new(); + + public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods + => _mods; + + public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections + => _modsForAllCollections; + + public IReadOnlyDictionary< string, ModCollection > Collections + => _collections; + + // These functions to check specific redirections or meta manipulations for existence are currently unused. + //public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority ) + //{ + // var mod = GetExistingMod( tag, collection, null ); + // if( mod == null ) + // { + // priority = 0; + // fullPath = null; + // return false; + // } + // + // priority = mod.Priority; + // if( mod.Default.Files.TryGetValue( gamePath, out var f ) ) + // { + // fullPath = f; + // return true; + // } + // + // fullPath = null; + // return false; + //} + // + //public bool IsRegistered( string tag, ModCollection? collection, MetaManipulation meta, out MetaManipulation? manipulation, + // out int priority ) + //{ + // var mod = GetExistingMod( tag, collection, null ); + // if( mod == null ) + // { + // priority = 0; + // manipulation = null; + // return false; + // } + // + // priority = mod.Priority; + // // IReadOnlySet has no TryGetValue for some reason. + // if( ( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var manip ) ) + // { + // manipulation = manip; + // return true; + // } + // + // manipulation = null; + // return false; + //} + + // These functions for setting single redirections or manips are currently unused. + //public RedirectResult Register( string tag, ModCollection? collection, Utf8GamePath path, FullPath file, int priority ) + //{ + // if( Mod.FilterFile( path ) ) + // { + // return RedirectResult.FilteredGamePath; + // } + // + // var mod = GetOrCreateMod( tag, collection, priority, out var created ); + // + // var changes = !mod.Default.Files.TryGetValue( path, out var oldFile ) || !oldFile.Equals( file ); + // mod.SetFile( path, file ); + // ApplyModChange( mod, collection, created, false ); + // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; + //} + // + //public RedirectResult Register( string tag, ModCollection? collection, MetaManipulation meta, int priority ) + //{ + // var mod = GetOrCreateMod( tag, collection, priority, out var created ); + // var changes = !( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var oldMeta ) + // || !oldMeta.Equals( meta ); + // mod.SetManipulation( meta ); + // ApplyModChange( mod, collection, created, false ); + // return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success; + //} + + public RedirectResult Register( string tag, ModCollection? collection, Dictionary< Utf8GamePath, FullPath > dict, + HashSet< MetaManipulation > manips, int priority ) + { + var mod = GetOrCreateMod( tag, collection, priority, out var created ); + mod.SetAll( dict, manips ); + ApplyModChange( mod, collection, created, false ); + return RedirectResult.Success; + } + + public RedirectResult Unregister( string tag, ModCollection? collection, int? priority ) + { + var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; + if( list == null ) + { + return RedirectResult.NotRegistered; + } + + var removed = _modsForAllCollections.RemoveAll( m => + { + if( m.Name != tag || priority != null && m.Priority != priority.Value ) + { + return false; + } + + ApplyModChange( m, collection, false, true ); + return true; + } ); + + if( removed == 0 ) + { + return RedirectResult.NotRegistered; + } + + if( list.Count == 0 && collection != null ) + { + _mods.Remove( collection ); + } + + return RedirectResult.Success; + } + + public string SetTemporaryCollection( string tag, string characterName ) + { + var collection = ModCollection.CreateNewTemporary( tag, characterName ); + _collections[ characterName ] = collection; + return collection.Name; + } + + public bool RemoveTemporaryCollection( string characterName ) + { + if( _collections.Remove( characterName, out var c ) ) + { + _mods.Remove( c ); + return true; + } + + return false; + } + + + // Apply any new changes to the temporary mod. + private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) + { + if( collection == null ) + { + if( removed ) + { + foreach( var c in Penumbra.CollectionManager ) + { + c.Remove( mod ); + } + } + else + { + foreach( var c in Penumbra.CollectionManager ) + { + c.Apply( mod, created ); + } + } + } + else + { + if( removed ) + { + collection.Remove( mod ); + } + else + { + collection.Apply( mod, created ); + } + } + } + + // Only find already existing mods, currently unused. + //private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) + //{ + // var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; + // if( list == null ) + // { + // return null; + // } + // + // if( priority != null ) + // { + // return list.Find( m => m.Priority == priority.Value && m.Name == tag ); + // } + // + // Mod.TemporaryMod? highestMod = null; + // var highestPriority = int.MinValue; + // foreach( var m in list ) + // { + // if( highestPriority < m.Priority && m.Name == tag ) + // { + // highestPriority = m.Priority; + // highestMod = m; + // } + // } + // + // return highestMod; + //} + + // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). + // Returns the found or created mod and whether it was newly created. + private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created ) + { + List< Mod.TemporaryMod > list; + if( collection == null ) + { + list = _modsForAllCollections; + } + else if( _mods.TryGetValue( collection, out var l ) ) + { + list = l; + } + else + { + list = new List< Mod.TemporaryMod >(); + _mods.Add( collection, list ); + } + + var mod = list.Find( m => m.Priority == priority && m.Name == tag ); + if( mod == null ) + { + mod = new Mod.TemporaryMod() + { + Name = tag, + Priority = priority, + }; + list.Add( mod ); + created = true; + } + else + { + created = false; + } + + return mod; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 24324553..a6fa0ffe 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -35,6 +35,24 @@ public partial class ModCollection private void ForceCacheUpdate() => CalculateEffectiveFileList(); + // Handle temporary mods for this collection. + public void Apply( Mod.TemporaryMod tempMod, bool created ) + { + if( created ) + { + _cache?.AddMod( tempMod, tempMod.TotalManipulations > 0 ); + } + else + { + _cache?.ReloadMod( tempMod, tempMod.TotalManipulations > 0 ); + } + } + + public void Remove( Mod.TemporaryMod tempMod ) + { + _cache?.RemoveMod( tempMod, tempMod.TotalManipulations > 0 ); + } + // Clear the current cache. private void ClearCache() diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 7ffdfb61..62b92f7f 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -20,10 +20,10 @@ public partial class ModCollection // It will only be setup if a collection gets activated in any way. private class Cache : IDisposable { - private readonly ModCollection _collection; + private readonly ModCollection _collection; private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); public IEnumerable< SingleArray< ModConflicts > > AllConflicts @@ -160,7 +160,11 @@ public partial class ModCollection _conflicts.Clear(); // Add all forced redirects. - Penumbra.Redirects.Apply( ResolvedFiles ); + foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( + Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< Mod.TemporaryMod >() ) ) + { + AddMod( tempMod, false ); + } foreach( var mod in Penumbra.ModManager ) { diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 7d82e28b..1c913a11 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -74,6 +74,17 @@ public partial class ModCollection public static ModCollection CreateNewEmpty( string name ) => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); + // Create a new temporary collection that does not save and has a negative index. + public static ModCollection CreateNewTemporary(string tag, string characterName) + { + var collection = new ModCollection($"{tag}_{characterName}_temporary", Empty); + collection.ModSettingChanged -= collection.SaveOnChange; + collection.InheritanceChanged -= collection.SaveOnChange; + collection.Index = ~Penumbra.TempMods.Collections.Count; + collection.CreateCache(); + return collection; + } + // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) => new(name, this); diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7f12e600..afc4f7d1 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -309,7 +309,8 @@ public unsafe partial class PathResolver } ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.CollectionManager.Character( actualName ); + // First check temporary character collections, then the own configuration. + return Penumbra.TempMods.Collections.TryGetValue(actualName, out var c) ? c : Penumbra.CollectionManager.Character( actualName ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 4f2439a9..5732522d 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -19,6 +19,8 @@ public interface IMetaManipulation< T > [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > { + public const int CurrentVersion = 0; + public enum Type : byte { Unknown = 0, diff --git a/Penumbra/Mods/Mod.TemporaryMod.cs b/Penumbra/Mods/Mod.TemporaryMod.cs index 6662f826..35a4795a 100644 --- a/Penumbra/Mods/Mod.TemporaryMod.cs +++ b/Penumbra/Mods/Mod.TemporaryMod.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using OtterGui.Classes; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; @@ -15,12 +17,27 @@ public sealed partial class Mod public int TotalManipulations => Default.Manipulations.Count; - public ISubMod Default { get; } = new SubMod(); + public ISubMod Default + => _default; public IReadOnlyList< IModGroup > Groups => Array.Empty< IModGroup >(); public IEnumerable< ISubMod > AllSubMods => new[] { Default }; + + private readonly SubMod _default = new(); + + public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) + => _default.FileData[ gamePath ] = fullPath; + + public bool SetManipulation( MetaManipulation manip ) + => _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip ); + + public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips ) + { + _default.FileData = dict; + _default.ManipulationData = manips; + } } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4470d29a..b892a602 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -57,7 +57,7 @@ public class MainClass : IDalamudPlugin { #if !DEBUG var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var dir = new DirectoryInfo( path ); try { @@ -78,7 +78,7 @@ public class MainClass : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false; if (!ret) PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." ); return !ret; @@ -105,7 +105,7 @@ public class Penumbra : IDisposable public static MetaFileManager MetaFileManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; - public static SimpleRedirectManager Redirects { get; private set; } = null!; + public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!; public static int ImcExceptions = 0; @@ -138,7 +138,7 @@ public class Penumbra : IDisposable } ResidentResources = new ResidentResourceManager(); - Redirects = new SimpleRedirectManager(); + TempMods = new TempModManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 73d1ff6e..d3a44af2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -856,8 +856,6 @@ public partial class ModEditWindow return newValue != currentValue; } - private const byte CurrentManipulationVersion = 0; - private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations ) { if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) ) @@ -865,7 +863,7 @@ public partial class ModEditWindow return; } - var text = Functions.ToCompressedBase64( manipulations, CurrentManipulationVersion ); + var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion ); if( text.Length > 0 ) { ImGui.SetClipboardText( text ); @@ -878,7 +876,7 @@ public partial class ModEditWindow { var clipboard = ImGui.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == CurrentManipulationVersion && manips != null ) + if( version == MetaManipulation.CurrentVersion && manips != null ) { foreach( var manip in manips ) { @@ -897,7 +895,7 @@ public partial class ModEditWindow { var clipboard = ImGui.GetClipboardText(); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); - if( version == CurrentManipulationVersion && manips != null ) + if( version == MetaManipulation.CurrentVersion && manips != null ) { _editor!.Meta.Clear(); foreach( var manip in manips ) diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs index 31b4b405..95be2069 100644 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -10,8 +11,10 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Api; +using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Structs; +using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.CharacterUtility; namespace Penumbra.UI; @@ -413,11 +416,84 @@ public partial class ConfigWindow foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) { var value = provider.GetValue( ipc ); - if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label )) + if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) ) { ImGui.TextUnformatted( label ); } } + + using( var collTree = ImRaii.TreeNode( "Collections" ) ) + { + if( collTree ) + { + using var table = ImRaii.Table( "##collTree", 4 ); + if( table ) + { + foreach( var (character, collection) in Penumbra.TempMods.Collections ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( character ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" ); + } + } + } + } + + using( var modTree = ImRaii.TreeNode( "Mods" ) ) + { + if( modTree ) + { + using var table = ImRaii.Table( "##modTree", 5 ); + + void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list ) + { + foreach( var mod in list ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Name ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Priority.ToString() ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( collectionName ); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.Default.Files.Count.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var (path, file) in mod.Default.Files ) + { + ImGui.TextUnformatted( $"{path} -> {file}" ); + } + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( mod.TotalManipulations.ToString() ); + if( ImGui.IsItemHovered() ) + { + using var tt = ImRaii.Tooltip(); + foreach( var manip in mod.Default.Manipulations ) + { + ImGui.TextUnformatted( manip.ToString() ); + } + } + } + } + + if( table ) + { + PrintList( "All", Penumbra.TempMods.ModsForAllCollections ); + foreach( var (collection, list) in Penumbra.TempMods.Mods ) + { + PrintList( collection.Name, list ); + } + } + } + } } // Helper to print a property and its value in a 2-column table.