mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
378 lines
No EOL
15 KiB
C#
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 );
|
|
}
|
|
}
|
|
} |