diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7712c56c..cac23ecb 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -8,8 +8,6 @@ using Lumina.Data; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Mod; -using Penumbra.Mods; namespace Penumbra.Api; @@ -78,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mod.Mod.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 17f11eae..30b9298d 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,8 +1,7 @@ -using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; @@ -14,122 +13,39 @@ public partial class ModCollection // Is invoked after the collections actually changed. public event CollectionChangeDelegate? CollectionChanged; - private int _currentIdx = 1; - private int _defaultIdx = 0; - private int _defaultNameIdx = 0; + // The collection currently selected for changing settings. + public ModCollection Current { get; private set; } = Empty; - public ModCollection Current - => this[ _currentIdx ]; + // The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } = Empty; - public ModCollection Default - => this[ _defaultIdx ]; + // A single collection that can not be deleted as a fallback for the current collection. + public ModCollection DefaultName { get; private set; } = Empty; - private readonly Dictionary< string, int > _character = new(); + // The list of character collections. + private readonly Dictionary< string, ModCollection > _characters = new(); + public IReadOnlyDictionary< string, ModCollection > Characters + => _characters; + + // If a name does not correspond to a character, return the default collection instead. public ModCollection Character( string name ) - => _character.TryGetValue( name, out var idx ) ? this[ idx ] : Default; - - public IEnumerable< (string, ModCollection) > Characters - => _character.Select( kvp => ( kvp.Key, this[ kvp.Value ] ) ); + => _characters.TryGetValue( name, out var c ) ? c : Default; public bool HasCharacterCollections - => _character.Count > 0; - - private void OnModChanged( Mod.Mod.ChangeType type, int idx, Mod.Mod mod ) - { - var meta = mod.Resources.MetaManipulations.Count > 0; - switch( type ) - { - case Mod.Mod.ChangeType.Added: - foreach( var collection in this ) - { - collection.AddMod( mod ); - } - - foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - case Mod.Mod.ChangeType.Removed: - var list = new List< ModSettings? >( _collections.Count ); - foreach( var collection in this ) - { - list.Add( collection[ idx ].Settings ); - collection.RemoveMod( mod, idx ); - } - - foreach( var (collection, _) in this.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - case Mod.Mod.ChangeType.Changed: - foreach( var collection in this.Where( - collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) - { - collection.Save(); - } - - foreach( var collection in this.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) - { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); - } - - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } - } - - private void CreateNecessaryCaches() - { - if( _defaultIdx > Empty.Index ) - { - Default.CreateCache(true); - } - - if( _currentIdx > Empty.Index ) - { - Current.CreateCache(false); - } - - foreach( var idx in _character.Values.Where( i => i > Empty.Index ) ) - { - _collections[ idx ].CreateCache(false); - } - } - - public void ForceCacheUpdates() - { - foreach( var collection in this ) - { - collection.ForceCacheUpdate(collection == Default); - } - } - - private void RemoveCache( int idx ) - { - if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) - { - _collections[ idx ].ClearCache(); - } - } - - public void SetCollection( ModCollection collection, Type type, string? characterName = null ) - => SetCollection( collection.Index, type, characterName ); + => _characters.Count > 0; + // Set a active collection, can be used to set Default, Current or Character collections. public void SetCollection( int newIdx, Type type, string? characterName = null ) { var oldCollectionIdx = type switch { - Type.Default => _defaultIdx, - Type.Current => _currentIdx, + Type.Default => Default.Index, + Type.Current => Current.Index, Type.Character => characterName?.Length > 0 - ? _character.TryGetValue( characterName, out var c ) - ? c - : _defaultIdx + ? _characters.TryGetValue( characterName, out var c ) + ? c.Index + : Default.Index : -1, _ => -1, }; @@ -142,24 +58,24 @@ public partial class ModCollection var newCollection = this[ newIdx ]; if( newIdx > Empty.Index ) { - newCollection.CreateCache(false); + newCollection.CreateCache( false ); } RemoveCache( oldCollectionIdx ); switch( type ) { case Type.Default: - _defaultIdx = newIdx; + Default = newCollection; Penumbra.Config.DefaultCollection = newCollection.Name; Penumbra.ResidentResources.Reload(); Default.SetFiles(); break; case Type.Current: - _currentIdx = newIdx; + Current = newCollection; Penumbra.Config.CurrentCollection = newCollection.Name; break; case Type.Character: - _character[ characterName! ] = newIdx; + _characters[ characterName! ] = newCollection; Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; break; } @@ -168,27 +84,32 @@ public partial class ModCollection Penumbra.Config.Save(); } + public void SetCollection( ModCollection collection, Type type, string? characterName = null ) + => SetCollection( collection.Index, type, characterName ); + + // Create a new character collection. Returns false if the character name already has a collection. public bool CreateCharacterCollection( string characterName ) { - if( _character.ContainsKey( characterName ) ) + if( _characters.ContainsKey( characterName ) ) { return false; } - _character[ characterName ] = Empty.Index; + _characters[ characterName ] = Empty; Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name; Penumbra.Config.Save(); CollectionChanged?.Invoke( null, Empty, Type.Character, characterName ); return true; } + // Remove a character collection if it exists. public void RemoveCharacterCollection( string characterName ) { - if( _character.TryGetValue( characterName, out var collection ) ) + if( _characters.TryGetValue( characterName, out var collection ) ) { - RemoveCache( collection ); - _character.Remove( characterName ); - CollectionChanged?.Invoke( this[ collection ], null, Type.Character, characterName ); + RemoveCache( collection.Index ); + _characters.Remove( characterName ); + CollectionChanged?.Invoke( collection, null, Type.Character, characterName ); } if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) @@ -197,36 +118,41 @@ public partial class ModCollection } } + // Obtain the index of a collection by name. private int GetIndexForCollectionName( string name ) - { - if( name.Length == 0 ) - { - return Empty.Index; - } + => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); - return _collections.IndexOf( c => c.Name == name ); - } + // Load default, current and character collections from config. + // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. public void LoadCollections() { var configChanged = false; - _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); - if( _defaultIdx < 0 ) + var defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + if( defaultIdx < 0 ) { PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); - _defaultIdx = Empty.Index; - Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; + Default = Empty; + Penumbra.Config.DefaultCollection = Default.Name; configChanged = true; } + else + { + Default = this[ defaultIdx ]; + } - _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); - if( _currentIdx < 0 ) + var currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + if( currentIdx < 0 ) { PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); - _currentIdx = _defaultNameIdx; - Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; + Current = DefaultName; + Penumbra.Config.DefaultCollection = Current.Name; configChanged = true; } + else + { + Current = this[ currentIdx ]; + } if( LoadCharacterCollections() || configChanged ) { @@ -236,6 +162,7 @@ public partial class ModCollection CreateNecessaryCaches(); } + // Load character collections. If a player name comes up multiple times, the last one is applied. private bool LoadCharacterCollections() { var configChanged = false; @@ -245,17 +172,71 @@ public partial class ModCollection if( idx < 0 ) { PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); - _character.Add( player, Empty.Index ); + _characters.Add( player, Empty ); Penumbra.Config.CharacterCollections[ player ] = Empty.Name; configChanged = true; } else { - _character.Add( player, idx ); + _characters.Add( player, this[ idx ] ); } } return configChanged; } + + + // Cache handling. + private void CreateNecessaryCaches() + { + Default.CreateCache( true ); + Current.CreateCache( false ); + + foreach( var collection in _characters.Values ) + { + collection.CreateCache( false ); + } + } + + private void RemoveCache( int idx ) + { + if( idx != Default.Index && idx != Current.Index && _characters.Values.All( c => c.Index != idx ) ) + { + _collections[ idx ].ClearCache(); + } + } + + private void ForceCacheUpdates() + { + foreach( var collection in this ) + { + collection.ForceCacheUpdate( collection == Default ); + } + } + + // Recalculate effective files for active collections on events. + private void OnModAddedActive( bool meta ) + { + foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } + + private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + { + foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } + + private void OnModChangedActive( bool meta, int modIdx ) + { + foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) ) + { + collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + } + } } } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 440f88ac..76961f81 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Dalamud.Logging; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; @@ -13,19 +14,23 @@ public partial class ModCollection { public enum Type : byte { - Inactive, - Default, - Character, - Current, + Inactive, // A collection was added or removed + Default, // The default collection was changed + Character, // A character collection was changed + Current, // The current collection was changed. } public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > { + // On addition, oldCollection is null. On deletion, newCollection is null. + // CharacterName is onls set for type == Character. public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, Type type, string? characterName = null ); - private readonly Mod.Mod.Manager _modManager; + private readonly Mods.Mod.Manager _modManager; + // The empty collection is always available and always has index 0. + // It can not be deleted or moved. private readonly List< ModCollection > _collections = new() { Empty, @@ -34,25 +39,28 @@ public partial class ModCollection public ModCollection this[ Index idx ] => _collections[ idx ]; - public ModCollection this[ int idx ] - => _collections[ idx ]; - public ModCollection? this[ string name ] => ByName( name, out var c ) ? c : null; + public int Count + => _collections.Count; + + // Obtain a collection case-independently by name. public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + // Default enumeration skips the empty collection. public IEnumerator< ModCollection > GetEnumerator() => _collections.Skip( 1 ).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mod.Mod.Manager manager ) + public Manager( Mods.Mod.Manager manager ) { _modManager = manager; + // The collection manager reacts to changes in mods by itself. _modManager.ModsRediscovered += OnModsRediscovered; _modManager.ModChange += OnModChanged; ReadCollections(); @@ -65,27 +73,143 @@ public partial class ModCollection _modManager.ModChange -= OnModChanged; } + // Add a new collection of the given name. + // If duplicate is not-null, the new collection will be a duplicate of it. + // If the name of the collection would result in an already existing filename, skip it. + // Returns true if the collection was successfully created and fires a Inactive event. + // Also sets the current collection to the new collection afterwards. + public bool AddCollection( string name, ModCollection? duplicate ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed.Length == 0 + || nameFixed == Empty.Name.ToLowerInvariant() + || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) + { + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; + } + + var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); + newCollection.Index = _collections.Count; + _collections.Add( newCollection ); + newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); + SetCollection( newCollection.Index, Type.Current ); + return true; + } + + // Remove the given collection if it exists and is neither the empty nor the default-named collection. + // If the removed collection was active, it also sets the corresponding collection to the appropriate default. + public bool RemoveCollection( int idx ) + { + if( idx <= Empty.Index || idx >= _collections.Count ) + { + PluginLog.Error( "Can not remove the empty collection." ); + return false; + } + + if( idx == DefaultName.Index ) + { + PluginLog.Error( "Can not remove the default collection." ); + return false; + } + + if( idx == Current.Index ) + { + SetCollection( DefaultName, Type.Current ); + } + + if( idx == Default.Index ) + { + SetCollection( Empty, Type.Default ); + } + + foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) + { + SetCollection( Empty, Type.Character, characterName ); + } + + var collection = _collections[ idx ]; + collection.Delete(); + _collections.RemoveAt( idx ); + for( var i = idx; i < _collections.Count; ++i ) + { + --_collections[ i ].Index; + } + + CollectionChanged?.Invoke( collection, null, Type.Inactive ); + return true; + } + + public bool RemoveCollection( ModCollection collection ) + => RemoveCollection( collection.Index ); + + private void OnModsRediscovered() { + // When mods are rediscovered, force all cache updates and set the files of the default collection. ForceCacheUpdates(); Default.SetFiles(); } + + // A changed mod forces changes for all collections, active and inactive. + private void OnModChanged( Mod.ChangeType type, int idx, Mod mod ) + { + switch( type ) + { + case Mod.ChangeType.Added: + foreach( var collection in this ) + { + collection.AddMod( mod ); + } + + OnModAddedActive( mod.Resources.MetaManipulations.Count > 0 ); + break; + case Mod.ChangeType.Removed: + var settings = new List< ModSettings? >( _collections.Count ); + foreach( var collection in this ) + { + settings.Add( collection[ idx ].Settings ); + collection.RemoveMod( mod, idx ); + } + + OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); + break; + case Mod.ChangeType.Changed: + foreach( var collection in this.Where( + collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + { + collection.Save(); + } + + OnModChangedActive( mod.Resources.MetaManipulations.Count > 0, mod.Index ); + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + // Add the collection with the default name if it does not exist. + // It should always be ensured that it exists, otherwise it will be created. + // This can also not be deleted, so there are always at least the empty and a collection with default name. private void AddDefaultCollection() { - var idx = _collections.IndexOf( c => c.Name == DefaultCollection ); + var idx = GetIndexForCollectionName( DefaultCollection ); if( idx >= 0 ) { - _defaultNameIdx = idx; + DefaultName = this[ idx ]; return; } var defaultCollection = CreateNewEmpty( DefaultCollection ); defaultCollection.Save(); - _defaultNameIdx = _collections.Count; + defaultCollection.Index = _collections.Count; _collections.Add( defaultCollection ); } + // Inheritances can not be setup before all collections are read, + // so this happens after reading the collections. + // During this iteration, we can also fix all settings that are not valid for the given mod anymore. private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) { foreach( var (collection, inheritance) in this.Zip( inheritances ) ) @@ -117,6 +241,9 @@ public partial class ModCollection } } + // Read all collection files in the Collection Directory. + // Ensure that the default named collection exists, and apply inheritances afterwards. + // Duplicate collection files are not deleted, just not added here. private void ReadCollections() { var collectionDir = new DirectoryInfo( CollectionDirectory ); @@ -152,89 +279,5 @@ public partial class ModCollection AddDefaultCollection(); ApplyInheritancesAndFixSettings( inheritances ); } - - public bool AddCollection( string name, ModCollection? duplicate ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 - || nameFixed == Empty.Name.ToLowerInvariant() - || _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) - { - PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); - return false; - } - - var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); - newCollection.Index = _collections.Count; - _collections.Add( newCollection ); - newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, Type.Inactive ); - SetCollection( newCollection.Index, Type.Current ); - return true; - } - - public bool RemoveCollection( ModCollection collection ) - => RemoveCollection( collection.Index ); - - public bool RemoveCollection( int idx ) - { - if( idx <= Empty.Index || idx >= _collections.Count ) - { - PluginLog.Error( "Can not remove the empty collection." ); - return false; - } - - if( idx == _defaultNameIdx ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - if( idx == _currentIdx ) - { - SetCollection( _defaultNameIdx, Type.Current ); - } - else if( _currentIdx > idx ) - { - --_currentIdx; - } - - if( idx == _defaultIdx ) - { - SetCollection( -1, Type.Default ); - } - else if( _defaultIdx > idx ) - { - --_defaultIdx; - } - - if( _defaultNameIdx > idx ) - { - --_defaultNameIdx; - } - - foreach( var (characterName, characterIdx) in _character.ToList() ) - { - if( idx == characterIdx ) - { - SetCollection( -1, Type.Character, characterName ); - } - else if( characterIdx > idx ) - { - _character[ characterName ] = characterIdx - 1; - } - } - - var collection = _collections[ idx ]; - collection.Delete(); - _collections.RemoveAt( idx ); - for( var i = idx; i < _collections.Count; ++i ) - { - --_collections[ i ].Index; - } - - CollectionChanged?.Invoke( collection, null, Type.Inactive ); - return true; - } } } \ No newline at end of file diff --git a/Penumbra/Collections/ConflictCache.cs b/Penumbra/Collections/ConflictCache.cs index 85db330d..1891a8bb 100644 --- a/Penumbra/Collections/ConflictCache.cs +++ b/Penumbra/Collections/ConflictCache.cs @@ -8,24 +8,26 @@ namespace Penumbra.Collections; public struct ConflictCache { - public readonly struct ModCacheStruct : IComparable< ModCacheStruct > + // A conflict stores all data about a mod conflict. + public readonly struct Conflict : IComparable< Conflict > { - public readonly object Conflict; + public readonly object Data; public readonly int Mod1; public readonly int Mod2; public readonly bool Mod1Priority; public readonly bool Solved; - public ModCacheStruct( int modIdx1, int modIdx2, int priority1, int priority2, object conflict ) + public Conflict( int modIdx1, int modIdx2, bool priority, bool solved, object data ) { Mod1 = modIdx1; Mod2 = modIdx2; - Conflict = conflict; - Mod1Priority = priority1 >= priority2; - Solved = priority1 != priority2; + Data = data; + Mod1Priority = priority; + Solved = solved; } - public int CompareTo( ModCacheStruct other ) + // Order: Mod1 -> Mod1 overwritten -> Mod2 -> File > MetaManipulation + public int CompareTo( Conflict other ) { var idxComp = Mod1.CompareTo( other.Mod1 ); if( idxComp != 0 ) @@ -44,55 +46,85 @@ public struct ConflictCache return idxComp; } - return Conflict switch + return Data switch { - Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ), - Utf8GamePath => -1, - MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ), - MetaManipulation => 1, - _ => 0, + Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ), + Utf8GamePath => -1, + MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ), + MetaManipulation => 1, + _ => 0, }; } } - private List< ModCacheStruct >? _conflicts; + private readonly List< Conflict > _conflicts = new(); + private bool _isSorted = true; - public IReadOnlyList< ModCacheStruct > Conflicts - => _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); + public ConflictCache() + { } - public IEnumerable< ModCacheStruct > ModConflicts( int modIdx ) + public IReadOnlyList< Conflict > Conflicts { - return _conflicts?.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ) - ?? Array.Empty< ModCacheStruct >(); + get + { + Sort(); + return _conflicts; + } } - public void Sort() - => _conflicts?.Sort(); + // Find all mod conflicts concerning the specified mod (in both directions). + public IEnumerable< Conflict > ModConflicts( int modIdx ) + { + return _conflicts.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ); + } + + private void Sort() + { + if( !_isSorted ) + { + _conflicts?.Sort(); + } + } + + // Add both directions for the mod. + // On same priority, it is assumed that mod1 is the earlier one. + // Also update older conflicts to refer to the highest-prioritized conflict. + private void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, object data ) + { + var solved = priority1 != priority2; + var priority = priority1 >= priority2; + var prioritizedMod = priority ? modIdx1 : modIdx2; + _conflicts.Add( new Conflict( modIdx1, modIdx2, priority, solved, data ) ); + _conflicts.Add( new Conflict( modIdx2, modIdx1, !priority, solved, data ) ); + for( var i = 0; i < _conflicts.Count; ++i ) + { + var c = _conflicts[ i ]; + if( data.Equals( c.Data ) ) + { + _conflicts[ i ] = c.Mod1Priority + ? new Conflict( prioritizedMod, c.Mod2, true, c.Solved || solved, data ) + : new Conflict( c.Mod1, prioritizedMod, false, c.Solved || solved, data ); + } + } + + _isSorted = false; + } public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath ) - { - _conflicts ??= new List< ModCacheStruct >( 2 ); - - _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) ); - _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) ); - } + => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath ); public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) - { - _conflicts ??= new List< ModCacheStruct >( 2 ); - _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) ); - _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) ); - } + => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation ); public void ClearConflicts() => _conflicts?.Clear(); public void ClearFileConflicts() - => _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath ); + => _conflicts?.RemoveAll( m => m.Data is Utf8GamePath ); public void ClearMetaConflicts() - => _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation ); + => _conflicts?.RemoveAll( m => m.Data is MetaManipulation ); public void ClearConflictsWithMod( int modIdx ) - => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx ); + => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == modIdx ); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 01a53f87..33726689 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -7,49 +7,53 @@ using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { + // Only active collections need to have a cache. private Cache? _cache; public bool HasCache => _cache != null; + // Only create, do not update. public void CreateCache( bool isDefault ) { - if( Index == 0 ) - { - return; - } - if( _cache == null ) { CalculateEffectiveFileList( true, isDefault ); } } + // Force an update with metadata for this cache. public void ForceCacheUpdate( bool isDefault ) => CalculateEffectiveFileList( true, isDefault ); + + // Clear the current cache. public void ClearCache() { _cache?.Dispose(); _cache = null; } + public FullPath? ResolvePath( Utf8GamePath path ) => _cache?.ResolvePath( path ); + // Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile( Utf8GamePath path, FullPath fullPath ) => _cache!.ResolvedFiles[ path ] = fullPath; + // Force a file resolve to be removed. internal void RemoveFile( Utf8GamePath path ) => _cache!.ResolvedFiles.Remove( path ); + // Obtain data from the cache. internal MetaManager? MetaCache => _cache?.MetaManipulations; @@ -62,14 +66,17 @@ public partial class ModCollection internal IReadOnlyDictionary< string, object? > ChangedItems => _cache?.ChangedItems ?? new Dictionary< string, object? >(); - internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); + internal IReadOnlyList< ConflictCache.Conflict > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); - internal IEnumerable< ConflictCache.ModCacheStruct > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.ModCacheStruct >(); + internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); + // Update the effective file list for the given cache. + // Creates a cache if necessary. public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) { + // Skip the empty collection. if( Index == 0 ) { return; @@ -87,8 +94,84 @@ public partial class ModCollection { Penumbra.ResidentResources.Reload(); } + } - _cache.Conflicts.Sort(); + // Set Metadata files. + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + _cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + _cache.MetaManipulations.SetFiles(); + } } @@ -106,8 +189,9 @@ public partial class ModCollection public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new(); public readonly MetaManager MetaManipulations; - public ConflictCache Conflicts; + public ConflictCache Conflicts = new(); + // Obtain currently changed items. Computes them if they haven't been computed before. public IReadOnlyDictionary< string, object? > ChangedItems { get @@ -117,6 +201,7 @@ public partial class ModCollection } } + // The cache reacts through events on its collection changing. public Cache( ModCollection collection ) { _collection = collection; @@ -133,6 +218,8 @@ public partial class ModCollection private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) { + // Recompute the file list if it was not just a non-conflicting priority change + // or a setting change for a disabled mod. if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any() || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) { @@ -143,9 +230,12 @@ public partial class ModCollection _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); } + // Inheritance changes are too big to check for relevance, + // just recompute everything. private void OnInheritanceChange( bool _ ) => _collection.CalculateEffectiveFileList( true, true ); + // Reset the shared file-seen cache. private static void ResetFileSeen( int size ) { if( size < FileSeen.Length ) @@ -160,6 +250,8 @@ public partial class ModCollection } } + + // Clear all local and global caches to prepare for recomputation. private void ClearStorageAndPrepare() { ResolvedFiles.Clear(); @@ -167,15 +259,15 @@ public partial class ModCollection RegisteredFiles.Clear(); _changedItems.Clear(); ResolvedSettings.Clear(); + Conflicts.ClearFileConflicts(); + // Obtains actual settings for this collection with all inheritances. ResolvedSettings.AddRange( _collection.ActualSettings ); } public void CalculateEffectiveFileList() { ClearStorageAndPrepare(); - - Conflicts.ClearFileConflicts(); - for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) + for( var i = 0; i < Penumbra.ModManager.Count; ++i ) { if( ResolvedSettings[ i ]?.Enabled == true ) { @@ -185,7 +277,6 @@ public partial class ModCollection } AddMetaFiles(); - Conflicts.Sort(); } private void SetChangedItems() @@ -204,6 +295,7 @@ public partial class ModCollection { identifier.Identify( _changedItems, resolved.ToGamePath() ); } + // TODO: Meta Manipulations } catch( Exception e ) { @@ -211,12 +303,12 @@ public partial class ModCollection } } - private void AddFiles( int idx ) { var mod = Penumbra.ModManager.Mods[ idx ]; ResetFileSeen( mod.Resources.ModFiles.Count ); // Iterate in reverse so that later groups take precedence before earlier ones. + // TODO: add group priorities. foreach( var group in mod.Meta.Groups.Values.Reverse() ) { switch( group.SelectionType ) @@ -240,7 +332,6 @@ public partial class ModCollection => !Penumbra.Config.DisableSoundStreaming && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) { if( FilterFile( gamePath ) ) @@ -250,11 +341,13 @@ public partial class ModCollection if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) { + // No current conflict, just add. RegisteredFiles.Add( gamePath, modIdx ); ResolvedFiles[ gamePath ] = file; } else { + // Conflict, check which mod has higher priority, replace if necessary, add conflict. var priority = ResolvedSettings[ modIdx ]!.Priority; var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); @@ -270,6 +363,8 @@ public partial class ModCollection { switch( file.Extension.ToLowerInvariant() ) { + // We do not care for those file types + case ".scp" when !Penumbra.Config.DisableSoundStreaming: case ".meta": case ".rgsp": return; @@ -279,10 +374,11 @@ public partial class ModCollection } } - private void AddPathsForOption( Option option, Mod.Mod mod, int modIdx, bool enabled ) + private void AddPathsForOption( Option option, Mod mod, int modIdx, bool enabled ) { foreach( var (file, paths) in option.OptionFiles ) { + // TODO: complete rework of options. var fullPath = new FullPath( mod.BasePath, file ); var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); if( idx < 0 ) @@ -309,7 +405,7 @@ public partial class ModCollection } } - private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod, int modIdx ) + private void AddFilesForSingle( OptionGroup singleGroup, Mod mod, int modIdx ) { Debug.Assert( singleGroup.SelectionType == SelectType.Single ); var settings = ResolvedSettings[ modIdx ]!; @@ -324,7 +420,7 @@ public partial class ModCollection } } - private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod, int modIdx ) + private void AddFilesForMulti( OptionGroup multiGroup, Mod mod, int modIdx ) { Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); var settings = ResolvedSettings[ modIdx ]!; @@ -340,7 +436,7 @@ public partial class ModCollection } } - private void AddRemainingFiles( Mod.Mod mod, int modIdx ) + private void AddRemainingFiles( Mod mod, int modIdx ) { for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) { @@ -431,81 +527,4 @@ public partial class ModCollection return candidate; } } - - [Conditional( "USE_EQP" )] - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqp.SetFiles(); - } - } - - [Conditional( "USE_EQDP" )] - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqdp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqdp.SetFiles(); - } - } - - [Conditional( "USE_GMP" )] - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerGmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Gmp.SetFiles(); - } - } - - [Conditional( "USE_EST" )] - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEst.ResetFiles(); - } - else - { - _cache.MetaManipulations.Est.SetFiles(); - } - } - - [Conditional( "USE_CMP" )] - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerCmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Cmp.SetFiles(); - } - } - - public void SetFiles() - { - if( _cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - _cache.MetaManipulations.SetFiles(); - } - } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 2cfd00ef..f4c1b32e 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,18 +1,21 @@ using System; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Collections; +// Different types a mod setting can change: public enum ModSettingChange { - Inheritance, - EnableState, - Priority, - Setting, + Inheritance, // it was set to inherit from other collections or not inherit anymore + EnableState, // it was enabled or disabled + Priority, // its priority was changed + Setting, // a specific setting was changed } public partial class ModCollection { + // If the change type is a bool, oldValue will be 1 for true and 0 for false. + // optionName will only be set for type == Setting. public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ); public event ModSettingChangeDelegate ModSettingChanged; @@ -99,13 +102,13 @@ public partial class ModCollection private bool FixInheritance( int idx, bool inherit ) { var settings = _settings[ idx ]; - if( inherit != ( settings == null ) ) + if( inherit == ( settings == null ) ) { - _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); - return true; + return false; } - return false; + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + return true; } private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited ) diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs new file mode 100644 index 00000000..cad2c209 --- /dev/null +++ b/Penumbra/Collections/ModCollection.File.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +// File operations like saving, loading and deleting for a collection. +public partial class ModCollection +{ + public static string CollectionDirectory + => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); + + // We need to remove all invalid path symbols from the collection name to be able to save it to file. + public FileInfo FileName + => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); + + // Custom serialization due to shared mod information across managers. + public void Save() + { + try + { + var file = FileName; + file.Directory?.Create(); + using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew ); + using var w = new StreamWriter( s, Encoding.UTF8 ); + using var j = new JsonTextWriter( w ); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); + j.WriteStartObject(); + j.WritePropertyName( nameof( Version ) ); + j.WriteValue( Version ); + j.WritePropertyName( nameof( Name ) ); + j.WriteValue( Name ); + j.WritePropertyName( nameof( Settings ) ); + + // Write all used and unused settings by mod directory name. + j.WriteStartObject(); + for( var i = 0; i < _settings.Count; ++i ) + { + var settings = _settings[ i ]; + if( settings != null ) + { + j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); + x.Serialize( j, settings ); + } + } + + foreach( var (modDir, settings) in _unusedSettings ) + { + j.WritePropertyName( modDir ); + x.Serialize( j, settings ); + } + + j.WriteEndObject(); + + // Inherit by collection name. + j.WritePropertyName( nameof( Inheritance ) ); + x.Serialize( j, Inheritance.Select( c => c.Name ) ); + j.WriteEndObject(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); + } + } + + public void Delete() + { + if( Index == 0 ) + { + return; + } + + var file = FileName; + if( !file.Exists ) + { + return; + } + + try + { + file.Delete(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); + } + } + + // Since inheritances depend on other collections existing, + // we return them as a list to be applied after reading all collections. + public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + { + inheritance = Array.Empty< string >(); + if( !file.Exists ) + { + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + return null; + } + + try + { + var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); + var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; + var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + // Custom deserialization that is converted with the constructor. + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() + ?? new Dictionary< string, ModSettings >(); + inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + + return new ModCollection( name, version, settings ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + } + + return null; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index 158e0159..390c9c87 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -1,20 +1,26 @@ using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Collections; +// ModCollections can inherit from an arbitrary number of other collections. +// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. +// Circular dependencies are resolved by distinctness. public partial class ModCollection { - private readonly List< ModCollection > _inheritance = new(); - + // A change in inheritance usually requires complete recomputation. public event Action< bool > InheritanceChanged; + private readonly List< ModCollection > _inheritance = new(); + public IReadOnlyList< ModCollection > Inheritance => _inheritance; + // Iterate over all collections inherited from in depth-first order. + // Skip already visited collections to avoid circular dependencies. public IEnumerable< ModCollection > GetFlattenedInheritance() { yield return this; @@ -27,6 +33,9 @@ public partial class ModCollection } } + // Add a new collection to the inheritance list. + // We do not check if this collection would be visited before, + // only that it is unique in the list itself. public bool AddInheritance( ModCollection collection ) { if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) @@ -35,6 +44,7 @@ public partial class ModCollection } _inheritance.Add( collection ); + // Changes in inherited collections may need to trigger further changes here. collection.ModSettingChanged += OnInheritedModSettingChange; collection.InheritanceChanged += OnInheritedInheritanceChange; InheritanceChanged.Invoke( false ); @@ -50,6 +60,7 @@ public partial class ModCollection InheritanceChanged.Invoke( false ); } + // Order in the inheritance list is relevant. public void MoveInheritance( int from, int to ) { if( _inheritance.Move( from, to ) ) @@ -58,6 +69,7 @@ public partial class ModCollection } } + // Carry changes in collections inherited from forward if they are relevant for this collection. private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) { if( _settings[ modIdx ] == null ) @@ -69,13 +81,16 @@ public partial class ModCollection private void OnInheritedInheritanceChange( bool _ ) => InheritanceChanged.Invoke( true ); + // Obtain the actual settings for a given mod via index. + // Also returns the collection the settings are taken from. + // If no collection provides settings for this mod, this collection is returned together with null. public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] { get { foreach( var collection in GetFlattenedInheritance() ) { - var settings = _settings[ idx ]; + var settings = collection._settings[ idx ]; if( settings != null ) { return ( settings, collection ); diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 19a89036..42903ea8 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,10 +1,12 @@ +using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Collections; public sealed partial class ModCollection { + // Migration to convert ModCollections from older versions to newer. private static class Migration { public static void Migrate( ModCollection collection ) @@ -24,9 +26,10 @@ public sealed partial class ModCollection } collection.Version = 1; + + // Remove all completely defaulted settings from active and inactive mods. for( var i = 0; i < collection._settings.Count; ++i ) { - var setting = collection._settings[ i ]; if( SettingIsDefaultV0( collection._settings[ i ] ) ) { collection._settings[ i ] = null; @@ -41,7 +44,11 @@ public sealed partial class ModCollection return true; } + // We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0( ModSettings? setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); } + + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + => new(name, 0, allSettings); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index ce90144d..9e4f7c60 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,21 +1,28 @@ -using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Text; -using Dalamud.Logging; -using Newtonsoft.Json.Linq; -using Penumbra.Mod; -using Penumbra.Util; +using Penumbra.Mods; namespace Penumbra.Collections; +public partial class ModCollection +{ + // Create the always available Empty Collection that will always sit at index 0, + // can not be deleted and does never create a cache. + private static ModCollection CreateEmpty() + { + var collection = CreateNewEmpty( EmptyCollection ); + collection.Index = 0; + collection._settings.Clear(); + return collection; + } +} + // A ModCollection is a named set of ModSettings to all of the users' installed mods. -// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. -// Active ModCollections build a cache of currently relevant data. +// Invariants: +// - Index is the collections index in the ModCollection.Manager +// - Settings has the same size as ModManager.Mods. +// - any change in settings or inheritance of the collection causes a Save. public partial class ModCollection { public const int CurrentVersion = 1; @@ -24,29 +31,28 @@ public partial class ModCollection public static readonly ModCollection Empty = CreateEmpty(); - private static ModCollection CreateEmpty() - { - var collection = CreateNewEmpty( EmptyCollection ); - collection.Index = 0; - collection._settings.Clear(); - return collection; - } - + // The collection name can contain invalid path characters, + // but after removing those and going to lower case it has to be unique. public string Name { get; private init; } public int Version { get; private set; } public int Index { get; private set; } = -1; + // If a ModSetting is null, it can be inherited from other collections. + // If no collection provides a setting for the mod, it is just disabled. private readonly List< ModSettings? > _settings; public IReadOnlyList< ModSettings? > Settings => _settings; + // Evaluates the settings along the whole inheritance tree. public IEnumerable< ModSettings? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + // Settings for deleted mods will be kept via directory name. private readonly Dictionary< string, ModSettings > _unusedSettings; + // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) { Name = name; @@ -58,6 +64,7 @@ public partial class ModCollection InheritanceChanged += SaveOnChange; } + // Constructor for reading from files. private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) { Name = name; @@ -79,15 +86,15 @@ public partial class ModCollection InheritanceChanged += SaveOnChange; } + // Create a new, unique empty collection of a given name. public static ModCollection CreateNewEmpty( string name ) => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) => new(name, this); - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) - => new(name, 0, allSettings); - + // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { var any = _unusedSettings.Count > 0; @@ -98,7 +105,8 @@ public partial class ModCollection } } - public void AddMod( Mod.Mod mod ) + // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + private void AddMod( Mods.Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -111,7 +119,8 @@ public partial class ModCollection } } - public void RemoveMod( Mod.Mod mod, int idx ) + // Move settings from the current mod list to the unused mod settings. + private void RemoveMod( Mods.Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) @@ -121,104 +130,4 @@ public partial class ModCollection _settings.RemoveAt( idx ); } - - public static string CollectionDirectory - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); - - public FileInfo FileName - => new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); - - public void Save() - { - try - { - var file = FileName; - file.Directory?.Create(); - using var s = file.Open( FileMode.Truncate ); - using var w = new StreamWriter( s, Encoding.UTF8 ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); - j.WriteStartObject(); - j.WritePropertyName( nameof( Version ) ); - j.WriteValue( Version ); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( Name ); - j.WritePropertyName( nameof( Settings ) ); - j.WriteStartObject(); - for( var i = 0; i < _settings.Count; ++i ) - { - var settings = _settings[ i ]; - if( settings != null ) - { - j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, settings ); - } - } - - foreach( var settings in _unusedSettings ) - { - j.WritePropertyName( settings.Key ); - x.Serialize( j, settings.Value ); - } - - j.WriteEndObject(); - j.WritePropertyName( nameof( Inheritance ) ); - x.Serialize( j, Inheritance ); - j.WriteEndObject(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public void Delete() - { - if( Index == 0 ) - { - return; - } - - var file = FileName; - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" ); - } - } - } - - public static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) - { - inheritance = Array.Empty< string >(); - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); - var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; - var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() - ?? new Dictionary< string, ModSettings >(); - inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); - - return new ModCollection( name, version, settings ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 56be9eb2..da2df11e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -5,8 +5,9 @@ using Dalamud.Logging; namespace Penumbra; + [Serializable] -public class Configuration : IPluginConfiguration +public partial class Configuration : IPluginConfiguration { private const int CurrentVersion = 1; @@ -36,7 +37,7 @@ public class Configuration : IPluginConfiguration public string CurrentCollection { get; set; } = "Default"; public string DefaultCollection { get; set; } = "Default"; - public string ForcedCollection { get; set; } = ""; + public bool SortFoldersFirst { get; set; } = false; public bool HasReadCharacterCollectionDesc { get; set; } = false; @@ -44,7 +45,6 @@ public class Configuration : IPluginConfiguration public Dictionary< string, string > CharacterCollections { get; set; } = new(); public Dictionary< string, string > ModSortOrder { get; set; } = new(); - public bool InvertModListOrder { internal get; set; } public static Configuration Load() { diff --git a/Penumbra/Importer/Models/ExtendedModPack.cs b/Penumbra/Importer/Models/ExtendedModPack.cs index c499ece3..45593faa 100644 --- a/Penumbra/Importer/Models/ExtendedModPack.cs +++ b/Penumbra/Importer/Models/ExtendedModPack.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Importer.Models { diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 46e3bcd5..a816d6a5 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -8,7 +8,7 @@ using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Importer.Models; -using Penumbra.Mod; +using Penumbra.Mods; using Penumbra.Util; using FileMode = System.IO.FileMode; diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index f43067d0..ada899fd 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Interop.Resolver; namespace Penumbra.Interop.Loader; @@ -30,8 +29,7 @@ public unsafe partial class ResourceLoader public ResourceType Extension; } - private readonly SortedDictionary< FullPath, DebugData > _debugList = new(); - private readonly List< (FullPath, DebugData?) > _deleteList = new(); + private readonly SortedList< FullPath, DebugData > _debugList = new(); public IReadOnlyDictionary< FullPath, DebugData > DebugList => _debugList; @@ -161,35 +159,22 @@ public unsafe partial class ResourceLoader public void UpdateDebugInfo() { - var manager = *ResourceManager; - _deleteList.Clear(); - foreach( var data in _debugList.Values ) + for( var i = 0; i < _debugList.Count; ++i ) { + var data = _debugList.Values[ i ]; var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 ); var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 ); if( modifiedResource == null ) { - _deleteList.Add( ( data.ManipulatedPath, null ) ); + _debugList.RemoveAt( i-- ); } else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource ) { - _deleteList.Add( ( data.ManipulatedPath, data with + _debugList[ _debugList.Keys[ i ] ] = data with { OriginalResource = ( Structs.ResourceHandle* )regularResource, ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource, - } ) ); - } - } - - foreach( var (path, data) in _deleteList ) - { - if( data == null ) - { - _debugList.Remove( path ); - } - else - { - _debugList[ path ] = data.Value; + }; } } } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index de7308a5..f7e78b12 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mod; using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -93,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Mod.Mod.Manager.ResolvePath( path ); + var resolved = Mods.Mod.Manager.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7a7b5e84..b0104fec 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -10,7 +10,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Component.GUI; using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.Mods; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 27d3ebcd..e2d88fee 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -7,7 +7,6 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index b678028d..5ac6dd0e 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -5,7 +5,6 @@ using Dalamud.Utility.Signatures; using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 7f2c89e7..844f5db9 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -5,7 +5,6 @@ using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 7dc29720..d489702e 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Meta.Manipulations; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Meta; diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index be9df0d4..57060fc9 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -5,11 +5,16 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.Collections; -using Penumbra.Mod; using Penumbra.Mods; namespace Penumbra; +public partial class Configuration +{ + public string ForcedCollection { internal get; set; } = ""; + public bool InvertModListOrder { internal get; set; } +} + public static class MigrateConfiguration { public static void Version0To1( Configuration config ) diff --git a/Penumbra/Mod/FullMod.cs b/Penumbra/Mods/FullMod.cs similarity index 96% rename from Penumbra/Mod/FullMod.cs rename to Penumbra/Mods/FullMod.cs index 6ffe9152..50ee92b5 100644 --- a/Penumbra/Mod/FullMod.cs +++ b/Penumbra/Mods/FullMod.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using Penumbra.GameData.ByteString; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // A complete Mod containing settings (i.e. dependent on a collection) // and the resulting cache. diff --git a/Penumbra/Mod/GroupInformation.cs b/Penumbra/Mods/GroupInformation.cs similarity index 99% rename from Penumbra/Mod/GroupInformation.cs rename to Penumbra/Mods/GroupInformation.cs index 7c86b5f3..f71e0d3c 100644 --- a/Penumbra/Mod/GroupInformation.cs +++ b/Penumbra/Mods/GroupInformation.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using Penumbra.GameData.ByteString; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public enum SelectType { diff --git a/Penumbra/Mod/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs similarity index 96% rename from Penumbra/Mod/Mod.SortOrder.cs rename to Penumbra/Mods/Mod.SortOrder.cs index 0e9c39eb..caaac4f9 100644 --- a/Penumbra/Mod/Mod.SortOrder.cs +++ b/Penumbra/Mods/Mod.SortOrder.cs @@ -1,7 +1,6 @@ using System; -using Penumbra.Mods; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public partial class Mod { diff --git a/Penumbra/Mod/Mod.cs b/Penumbra/Mods/Mod.cs similarity index 97% rename from Penumbra/Mod/Mod.cs rename to Penumbra/Mods/Mod.cs index d5f7cc2f..427d13ba 100644 --- a/Penumbra/Mod/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -3,9 +3,8 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; -using Penumbra.Mods; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Mod contains all permanent information about a mod, // and is independent of collections or settings. @@ -23,7 +22,7 @@ public partial class Mod public FileInfo MetaFile { get; set; } public int Index { get; private set; } = -1; - private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) + private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources) { BasePath = basePath; Meta = meta; diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs similarity index 99% rename from Penumbra/Mod/ModCleanup.cs rename to Penumbra/Mods/ModCleanup.cs index 0a2b3e4a..d09a26b8 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -8,10 +8,9 @@ using System.Security.Cryptography; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Importer; -using Penumbra.Mods; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public class ModCleanup { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index ba946687..4cb331aa 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Penumbra.Mod; namespace Penumbra.Mods; @@ -37,7 +36,7 @@ public static partial class ModFileSystem // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. // Saves and returns true if anything changed. - public static bool Rename( this Mod.Mod mod, string newName ) + public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName ) { if( RenameNoSave( mod, newName ) ) { @@ -63,7 +62,7 @@ public static partial class ModFileSystem // Move a single mod to the target folder. // Returns true and saves if anything changed. - public static bool Move( this Mod.Mod mod, ModFolder target ) + public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target ) { if( MoveNoSave( mod, target ) ) { @@ -76,7 +75,7 @@ public static partial class ModFileSystem // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. // Creates all necessary Subfolders. - public static void Move( this Mod.Mod mod, string sortOrder ) + public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder ) { var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); var folder = Root; @@ -137,10 +136,10 @@ public static partial class ModFileSystem } // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( Mod.Mod mod ) + private static void SaveMod( global::Penumbra.Mods.Mod mod ) { if( ReferenceEquals( mod.Order.ParentFolder, Root ) - && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) + && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) { Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name ); } @@ -184,7 +183,7 @@ public static partial class ModFileSystem return true; } - private static bool RenameNoSave( Mod.Mod mod, string newName ) + private static bool RenameNoSave( global::Penumbra.Mods.Mod mod, string newName ) { newName = newName.Replace( '/', '\\' ); if( mod.Order.SortOrderName == newName ) @@ -193,12 +192,12 @@ public static partial class ModFileSystem } mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.Order = new Mod.Mod.SortOrder( mod.Order.ParentFolder, newName ); + mod.Order = new global::Penumbra.Mods.Mod.SortOrder( mod.Order.ParentFolder, newName ); mod.Order.ParentFolder.AddMod( mod ); return true; } - private static bool MoveNoSave( Mod.Mod mod, ModFolder target ) + private static bool MoveNoSave( global::Penumbra.Mods.Mod mod, ModFolder target ) { var oldParent = mod.Order.ParentFolder; if( ReferenceEquals( target, oldParent ) ) @@ -207,7 +206,7 @@ public static partial class ModFileSystem } oldParent.RemoveMod( mod ); - mod.Order = new Mod.Mod.SortOrder( target, mod.Order.SortOrderName ); + mod.Order = new global::Penumbra.Mods.Mod.SortOrder( target, mod.Order.SortOrderName ); target.AddMod( mod ); return true; } diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs index e2369819..80a1df51 100644 --- a/Penumbra/Mods/ModFolder.cs +++ b/Penumbra/Mods/ModFolder.cs @@ -1,247 +1,245 @@ using System; using System.Collections.Generic; using System.Linq; -using Penumbra.Mod; -namespace Penumbra.Mods +namespace Penumbra.Mods; + +public partial class ModFolder { - public partial class ModFolder + public ModFolder? Parent; + + public string FullName { - public ModFolder? Parent; - - public string FullName + get { - get - { - var parentPath = Parent?.FullName ?? string.Empty; - return parentPath.Any() ? $"{parentPath}/{Name}" : Name; - } - } - - private string _name = string.Empty; - - public string Name - { - get => _name; - set => _name = value.Replace( '/', '\\' ); - } - - public List< ModFolder > SubFolders { get; } = new(); - public List< Mod.Mod > Mods { get; } = new(); - - public ModFolder( ModFolder parent, string name ) - { - Parent = parent; - Name = name; - } - - public override string ToString() - => FullName; - - public int TotalDescendantMods() - => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); - - public int TotalDescendantFolders() - => SubFolders.Sum( f => f.TotalDescendantFolders() ); - - // Return all descendant mods in the specified order. - public IEnumerable< Mod.Mod > AllMods( bool foldersFirst ) - { - if( foldersFirst ) - { - return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); - } - - return GetSortedEnumerator().SelectMany( f => - { - if( f is ModFolder folder ) - { - return folder.AllMods( false ); - } - - return new[] { ( Mod.Mod )f }; - } ); - } - - // Return all descendant subfolders. - public IEnumerable< ModFolder > AllFolders() - => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); - - // Iterate through all descendants in the specified order, returning subfolders as well as mods. - public IEnumerable< object > GetItems( bool foldersFirst ) - => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); - - // Find a subfolder by name. Returns true and sets folder to it if it exists. - public bool FindSubFolder( string name, out ModFolder folder ) - { - var subFolder = new ModFolder( this, name ); - var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); - folder = idx >= 0 ? SubFolders[ idx ] : this; - return idx >= 0; - } - - // Checks if an equivalent subfolder as folder already exists and returns its index. - // If it does not exist, inserts folder as a subfolder and returns the new index. - // Also sets this as folders parent. - public int FindOrAddSubFolder( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - SubFolders.Insert( idx, folder ); - folder.Parent = this; - return idx; - } - - // Checks if a subfolder with the given name already exists and returns it and its index. - // If it does not exists, creates and inserts it and returns the new subfolder and its index. - public (ModFolder, int) FindOrCreateSubFolder( string name ) - { - var subFolder = new ModFolder( this, name ); - var idx = FindOrAddSubFolder( subFolder ); - return ( SubFolders[ idx ], idx ); - } - - // Remove folder as a subfolder if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveSubFolder( ModFolder folder ) - { - RemoveFolderIgnoreEmpty( folder ); - CheckEmpty(); - } - - // Add the given mod as a child, if it is not already a child. - // Returns the index of the found or inserted mod. - public int AddMod( Mod.Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - Mods.Insert( idx, mod ); - - return idx; - } - - // Remove mod as a child if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveMod( Mod.Mod mod ) - { - RemoveModIgnoreEmpty( mod ); - CheckEmpty(); + var parentPath = Parent?.FullName ?? string.Empty; + return parentPath.Any() ? $"{parentPath}/{Name}" : Name; } } - // Internals - public partial class ModFolder + private string _name = string.Empty; + + public string Name { - // Create a Root folder without parent. - internal static ModFolder CreateRoot() - => new( null!, string.Empty ); + get => _name; + set => _name = value.Replace( '/', '\\' ); + } - internal class ModFolderComparer : IComparer< ModFolder > + public List< ModFolder > SubFolders { get; } = new(); + public List< Mod > Mods { get; } = new(); + + public ModFolder( ModFolder parent, string name ) + { + Parent = parent; + Name = name; + } + + public override string ToString() + => FullName; + + public int TotalDescendantMods() + => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); + + public int TotalDescendantFolders() + => SubFolders.Sum( f => f.TotalDescendantFolders() ); + + // Return all descendant mods in the specified order. + public IEnumerable< Mod > AllMods( bool foldersFirst ) + { + if( foldersFirst ) { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. - public int Compare( ModFolder? x, ModFolder? y ) - => ReferenceEquals( x, y ) - ? 0 - : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); + return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); } - internal class ModDataComparer : IComparer< Mod.Mod > + return GetSortedEnumerator().SelectMany( f => { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. - // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. - public int Compare( Mod.Mod? x, Mod.Mod? y ) + if( f is ModFolder folder ) { - if( ReferenceEquals( x, y ) ) - { - return 0; - } - - var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); - if( cmp != 0 ) - { - return cmp; - } - - return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); + return folder.AllMods( false ); } + + return new[] { ( Mod )f }; + } ); + } + + // Return all descendant subfolders. + public IEnumerable< ModFolder > AllFolders() + => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); + + // Iterate through all descendants in the specified order, returning subfolders as well as mods. + public IEnumerable< object > GetItems( bool foldersFirst ) + => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); + + // Find a subfolder by name. Returns true and sets folder to it if it exists. + public bool FindSubFolder( string name, out ModFolder folder ) + { + var subFolder = new ModFolder( this, name ); + var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); + folder = idx >= 0 ? SubFolders[ idx ] : this; + return idx >= 0; + } + + // Checks if an equivalent subfolder as folder already exists and returns its index. + // If it does not exist, inserts folder as a subfolder and returns the new index. + // Also sets this as folders parent. + public int FindOrAddSubFolder( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx >= 0 ) + { + return idx; } - internal static readonly ModFolderComparer FolderComparer = new(); - internal static readonly ModDataComparer ModComparer = new(); + idx = ~idx; + SubFolders.Insert( idx, folder ); + folder.Parent = this; + return idx; + } - // Get an enumerator for actually sorted objects instead of folder-first objects. - private IEnumerable< object > GetSortedEnumerator() + // Checks if a subfolder with the given name already exists and returns it and its index. + // If it does not exists, creates and inserts it and returns the new subfolder and its index. + public (ModFolder, int) FindOrCreateSubFolder( string name ) + { + var subFolder = new ModFolder( this, name ); + var idx = FindOrAddSubFolder( subFolder ); + return ( SubFolders[ idx ], idx ); + } + + // Remove folder as a subfolder if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveSubFolder( ModFolder folder ) + { + RemoveFolderIgnoreEmpty( folder ); + CheckEmpty(); + } + + // Add the given mod as a child, if it is not already a child. + // Returns the index of the found or inserted mod. + public int AddMod( Mod mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) { - var modIdx = 0; - foreach( var folder in SubFolders ) - { - var folderString = folder.Name; - for( ; modIdx < Mods.Count; ++modIdx ) - { - var mod = Mods[ modIdx ]; - var modString = mod.Order.SortOrderName; - if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) - { - yield return mod; - } - else - { - break; - } - } + return idx; + } - yield return folder; + idx = ~idx; + Mods.Insert( idx, mod ); + + return idx; + } + + // Remove mod as a child if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveMod( Mod mod ) + { + RemoveModIgnoreEmpty( mod ); + CheckEmpty(); + } +} + +// Internals +public partial class ModFolder +{ + // Create a Root folder without parent. + internal static ModFolder CreateRoot() + => new(null!, string.Empty); + + internal class ModFolderComparer : IComparer< ModFolder > + { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + + // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. + public int Compare( ModFolder? x, ModFolder? y ) + => ReferenceEquals( x, y ) + ? 0 + : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); + } + + internal class ModDataComparer : IComparer< Mod > + { + public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; + + // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. + // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. + public int Compare( Mod? x, Mod? y ) + { + if( ReferenceEquals( x, y ) ) + { + return 0; } + var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); + if( cmp != 0 ) + { + return cmp; + } + + return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); + } + } + + internal static readonly ModFolderComparer FolderComparer = new(); + internal static readonly ModDataComparer ModComparer = new(); + + // Get an enumerator for actually sorted objects instead of folder-first objects. + private IEnumerable< object > GetSortedEnumerator() + { + var modIdx = 0; + foreach( var folder in SubFolders ) + { + var folderString = folder.Name; for( ; modIdx < Mods.Count; ++modIdx ) { - yield return Mods[ modIdx ]; + var mod = Mods[ modIdx ]; + var modString = mod.Order.SortOrderName; + if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) + { + yield return mod; + } + else + { + break; + } } + + yield return folder; } - private void CheckEmpty() + for( ; modIdx < Mods.Count; ++modIdx ) { - if( Mods.Count == 0 && SubFolders.Count == 0 ) - { - Parent?.RemoveSubFolder( this ); - } + yield return Mods[ modIdx ]; + } + } + + private void CheckEmpty() + { + if( Mods.Count == 0 && SubFolders.Count == 0 ) + { + Parent?.RemoveSubFolder( this ); + } + } + + // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveFolderIgnoreEmpty( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx < 0 ) + { + return; } - // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveFolderIgnoreEmpty( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx < 0 ) - { - return; - } + SubFolders[ idx ].Parent = null; + SubFolders.RemoveAt( idx ); + } - SubFolders[ idx ].Parent = null; - SubFolders.RemoveAt( idx ); - } - - // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveModIgnoreEmpty( Mod.Mod mod ) + // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveModIgnoreEmpty( Mod mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - Mods.RemoveAt( idx ); - } + Mods.RemoveAt( idx ); } } } \ No newline at end of file diff --git a/Penumbra/Mod/ModFunctions.cs b/Penumbra/Mods/ModFunctions.cs similarity index 99% rename from Penumbra/Mod/ModFunctions.cs rename to Penumbra/Mods/ModFunctions.cs index 7372cfa0..72787222 100644 --- a/Penumbra/Mod/ModFunctions.cs +++ b/Penumbra/Mods/ModFunctions.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using Penumbra.GameData.ByteString; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Functions that do not really depend on only one component of a mod. public static class ModFunctions diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs index c8149a65..56673d20 100644 --- a/Penumbra/Mods/ModManager.Directory.cs +++ b/Penumbra/Mods/ModManager.Directory.cs @@ -2,16 +2,14 @@ using System; using System.Collections.Generic; using System.IO; using Dalamud.Logging; -using Penumbra.Meta.Manipulations; -using Penumbra.Mod; namespace Penumbra.Mods; public partial class ModManagerNew { - private readonly List< Mod.Mod > _mods = new(); + private readonly List< Mod > _mods = new(); - public IReadOnlyList< Mod.Mod > Mods + public IReadOnlyList< Mod > Mods => _mods; public void DiscoverMods() @@ -37,6 +35,7 @@ public partial class ModManagerNew //Collections.RecreateCaches(); } } + public partial class ModManagerNew { public DirectoryInfo BasePath { get; private set; } = null!; diff --git a/Penumbra/Mod/ModManager.cs b/Penumbra/Mods/ModManager.cs similarity index 99% rename from Penumbra/Mod/ModManager.cs rename to Penumbra/Mods/ModManager.cs index 5f30c6bd..8c1f90df 100644 --- a/Penumbra/Mod/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -6,11 +6,10 @@ using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; public partial class Mod { diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 7160d874..4ecf5fb3 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using Dalamud.Logging; -using Penumbra.Mod; using Penumbra.Util; namespace Penumbra.Mods; @@ -12,7 +11,7 @@ namespace Penumbra.Mods; // Contains all change functions on a specific mod that also require corresponding changes to collections. public static class ModManagerEditExtensions { - public static bool RenameMod( this Mod.Mod.Manager manager, string newName, Mod.Mod mod ) + public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod ) { if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) { @@ -25,14 +24,14 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeSortOrder( this Mod.Mod.Manager manager, Mod.Mod mod, string newSortOrder ) + public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder ) { if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) { return false; } - var inRoot = new Mod.Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); + var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) { mod.Order = inRoot; @@ -49,7 +48,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RenameModFolder( this Mod.Mod.Manager manager, Mod.Mod mod, DirectoryInfo newDir, bool move = true ) + public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true ) { if( move ) { @@ -73,7 +72,7 @@ public static class ModManagerEditExtensions var oldBasePath = mod.BasePath; mod.BasePath = newDir; - mod.MetaFile = Mod.Mod.MetaFileInfo( newDir ); + mod.MetaFile = Mod.MetaFileInfo( newDir ); manager.UpdateMod( mod ); if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) ) @@ -95,7 +94,7 @@ public static class ModManagerEditExtensions return true; } - public static bool ChangeModGroup( this Mod.Mod.Manager manager, string oldGroupName, string newGroupName, Mod.Mod mod, + public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod, SelectType type = SelectType.Single ) { if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) @@ -157,7 +156,7 @@ public static class ModManagerEditExtensions return true; } - public static bool RemoveModOption( this Mod.Mod.Manager manager, int optionIdx, OptionGroup group, Mod.Mod mod ) + public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod ) { if( optionIdx < 0 || optionIdx >= group.Options.Count ) { diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mods/ModMeta.cs similarity index 80% rename from Penumbra/Mod/ModMeta.cs rename to Penumbra/Mods/ModMeta.cs index d4c18b3b..228f3bb2 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -5,47 +5,20 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Penumbra.GameData.ByteString; +using Penumbra.Util; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains descriptive data about the mod as well as possible settings and fileswaps. public class ModMeta { public uint FileVersion { get; set; } - public string Name - { - get => _name; - set - { - _name = value; - LowerName = value.ToLowerInvariant(); - } - } - - private string _name = "Mod"; - - [JsonIgnore] - public string LowerName { get; private set; } = "mod"; - - private string _author = ""; - - public string Author - { - get => _author; - set - { - _author = value; - LowerAuthor = value.ToLowerInvariant(); - } - } - - [JsonIgnore] - public string LowerAuthor { get; private set; } = ""; - - public string Description { get; set; } = ""; - public string Version { get; set; } = ""; - public string Website { get; set; } = ""; + public LowerString Name { get; set; } = "Mod"; + public LowerString Author { get; set; } = LowerString.Empty; + public string Description { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Website { get; set; } = string.Empty; [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mods/ModResources.cs similarity index 99% rename from Penumbra/Mod/ModResources.cs rename to Penumbra/Mods/ModResources.cs index 590cda79..cd05fc7e 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mods/ModResources.cs @@ -5,7 +5,7 @@ using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta; -namespace Penumbra.Mod; +namespace Penumbra.Mods; [Flags] public enum ResourceChange diff --git a/Penumbra/Mod/ModSettings.cs b/Penumbra/Mods/ModSettings.cs similarity index 98% rename from Penumbra/Mod/ModSettings.cs rename to Penumbra/Mods/ModSettings.cs index 4f82df49..aadc0242 100644 --- a/Penumbra/Mod/ModSettings.cs +++ b/Penumbra/Mods/ModSettings.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains the settings for a given mod. public class ModSettings diff --git a/Penumbra/Mod/NamedModSettings.cs b/Penumbra/Mods/NamedModSettings.cs similarity index 98% rename from Penumbra/Mod/NamedModSettings.cs rename to Penumbra/Mods/NamedModSettings.cs index 45770e7e..5a0ded71 100644 --- a/Penumbra/Mod/NamedModSettings.cs +++ b/Penumbra/Mods/NamedModSettings.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Penumbra.Mod; +namespace Penumbra.Mods; // Contains settings with the option selections stored by names instead of index. // This is meant to make them possibly more portable when we support importing collections from other users. diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 61a803ea..322c9f7e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -10,13 +9,12 @@ using Lumina.Excel.GeneratedSheets; using Penumbra.Api; using Penumbra.GameData.Enums; using Penumbra.Interop; -using Penumbra.Mods; using Penumbra.UI; using Penumbra.Util; using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra; @@ -34,8 +32,9 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; + public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Mod.Manager ModManager { get; private set; } = null!; + public static Mod.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; @@ -65,9 +64,10 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); + MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod.Mod.Manager(); + ModManager = new Mod.Manager(); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); ObjectReloader = new ObjectReloader(); @@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin PathResolver.Dispose(); ResourceLogger.Dispose(); + MetaFileManager.Dispose(); ResourceLoader.Dispose(); CharacterUtility.Dispose(); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 4775526e..760da897 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -7,7 +7,6 @@ using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; using Penumbra.Collections; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -22,7 +21,7 @@ public partial class SettingsInterface private readonly Selector _selector; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; + private ModCollection[] _collections = null!; private int _currentCollectionIndex; private int _currentDefaultIndex; private readonly Dictionary< string, int > _currentCharacterIndices = new(); @@ -192,6 +191,65 @@ public partial class SettingsInterface } } + private static void DrawInheritance( ModCollection collection ) + { + ImGui.PushID( collection.Index ); + if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) ) + { + foreach( var inheritance in collection.Inheritance ) + { + DrawInheritance( inheritance ); + } + } + + ImGui.PopID(); + } + + private void DrawCurrentCollectionInheritance() + { + if( !ImGui.BeginListBox( "##inheritanceList", + new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) ) + { + return; + } + + using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox ); + DrawInheritance( _collections[ _currentCollectionIndex + 1 ] ); + } + + private static int _newInheritanceIdx = 0; + + private void DrawNewInheritanceSelection() + { + ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); + if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) ) + { + using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); + foreach( var collection in Penumbra.CollectionManager ) + { + if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) ) + { + _newInheritanceIdx = collection.Index; + } + } + } + + ImGui.SameLine(); + var valid = _newInheritanceIdx > ModCollection.Empty.Index + && _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx + && _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx ); + using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid ); + using var font = ImGuiRaii.PushFont( UiBuilder.IconFont ); + if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid ) + { + _collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] ); + } + + style.Pop(); + font.Pop(); + ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." ); + } + private void DrawDefaultCollectionSelector() { var index = _currentDefaultIndex; @@ -344,12 +402,14 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) .Push( ImGui.EndChild ); - if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) ) + if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ) ) { DrawCurrentCollectionSelector( true ); - ImGuiHelpers.ScaledDummy( 0, 10 ); DrawNewCollectionInput(); + ImGuiHelpers.ScaledDummy( 0, 10 ); + DrawCurrentCollectionInheritance(); + DrawNewInheritanceSelection(); } raii.Pop(); diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 04c0dbd1..8420cb5c 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -6,8 +6,8 @@ using ImGuiNET; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Mods; using Penumbra.UI.Custom; +using Penumbra.Util; namespace Penumbra.UI; @@ -17,10 +17,8 @@ public partial class SettingsInterface { private const string LabelTab = "Effective Changes"; - private string _gamePathFilter = string.Empty; - private string _gamePathFilterLower = string.Empty; - private string _filePathFilter = string.Empty; - private string _filePathFilterLower = string.Empty; + private LowerString _gamePathFilter = LowerString.Empty; + private LowerString _filePathFilter = LowerString.Empty; private const float LeftTextLength = 600; @@ -57,47 +55,49 @@ public partial class SettingsInterface } ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale ); - if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) + var tmp = _gamePathFilter.Text; + if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref tmp, 256 ) ) { - _gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); + _gamePathFilter = tmp; } ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) + tmp = _filePathFilter.Text; + if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref tmp, 256 ) ) { - _filePathFilterLower = _filePathFilter.ToLowerInvariant(); + _filePathFilter = tmp; } } private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilter.Lower ); } private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp ) { - if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Value.ToString().Contains( _filePathFilter.Lower ); } - private bool CheckFilters( (string, string, string) kvp ) + private bool CheckFilters( (string, LowerString) kvp ) { - if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) ) + if( _gamePathFilter.Length > 0 && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilter.Lower ) ) { return false; } - return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); + return _filePathFilter.Length == 0 || kvp.Item2.Contains( _filePathFilter.Lower ); } private void DrawFilteredRows( ModCollection active ) @@ -113,49 +113,43 @@ public partial class SettingsInterface return; } - foreach( var (mp, mod, _) in cache.Cmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Cmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Eqp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Eqp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Eqdp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Eqdp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Gmp.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Gmp.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Est.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Est.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); } - foreach( var (mp, mod, _) in cache.Imc.Manipulations - .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name, - Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) ) + foreach( var (mp, mod) in cache.Imc.Manipulations + .Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) ) .Where( CheckFilters ) ) { DrawLine( mp, mod ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index b797902e..7c35efea 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Dalamud.Logging; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.Util; @@ -15,19 +14,19 @@ public class ModListCache : IDisposable public const uint ConflictingModColor = 0xFFAAAAFFu; public const uint HandledConflictModColor = 0xFF88DDDDu; - private readonly Mod.Mod.Manager _manager; + private readonly Mods.Mod.Manager _manager; private readonly List< FullMod > _modsInOrder = new(); private readonly List< (bool visible, uint color) > _visibleMods = new(); private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); private readonly IReadOnlySet< string > _newMods; - private string _modFilter = string.Empty; - private string _modFilterChanges = string.Empty; - private string _modFilterAuthor = string.Empty; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - private bool _listResetNecessary; - private bool _filterResetNecessary; + private LowerString _modFilter = LowerString.Empty; + private LowerString _modFilterAuthor = LowerString.Empty; + private LowerString _modFilterChanges = LowerString.Empty; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private bool _listResetNecessary; + private bool _filterResetNecessary; public ModFilter StateFilter @@ -44,7 +43,7 @@ public class ModListCache : IDisposable } } - public ModListCache( Mod.Mod.Manager manager, IReadOnlySet< string > newMods ) + public ModListCache( Mods.Mod.Manager manager, IReadOnlySet< string > newMods ) { _manager = manager; _newMods = newMods; @@ -123,20 +122,20 @@ public class ModListCache : IDisposable if( lower.StartsWith( "c:" ) ) { _modFilterChanges = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterAuthor = string.Empty; + _modFilter = LowerString.Empty; + _modFilterAuthor = LowerString.Empty; } else if( lower.StartsWith( "a:" ) ) { _modFilterAuthor = lower[ 2.. ]; - _modFilter = string.Empty; - _modFilterChanges = string.Empty; + _modFilter = LowerString.Empty; + _modFilterChanges = LowerString.Empty; } else { _modFilter = lower; - _modFilterAuthor = string.Empty; - _modFilterChanges = string.Empty; + _modFilterAuthor = LowerString.Empty; + _modFilterChanges = LowerString.Empty; } ResetFilters(); @@ -233,12 +232,12 @@ public class ModListCache : IDisposable { var ret = ( false, 0u ); - if( _modFilter.Length > 0 && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) + if( _modFilter.Length > 0 && !mod.Data.Meta.Name.Contains( _modFilter ) ) { return ret; } - if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) ) + if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.Author.Contains( _modFilterAuthor ) ) { return ret; } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index d3444394..66d40c65 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Meta.Manipulations; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -201,7 +200,7 @@ public partial class SettingsInterface raii.Push( ImGui.EndListBox ); using var indent = ImGuiRaii.PushIndent( 0 ); - Mod.Mod? oldBadMod = null; + Mods.Mod? oldBadMod = null; foreach( var conflict in conflicts ) { var badMod = Penumbra.ModManager[ conflict.Mod2 ]; @@ -224,14 +223,14 @@ public partial class SettingsInterface indent.Push( 30f ); } - if( conflict.Conflict is Utf8GamePath p ) + if( conflict.Data is Utf8GamePath p ) { unsafe { ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); } } - else if( conflict.Conflict is MetaManipulation m ) + else if( conflict.Data is MetaManipulation m ) { ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 5bb4acaa..7a80f150 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -5,7 +5,6 @@ using Dalamud.Interface; using ImGuiNET; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index e6f3b170..25fc4a4c 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -6,7 +6,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -69,7 +68,7 @@ public partial class SettingsInterface _currentWebsite = Meta?.Website ?? ""; } - private Mod.FullMod? Mod + private Mods.FullMod? Mod => _selector.Mod; private ModMeta? Meta @@ -77,7 +76,7 @@ public partial class SettingsInterface private void DrawName() { - var name = Meta!.Name; + var name = Meta!.Name.Text; var modManager = Penumbra.ModManager; if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) ) { @@ -122,7 +121,7 @@ public partial class SettingsInterface ImGui.TextColored( GreyColor, "by" ); ImGui.SameLine(); - var author = Meta!.Author; + var author = Meta!.Author.Text; if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) && author != Meta.Author ) { @@ -228,7 +227,7 @@ public partial class SettingsInterface } } - public static bool DrawSortOrder( Mod.Mod mod, Mod.Mod.Manager manager, Selector selector ) + public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector ) { var currentSortOrder = mod.Order.FullPath; ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 5984ac48..6ccf9d3f 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -10,7 +10,6 @@ using Dalamud.Logging; using ImGuiNET; using Penumbra.Collections; using Penumbra.Importer; -using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; @@ -410,11 +409,11 @@ public partial class SettingsInterface // Selection private partial class Selector { - public Mod.FullMod? Mod { get; private set; } + public Mods.FullMod? Mod { get; private set; } private int _index; private string _nextDir = string.Empty; - private void SetSelection( int idx, Mod.FullMod? info ) + private void SetSelection( int idx, Mods.FullMod? info ) { Mod = info; if( idx != _index ) @@ -480,7 +479,7 @@ public partial class SettingsInterface private partial class Selector { // === Mod === - private void DrawModOrderPopup( string popupName, Mod.FullMod mod, bool firstOpen ) + private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen ) { if( !ImGui.BeginPopup( popupName ) ) { @@ -664,7 +663,7 @@ public partial class SettingsInterface idx += sub.TotalDescendantMods(); } } - else if( item is Mod.Mod _ ) + else if( item is Mods.Mod _ ) { var (mod, visible, color) = Cache.GetMod( idx ); if( mod != null && visible ) @@ -721,7 +720,7 @@ public partial class SettingsInterface } } - private void DrawMod( Mod.FullMod mod, int modIndex, uint color ) + private void DrawMod( Mods.FullMod mod, int modIndex, uint color ) { using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index d258231a..ee11e961 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -10,7 +10,6 @@ using Dalamud.Logging; using ImGuiNET; using Penumbra.GameData.ByteString; using Penumbra.Interop; -using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; diff --git a/Penumbra/Util/LowerString.cs b/Penumbra/Util/LowerString.cs new file mode 100644 index 00000000..4942a5bc --- /dev/null +++ b/Penumbra/Util/LowerString.cs @@ -0,0 +1,122 @@ +using System; +using ImGuiNET; +using Newtonsoft.Json; + +namespace Penumbra.Util; + +[JsonConverter( typeof( Converter ) )] +public readonly struct LowerString : IEquatable< LowerString >, IComparable< LowerString > +{ + public static readonly LowerString Empty = new(string.Empty); + + public readonly string Text = string.Empty; + public readonly string Lower = string.Empty; + + public LowerString( string text ) + { + Text = string.Intern( text ); + Lower = string.Intern( text.ToLowerInvariant() ); + } + + + public int Length + => Text.Length; + + public int Count + => Length; + + public bool Equals( LowerString other ) + => string.Equals( Lower, other.Lower, StringComparison.InvariantCulture ); + + public bool Equals( string other ) + => string.Equals( Lower, other, StringComparison.InvariantCultureIgnoreCase ); + + public int CompareTo( LowerString other ) + => string.Compare( Lower, other.Lower, StringComparison.InvariantCulture ); + + public int CompareTo( string other ) + => string.Compare( Lower, other, StringComparison.InvariantCultureIgnoreCase ); + + public bool Contains( LowerString other ) + => Lower.Contains( other.Lower, StringComparison.InvariantCulture ); + + public bool Contains( string other ) + => Lower.Contains( other, StringComparison.InvariantCultureIgnoreCase ); + + public bool StartsWith( LowerString other ) + => Lower.StartsWith( other.Lower, StringComparison.InvariantCulture ); + + public bool StartsWith( string other ) + => Lower.StartsWith( other, StringComparison.InvariantCultureIgnoreCase ); + + public bool EndsWith( LowerString other ) + => Lower.EndsWith( other.Lower, StringComparison.InvariantCulture ); + + public bool EndsWith( string other ) + => Lower.EndsWith( other, StringComparison.InvariantCultureIgnoreCase ); + + public override string ToString() + => Text; + + public static implicit operator string( LowerString s ) + => s.Text; + + public static implicit operator LowerString( string s ) + => new(s); + + private class Converter : JsonConverter< LowerString > + { + public override void WriteJson( JsonWriter writer, LowerString value, JsonSerializer serializer ) + { + writer.WriteValue( value.Text ); + } + + public override LowerString ReadJson( JsonReader reader, Type objectType, LowerString existingValue, bool hasExistingValue, + JsonSerializer serializer ) + { + if( reader.Value is string text ) + { + return new LowerString( text ); + } + + return existingValue; + } + } + + public static bool InputWithHint( string label, string hint, ref LowerString s, uint maxLength = 128, + ImGuiInputTextFlags flags = ImGuiInputTextFlags.None ) + { + var tmp = s.Text; + if( !ImGui.InputTextWithHint( label, hint, ref tmp, maxLength, flags ) || tmp == s.Text ) + { + return false; + } + + s = new LowerString( tmp ); + return true; + } + + public override bool Equals( object? obj ) + => obj is LowerString lowerString && Equals( lowerString ); + + public override int GetHashCode() + => Text.GetHashCode(); + + public static bool operator ==( LowerString lhs, LowerString rhs ) + => lhs.Equals( rhs ); + + public static bool operator !=( LowerString lhs, LowerString rhs ) + => lhs.Equals( rhs ); + + public static bool operator ==( LowerString lhs, string rhs ) + => lhs.Equals( rhs ); + + public static bool operator !=( LowerString lhs, string rhs ) + => lhs.Equals( rhs ); + + public static bool operator ==( string lhs, LowerString rhs ) + => rhs.Equals( lhs ); + + public static bool operator !=( string lhs, LowerString rhs ) + => rhs.Equals( lhs ); +} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index 47d26e1b..75c03c3e 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -7,7 +7,7 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.GameData.Files; -using Penumbra.Mod; +using Penumbra.Mods; namespace Penumbra.Util; @@ -74,7 +74,7 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( Mod.Mod mod, string from, string to ) + public static bool ChangeModMaterials( Mods.Mod mod, string from, string to ) { if( ValidStrings( from, to ) ) { diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs index 19e8b47e..76fad9e2 100644 --- a/Penumbra/Util/TempFile.cs +++ b/Penumbra/Util/TempFile.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Linq; namespace Penumbra.Util;