mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
283 lines
No EOL
11 KiB
C#
283 lines
No EOL
11 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 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( ModCollection? oldCollection, ModCollection? newCollection, Type type,
|
|
string? characterName = null );
|
|
|
|
private readonly Mods.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 Manager( Mods.Mod.Manager manager )
|
|
{
|
|
_modManager = manager;
|
|
|
|
// The collection manager reacts to changes in mods by itself.
|
|
_modManager.ModsRediscovered += OnModsRediscovered;
|
|
_modManager.ModChange += OnModChanged;
|
|
ReadCollections();
|
|
LoadCollections();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_modManager.ModsRediscovered -= OnModsRediscovered;
|
|
_modManager.ModChange -= OnModChanged;
|
|
}
|
|
|
|
// 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 )
|
|
{
|
|
var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
|
if( nameFixed.Length == 0
|
|
|| nameFixed == 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 ) ?? CreateNewEmpty( name );
|
|
newCollection.Index = _collections.Count;
|
|
_collections.Add( newCollection );
|
|
newCollection.Save();
|
|
CollectionChanged?.Invoke( null, newCollection, Type.Inactive );
|
|
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.
|
|
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 );
|
|
for( var i = idx; i < _collections.Count; ++i )
|
|
{
|
|
--_collections[ i ].Index;
|
|
}
|
|
|
|
CollectionChanged?.Invoke( collection, null, Type.Inactive );
|
|
return true;
|
|
}
|
|
|
|
public bool RemoveCollection( ModCollection collection )
|
|
=> RemoveCollection( collection.Index );
|
|
|
|
|
|
private void OnModsRediscovered()
|
|
{
|
|
// When mods are rediscovered, force all cache updates and set the files of the default collection.
|
|
ForceCacheUpdates();
|
|
Default.SetFiles();
|
|
}
|
|
|
|
|
|
// A changed mod forces changes for all collections, active and inactive.
|
|
private void OnModChanged( Mod.ChangeType type, int idx, Mod mod )
|
|
{
|
|
switch( type )
|
|
{
|
|
case Mod.ChangeType.Added:
|
|
foreach( var collection in this )
|
|
{
|
|
collection.AddMod( mod );
|
|
}
|
|
|
|
OnModAddedActive( mod.Resources.MetaManipulations.Count > 0 );
|
|
break;
|
|
case Mod.ChangeType.Removed:
|
|
var settings = new List< ModSettings? >( _collections.Count );
|
|
foreach( var collection in this )
|
|
{
|
|
settings.Add( collection[ idx ].Settings );
|
|
collection.RemoveMod( mod, idx );
|
|
}
|
|
|
|
OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings );
|
|
break;
|
|
case Mod.ChangeType.Changed:
|
|
foreach( var collection in this.Where(
|
|
collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) )
|
|
{
|
|
collection.Save();
|
|
}
|
|
|
|
OnModChangedActive( mod.Resources.MetaManipulations.Count > 0, mod.Index );
|
|
break;
|
|
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
// During this iteration, we can also fix all settings that are not valid for the given mod anymore.
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
ApplyInheritancesAndFixSettings( inheritances );
|
|
}
|
|
}
|
|
} |