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