From bc47e08e08d355434b195b5224e7577fad69c7f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Mar 2022 15:01:24 +0100 Subject: [PATCH] Collection inheritance start. --- Penumbra/Mod/ModCache.cs | 87 +++++- Penumbra/Mods/CollectionManager.cs | 117 +++++++++ Penumbra/Mods/ModCollection.Changes.cs | 113 ++++++++ Penumbra/Mods/ModCollection.Inheritance.cs | 72 +++++ Penumbra/Mods/ModCollection.Migration.cs | 47 ++++ Penumbra/Mods/ModCollection.cs | 188 +++++++++++++ Penumbra/Mods/ModCollectionCache.cs | 291 +++++++++++++++++++++ 7 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Mods/ModCollection.Changes.cs create mode 100644 Penumbra/Mods/ModCollection.Inheritance.cs create mode 100644 Penumbra/Mods/ModCollection.Migration.cs diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs index 3c413428..6fc6486c 100644 --- a/Penumbra/Mod/ModCache.cs +++ b/Penumbra/Mod/ModCache.cs @@ -1,11 +1,96 @@ +using System; using System.Collections.Generic; using System.Linq; using Penumbra.GameData.ByteString; -using Penumbra.Meta; using Penumbra.Meta.Manipulations; namespace Penumbra.Mod; +public struct ModCache2 +{ + public readonly struct ModCacheStruct : IComparable< ModCacheStruct > + { + public readonly object Conflict; + public readonly int Mod1; + public readonly int Mod2; + public readonly bool Mod1Priority; + public readonly bool Solved; + + public ModCacheStruct( int modIdx1, int modIdx2, int priority1, int priority2, object conflict ) + { + Mod1 = modIdx1; + Mod2 = modIdx2; + Conflict = conflict; + Mod1Priority = priority1 >= priority2; + Solved = priority1 != priority2; + } + + public int CompareTo( ModCacheStruct other ) + { + var idxComp = Mod1.CompareTo( other.Mod1 ); + if( idxComp != 0 ) + { + return idxComp; + } + + if( Mod1Priority != other.Mod1Priority ) + { + return Mod1Priority ? 1 : -1; + } + + idxComp = Mod2.CompareTo( other.Mod2 ); + if( idxComp != 0 ) + { + return idxComp; + } + + return Conflict switch + { + Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ), + Utf8GamePath => -1, + MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ), + MetaManipulation => 1, + _ => 0, + }; + } + } + + private List< ModCacheStruct >? _conflicts; + + public IReadOnlyList< ModCacheStruct > Conflicts + => _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); + + public void Sort() + => _conflicts?.Sort(); + + public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath ) + { + _conflicts ??= new List< ModCacheStruct >( 2 ); + + _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) ); + _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) ); + } + + public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) + { + _conflicts ??= new List< ModCacheStruct >( 2 ); + _conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) ); + _conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) ); + } + + public void ClearConflicts() + => _conflicts?.Clear(); + + public void ClearFileConflicts() + => _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath ); + + public void ClearMetaConflicts() + => _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation ); + + public void ClearConflictsWithMod( int modIdx ) + => _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx ); +} + // The ModCache contains volatile information dependent on all current settings in a collection. public class ModCache { diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index b99d9657..a00b01be 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -9,6 +10,122 @@ 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, diff --git a/Penumbra/Mods/ModCollection.Changes.cs b/Penumbra/Mods/ModCollection.Changes.cs new file mode 100644 index 00000000..0423ffe8 --- /dev/null +++ b/Penumbra/Mods/ModCollection.Changes.cs @@ -0,0 +1,113 @@ +using System; +using Penumbra.Mod; + +namespace Penumbra.Mods; + +public enum ModSettingChange +{ + Inheritance, + EnableState, + Priority, + Setting, +} + +public partial class ModCollection2 +{ + public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName ); + public event ModSettingChangeDelegate ModSettingChanged; + + // Enable or disable the mod inheritance of mod idx. + public void SetModInheritance( int idx, bool inherit ) + { + if( FixInheritance( idx, inherit ) ) + { + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null ); + } + } + + // Set the enabled state mod idx to newValue if it differs from the current priority. + // If mod idx is currently inherited, stop the inheritance. + public void SetModState( int idx, bool newValue ) + { + var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; + if( newValue != oldValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Enabled = newValue; + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null ); + } + } + + // Set the priority of mod idx to newValue if it differs from the current priority. + // If mod idx is currently inherited, stop the inheritance. + public void SetModPriority( int idx, int newValue ) + { + var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; + if( newValue != oldValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Priority = newValue; + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null ); + } + } + + // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. + // If mod idx is currently inherited, stop the inheritance. + public void SetModSetting( int idx, string settingName, int newValue ) + { + var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; + var oldValue = settings != null + ? settings.TryGetValue( settingName, out var v ) ? v : newValue + : Penumbra.ModManager.Mods[ idx ].Meta.Groups.ContainsKey( settingName ) + ? 0 + : newValue; + if( oldValue != newValue ) + { + var inheritance = FixInheritance( idx, true ); + _settings[ idx ]!.Settings[ settingName ] = newValue; + _settings[ idx ]!.FixSpecificSetting( settingName, Penumbra.ModManager.Mods[ idx ].Meta ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName ); + } + } + + // Change one of the available mod settings for mod idx discerned by type. + // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. + // The setting will also be automatically fixed if it is invalid for that setting group. + // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. + public void ChangeModSetting( ModSettingChange type, int idx, int newValue, string? settingName = null ) + { + switch( type ) + { + case ModSettingChange.Inheritance: + SetModInheritance( idx, newValue != 0 ); + break; + case ModSettingChange.EnableState: + SetModState( idx, newValue != 0 ); + break; + case ModSettingChange.Priority: + SetModPriority( idx, newValue ); + break; + case ModSettingChange.Setting: + SetModSetting( idx, settingName ?? string.Empty, newValue ); + break; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + + // Set inheritance of a mod without saving, + // to be used as an intermediary. + private bool FixInheritance( int idx, bool inherit ) + { + var settings = _settings[ idx ]; + if( inherit != ( settings == null ) ) + { + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + return true; + } + + return false; + } + + private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4 ) + => Save(); +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.Inheritance.cs b/Penumbra/Mods/ModCollection.Inheritance.cs new file mode 100644 index 00000000..13ba09ca --- /dev/null +++ b/Penumbra/Mods/ModCollection.Inheritance.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class ModCollection2 +{ + private readonly List< ModCollection2 > _inheritance = new(); + + public event Action InheritanceChanged; + + public IReadOnlyList< ModCollection2 > Inheritance + => _inheritance; + + public IEnumerable< ModCollection2 > GetFlattenedInheritance() + { + yield return this; + + foreach( var collection in _inheritance.SelectMany( c => c._inheritance ) + .Where( c => !ReferenceEquals( this, c ) ) + .Distinct() ) + { + yield return collection; + } + } + + public bool AddInheritance( ModCollection2 collection ) + { + if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) + { + return false; + } + + _inheritance.Add( collection ); + InheritanceChanged.Invoke(); + return true; + } + + public void RemoveInheritance( int idx ) + { + _inheritance.RemoveAt( idx ); + InheritanceChanged.Invoke(); + } + + public void MoveInheritance( int from, int to ) + { + if( _inheritance.Move( from, to ) ) + { + InheritanceChanged.Invoke(); + } + } + + public (ModSettings? Settings, ModCollection2 Collection) this[ int idx ] + { + get + { + foreach( var collection in GetFlattenedInheritance() ) + { + var settings = _settings[ idx ]; + if( settings != null ) + { + return ( settings, collection ); + } + } + + return ( null, this ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.Migration.cs b/Penumbra/Mods/ModCollection.Migration.cs new file mode 100644 index 00000000..76b40ae7 --- /dev/null +++ b/Penumbra/Mods/ModCollection.Migration.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Penumbra.Mod; + +namespace Penumbra.Mods; + +public partial class ModCollection2 +{ + private static class Migration + { + public static void Migrate( ModCollection2 collection ) + { + var changes = MigrateV0ToV1( collection ); + if( changes ) + { + collection.Save(); + } + } + + private static bool MigrateV0ToV1( ModCollection2 collection ) + { + if( collection.Version > 0 ) + { + return false; + } + + collection.Version = 1; + for( var i = 0; i < collection._settings.Count; ++i ) + { + var setting = collection._settings[ i ]; + if( SettingIsDefaultV0( collection._settings[ i ] ) ) + { + collection._settings[ i ] = null; + } + } + + foreach( var (key, _) in collection._unusedSettings.Where( kvp => SettingIsDefaultV0( kvp.Value ) ).ToList() ) + { + collection._unusedSettings.Remove( key ); + } + + return true; + } + + private static bool SettingIsDefaultV0( ModSettings? setting ) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index 8b285c36..9a00250b 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -4,7 +4,9 @@ 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; @@ -12,6 +14,192 @@ 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. diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 6af67ad3..d7d56224 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -13,6 +13,297 @@ 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