diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 3232931a..03cf6885 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,15 +16,15 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - return Penumbra.CollectionManager.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new - { - x.Settings.Enabled, - x.Settings.Priority, - x.Data.BasePath.Name, - x.Data.Meta, - BasePath = x.Data.BasePath.FullName, - Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), - } ); + return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new + { + x.Second?.Enabled, + x.Second?.Priority, + x.First.BasePath.Name, + x.First.Meta, + BasePath = x.First.BasePath.FullName, + Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), + } ); } [Route( HttpVerbs.Post, "/mods" )] @@ -34,7 +34,7 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/files" )] public object GetFiles() { - return Penumbra.CollectionManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), o => o.Value.FullName ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 540e1432..6a373d14 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -5,6 +5,7 @@ using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; using Lumina.Data; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Mods; @@ -76,7 +77,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, ModManager _, ModCollection collection ) + private static string ResolvePath( string path, ModManager _, ModCollection2 collection ) { if( !Penumbra.Config.EnableMods ) { @@ -84,24 +85,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi } var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty; - var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); - ret ??= Penumbra.CollectionManager.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); + var ret = collection.ResolvePath( gamePath ); return ret?.ToString() ?? path; } public string ResolvePath( string path ) { CheckInitialized(); - return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.DefaultCollection ); + return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default ); } public string ResolvePath( string path, string characterName ) { CheckInitialized(); return ResolvePath( path, Penumbra.ModManager, - Penumbra.CollectionManager.CharacterCollection.TryGetValue( characterName, out var collection ) - ? collection - : ModCollection.Empty ); + Penumbra.CollectionManager.Character( characterName ) ); } private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource @@ -136,12 +134,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi { if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) ) { - collection = ModCollection.Empty; + collection = ModCollection2.Empty; } - if( collection.Cache != null ) + if( collection.HasCache ) { - return collection.Cache.ChangedItems; + return collection.ChangedItems; } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs new file mode 100644 index 00000000..3250e10d --- /dev/null +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Dalamud.Logging; +using Penumbra.Meta.Manager; +using Penumbra.Mod; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public sealed partial class CollectionManager2 +{ + // Is invoked after the collections actually changed. + public event CollectionChangeDelegate? CollectionChanged; + + private int _currentIdx = -1; + private int _defaultIdx = -1; + private int _defaultNameIdx = 0; + + public ModCollection2 Current + => this[ _currentIdx ]; + + public ModCollection2 Default + => this[ _defaultIdx ]; + + private readonly Dictionary< string, int > _character = new(); + + public ModCollection2 Character( string name ) + => _character.TryGetValue( name, out var idx ) ? _collections[ idx ] : Default; + + public bool HasCharacterCollections + => _character.Count > 0; + + private void OnModChanged( ModChangeType type, int idx, ModData mod ) + { + switch( type ) + { + case ModChangeType.Added: + foreach( var collection in _collections ) + { + collection.AddMod( mod ); + } + + foreach( var collection in _collections.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + case ModChangeType.Removed: + var list = new List< ModSettings? >( _collections.Count ); + foreach( var collection in _collections ) + { + list.Add( collection[ idx ].Settings ); + collection.RemoveMod( mod, idx ); + } + + foreach( var (collection, _) in _collections.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + case ModChangeType.Changed: + foreach( var collection in _collections.Where( + collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + { + collection.Save(); + } + + foreach( var collection in _collections.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) ) + { + collection.UpdateCache(); + } + + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + private void CreateNecessaryCaches() + { + if( _defaultIdx >= 0 ) + { + Default.CreateCache(); + } + + if( _currentIdx >= 0 ) + { + Current.CreateCache(); + } + + foreach( var idx in _character.Values.Where( i => i >= 0 ) ) + { + _collections[ idx ].CreateCache(); + } + } + + public void UpdateCaches() + { + foreach( var collection in _collections ) + { + collection.UpdateCache(); + } + } + + private void RemoveCache( int idx ) + { + if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) ) + { + _collections[ idx ].ClearCache(); + } + } + + public void SetCollection( string name, CollectionType type, string? characterName = null ) + => SetCollection( GetIndexForCollectionName( name ), type, characterName ); + + public void SetCollection( ModCollection2 collection, CollectionType type, string? characterName = null ) + => SetCollection( GetIndexForCollectionName( collection.Name ), type, characterName ); + + public void SetCollection( int newIdx, CollectionType type, string? characterName = null ) + { + var oldCollectionIdx = type switch + { + CollectionType.Default => _defaultIdx, + CollectionType.Current => _currentIdx, + CollectionType.Character => characterName?.Length > 0 + ? _character.TryGetValue( characterName, out var c ) + ? c + : _defaultIdx + : -2, + _ => -2, + }; + + if( oldCollectionIdx == -2 || newIdx == oldCollectionIdx ) + { + return; + } + + var newCollection = this[ newIdx ]; + if( newIdx >= 0 ) + { + newCollection.CreateCache(); + } + + RemoveCache( oldCollectionIdx ); + switch( type ) + { + case CollectionType.Default: + _defaultIdx = newIdx; + Penumbra.Config.DefaultCollection = newCollection.Name; + Penumbra.ResidentResources.Reload(); + Default.SetFiles(); + break; + case CollectionType.Current: + _currentIdx = newIdx; + Penumbra.Config.CurrentCollection = newCollection.Name; + break; + case CollectionType.Character: + _character[ characterName! ] = newIdx; + Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; + break; + } + + CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName ); + Penumbra.Config.Save(); + } + + public bool CreateCharacterCollection( string characterName ) + { + if( _character.ContainsKey( characterName ) ) + { + return false; + } + + _character[ characterName ] = -1; + Penumbra.Config.CharacterCollections[ characterName ] = ModCollection2.Empty.Name; + Penumbra.Config.Save(); + CollectionChanged?.Invoke( null, ModCollection2.Empty, CollectionType.Character, characterName ); + return true; + } + + public void RemoveCharacterCollection( string characterName ) + { + if( _character.TryGetValue( characterName, out var collection ) ) + { + RemoveCache( collection ); + _character.Remove( characterName ); + CollectionChanged?.Invoke( this[ collection ], null, CollectionType.Character, characterName ); + } + + if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) + { + Penumbra.Config.Save(); + } + } + + private int GetIndexForCollectionName( string name ) + { + if( name.Length == 0 || name == ModCollection2.DefaultCollection ) + { + return -1; + } + + var idx = _collections.IndexOf( c => c.Name == Penumbra.Config.DefaultCollection ); + return idx < 0 ? -2 : idx; + } + + public void LoadCollections() + { + var configChanged = false; + _defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); + if( _defaultIdx == -2 ) + { + PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); + _defaultIdx = -1; + Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; + configChanged = true; + } + + _currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); + if( _currentIdx == -2 ) + { + PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); + _currentIdx = _defaultNameIdx; + Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; + configChanged = true; + } + + if( LoadCharacterCollections() || configChanged ) + { + Penumbra.Config.Save(); + } + + CreateNecessaryCaches(); + } + + private bool LoadCharacterCollections() + { + var configChanged = false; + foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() ) + { + var idx = GetIndexForCollectionName( collectionName ); + if( idx == -2 ) + { + PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); + _character.Add( player, -1 ); + Penumbra.Config.CharacterCollections[ player ] = ModCollection2.Empty.Name; + configChanged = true; + } + else + { + _character.Add( player, idx ); + } + } + + return configChanged; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs new file mode 100644 index 00000000..7fd81136 --- /dev/null +++ b/Penumbra/Collections/CollectionManager.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public enum CollectionType : byte +{ + Inactive, + Default, + Character, + Current, +} + +public sealed partial class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > +{ + public delegate void CollectionChangeDelegate( ModCollection2? oldCollection, ModCollection2? newCollection, CollectionType type, + string? characterName = null ); + + private readonly ModManager _modManager; + + private readonly List< ModCollection2 > _collections = new(); + + public ModCollection2 this[ Index idx ] + => idx.Value == -1 ? ModCollection2.Empty : _collections[ idx ]; + + public ModCollection2? this[ string name ] + => ByName( name, out var c ) ? c : null; + + public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) + => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); + + public IEnumerator< ModCollection2 > GetEnumerator() + => _collections.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public CollectionManager2( ModManager manager ) + { + _modManager = manager; + + _modManager.ModsRediscovered += OnModsRediscovered; + _modManager.ModChange += OnModChanged; + ReadCollections(); + LoadCollections(); + } + + public void Dispose() + { + _modManager.ModsRediscovered -= OnModsRediscovered; + _modManager.ModChange -= OnModChanged; + } + + private void OnModsRediscovered() + { + UpdateCaches(); + Default.SetFiles(); + } + + private void AddDefaultCollection() + { + var idx = _collections.IndexOf( c => c.Name == ModCollection2.DefaultCollection ); + if( idx >= 0 ) + { + _defaultNameIdx = idx; + return; + } + + var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + defaultCollection.Save(); + _defaultNameIdx = _collections.Count; + _collections.Add( defaultCollection ); + } + + private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + { + foreach( var (collection, inheritance) in this.Zip( inheritances ) ) + { + var changes = false; + foreach( var subCollectionName in inheritance ) + { + if( !ByName( subCollectionName, out var subCollection ) ) + { + changes = true; + PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); + } + else if( !collection.AddInheritance( subCollection ) ) + { + changes = true; + PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); + } + } + + foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) + { + changes |= setting!.FixInvalidSettings( mod.Meta ); + } + + if( changes ) + { + collection.Save(); + } + } + } + + private void ReadCollections() + { + var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); + var inheritances = new List< IReadOnlyList< string > >(); + if( collectionDir.Exists ) + { + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) + { + var collection = ModCollection2.LoadFromFile( file, out var inheritance ); + if( collection == null || collection.Name.Length == 0 ) + { + continue; + } + + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( this[ collection.Name ] != null ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + inheritances.Add( inheritance ); + _collections.Add( collection ); + } + } + } + + AddDefaultCollection(); + ApplyInheritancesAndFixSettings( inheritances ); + } + + public bool AddCollection( string name, ModCollection2? duplicate ) + { + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( nameFixed.Length == 0 + || nameFixed == ModCollection2.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 ) ?? ModCollection2.CreateNewEmpty( name ); + _collections.Add( newCollection ); + newCollection.Save(); + CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); + SetCollection( _collections.Count - 1, CollectionType.Current ); + return true; + } + + public bool RemoveCollection( int idx ) + { + if( idx < 0 || 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, CollectionType.Current ); + } + else if( _currentIdx > idx ) + { + --_currentIdx; + } + + if( idx == _defaultIdx ) + { + SetCollection( -1, CollectionType.Default ); + } + else if( _defaultIdx > idx ) + { + --_defaultIdx; + } + + if( _defaultNameIdx > idx ) + { + --_defaultNameIdx; + } + + foreach( var (characterName, characterIdx) in _character.ToList() ) + { + if( idx == characterIdx ) + { + SetCollection( -1, CollectionType.Character, characterName ); + } + else if( characterIdx > idx ) + { + _character[ characterName ] = characterIdx - 1; + } + } + + var collection = _collections[ idx ]; + collection.Delete(); + _collections.RemoveAt( idx ); + CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); + return true; + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs new file mode 100644 index 00000000..697dde2f --- /dev/null +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manager; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public partial class ModCollection2 +{ + private Cache? _cache; + + public bool HasCache + => _cache != null; + + public void CreateCache() + { + if( _cache == null ) + { + _cache = new Cache( this ); + _cache.CalculateEffectiveFileList(); + } + } + + public void UpdateCache() + => _cache?.CalculateEffectiveFileList(); + + public void ClearCache() + => _cache = null; + + public FullPath? ResolvePath( Utf8GamePath path ) + => _cache?.ResolvePath( path ); + + internal void ForceFile( Utf8GamePath path, FullPath fullPath ) + => _cache!.ResolvedFiles[ path ] = fullPath; + + internal void RemoveFile( Utf8GamePath path ) + => _cache!.ResolvedFiles.Remove( path ); + + internal MetaManager? MetaCache + => _cache?.MetaManipulations; + + internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + + internal IReadOnlySet< FullPath > MissingFiles + => _cache?.MissingFiles ?? new HashSet< FullPath >(); + + internal IReadOnlyDictionary< string, object? > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + + internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); + + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) + { + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); + _cache ??= new Cache( this ); + _cache.CalculateEffectiveFileList(); + if( withMetaManipulations ) + { + _cache.UpdateMetaManipulations(); + } + + if( reloadResident ) + { + Penumbra.ResidentResources.Reload(); + } + } + + + // The ModCollectionCache contains all required temporary data to use a collection. + // It will only be setup if a collection gets activated in any way. + private class Cache + { + // Shared caches to avoid allocations. + private static readonly BitArray FileSeen = new(256); + private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); + private static readonly List< ModSettings? > ResolvedSettings = new(128); + + private readonly ModCollection2 _collection; + private readonly SortedList< string, object? > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly MetaManager MetaManipulations; + public ConflictCache Conflicts; + + public IReadOnlyDictionary< string, object? > ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } + + public Cache( ModCollection2 collection ) + { + _collection = collection; + MetaManipulations = new MetaManager( collection ); + } + + private static void ResetFileSeen( int size ) + { + if( size < FileSeen.Length ) + { + FileSeen.Length = size; + FileSeen.SetAll( false ); + } + else + { + FileSeen.SetAll( false ); + FileSeen.Length = size; + } + } + + private void ClearStorageAndPrepare() + { + ResolvedFiles.Clear(); + MissingFiles.Clear(); + RegisteredFiles.Clear(); + _changedItems.Clear(); + ResolvedSettings.Clear(); + ResolvedSettings.AddRange( _collection.ActualSettings ); + } + + public void CalculateEffectiveFileList() + { + ClearStorageAndPrepare(); + + for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) + { + if( ResolvedSettings[ i ]?.Enabled == true ) + { + AddFiles( i ); + AddSwaps( i ); + } + } + + AddMetaFiles(); + Conflicts.Sort(); + } + + private void SetChangedItems() + { + if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) + { + return; + } + + try + { + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = GameData.GameData.GetIdentifier(); + foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) + { + identifier.Identify( _changedItems, resolved.ToGamePath() ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unknown Error:\n{e}" ); + } + } + + + 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. + foreach( var group in mod.Meta.Groups.Values.Reverse() ) + { + switch( group.SelectionType ) + { + case SelectType.Single: + AddFilesForSingle( group, mod, idx ); + break; + case SelectType.Multi: + AddFilesForMulti( group, mod, idx ); + break; + default: throw new InvalidEnumArgumentException(); + } + } + + AddRemainingFiles( mod, idx ); + } + + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + private static bool FilterFile( Utf8GamePath gamePath ) + => !Penumbra.Config.DisableSoundStreaming + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); + + + private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) + { + if( FilterFile( gamePath ) ) + { + return; + } + + if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) + { + RegisteredFiles.Add( gamePath, modIdx ); + ResolvedFiles[ gamePath ] = file; + } + else + { + var priority = ResolvedSettings[ modIdx ]!.Priority; + var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; + Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); + if( priority > oldPriority ) + { + ResolvedFiles[ gamePath ] = file; + RegisteredFiles[ gamePath ] = modIdx; + } + } + } + + private void AddMissingFile( FullPath file ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + return; + default: + MissingFiles.Add( file ); + return; + } + } + + private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) + { + foreach( var (file, paths) in option.OptionFiles ) + { + var fullPath = new FullPath( mod.BasePath, file ); + var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); + if( idx < 0 ) + { + AddMissingFile( fullPath ); + continue; + } + + var registeredFile = mod.Resources.ModFiles[ idx ]; + if( !registeredFile.Exists ) + { + AddMissingFile( registeredFile ); + continue; + } + + FileSeen.Set( idx, true ); + if( enabled ) + { + foreach( var path in paths ) + { + AddFile( modIdx, path, registeredFile ); + } + } + } + } + + private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) + { + Debug.Assert( singleGroup.SelectionType == SelectType.Single ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) + { + setting = 0; + } + + for( var i = 0; i < singleGroup.Options.Count; ++i ) + { + AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); + } + } + + private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) + { + Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); + var settings = ResolvedSettings[ modIdx ]!; + if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) + { + return; + } + + // Also iterate options in reverse so that later options take precedence before earlier ones. + for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) + { + AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); + } + } + + private void AddRemainingFiles( ModData mod, int modIdx ) + { + for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) + { + if( FileSeen.Get( i ) ) + { + continue; + } + + var file = mod.Resources.ModFiles[ i ]; + if( file.Exists ) + { + if( file.ToGamePath( mod.BasePath, out var gamePath ) ) + { + AddFile( modIdx, gamePath, file ); + } + else + { + PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); + } + } + else + { + MissingFiles.Add( file ); + } + } + } + + private void AddMetaFiles() + => MetaManipulations.Imc.SetFiles(); + + private void AddSwaps( int modIdx ) + { + var mod = Penumbra.ModManager.Mods[ modIdx ]; + foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + { + AddFile( modIdx, gamePath, swapPath ); + } + } + + private void AddManipulations( int modIdx ) + { + var mod = Penumbra.ModManager.Mods[ modIdx ]; + foreach( var manip in mod.Resources.MetaManipulations.GetManipulationsForConfig( ResolvedSettings[ modIdx ]!, mod.Meta ) ) + { + if( !MetaManipulations.TryGetValue( manip, out var oldModIdx ) ) + { + MetaManipulations.ApplyMod( manip, modIdx ); + } + else + { + var priority = ResolvedSettings[ modIdx ]!.Priority; + var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; + Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, manip ); + if( priority > oldPriority ) + { + MetaManipulations.ApplyMod( manip, modIdx ); + } + } + } + } + + public void UpdateMetaManipulations() + { + MetaManipulations.Reset(); + Conflicts.ClearMetaConflicts(); + + foreach( var mod in Penumbra.ModManager.Mods.Zip( ResolvedSettings ) + .Select( ( m, i ) => ( m.First, m.Second, i ) ) + .Where( m => m.Second?.Enabled == true && m.First.Resources.MetaManipulations.Count > 0 ) ) + { + AddManipulations( mod.i ); + } + } + + public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) + { + return null; + } + + 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/Mods/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs similarity index 99% rename from Penumbra/Mods/ModCollection.Changes.cs rename to Penumbra/Collections/ModCollection.Changes.cs index 0423ffe8..02f6f240 100644 --- a/Penumbra/Mods/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -1,7 +1,7 @@ using System; using Penumbra.Mod; -namespace Penumbra.Mods; +namespace Penumbra.Collections; public enum ModSettingChange { diff --git a/Penumbra/Mods/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs similarity index 97% rename from Penumbra/Mods/ModCollection.Inheritance.cs rename to Penumbra/Collections/ModCollection.Inheritance.cs index 13ba09ca..dee20aef 100644 --- a/Penumbra/Mods/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -4,7 +4,7 @@ using System.Linq; using Penumbra.Mod; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Collections; public partial class ModCollection2 { @@ -53,7 +53,7 @@ public partial class ModCollection2 } } - public (ModSettings? Settings, ModCollection2 Collection) this[ int idx ] + public (ModSettings? Settings, ModCollection2 Collection) this[ Index idx ] { get { diff --git a/Penumbra/Mods/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs similarity index 94% rename from Penumbra/Mods/ModCollection.Migration.cs rename to Penumbra/Collections/ModCollection.Migration.cs index 76b40ae7..abd935ab 100644 --- a/Penumbra/Mods/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,9 +1,9 @@ using System.Linq; using Penumbra.Mod; -namespace Penumbra.Mods; +namespace Penumbra.Collections; -public partial class ModCollection2 +public sealed partial class ModCollection2 { private static class Migration { diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs new file mode 100644 index 00000000..5f6c46ea --- /dev/null +++ b/Penumbra/Collections/ModCollection.cs @@ -0,0 +1,209 @@ +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; + +namespace Penumbra.Collections; + +// 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. +public partial class ModCollection2 +{ + public const int CurrentVersion = 1; + public const string DefaultCollection = "Default"; + + public static readonly ModCollection2 Empty = CreateNewEmpty( "None" ); + + public string Name { get; private init; } + public int Version { get; private set; } + + private readonly List< ModSettings? > _settings; + + public IReadOnlyList< ModSettings? > Settings + => _settings; + + public IEnumerable< ModSettings? > ActualSettings + => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + + private readonly Dictionary< string, ModSettings > _unusedSettings; + + + private ModCollection2( string name, ModCollection2 duplicate ) + { + Name = name; + Version = duplicate.Version; + _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); + _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + _inheritance = duplicate._inheritance.ToList(); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) + { + Name = name; + Version = version; + _unusedSettings = allSettings; + _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); + for( var i = 0; i < Penumbra.ModManager.Count; ++i ) + { + var modName = Penumbra.ModManager[ i ].BasePath.Name; + if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) + { + _unusedSettings.Remove( modName ); + _settings[ i ] = settings; + } + } + + Migration.Migrate( this ); + ModSettingChanged += SaveOnChange; + InheritanceChanged += Save; + } + + public static ModCollection2 CreateNewEmpty( string name ) + => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + + public ModCollection2 Duplicate( string name ) + => new(name, this); + + internal static ModCollection2 MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + => new(name, 0, allSettings); + + private void CleanUnavailableSettings() + { + var any = _unusedSettings.Count > 0; + _unusedSettings.Clear(); + if( any ) + { + Save(); + } + } + + public void AddMod( ModData mod ) + { + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + _settings.Add( settings ); + _unusedSettings.Remove( mod.BasePath.Name ); + } + else + { + _settings.Add( null ); + } + } + + public void RemoveMod( ModData mod, int idx ) + { + var settings = _settings[ idx ]; + if( settings != null ) + { + _unusedSettings.Add( mod.BasePath.Name, settings ); + } + + _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() + { + 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 ModCollection2? 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 ModCollection2( 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/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index d89a18b7..3d869944 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods; using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; @@ -91,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path ); + var resolved = ModManager.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 7907b172..ba8d6b05 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; 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; @@ -90,10 +91,10 @@ public unsafe partial class PathResolver // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // It contains any DrawObjects that correspond to a human actor, even those without specific collections. - internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new(); + internal readonly Dictionary< IntPtr, (ModCollection2, int) > DrawObjectToObject = new(); // This map links files to their corresponding collection, if it is non-default. - internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new(); + internal readonly ConcurrentDictionary< Utf8String, ModCollection2 > PathCollections = new(); internal GameObject* LastGameObject = null; @@ -158,11 +159,11 @@ public unsafe partial class PathResolver } // Identify the correct collection for a GameObject by index and name. - private static ModCollection IdentifyCollection( GameObject* gameObject ) + private static ModCollection2 IdentifyCollection( GameObject* gameObject ) { if( gameObject == null ) { - return Penumbra.CollectionManager.DefaultCollection; + return Penumbra.CollectionManager.Default; } var name = gameObject->ObjectIndex switch @@ -175,13 +176,11 @@ public unsafe partial class PathResolver } ?? new Utf8String( gameObject->Name ).ToString(); - return Penumbra.CollectionManager.CharacterCollection.TryGetValue( name, out var col ) - ? col - : Penumbra.CollectionManager.DefaultCollection; + return Penumbra.CollectionManager.Character( name ); } // Update collections linked to Game/DrawObjects due to a change in collection configuration. - private void CheckCollections( ModCollection? _1, ModCollection? _2, CollectionType type, string? name ) + private void CheckCollections( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? name ) { if( type is not (CollectionType.Character or CollectionType.Default) ) { @@ -201,7 +200,7 @@ public unsafe partial class PathResolver } // Use the stored information to find the GameObject and Collection linked to a DrawObject. - private GameObject* FindParent( IntPtr drawObject, out ModCollection collection ) + private GameObject* FindParent( IntPtr drawObject, out ModCollection2 collection ) { if( DrawObjectToObject.TryGetValue( drawObject, out var data ) ) { @@ -226,7 +225,7 @@ public unsafe partial class PathResolver // Special handling for paths so that we do not store non-owned temporary strings in the dictionary. - private void SetCollection( Utf8String path, ModCollection collection ) + private void SetCollection( Utf8String path, ModCollection2 collection ) { if( PathCollections.ContainsKey( path ) || path.IsOwned ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 5dcd2612..51aeeae8 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -3,6 +3,7 @@ using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; @@ -40,7 +41,7 @@ public unsafe partial class PathResolver return ret; } - private ModCollection? _mtrlCollection; + private ModCollection2? _mtrlCollection; private void LoadMtrlHelper( IntPtr mtrlResourceHandle ) { @@ -55,7 +56,7 @@ public unsafe partial class PathResolver } // Check specifically for shpk and tex files whether we are currently in a material load. - private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection ) + private bool HandleMaterialSubFiles( ResourceType type, out ModCollection2? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { @@ -95,7 +96,7 @@ public unsafe partial class PathResolver } // Materials need to be set per collection so they can load their textures independently from each other. - private void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, + private static void HandleMtrlCollection( ModCollection2 collection, string path, bool nonDefault, ResourceType type, FullPath? resolved, out (FullPath?, object?) data ) { if( nonDefault && type == ResourceType.Mtrl ) diff --git a/Penumbra/Interop/Resolver/PathResolver.Meta.cs b/Penumbra/Interop/Resolver/PathResolver.Meta.cs index ee556c25..d32eee9f 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Meta.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Meta.cs @@ -2,6 +2,7 @@ using System; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; +using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -160,15 +161,15 @@ public unsafe partial class PathResolver RspSetupCharacterHook?.Dispose(); } - private ModCollection? GetCollection( IntPtr drawObject ) + private ModCollection2? GetCollection( IntPtr drawObject ) { var parent = FindParent( drawObject, out var collection ); - if( parent == null || collection == Penumbra.CollectionManager.DefaultCollection ) + if( parent == null || collection == Penumbra.CollectionManager.Default ) { return null; } - return collection.Cache == null ? Penumbra.CollectionManager.ForcedCollection : collection; + return collection.HasCache ? collection : null; } @@ -194,7 +195,7 @@ public unsafe partial class PathResolver } } - public static MetaChanger ChangeEqp( ModCollection collection ) + public static MetaChanger ChangeEqp( ModCollection2 collection ) { #if USE_EQP collection.SetEqpFiles(); @@ -232,7 +233,7 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeEqdp( ModCollection collection ) + public static MetaChanger ChangeEqdp( ModCollection2 collection ) { #if USE_EQDP collection.SetEqdpFiles(); @@ -268,13 +269,13 @@ public unsafe partial class PathResolver return new MetaChanger( MetaManipulation.Type.Unknown ); } - public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection ) + public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection2? collection ) { if( resolver.LastGameObject != null ) { collection = IdentifyCollection( resolver.LastGameObject ); #if USE_CMP - if( collection != Penumbra.CollectionManager.DefaultCollection && collection.Cache != null ) + if( collection != Penumbra.CollectionManager.Default && collection.HasCache ) { collection.SetCmpFiles(); return new MetaChanger( MetaManipulation.Type.Rsp ); @@ -309,25 +310,25 @@ public unsafe partial class PathResolver case MetaManipulation.Type.Eqdp: if( --_eqdpCounter == 0 ) { - Penumbra.CollectionManager.DefaultCollection.SetEqdpFiles(); + Penumbra.CollectionManager.Default.SetEqdpFiles(); } break; case MetaManipulation.Type.Eqp: if( --_eqpCounter == 0 ) { - Penumbra.CollectionManager.DefaultCollection.SetEqpFiles(); + Penumbra.CollectionManager.Default.SetEqpFiles(); } break; case MetaManipulation.Type.Est: - Penumbra.CollectionManager.DefaultCollection.SetEstFiles(); + Penumbra.CollectionManager.Default.SetEstFiles(); break; case MetaManipulation.Type.Gmp: - Penumbra.CollectionManager.DefaultCollection.SetGmpFiles(); + Penumbra.CollectionManager.Default.SetGmpFiles(); break; case MetaManipulation.Type.Rsp: - Penumbra.CollectionManager.DefaultCollection.SetCmpFiles(); + Penumbra.CollectionManager.Default.SetCmpFiles(); break; } } diff --git a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs index e6d443ed..eea23033 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Resolve.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Resolve.cs @@ -1,8 +1,8 @@ using System; using System.Runtime.CompilerServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Collections; using Penumbra.GameData.ByteString; -using Penumbra.Mods; namespace Penumbra.Interop.Resolver; @@ -104,7 +104,7 @@ public unsafe partial class PathResolver [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path ) => ResolvePathDetour( FindParent( drawObject, out var collection ) == null - ? Penumbra.CollectionManager.DefaultCollection + ? Penumbra.CollectionManager.Default : collection, path ); // Weapons have the characters DrawObject as a parent, @@ -123,14 +123,14 @@ public unsafe partial class PathResolver { var parent = FindParent( ( IntPtr )parentObject, out var collection ); return ResolvePathDetour( parent == null - ? Penumbra.CollectionManager.DefaultCollection + ? Penumbra.CollectionManager.Default : collection, path ); } } // Just add or remove the resolved path. [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] - private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path ) + private IntPtr ResolvePathDetour( ModCollection2 collection, IntPtr path ) { if( path == IntPtr.Zero ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 9dc67a17..4c85380c 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.Interop.Loader; @@ -39,27 +40,17 @@ public partial class PathResolver : IDisposable var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ); if( !nonDefault ) { - collection = Penumbra.CollectionManager.DefaultCollection; + collection = Penumbra.CollectionManager.Default; } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath ); - if( resolved == null ) - { - resolved = Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath ); - if( resolved == null ) - { - // We also need to handle defaulted materials against a non-default collection. - HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data ); - return true; - } - - collection = Penumbra.CollectionManager.ForcedCollection; - } + var resolved = collection!.ResolvePath( gamePath ); // Since mtrl files load their files separately, we need to add the new, resolved path // so that the functions loading tex and shpk can find that path and use its collection. - HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data ); + // We also need to handle defaulted materials against a non-default collection. + var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName; + HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data ); return true; } @@ -113,14 +104,14 @@ public partial class PathResolver : IDisposable Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; } - private void OnCollectionChange( ModCollection? _1, ModCollection? _2, CollectionType type, string? characterName ) + private void OnCollectionChange( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? characterName ) { if( type != CollectionType.Character ) { return; } - if( Penumbra.CollectionManager.CharacterCollection.Count > 0 ) + if( Penumbra.CollectionManager.HasCharacterCollections ) { Enable(); } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index 6cfa47b8..ce838fe9 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerCmp : IDisposable { - public CmpFile? File = null; - public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new(); + public CmpFile? File = null; + public readonly Dictionary< RspManipulation, int > Manipulations = new(); public MetaManagerCmp() { } @@ -38,14 +38,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, Mod.Mod mod ) + public bool ApplyMod( RspManipulation m, int modIdx ) { #if USE_CMP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; File ??= new CmpFile(); return m.Apply( File ); #else diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 1dba4520..785d3d2e 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -16,7 +16,7 @@ public partial class MetaManager { public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 1]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, int > Manipulations = new(); public MetaManagerEqdp() { } @@ -50,14 +50,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, Mod.Mod mod ) + public bool ApplyMod( EqdpManipulation m, int modIdx ) { #if USE_EQDP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar return m.Apply( file ); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index dd7c63ff..92d8f3d8 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerEqp : IDisposable { - public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new(); + public ExpandedEqpFile? File = null; + public readonly Dictionary< EqpManipulation, int > Manipulations = new(); public MetaManagerEqp() { } @@ -38,14 +38,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, Mod.Mod mod ) + public bool ApplyMod( EqpManipulation m, int modIdx ) { #if USE_EQP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; File ??= new ExpandedEqpFile(); return m.Apply( File ); #else diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index f53d145d..c6901f2f 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -16,7 +16,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< EstManipulation, int > Manipulations = new(); public MetaManagerEst() { } @@ -49,14 +49,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, Mod.Mod mod ) + public bool ApplyMod( EstManipulation m, int modIdx ) { #if USE_EST - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var file = m.Slot switch { EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 8f43ac00..4bdee5c3 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -12,8 +12,8 @@ public partial class MetaManager { public struct MetaManagerGmp : IDisposable { - public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new(); + public ExpandedGmpFile? File = null; + public readonly Dictionary< GmpManipulation, int > Manipulations = new(); public MetaManagerGmp() { } @@ -37,15 +37,11 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, Mod.Mod mod ) + public bool ApplyMod( GmpManipulation m, int modIdx ) { #if USE_GMP - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - - File ??= new ExpandedGmpFile(); + Manipulations[ m ] = modIdx; + File ??= new ExpandedGmpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index 4e3d302a..2a6e3a37 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using System.Diagnostics; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -18,14 +16,14 @@ public partial class MetaManager { public readonly struct MetaManagerImc : IDisposable { - public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new(); + public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); + public readonly Dictionary< ImcManipulation, int > Manipulations = new(); - private readonly ModCollection _collection; - private static int _imcManagerCount; + private readonly ModCollection2 _collection; + private static int _imcManagerCount; - public MetaManagerImc( ModCollection collection ) + public MetaManagerImc( ModCollection2 collection ) { _collection = collection; SetupDelegate(); @@ -34,37 +32,43 @@ public partial class MetaManager [Conditional( "USE_IMC" )] public void SetFiles() { - if( _collection.Cache == null ) + if( !_collection.HasCache ) { return; } foreach( var path in Files.Keys ) { - _collection.Cache.ResolvedFiles[ path ] = CreateImcPath( path ); + _collection.ForceFile( path, CreateImcPath( path ) ); } } [Conditional( "USE_IMC" )] public void Reset() { - foreach( var (path, file) in Files ) + if( _collection.HasCache ) { - _collection.Cache?.ResolvedFiles.Remove( path ); - file.Reset(); + foreach( var (path, file) in Files ) + { + _collection.RemoveFile( path ); + file.Reset(); + } + } + else + { + foreach( var (_, file) in Files ) + { + file.Reset(); + } } Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, Mod.Mod mod ) + public bool ApplyMod( ImcManipulation m, int modIdx ) { #if USE_IMC - if( !Manipulations.TryAdd( m, mod ) ) - { - return false; - } - + Manipulations[ m ] = modIdx; var path = m.GamePath(); if( !Files.TryGetValue( path, out var file ) ) { @@ -78,9 +82,9 @@ public partial class MetaManager Files[ path ] = file; var fullPath = CreateImcPath( path ); - if( _collection.Cache != null ) + if( _collection.HasCache ) { - _collection.Cache.ResolvedFiles[ path ] = fullPath; + _collection.ForceFile( path, fullPath ); } return true; @@ -135,8 +139,8 @@ public partial class MetaManager PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path ); ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) - && collection.Cache != null - && collection.Cache.MetaManipulations.Imc.Files.TryGetValue( + && collection.HasCache + && collection.MetaCache!.Imc.Files.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, @@ -152,9 +156,8 @@ public partial class MetaManager { // Only check imcs. if( resource->FileType != ResourceType.Imc - || resolveData is not ModCollection collection - || collection.Cache == null - || !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file ) + || resolveData is not ModCollection2 { HasCache: true } collection + || !collection.MetaCache!.Imc.Files.TryGetValue( gamePath, out var file ) || !file.ChangesSinceLoad ) { return; diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index d0932ae4..0672d953 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,8 +1,8 @@ using System; using System.Runtime.CompilerServices; +using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -28,19 +28,19 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod ) + public bool TryGetValue( MetaManipulation manip, out int modIdx ) { - mod = manip.ManipulationType switch + modIdx = manip.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, - MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, - MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, - MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, - MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, - MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, + MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : -1, + MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : -1, + MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : -1, + MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : -1, + MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : -1, + MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : -1, _ => throw new ArgumentOutOfRangeException(), }; - return mod != null; + return modIdx != -1; } public int Count @@ -51,7 +51,7 @@ public partial class MetaManager : IDisposable + Est.Manipulations.Count + Eqp.Manipulations.Count; - public MetaManager( ModCollection collection ) + public MetaManager( ModCollection2 collection ) => Imc = new MetaManagerImc( collection ); public void SetFiles() @@ -84,16 +84,16 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) + public bool ApplyMod( MetaManipulation m, int modIdx ) { return m.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), - MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), - MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), - MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), - MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), - MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), + MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, modIdx ), + MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, modIdx ), + MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, modIdx ), + MetaManipulation.Type.Est => Est.ApplyMod( m.Est, modIdx ), + MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, modIdx ), + MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, modIdx ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index 2f4fd5a7..742b1d5d 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json.Linq; +using Penumbra.Collections; using Penumbra.Mod; using Penumbra.Mods; @@ -33,8 +34,8 @@ public static class MigrateConfiguration return; } - var defaultCollection = new ModCollection(); - var defaultCollectionFile = defaultCollection.FileName(); + var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); + var defaultCollectionFile = defaultCollection.FileName; if( defaultCollectionFile.Exists ) { return; @@ -46,6 +47,7 @@ public static class MigrateConfiguration var data = JArray.Parse( text ); var maxPriority = 0; + var dict = new Dictionary< string, ModSettings >(); foreach( var setting in data.Cast< JObject >() ) { var modName = ( string )setting[ "FolderName" ]!; @@ -54,24 +56,25 @@ public static class MigrateConfiguration var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); - var save = new ModSettings() + dict[ modName ] = new ModSettings() { Enabled = enabled, Priority = priority, Settings = settings!, }; - defaultCollection.Settings.Add( modName, save ); + ; maxPriority = Math.Max( maxPriority, priority ); } if( !config.InvertModListOrder ) { - foreach( var setting in defaultCollection.Settings.Values ) + foreach( var setting in dict.Values ) { setting.Priority = maxPriority - setting.Priority; } } + defaultCollection = ModCollection2.MigrateFromV0( ModCollection2.DefaultCollection, dict ); defaultCollection.Save(); } catch( Exception e ) diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 6fc6486c..80e7451c 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Mod; -public struct ModCache2 +public struct ConflictCache { public readonly struct ModCacheStruct : IComparable< ModCacheStruct > { diff --git a/Penumbra/Mod/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs index 38c316e2..654c2174 100644 --- a/Penumbra/Mod/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -9,6 +9,7 @@ using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Importer; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Mod; @@ -60,8 +61,8 @@ public class ModCleanup private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) { - var idx = Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[idx]; + var idx = Penumbra.ModManager.AddMod( newDir ); + var newMod = Penumbra.ModManager.Mods[ idx ]; newMod.Move( newSortOrder ); newMod.ComputeChangedItems(); ModFileSystem.InvokeChange(); @@ -509,21 +510,23 @@ public class ModCleanup } } - if( option.OptionFiles.Any() ) + if( option.OptionFiles.Count > 0 ) { group.Options.Add( option ); } } - if( group.Options.Any() ) + if( group.Options.Count > 0 ) { meta.Groups.Add( groupDir.Name, group ); } } - foreach( var collection in Penumbra.CollectionManager.Collections ) + // TODO + var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); + foreach( var collection in Penumbra.CollectionManager ) { - collection.UpdateSetting( baseDir, meta, true ); + collection.Settings[ idx ]?.FixInvalidSettings( meta ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs deleted file mode 100644 index a00b01be..00000000 --- a/Penumbra/Mods/CollectionManager.cs +++ /dev/null @@ -1,574 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 > -{ - private readonly ModManager _modManager; - - private readonly List< ModCollection2 > _collections = new(); - - public ModCollection2 this[ int idx ] - => _collections[ idx ]; - - public ModCollection2? this[ string name ] - => ByName( name, out var c ) ? c : null; - - public ModCollection2 Default - => this[ ModCollection2.DefaultCollection ]!; - - public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection ) - => _collections.FindFirst( c => c.Name == name, out collection ); - - public IEnumerator< ModCollection2 > GetEnumerator() - => _collections.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public CollectionManager2( ModManager manager ) - { - _modManager = manager; - - //_modManager.ModsRediscovered += OnModsRediscovered; - //_modManager.ModChange += OnModChanged; - ReadCollections(); - //LoadConfigCollections( Penumbra.Config ); - } - - public void Dispose() - { } - - private void AddDefaultCollection() - { - if( this[ ModCollection.DefaultCollection ] != null ) - { - return; - } - - var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection ); - defaultCollection.Save(); - _collections.Add( defaultCollection ); - } - - private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) - { - foreach( var (collection, inheritance) in this.Zip( inheritances ) ) - { - var changes = false; - foreach( var subCollectionName in inheritance ) - { - if( !ByName( subCollectionName, out var subCollection ) ) - { - changes = true; - PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." ); - } - else if( !collection.AddInheritance( subCollection ) ) - { - changes = true; - PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." ); - } - } - - foreach( var (setting, mod) in collection.Settings.Zip( Penumbra.ModManager.Mods ).Where( s => s.First != null ) ) - { - changes |= setting!.FixInvalidSettings( mod.Meta ); - } - - if( changes ) - { - collection.Save(); - } - } - } - - private void ReadCollections() - { - var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory ); - var inheritances = new List< IReadOnlyList< string > >(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection2.LoadFromFile( file, out var inheritance ); - if( collection == null || collection.Name.Length == 0 ) - { - continue; - } - - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( this[ collection.Name ] != null ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - inheritances.Add( inheritance ); - _collections.Add( collection ); - } - } - } - - AddDefaultCollection(); - ApplyInheritancesAndFixSettings( inheritances ); - } -} - -public enum CollectionType : byte -{ - Inactive, - Default, - Forced, - Character, - Current, -} - -public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, CollectionType type, - string? characterName = null ); - -// Contains all collections and respective functions, as well as the collection settings. -public sealed class CollectionManager : IDisposable -{ - private readonly ModManager _manager; - - public List< ModCollection > Collections { get; } = new(); - public Dictionary< string, ModCollection > CharacterCollection { get; } = new(); - - public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty; - public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty; - public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty; - - public bool IsActive( ModCollection collection ) - => ReferenceEquals( collection, DefaultCollection ) || ReferenceEquals( collection, ForcedCollection ); - - public ModCollection Default - => ByName( ModCollection.DefaultCollection )!; - - public ModCollection? ByName( string name ) - => name.Length > 0 - ? Collections.Find( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ) ) - : ModCollection.Empty; - - public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) - { - if( name.Length > 0 ) - { - return Collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); - } - - collection = ModCollection.Empty; - return true; - } - - // Is invoked after the collections actually changed. - public event CollectionChangeDelegate? CollectionChanged; - - public CollectionManager( ModManager manager ) - { - _manager = manager; - - _manager.ModsRediscovered += OnModsRediscovered; - _manager.ModChange += OnModChanged; - ReadCollections(); - LoadConfigCollections( Penumbra.Config ); - } - - public void Dispose() - { - _manager.ModsRediscovered -= OnModsRediscovered; - _manager.ModChange -= OnModChanged; - } - - private void OnModsRediscovered() - { - RecreateCaches(); - DefaultCollection.SetFiles(); - } - - private void OnModChanged( ModChangeType type, int idx, ModData mod ) - { - switch( type ) - { - case ModChangeType.Added: - foreach( var collection in Collections ) - { - collection.AddMod( mod ); - } - - break; - case ModChangeType.Removed: - RemoveModFromCaches( mod.BasePath ); - break; - case ModChangeType.Changed: - // TODO - break; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); - } - } - - public void CreateNecessaryCaches() - { - AddCache( DefaultCollection ); - AddCache( ForcedCollection ); - foreach( var (_, collection) in CharacterCollection ) - { - AddCache( collection ); - } - } - - public void RecreateCaches() - { - foreach( var collection in Collections.Where( c => c.Cache != null ) ) - { - collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - CreateNecessaryCaches(); - } - - public void RemoveModFromCaches( DirectoryInfo modDir ) - { - foreach( var collection in Collections ) - { - collection.Cache?.RemoveMod( modDir ); - } - } - - internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta ) - { - foreach( var collection in Collections ) - { - if( metaChanges ) - { - collection.UpdateSetting( mod ); - } - - if( fileChanges.HasFlag( ResourceChange.Files ) - && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) - && settings.Enabled ) - { - collection.Cache?.CalculateEffectiveFileList(); - } - - if( reloadMeta ) - { - collection.Cache?.UpdateMetaManipulations(); - } - } - - if( reloadMeta && DefaultCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled ) - { - Penumbra.ResidentResources.Reload(); - } - } - - public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) - { - var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if( nameFixed.Length == 0 || 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 = new ModCollection( name, settings ); - Collections.Add( newCollection ); - newCollection.Save(); - CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive ); - SetCollection( newCollection, CollectionType.Current ); - return true; - } - - public bool RemoveCollection( string name ) - { - if( name == ModCollection.DefaultCollection ) - { - PluginLog.Error( "Can not remove the default collection." ); - return false; - } - - var idx = Collections.IndexOf( c => c.Name == name ); - if( idx < 0 ) - { - return false; - } - - var collection = Collections[ idx ]; - - if( CurrentCollection == collection ) - { - SetCollection( Default, CollectionType.Current ); - } - - if( ForcedCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Forced ); - } - - if( DefaultCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Default ); - } - - foreach( var (characterName, characterCollection) in CharacterCollection.ToArray() ) - { - if( characterCollection == collection ) - { - SetCollection( ModCollection.Empty, CollectionType.Character, characterName ); - } - } - - collection.Delete(); - Collections.RemoveAt( idx ); - CollectionChanged?.Invoke( collection, null, CollectionType.Inactive ); - return true; - } - - private void AddCache( ModCollection collection ) - { - if( collection.Cache == null && collection.Name != string.Empty ) - { - collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - } - - private void RemoveCache( ModCollection collection ) - { - if( collection.Name != ForcedCollection.Name - && collection.Name != CurrentCollection.Name - && collection.Name != DefaultCollection.Name - && CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) ) - { - collection.ClearCache(); - } - } - - public void SetCollection( ModCollection newCollection, CollectionType type, string? characterName = null ) - { - var oldCollection = type switch - { - CollectionType.Default => DefaultCollection, - CollectionType.Forced => ForcedCollection, - CollectionType.Current => CurrentCollection, - CollectionType.Character => characterName?.Length > 0 - ? CharacterCollection.TryGetValue( characterName, out var c ) - ? c - : ModCollection.Empty - : null, - _ => null, - }; - - if( oldCollection == null || newCollection.Name == oldCollection.Name ) - { - return; - } - - AddCache( newCollection ); - RemoveCache( oldCollection ); - switch( type ) - { - case CollectionType.Default: - DefaultCollection = newCollection; - Penumbra.Config.DefaultCollection = newCollection.Name; - Penumbra.ResidentResources.Reload(); - DefaultCollection.SetFiles(); - break; - case CollectionType.Forced: - ForcedCollection = newCollection; - Penumbra.Config.ForcedCollection = newCollection.Name; - Penumbra.ResidentResources.Reload(); - break; - case CollectionType.Current: - CurrentCollection = newCollection; - Penumbra.Config.CurrentCollection = newCollection.Name; - break; - case CollectionType.Character: - CharacterCollection[ characterName! ] = newCollection; - Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; - break; - } - - CollectionChanged?.Invoke( oldCollection, newCollection, type, characterName ); - - Penumbra.Config.Save(); - } - - public bool CreateCharacterCollection( string characterName ) - { - if( CharacterCollection.ContainsKey( characterName ) ) - { - return false; - } - - CharacterCollection[ characterName ] = ModCollection.Empty; - Penumbra.Config.CharacterCollections[ characterName ] = string.Empty; - Penumbra.Config.Save(); - CollectionChanged?.Invoke( null, ModCollection.Empty, CollectionType.Character, characterName ); - return true; - } - - public void RemoveCharacterCollection( string characterName ) - { - if( CharacterCollection.TryGetValue( characterName, out var collection ) ) - { - RemoveCache( collection ); - CharacterCollection.Remove( characterName ); - CollectionChanged?.Invoke( collection, null, CollectionType.Character, characterName ); - } - - if( Penumbra.Config.CharacterCollections.Remove( characterName ) ) - { - Penumbra.Config.Save(); - } - } - - private bool LoadCurrentCollection( Configuration config ) - { - if( ByName( config.CurrentCollection, out var currentCollection ) ) - { - CurrentCollection = currentCollection; - AddCache( CurrentCollection ); - return false; - } - - PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." ); - CurrentCollection = Default; - if( CurrentCollection.Cache == null ) - { - CurrentCollection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ); - } - - config.CurrentCollection = ModCollection.DefaultCollection; - return true; - } - - private bool LoadForcedCollection( Configuration config ) - { - if( config.ForcedCollection.Length == 0 ) - { - ForcedCollection = ModCollection.Empty; - return false; - } - - if( ByName( config.ForcedCollection, out var forcedCollection ) ) - { - ForcedCollection = forcedCollection; - AddCache( ForcedCollection ); - return false; - } - - PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." ); - ForcedCollection = ModCollection.Empty; - config.ForcedCollection = string.Empty; - return true; - } - - private bool LoadDefaultCollection( Configuration config ) - { - if( config.DefaultCollection.Length == 0 ) - { - DefaultCollection = ModCollection.Empty; - return false; - } - - if( ByName( config.DefaultCollection, out var defaultCollection ) ) - { - DefaultCollection = defaultCollection; - AddCache( DefaultCollection ); - return false; - } - - PluginLog.Error( $"Last choice of DefaultCollection {config.DefaultCollection} is not available, reset to None." ); - DefaultCollection = ModCollection.Empty; - config.DefaultCollection = string.Empty; - return true; - } - - private bool LoadCharacterCollections( Configuration config ) - { - var configChanged = false; - foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) - { - if( collectionName.Length == 0 ) - { - CharacterCollection.Add( player, ModCollection.Empty ); - } - else if( ByName( collectionName, out var charCollection ) ) - { - AddCache( charCollection ); - CharacterCollection.Add( player, charCollection ); - } - else - { - PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); - CharacterCollection.Add( player, ModCollection.Empty ); - config.CharacterCollections[ player ] = string.Empty; - configChanged = true; - } - } - - return configChanged; - } - - private void LoadConfigCollections( Configuration config ) - { - var configChanged = LoadCurrentCollection( config ); - configChanged |= LoadDefaultCollection( config ); - configChanged |= LoadForcedCollection( config ); - configChanged |= LoadCharacterCollections( config ); - - if( configChanged ) - { - config.Save(); - } - } - - private void ReadCollections() - { - var collectionDir = ModCollection.CollectionDir(); - if( collectionDir.Exists ) - { - foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) - { - var collection = ModCollection.LoadFromFile( file ); - if( collection == null || collection.Name == string.Empty ) - { - continue; - } - - if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) - { - PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); - } - - if( ByName( collection.Name ) != null ) - { - PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); - } - else - { - Collections.Add( collection ); - } - } - } - - if( ByName( ModCollection.DefaultCollection ) == null ) - { - var defaultCollection = new ModCollection(); - defaultCollection.Save(); - Collections.Add( defaultCollection ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs deleted file mode 100644 index 9a00250b..00000000 --- a/Penumbra/Mods/ModCollection.cs +++ /dev/null @@ -1,523 +0,0 @@ -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.GameData.ByteString; -using Penumbra.Meta.Manager; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class ModCollection2 -{ - public const int CurrentVersion = 1; - public const string DefaultCollection = "Default"; - - public string Name { get; private init; } - public int Version { get; private set; } - - private readonly List< ModSettings? > _settings; - - public IReadOnlyList< ModSettings? > Settings - => _settings; - - public IEnumerable< ModSettings? > ActualSettings - => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); - - private readonly Dictionary< string, ModSettings > _unusedSettings; - - private ModCollection2( string name, ModCollection2 duplicate ) - { - Name = name; - Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); - _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); - _inheritance = duplicate._inheritance.ToList(); - ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; - } - - private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings ) - { - Name = name; - Version = version; - _unusedSettings = allSettings; - _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) - { - var modName = Penumbra.ModManager[ i ].BasePath.Name; - if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) - { - _unusedSettings.Remove( modName ); - _settings[ i ] = settings; - } - } - - Migration.Migrate( this ); - ModSettingChanged += SaveOnChange; - InheritanceChanged += Save; - } - - public static ModCollection2 CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); - - public ModCollection2 Duplicate( string name ) - => new(name, this); - - private void CleanUnavailableSettings() - { - var any = _unusedSettings.Count > 0; - _unusedSettings.Clear(); - if( any ) - { - Save(); - } - } - - public void AddMod( ModData mod ) - { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - _settings.Add( settings ); - _unusedSettings.Remove( mod.BasePath.Name ); - } - else - { - _settings.Add( null ); - } - } - - public void RemoveMod( ModData mod, int idx ) - { - var settings = _settings[ idx ]; - if( settings != null ) - { - _unusedSettings.Add( mod.BasePath.Name, settings ); - } - - _settings.RemoveAt( idx ); - } - - public static string CollectionDirectory - => Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ); - - private 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() - { - 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 ModCollection2? 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 ModCollection2( name, version, settings ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } -} - -// 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. -public class ModCollection -{ - public const string DefaultCollection = "Default"; - - public string Name { get; set; } - - public Dictionary< string, ModSettings > Settings { get; } - - public ModCollection() - { - Name = DefaultCollection; - Settings = new Dictionary< string, ModSettings >(); - } - - public ModCollection( string name, Dictionary< string, ModSettings > settings ) - { - Name = name; - Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); - } - - public Mod.Mod GetMod( ModData mod ) - { - if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) ) - { - return ret; - } - - if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - return new Mod.Mod( settings, mod ); - } - - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); - Save(); - return new Mod.Mod( newSettings, mod ); - } - - private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) - { - var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); - - foreach( var s in removeList ) - { - Settings.Remove( s.Key ); - } - - return removeList.Length > 0; - } - - public void CreateCache( IEnumerable< ModData > data ) - { - Cache = new ModCollectionCache( this ); - var changedSettings = false; - foreach( var mod in data ) - { - if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) - { - Cache.AddMod( settings, mod, false ); - } - else - { - changedSettings = true; - var newSettings = ModSettings.DefaultSettings( mod.Meta ); - Settings.Add( mod.BasePath.Name, newSettings ); - Cache.AddMod( newSettings, mod, false ); - } - } - - if( changedSettings ) - { - Save(); - } - - CalculateEffectiveFileList( true, false ); - } - - public void ClearCache() - => Cache = null; - - public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear ) - { - if( !Settings.TryGetValue( modPath.Name, out var settings ) ) - { - return; - } - - if( clear ) - { - settings.Settings.Clear(); - } - - if( settings.FixInvalidSettings( meta ) ) - { - Save(); - } - } - - public void UpdateSetting( ModData mod ) - => UpdateSetting( mod.BasePath, mod.Meta, false ); - - public void UpdateSettings( bool forceSave ) - { - if( Cache == null ) - { - return; - } - - var changes = false; - foreach( var mod in Cache.AvailableMods.Values ) - { - changes |= mod.FixSettings(); - } - - if( forceSave || changes ) - { - Save(); - } - } - - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) - { - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); - Cache ??= new ModCollectionCache( this ); - UpdateSettings( false ); - Cache.CalculateEffectiveFileList(); - if( withMetaManipulations ) - { - Cache.UpdateMetaManipulations(); - } - - if( reloadResident ) - { - Penumbra.ResidentResources.Reload(); - } - } - - - [JsonIgnore] - public ModCollectionCache? Cache { get; private set; } - - public static ModCollection? LoadFromFile( FileInfo file ) - { - if( !file.Exists ) - { - PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); - return null; - } - - try - { - var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) ); - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); - } - - return null; - } - - private void SaveToFile( FileInfo file ) - { - try - { - File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" ); - } - } - - public static DirectoryInfo CollectionDir() - => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" )); - - private static FileInfo FileName( DirectoryInfo collectionDir, string name ) - => new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" )); - - public FileInfo FileName() - => new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - $"{Name.RemoveInvalidPathSymbols()}.json" )); - - public void Save() - { - try - { - var dir = CollectionDir(); - dir.Create(); - var file = FileName( dir, Name ); - SaveToFile( file ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); - } - } - - public static ModCollection? Load( string name ) - { - var file = FileName( CollectionDir(), name ); - return file.Exists ? LoadFromFile( file ) : null; - } - - public void Delete() - { - var file = FileName( CollectionDir(), Name ); - if( file.Exists ) - { - try - { - file.Delete(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" ); - } - } - } - - public void AddMod( ModData data ) - { - if( Cache == null ) - { - return; - } - - Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings ) - ? settings - : ModSettings.DefaultSettings( data.Meta ), - data ); - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); - - - [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(); - } - } - - public static readonly ModCollection Empty = new() { Name = "" }; -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs deleted file mode 100644 index d7d56224..00000000 --- a/Penumbra/Mods/ModCollectionCache.cs +++ /dev/null @@ -1,662 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.Meta.Manager; -using Penumbra.Mod; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// The ModCollectionCache contains all required temporary data to use a collection. -// It will only be setup if a collection gets activated in any way. -public class ModCollectionCache2 -{ - // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); - private static readonly List< ModSettings? > ResolvedSettings = new(128); - - private readonly ModCollection2 _collection; - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly MetaManager MetaManipulations; - private ModCache2 _cache; - - public IReadOnlyDictionary< string, object? > ChangedItems - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - public ModCollectionCache2( ModCollection2 collection ) - => _collection = collection; - - //MetaManipulations = new MetaManager( collection ); - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - private void ClearStorageAndPrepare() - { - ResolvedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); - _changedItems.Clear(); - _cache.ClearFileConflicts(); - - ResolvedSettings.Clear(); - ResolvedSettings.AddRange( _collection.ActualSettings ); - } - - public void CalculateEffectiveFileList() - { - ClearStorageAndPrepare(); - - for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i ) - { - if( ResolvedSettings[ i ]?.Enabled == true ) - { - AddFiles( i ); - AddSwaps( i ); - } - } - - AddMetaFiles(); - } - - private void SetChangedItems() - { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) - { - return; - } - - try - { - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) - { - identifier.Identify( _changedItems, resolved.ToGamePath() ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Unknown Error:\n{e}" ); - } - } - - - 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. - foreach( var group in mod.Meta.Groups.Values.Reverse() ) - { - switch( group.SelectionType ) - { - case SelectType.Single: - AddFilesForSingle( group, mod, idx ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod, idx ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod, idx ); - } - - private static bool FilterFile( Utf8GamePath gamePath ) - { - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - if( !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) - { - return true; - } - - return false; - } - - - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) - { - RegisteredFiles.Add( gamePath, modIdx ); - ResolvedFiles[ gamePath ] = file; - } - else - { - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - _cache.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); - if( priority > oldPriority ) - { - ResolvedFiles[ gamePath ] = file; - RegisteredFiles[ gamePath ] = modIdx; - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - var fullPath = new FullPath( mod.BasePath, file ); - var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) - { - AddFile( modIdx, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( ModData mod, int modIdx ) - { - for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.BasePath, out var gamePath ) ) - { - AddFile( modIdx, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); - } - } - else - { - MissingFiles.Add( file ); - } - } - } - - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - private void AddSwaps( int modIdx ) - { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) - { - AddFile( modIdx, gamePath, swapPath ); - } - } - - // TODO Manipulations - public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath ); -} - -// The ModCollectionCache contains all required temporary data to use a collection. -// It will only be setup if a collection gets activated in any way. -public class ModCollectionCache -{ - // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256); - - public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); - - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly HashSet< FullPath > MissingFiles = new(); - public readonly MetaManager MetaManipulations; - - public IReadOnlyDictionary< string, object? > ChangedItems - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - public ModCollectionCache( ModCollection collection ) - => MetaManipulations = new MetaManager( collection ); - - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - public void CalculateEffectiveFileList() - { - ResolvedFiles.Clear(); - MissingFiles.Clear(); - RegisteredFiles.Clear(); - _changedItems.Clear(); - - foreach( var mod in AvailableMods.Values - .Where( m => m.Settings.Enabled ) - .OrderByDescending( m => m.Settings.Priority ) ) - { - mod.Cache.ClearFileConflicts(); - AddFiles( mod ); - AddSwaps( mod ); - } - - AddMetaFiles(); - } - - private void SetChangedItems() - { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) - { - return; - } - - try - { - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) - { - identifier.Identify( _changedItems, resolved.ToGamePath() ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Unknown Error:\n{e}" ); - } - } - - - private void AddFiles( Mod.Mod mod ) - { - ResetFileSeen( mod.Data.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - foreach( var group in mod.Data.Meta.Groups.Values.Reverse() ) - { - switch( group.SelectionType ) - { - case SelectType.Single: - AddFilesForSingle( group, mod ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod ); - } - - private static bool FilterFile( Utf8GamePath gamePath ) - { - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - if( !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) ) - { - return true; - } - - return false; - } - - - private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) - { - RegisteredFiles.Add( gamePath, mod ); - ResolvedFiles[ gamePath ] = file; - } - else - { - mod.Cache.AddConflict( oldMod, gamePath ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, gamePath ); - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - var fullPath = new FullPath( mod.Data.BasePath, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Data.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) - { - AddFile( mod, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - - if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - - if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) - { - return; - } - - // Also iterate options in reverse so that later options take precedence before earlier ones. - for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) - { - AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( Mod.Mod mod ) - { - for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Data.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) - { - AddFile( mod, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.Data.BasePath.FullName} to GamePath." ); - } - } - else - { - MissingFiles.Add( file ); - } - } - } - - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - private void AddSwaps( Mod.Mod mod ) - { - foreach( var (key, value) in mod.Data.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) - { - if( !RegisteredFiles.TryGetValue( key, out var oldMod ) ) - { - RegisteredFiles.Add( key, mod ); - ResolvedFiles.Add( key, value ); - } - else - { - mod.Cache.AddConflict( oldMod, key ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, key ); - } - } - } - } - - private void AddManipulations( Mod.Mod mod ) - { - foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) - { - if( !MetaManipulations.TryGetValue( manip, out var oldMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - else - { - mod.Cache.AddConflict( oldMod!, manip ); - if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod!.Settings.Priority ) - { - oldMod.Cache.AddConflict( mod, manip ); - } - } - } - } - - public void UpdateMetaManipulations() - { - MetaManipulations.Reset(); - - foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) - { - mod.Cache.ClearMetaConflicts(); - AddManipulations( mod ); - } - } - - public void RemoveMod( DirectoryInfo basePath ) - { - if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) ) - { - return; - } - - AvailableMods.Remove( basePath.Name ); - if( !mod.Settings.Enabled ) - { - return; - } - - CalculateEffectiveFileList(); - if( mod.Data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - - public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) - { - if( AvailableMods.ContainsKey( data.BasePath.Name ) ) - { - return; - } - - AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data ); - - if( !updateFileList || !settings.Enabled ) - { - return; - } - - CalculateEffectiveFileList(); - if( data.Resources.MetaManipulations.Count > 0 ) - { - UpdateMetaManipulations(); - } - } - - public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } - - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath ); -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index a23a9a32..b88a9bca 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -263,7 +263,7 @@ public class ModManager : IEnumerable< ModData > mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); } - Penumbra.CollectionManager.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); // TODO + // TODO: more specific mod changes? ModChange?.Invoke( ModChangeType.Changed, idx, mod ); return true; } @@ -271,10 +271,6 @@ public class ModManager : IEnumerable< ModData > public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); - public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath ) - { - var ret = Penumbra.CollectionManager.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - ret ??= Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); - return ret; - } + public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); } \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index 93c96c86..5083075a 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.IO; using Dalamud.Logging; using Penumbra.Mod; +using Penumbra.Util; namespace Penumbra.Mods; @@ -82,20 +83,13 @@ public static class ModManagerEditExtensions manager.Config.Save(); } - foreach( var collection in Penumbra.CollectionManager.Collections ) + var idx = manager.Mods.IndexOf( mod ); + foreach( var collection in Penumbra.CollectionManager ) { - if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) + if( collection.Settings[ idx ] != null ) { - collection.Settings[ newDir.Name ] = settings; - collection.Settings.Remove( oldBasePath.Name ); collection.Save(); } - - if( collection.Cache != null ) - { - collection.Cache.RemoveMod( newDir ); - collection.AddMod( mod ); - } } return true; @@ -140,9 +134,13 @@ public static class ModManagerEditExtensions mod.SaveMeta(); - foreach( var collection in Penumbra.CollectionManager.Collections ) + // TODO to indices + var idx = Penumbra.ModManager.Mods.IndexOf( mod ); + + foreach( var collection in Penumbra.CollectionManager ) { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + var settings = collection.Settings[ idx ]; + if( settings == null ) { continue; } @@ -176,9 +174,11 @@ public static class ModManagerEditExtensions return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); } - foreach( var collection in Penumbra.CollectionManager.Collections ) + var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO + foreach( var collection in Penumbra.CollectionManager ) { - if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + var settings = collection.Settings[ idx ]; + if( settings == null ) { continue; } @@ -199,10 +199,10 @@ public static class ModManagerEditExtensions { settings.Settings[ group.GroupName ] = newSetting; collection.Save(); - if( collection.Cache != null && settings.Enabled ) + if( collection.HasCache && settings.Enabled ) { collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - Penumbra.CollectionManager.IsActive( collection ) ); + Penumbra.CollectionManager.Default == collection ); } } } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index e2116f65..70535479 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -12,7 +13,7 @@ using Penumbra.Interop; using Penumbra.Mods; using Penumbra.UI; using Penumbra.Util; -using System.Linq; +using Penumbra.Collections; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; @@ -34,7 +35,7 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; + public static CollectionManager2 CollectionManager { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!; public ResourceLogger ResourceLogger { get; } @@ -67,7 +68,7 @@ public class Penumbra : IDalamudPlugin ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new ModManager(); ModManager.DiscoverMods(); - CollectionManager = new CollectionManager( ModManager ); + CollectionManager = new CollectionManager2( ModManager ); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -110,10 +111,15 @@ public class Penumbra : IDalamudPlugin ResourceLoader.EnableFullLogging(); } - if (CollectionManager.CharacterCollection.Count > 0) + if( CollectionManager.HasCharacterCollections ) + { PathResolver.Enable(); + } ResidentResources.Reload(); + //var c = ModCollection2.LoadFromFile( new FileInfo(@"C:\Users\Ozy\AppData\Roaming\XIVLauncher\pluginConfigs\Penumbra\collections\Rayla.json"), + // out var inheritance ); + //c?.Save(); } public bool Enable() @@ -217,10 +223,9 @@ public class Penumbra : IDalamudPlugin type = type.ToLowerInvariant(); collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) - ? ModCollection.Empty - : CollectionManager.Collections.FirstOrDefault( c - => string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) ); + var collection = string.Equals( collectionName, ModCollection2.Empty.Name, StringComparison.InvariantCultureIgnoreCase ) + ? ModCollection2.Empty + : CollectionManager[collectionName]; if( collection == null ) { Dalamud.Chat.Print( $"The collection {collection} does not exist." ); @@ -230,7 +235,7 @@ public class Penumbra : IDalamudPlugin switch( type ) { case "default": - if( collection == CollectionManager.DefaultCollection ) + if( collection == CollectionManager.Default ) { Dalamud.Chat.Print( $"{collection.Name} already is the default collection." ); return false; @@ -240,20 +245,9 @@ public class Penumbra : IDalamudPlugin Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); SettingsInterface.ResetDefaultCollection(); return true; - case "forced": - if( collection == CollectionManager.ForcedCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." ); - return false; - } - - CollectionManager.SetCollection( collection, CollectionType.Forced ); - Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." ); - SettingsInterface.ResetForcedCollection(); - return true; default: Dalamud.Chat.Print( - "Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} " ); + "Second command argument is not default, the correct command format is: /penumbra collection default " ); return false; } } diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 4bcb4224..2d94eaef 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -6,6 +6,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Logging; using ImGuiNET; +using Penumbra.Collections; using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; @@ -21,7 +22,7 @@ public partial class SettingsInterface private readonly Selector _selector; private string _collectionNames = null!; private string _collectionNamesWithNone = null!; - private ModCollection[] _collections = null!; + private ModCollection2[] _collections = null!; private int _currentCollectionIndex; private int _currentForcedIndex; private int _currentDefaultIndex; @@ -31,14 +32,14 @@ public partial class SettingsInterface private void UpdateNames() { - _collections = Penumbra.CollectionManager.Collections.Prepend( ModCollection.Empty ).ToArray(); + _collections = Penumbra.CollectionManager.Prepend( ModCollection2.Empty ).ToArray(); _collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0'; _collectionNamesWithNone = "None\0" + _collectionNames; UpdateIndices(); } - private int GetIndex( ModCollection collection ) + private int GetIndex( ModCollection2 collection ) { var ret = _collections.IndexOf( c => c.Name == collection.Name ); if( ret < 0 ) @@ -175,7 +176,7 @@ public partial class SettingsInterface } } - public void SetCurrentCollection( ModCollection collection, bool force = false ) + public void SetCurrentCollection( ModCollection2 collection, bool force = false ) { var idx = Array.IndexOf( _collections, collection ) - 1; if( idx >= 0 ) diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 672e5a8c..cbf17b9b 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Util; using Penumbra.Mods; @@ -99,9 +100,9 @@ public partial class SettingsInterface return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower ); } - private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced ) + private void DrawFilteredRows( ModCollection2 active ) { - void DrawFileLines( ModCollectionCache cache ) + void DrawFileLines( ModCollection2.Cache cache ) { foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) { @@ -116,15 +117,7 @@ public partial class SettingsInterface //} } - if( active != null ) - { - DrawFileLines( active ); - } - - if( forced != null ) - { - DrawFileLines( forced ); - } + DrawFileLines( active ); } public void Draw() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs index a87a8b22..fd0a0951 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -151,7 +151,7 @@ namespace Penumbra.UI { foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) { - var mod = Penumbra.CollectionManager.CurrentCollection.GetMod( modData ); + var mod = Penumbra.CollectionManager.Current.GetMod( modData ); _modsInOrder.Add( mod ); _visibleMods.Add( CheckFilters( mod ) ); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 3374a522..1ae71c8b 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -8,6 +8,7 @@ using System.Windows.Forms.VisualStyles; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; +using Penumbra.Collections; using Penumbra.Importer; using Penumbra.Mod; using Penumbra.Mods; @@ -606,10 +607,10 @@ public partial class SettingsInterface Cache = new ModListCache( Penumbra.ModManager, newMods ); } - private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) + private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection2 collection ) { - if( collection == ModCollection.Empty - || collection == Penumbra.CollectionManager.CurrentCollection ) + if( collection == ModCollection2.Empty + || collection == Penumbra.CollectionManager.Current ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); @@ -632,16 +633,13 @@ public partial class SettingsInterface var comboSize = size * ImGui.GetIO().FontGlobalScale; var offset = comboSize + textSize; - var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth() - - offset - - SelectorPanelWidth * _selectorScalingFactor - - 4 * ImGui.GetStyle().ItemSpacing.X ) - / 2, 5f ); + var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth() + - offset + - SelectorPanelWidth * _selectorScalingFactor + - 3 * ImGui.GetStyle().ItemSpacing.X, 5f ); ImGui.SameLine(); - DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.DefaultCollection ); + DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default ); - ImGui.SameLine(); - DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.CollectionManager.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize );