Penumbra/Penumbra/Collections/CollectionManager.cs

378 lines
No EOL
15 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using OtterGui.Filesystem;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
public partial class ModCollection
{
public enum Type : byte
{
Inactive, // A collection was added or removed
Default, // The default collection was changed
Character, // A character collection was changed
Current, // The current collection was changed.
}
public sealed partial class Manager : IDisposable, IEnumerable< ModCollection >
{
// On addition, oldCollection is null. On deletion, newCollection is null.
// CharacterName is onls set for type == Character.
public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection,
string? characterName = null );
private readonly Mod.Manager _modManager;
// The empty collection is always available and always has index 0.
// It can not be deleted or moved.
private readonly List< ModCollection > _collections = new()
{
Empty,
};
public ModCollection this[ Index idx ]
=> _collections[ idx ];
public ModCollection? this[ string name ]
=> ByName( name, out var c ) ? c : null;
public int Count
=> _collections.Count;
// Obtain a collection case-independently by name.
public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection )
=> _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection );
// Default enumeration skips the empty collection.
public IEnumerator< ModCollection > GetEnumerator()
=> _collections.Skip( 1 ).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public IEnumerable< ModCollection > GetEnumeratorWithEmpty()
=> _collections;
public Manager( Mod.Manager manager )
{
_modManager = manager;
// The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChanged;
CollectionChanged += SaveOnChange;
ReadCollections();
LoadCollections();
}
public void Dispose()
{
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChanged;
}
// Returns true if the name is not empty, it is not the name of the empty collection
// and no existing collection results in the same filename as name.
public bool CanAddCollection( string name, out string fixedName )
{
if( name.Length == 0 )
{
fixedName = string.Empty;
return false;
}
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if( name.Length == 0
|| name == Empty.Name.ToLowerInvariant()
|| _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) )
{
fixedName = string.Empty;
return false;
}
fixedName = name;
return true;
}
// Add a new collection of the given name.
// If duplicate is not-null, the new collection will be a duplicate of it.
// If the name of the collection would result in an already existing filename, skip it.
// Returns true if the collection was successfully created and fires a Inactive event.
// Also sets the current collection to the new collection afterwards.
public bool AddCollection( string name, ModCollection? duplicate )
{
if( !CanAddCollection( name, out var fixedName ) )
{
PluginLog.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." );
return false;
}
var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name );
newCollection.Index = _collections.Count;
_collections.Add( newCollection );
newCollection.Save();
CollectionChanged.Invoke( Type.Inactive, null, newCollection );
SetCollection( newCollection.Index, Type.Current );
return true;
}
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
// Also removes the collection from inheritances of all other collections.
public bool RemoveCollection( int idx )
{
if( idx <= Empty.Index || idx >= _collections.Count )
{
PluginLog.Error( "Can not remove the empty collection." );
return false;
}
if( idx == DefaultName.Index )
{
PluginLog.Error( "Can not remove the default collection." );
return false;
}
if( idx == Current.Index )
{
SetCollection( DefaultName, Type.Current );
}
if( idx == Default.Index )
{
SetCollection( Empty, Type.Default );
}
foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() )
{
SetCollection( Empty, Type.Character, characterName );
}
var collection = _collections[ idx ];
collection.Delete();
_collections.RemoveAt( idx );
foreach( var c in _collections )
{
var inheritedIdx = c._inheritance.IndexOf( collection );
if( inheritedIdx >= 0 )
{
c.RemoveInheritance( inheritedIdx );
}
if( c.Index > idx )
{
--c.Index;
}
}
CollectionChanged.Invoke( Type.Inactive, collection, null );
return true;
}
public bool RemoveCollection( ModCollection collection )
=> RemoveCollection( collection.Index );
private void OnModDiscoveryStarted()
{
foreach( var collection in this )
{
collection.PrepareModDiscovery();
}
}
private void OnModDiscoveryFinished()
{
// First, re-apply all mod settings.
foreach( var collection in this )
{
collection.ApplyModSettings();
}
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach( var collection in this )
{
collection.ForceCacheUpdate( collection == Default );
}
}
// A changed mod path forces changes for all collections, active and inactive.
private void OnModPathChanged( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory )
{
switch( type )
{
case ModPathChangeType.Added:
foreach( var collection in this )
{
collection.AddMod( mod );
}
OnModAddedActive( mod.TotalManipulations > 0 );
break;
case ModPathChangeType.Deleted:
var settings = new List< ModSettings? >( _collections.Count );
foreach( var collection in this )
{
settings.Add( collection._settings[ mod.Index ] );
collection.RemoveMod( mod, mod.Index );
}
OnModRemovedActive( mod.TotalManipulations > 0, settings );
break;
case ModPathChangeType.Moved:
foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) )
{
collection.Save();
}
OnModChangedActive( mod.TotalManipulations > 0, mod.Index );
break;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
}
}
// Automatically update all relevant collections when a mod is changed.
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
// And also updating effective file and meta manipulation lists if necessary.
private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
{
var (handleChanges, recomputeList, withMeta) = type switch
{
ModOptionChangeType.GroupRenamed => ( true, false, false ),
ModOptionChangeType.GroupAdded => ( true, false, false ),
ModOptionChangeType.GroupDeleted => ( true, true, true ),
ModOptionChangeType.GroupMoved => ( true, false, false ),
ModOptionChangeType.GroupTypeChanged => ( true, true, true ),
ModOptionChangeType.PriorityChanged => ( true, true, true ),
ModOptionChangeType.OptionAdded => ( true, true, true ),
ModOptionChangeType.OptionDeleted => ( true, true, true ),
ModOptionChangeType.OptionMoved => ( true, false, false ),
ModOptionChangeType.OptionFilesChanged => ( false, true, false ),
ModOptionChangeType.OptionSwapsChanged => ( false, true, false ),
ModOptionChangeType.OptionMetaChanged => ( false, true, true ),
ModOptionChangeType.OptionUpdated => ( false, true, true ),
ModOptionChangeType.DisplayChange => ( false, false, false ),
_ => ( false, false, false ),
};
if( handleChanges )
{
foreach( var collection in this )
{
if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false )
{
collection.Save();
}
}
}
if( recomputeList )
{
foreach( var collection in this.Where( c => c.HasCache ) )
{
if( collection[ mod.Index ].Settings is { Enabled: true } )
{
collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default );
}
}
}
}
// Add the collection with the default name if it does not exist.
// It should always be ensured that it exists, otherwise it will be created.
// This can also not be deleted, so there are always at least the empty and a collection with default name.
private void AddDefaultCollection()
{
var idx = GetIndexForCollectionName( DefaultCollection );
if( idx >= 0 )
{
DefaultName = this[ idx ];
return;
}
var defaultCollection = CreateNewEmpty( DefaultCollection );
defaultCollection.Save();
defaultCollection.Index = _collections.Count;
_collections.Add( defaultCollection );
}
// Inheritances can not be setup before all collections are read,
// so this happens after reading the collections.
private void ApplyInheritances( 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." );
}
}
if( changes )
{
collection.Save();
}
}
}
// Read all collection files in the Collection Directory.
// Ensure that the default named collection exists, and apply inheritances afterwards.
// Duplicate collection files are not deleted, just not added here.
private void ReadCollections()
{
var collectionDir = new DirectoryInfo( CollectionDirectory );
var inheritances = new List< IReadOnlyList< string > >();
if( collectionDir.Exists )
{
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
{
var collection = 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 );
collection.Index = _collections.Count;
_collections.Add( collection );
}
}
}
AddDefaultCollection();
ApplyInheritances( inheritances );
}
}
}