This commit is contained in:
Ottermandias 2022-03-26 18:34:32 +01:00
parent bc47e08e08
commit 9a0b0bfa0f
35 changed files with 1365 additions and 1997 deletions

View file

@ -16,14 +16,14 @@ public class ModsController : WebApiController
[Route( HttpVerbs.Get, "/mods" )]
public object? GetMods()
{
return Penumbra.CollectionManager.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new
return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).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 ),
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 ),
} );
}
@ -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
)

View file

@ -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." );

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -1,7 +1,7 @@
using System;
using Penumbra.Mod;
namespace Penumbra.Mods;
namespace Penumbra.Collections;
public enum ModSettingChange
{

View file

@ -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
{

View file

@ -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
{

View file

@ -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;
}
}

View file

@ -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 );
}

View file

@ -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 )
{

View file

@ -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 )

View file

@ -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;
}
}

View file

@ -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 )
{

View file

@ -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();
}

View file

@ -13,7 +13,7 @@ public partial class MetaManager
public struct MetaManagerCmp : IDisposable
{
public CmpFile? File = null;
public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new();
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

View file

@ -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 );

View file

@ -13,7 +13,7 @@ public partial class MetaManager
public struct MetaManagerEqp : IDisposable
{
public ExpandedEqpFile? File = null;
public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new();
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

View file

@ -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 ),

View file

@ -13,7 +13,7 @@ public partial class MetaManager
public struct MetaManagerGmp : IDisposable
{
public ExpandedGmpFile? File = null;
public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new();
public readonly Dictionary< GmpManipulation, int > Manipulations = new();
public MetaManagerGmp()
{ }
@ -37,14 +37,10 @@ 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;
}
Manipulations[ m ] = modIdx;
File ??= new ExpandedGmpFile();
return m.Apply( File );
#else

View file

@ -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;
@ -19,13 +17,13 @@ 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< ImcManipulation, int > Manipulations = new();
private readonly ModCollection _collection;
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()
{
if( _collection.HasCache )
{
foreach( var (path, file) in Files )
{
_collection.Cache?.ResolvedFiles.Remove( path );
_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;

View file

@ -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,
};

View file

@ -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 )

View file

@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Mod;
public struct ModCache2
public struct ConflictCache
{
public readonly struct ModCacheStruct : IComparable< ModCacheStruct >
{

View file

@ -9,6 +9,7 @@ using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Mod;
@ -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 );
}
}
}

View file

@ -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 );
}
}
}

View file

@ -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 = "" };
}

View file

@ -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 );
}

View file

@ -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 );
}

View file

@ -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 );
}
}
}

View file

@ -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} <collectionName>" );
"Second command argument is not default, the correct command format is: /penumbra collection default <collectionName>" );
return false;
}
}

View file

@ -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 )

View file

@ -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,17 +117,9 @@ public partial class SettingsInterface
//}
}
if( active != null )
{
DrawFileLines( active );
}
if( forced != null )
{
DrawFileLines( forced );
}
}
public void Draw()
{
if( !ImGui.BeginTabItem( LabelTab ) )

View file

@ -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 ) );
}

View file

@ -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()
var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth()
- offset
- SelectorPanelWidth * _selectorScalingFactor
- 4 * ImGui.GetStyle().ItemSpacing.X )
/ 2, 5f );
- 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 );