Complete mod collection cleanup, initial stuff for inheritance. Some further cleanup.

This commit is contained in:
Ottermandias 2022-03-28 17:25:59 +02:00
parent 7915d516e2
commit 1861c40a4f
48 changed files with 1151 additions and 898 deletions

View file

@ -8,8 +8,6 @@ using Lumina.Data;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Api;
@ -78,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_penumbra!.ObjectReloader.RedrawAll( setting );
}
private static string ResolvePath( string path, Mod.Mod.Manager _, ModCollection collection )
private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection )
{
if( !Penumbra.Config.EnableMods )
{

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Logging;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
@ -14,122 +13,39 @@ public partial class ModCollection
// Is invoked after the collections actually changed.
public event CollectionChangeDelegate? CollectionChanged;
private int _currentIdx = 1;
private int _defaultIdx = 0;
private int _defaultNameIdx = 0;
// The collection currently selected for changing settings.
public ModCollection Current { get; private set; } = Empty;
public ModCollection Current
=> this[ _currentIdx ];
// The collection used for general file redirections and all characters not specifically named.
public ModCollection Default { get; private set; } = Empty;
public ModCollection Default
=> this[ _defaultIdx ];
// A single collection that can not be deleted as a fallback for the current collection.
public ModCollection DefaultName { get; private set; } = Empty;
private readonly Dictionary< string, int > _character = new();
// The list of character collections.
private readonly Dictionary< string, ModCollection > _characters = new();
public IReadOnlyDictionary< string, ModCollection > Characters
=> _characters;
// If a name does not correspond to a character, return the default collection instead.
public ModCollection Character( string name )
=> _character.TryGetValue( name, out var idx ) ? this[ idx ] : Default;
public IEnumerable< (string, ModCollection) > Characters
=> _character.Select( kvp => ( kvp.Key, this[ kvp.Value ] ) );
=> _characters.TryGetValue( name, out var c ) ? c : Default;
public bool HasCharacterCollections
=> _character.Count > 0;
private void OnModChanged( Mod.Mod.ChangeType type, int idx, Mod.Mod mod )
{
var meta = mod.Resources.MetaManipulations.Count > 0;
switch( type )
{
case Mod.Mod.ChangeType.Added:
foreach( var collection in this )
{
collection.AddMod( mod );
}
foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
break;
case Mod.Mod.ChangeType.Removed:
var list = new List< ModSettings? >( _collections.Count );
foreach( var collection in this )
{
list.Add( collection[ idx ].Settings );
collection.RemoveMod( mod, idx );
}
foreach( var (collection, _) in this.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
break;
case Mod.Mod.ChangeType.Changed:
foreach( var collection in this.Where(
collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) )
{
collection.Save();
}
foreach( var collection in this.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
break;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
}
}
private void CreateNecessaryCaches()
{
if( _defaultIdx > Empty.Index )
{
Default.CreateCache(true);
}
if( _currentIdx > Empty.Index )
{
Current.CreateCache(false);
}
foreach( var idx in _character.Values.Where( i => i > Empty.Index ) )
{
_collections[ idx ].CreateCache(false);
}
}
public void ForceCacheUpdates()
{
foreach( var collection in this )
{
collection.ForceCacheUpdate(collection == Default);
}
}
private void RemoveCache( int idx )
{
if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) )
{
_collections[ idx ].ClearCache();
}
}
public void SetCollection( ModCollection collection, Type type, string? characterName = null )
=> SetCollection( collection.Index, type, characterName );
=> _characters.Count > 0;
// Set a active collection, can be used to set Default, Current or Character collections.
public void SetCollection( int newIdx, Type type, string? characterName = null )
{
var oldCollectionIdx = type switch
{
Type.Default => _defaultIdx,
Type.Current => _currentIdx,
Type.Default => Default.Index,
Type.Current => Current.Index,
Type.Character => characterName?.Length > 0
? _character.TryGetValue( characterName, out var c )
? c
: _defaultIdx
? _characters.TryGetValue( characterName, out var c )
? c.Index
: Default.Index
: -1,
_ => -1,
};
@ -142,24 +58,24 @@ public partial class ModCollection
var newCollection = this[ newIdx ];
if( newIdx > Empty.Index )
{
newCollection.CreateCache(false);
newCollection.CreateCache( false );
}
RemoveCache( oldCollectionIdx );
switch( type )
{
case Type.Default:
_defaultIdx = newIdx;
Default = newCollection;
Penumbra.Config.DefaultCollection = newCollection.Name;
Penumbra.ResidentResources.Reload();
Default.SetFiles();
break;
case Type.Current:
_currentIdx = newIdx;
Current = newCollection;
Penumbra.Config.CurrentCollection = newCollection.Name;
break;
case Type.Character:
_character[ characterName! ] = newIdx;
_characters[ characterName! ] = newCollection;
Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name;
break;
}
@ -168,27 +84,32 @@ public partial class ModCollection
Penumbra.Config.Save();
}
public void SetCollection( ModCollection collection, Type type, string? characterName = null )
=> SetCollection( collection.Index, type, characterName );
// Create a new character collection. Returns false if the character name already has a collection.
public bool CreateCharacterCollection( string characterName )
{
if( _character.ContainsKey( characterName ) )
if( _characters.ContainsKey( characterName ) )
{
return false;
}
_character[ characterName ] = Empty.Index;
_characters[ characterName ] = Empty;
Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name;
Penumbra.Config.Save();
CollectionChanged?.Invoke( null, Empty, Type.Character, characterName );
return true;
}
// Remove a character collection if it exists.
public void RemoveCharacterCollection( string characterName )
{
if( _character.TryGetValue( characterName, out var collection ) )
if( _characters.TryGetValue( characterName, out var collection ) )
{
RemoveCache( collection );
_character.Remove( characterName );
CollectionChanged?.Invoke( this[ collection ], null, Type.Character, characterName );
RemoveCache( collection.Index );
_characters.Remove( characterName );
CollectionChanged?.Invoke( collection, null, Type.Character, characterName );
}
if( Penumbra.Config.CharacterCollections.Remove( characterName ) )
@ -197,36 +118,41 @@ public partial class ModCollection
}
}
// Obtain the index of a collection by name.
private int GetIndexForCollectionName( string name )
{
if( name.Length == 0 )
{
return Empty.Index;
}
=> name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name );
return _collections.IndexOf( c => c.Name == name );
}
// Load default, current and character collections from config.
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
public void LoadCollections()
{
var configChanged = false;
_defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection );
if( _defaultIdx < 0 )
var defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection );
if( defaultIdx < 0 )
{
PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." );
_defaultIdx = Empty.Index;
Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name;
Default = Empty;
Penumbra.Config.DefaultCollection = Default.Name;
configChanged = true;
}
else
{
Default = this[ defaultIdx ];
}
_currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection );
if( _currentIdx < 0 )
var currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection );
if( currentIdx < 0 )
{
PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." );
_currentIdx = _defaultNameIdx;
Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name;
Current = DefaultName;
Penumbra.Config.DefaultCollection = Current.Name;
configChanged = true;
}
else
{
Current = this[ currentIdx ];
}
if( LoadCharacterCollections() || configChanged )
{
@ -236,6 +162,7 @@ public partial class ModCollection
CreateNecessaryCaches();
}
// Load character collections. If a player name comes up multiple times, the last one is applied.
private bool LoadCharacterCollections()
{
var configChanged = false;
@ -245,17 +172,71 @@ public partial class ModCollection
if( idx < 0 )
{
PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." );
_character.Add( player, Empty.Index );
_characters.Add( player, Empty );
Penumbra.Config.CharacterCollections[ player ] = Empty.Name;
configChanged = true;
}
else
{
_character.Add( player, idx );
_characters.Add( player, this[ idx ] );
}
}
return configChanged;
}
// Cache handling.
private void CreateNecessaryCaches()
{
Default.CreateCache( true );
Current.CreateCache( false );
foreach( var collection in _characters.Values )
{
collection.CreateCache( false );
}
}
private void RemoveCache( int idx )
{
if( idx != Default.Index && idx != Current.Index && _characters.Values.All( c => c.Index != idx ) )
{
_collections[ idx ].ClearCache();
}
}
private void ForceCacheUpdates()
{
foreach( var collection in this )
{
collection.ForceCacheUpdate( collection == Default );
}
}
// Recalculate effective files for active collections on events.
private void OnModAddedActive( bool meta )
{
foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
}
private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings )
{
foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
}
private void OnModChangedActive( bool meta, int modIdx )
{
foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
}
}
}
}

View file

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
@ -13,19 +14,23 @@ public partial class ModCollection
{
public enum Type : byte
{
Inactive,
Default,
Character,
Current,
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 Mod.Mod.Manager _modManager;
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,
@ -34,25 +39,28 @@ public partial class ModCollection
public ModCollection this[ Index idx ]
=> _collections[ idx ];
public ModCollection this[ int 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( Mod.Mod.Manager manager )
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();
@ -65,27 +73,143 @@ public partial class ModCollection
_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 = _collections.IndexOf( c => c.Name == DefaultCollection );
var idx = GetIndexForCollectionName( DefaultCollection );
if( idx >= 0 )
{
_defaultNameIdx = idx;
DefaultName = this[ idx ];
return;
}
var defaultCollection = CreateNewEmpty( DefaultCollection );
defaultCollection.Save();
_defaultNameIdx = _collections.Count;
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 ) )
@ -117,6 +241,9 @@ public partial class ModCollection
}
}
// 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 );
@ -152,89 +279,5 @@ public partial class ModCollection
AddDefaultCollection();
ApplyInheritancesAndFixSettings( inheritances );
}
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;
}
public bool RemoveCollection( ModCollection collection )
=> RemoveCollection( collection.Index );
public bool RemoveCollection( int idx )
{
if( idx <= Empty.Index || 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, Type.Current );
}
else if( _currentIdx > idx )
{
--_currentIdx;
}
if( idx == _defaultIdx )
{
SetCollection( -1, Type.Default );
}
else if( _defaultIdx > idx )
{
--_defaultIdx;
}
if( _defaultNameIdx > idx )
{
--_defaultNameIdx;
}
foreach( var (characterName, characterIdx) in _character.ToList() )
{
if( idx == characterIdx )
{
SetCollection( -1, Type.Character, characterName );
}
else if( characterIdx > idx )
{
_character[ characterName ] = characterIdx - 1;
}
}
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;
}
}
}

View file

@ -8,24 +8,26 @@ namespace Penumbra.Collections;
public struct ConflictCache
{
public readonly struct ModCacheStruct : IComparable< ModCacheStruct >
// A conflict stores all data about a mod conflict.
public readonly struct Conflict : IComparable< Conflict >
{
public readonly object Conflict;
public readonly object Data;
public readonly int Mod1;
public readonly int Mod2;
public readonly bool Mod1Priority;
public readonly bool Solved;
public ModCacheStruct( int modIdx1, int modIdx2, int priority1, int priority2, object conflict )
public Conflict( int modIdx1, int modIdx2, bool priority, bool solved, object data )
{
Mod1 = modIdx1;
Mod2 = modIdx2;
Conflict = conflict;
Mod1Priority = priority1 >= priority2;
Solved = priority1 != priority2;
Data = data;
Mod1Priority = priority;
Solved = solved;
}
public int CompareTo( ModCacheStruct other )
// Order: Mod1 -> Mod1 overwritten -> Mod2 -> File > MetaManipulation
public int CompareTo( Conflict other )
{
var idxComp = Mod1.CompareTo( other.Mod1 );
if( idxComp != 0 )
@ -44,55 +46,85 @@ public struct ConflictCache
return idxComp;
}
return Conflict switch
return Data switch
{
Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ),
Utf8GamePath => -1,
MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ),
MetaManipulation => 1,
_ => 0,
Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ),
Utf8GamePath => -1,
MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ),
MetaManipulation => 1,
_ => 0,
};
}
}
private List< ModCacheStruct >? _conflicts;
private readonly List< Conflict > _conflicts = new();
private bool _isSorted = true;
public IReadOnlyList< ModCacheStruct > Conflicts
=> _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >();
public ConflictCache()
{ }
public IEnumerable< ModCacheStruct > ModConflicts( int modIdx )
public IReadOnlyList< Conflict > Conflicts
{
return _conflicts?.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx )
?? Array.Empty< ModCacheStruct >();
get
{
Sort();
return _conflicts;
}
}
public void Sort()
=> _conflicts?.Sort();
// Find all mod conflicts concerning the specified mod (in both directions).
public IEnumerable< Conflict > ModConflicts( int modIdx )
{
return _conflicts.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx );
}
private void Sort()
{
if( !_isSorted )
{
_conflicts?.Sort();
}
}
// Add both directions for the mod.
// On same priority, it is assumed that mod1 is the earlier one.
// Also update older conflicts to refer to the highest-prioritized conflict.
private void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, object data )
{
var solved = priority1 != priority2;
var priority = priority1 >= priority2;
var prioritizedMod = priority ? modIdx1 : modIdx2;
_conflicts.Add( new Conflict( modIdx1, modIdx2, priority, solved, data ) );
_conflicts.Add( new Conflict( modIdx2, modIdx1, !priority, solved, data ) );
for( var i = 0; i < _conflicts.Count; ++i )
{
var c = _conflicts[ i ];
if( data.Equals( c.Data ) )
{
_conflicts[ i ] = c.Mod1Priority
? new Conflict( prioritizedMod, c.Mod2, true, c.Solved || solved, data )
: new Conflict( c.Mod1, prioritizedMod, false, c.Solved || solved, data );
}
}
_isSorted = false;
}
public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath )
{
_conflicts ??= new List< ModCacheStruct >( 2 );
_conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) );
_conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) );
}
=> AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath );
public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation )
{
_conflicts ??= new List< ModCacheStruct >( 2 );
_conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) );
_conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) );
}
=> AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation );
public void ClearConflicts()
=> _conflicts?.Clear();
public void ClearFileConflicts()
=> _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath );
=> _conflicts?.RemoveAll( m => m.Data is Utf8GamePath );
public void ClearMetaConflicts()
=> _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation );
=> _conflicts?.RemoveAll( m => m.Data is MetaManipulation );
public void ClearConflictsWithMod( int modIdx )
=> _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == ~modIdx );
=> _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == modIdx );
}

View file

@ -7,49 +7,53 @@ using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manager;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
public partial class ModCollection
{
// Only active collections need to have a cache.
private Cache? _cache;
public bool HasCache
=> _cache != null;
// Only create, do not update.
public void CreateCache( bool isDefault )
{
if( Index == 0 )
{
return;
}
if( _cache == null )
{
CalculateEffectiveFileList( true, isDefault );
}
}
// Force an update with metadata for this cache.
public void ForceCacheUpdate( bool isDefault )
=> CalculateEffectiveFileList( true, isDefault );
// Clear the current cache.
public void ClearCache()
{
_cache?.Dispose();
_cache = null;
}
public FullPath? ResolvePath( Utf8GamePath path )
=> _cache?.ResolvePath( path );
// Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile( Utf8GamePath path, FullPath fullPath )
=> _cache!.ResolvedFiles[ path ] = fullPath;
// Force a file resolve to be removed.
internal void RemoveFile( Utf8GamePath path )
=> _cache!.ResolvedFiles.Remove( path );
// Obtain data from the cache.
internal MetaManager? MetaCache
=> _cache?.MetaManipulations;
@ -62,14 +66,17 @@ public partial class ModCollection
internal IReadOnlyDictionary< string, object? > ChangedItems
=> _cache?.ChangedItems ?? new Dictionary< string, object? >();
internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >();
internal IReadOnlyList< ConflictCache.Conflict > Conflicts
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
internal IEnumerable< ConflictCache.ModCacheStruct > ModConflicts( int modIdx )
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.ModCacheStruct >();
internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx )
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >();
// Update the effective file list for the given cache.
// Creates a cache if necessary.
public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident )
{
// Skip the empty collection.
if( Index == 0 )
{
return;
@ -87,8 +94,84 @@ public partial class ModCollection
{
Penumbra.ResidentResources.Reload();
}
}
_cache.Conflicts.Sort();
// Set Metadata files.
[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();
}
}
@ -106,8 +189,9 @@ public partial class ModCollection
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new();
public readonly MetaManager MetaManipulations;
public ConflictCache Conflicts;
public ConflictCache Conflicts = new();
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary< string, object? > ChangedItems
{
get
@ -117,6 +201,7 @@ public partial class ModCollection
}
}
// The cache reacts through events on its collection changing.
public Cache( ModCollection collection )
{
_collection = collection;
@ -133,6 +218,8 @@ public partial class ModCollection
private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ )
{
// Recompute the file list if it was not just a non-conflicting priority change
// or a setting change for a disabled mod.
if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any()
|| type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled )
{
@ -143,9 +230,12 @@ public partial class ModCollection
_collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection );
}
// Inheritance changes are too big to check for relevance,
// just recompute everything.
private void OnInheritanceChange( bool _ )
=> _collection.CalculateEffectiveFileList( true, true );
// Reset the shared file-seen cache.
private static void ResetFileSeen( int size )
{
if( size < FileSeen.Length )
@ -160,6 +250,8 @@ public partial class ModCollection
}
}
// Clear all local and global caches to prepare for recomputation.
private void ClearStorageAndPrepare()
{
ResolvedFiles.Clear();
@ -167,15 +259,15 @@ public partial class ModCollection
RegisteredFiles.Clear();
_changedItems.Clear();
ResolvedSettings.Clear();
Conflicts.ClearFileConflicts();
// Obtains actual settings for this collection with all inheritances.
ResolvedSettings.AddRange( _collection.ActualSettings );
}
public void CalculateEffectiveFileList()
{
ClearStorageAndPrepare();
Conflicts.ClearFileConflicts();
for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i )
for( var i = 0; i < Penumbra.ModManager.Count; ++i )
{
if( ResolvedSettings[ i ]?.Enabled == true )
{
@ -185,7 +277,6 @@ public partial class ModCollection
}
AddMetaFiles();
Conflicts.Sort();
}
private void SetChangedItems()
@ -204,6 +295,7 @@ public partial class ModCollection
{
identifier.Identify( _changedItems, resolved.ToGamePath() );
}
// TODO: Meta Manipulations
}
catch( Exception e )
{
@ -211,12 +303,12 @@ public partial class ModCollection
}
}
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.
// TODO: add group priorities.
foreach( var group in mod.Meta.Groups.Values.Reverse() )
{
switch( group.SelectionType )
@ -240,7 +332,6 @@ public partial class ModCollection
=> !Penumbra.Config.DisableSoundStreaming
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' );
private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file )
{
if( FilterFile( gamePath ) )
@ -250,11 +341,13 @@ public partial class ModCollection
if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) )
{
// No current conflict, just add.
RegisteredFiles.Add( gamePath, modIdx );
ResolvedFiles[ gamePath ] = file;
}
else
{
// Conflict, check which mod has higher priority, replace if necessary, add conflict.
var priority = ResolvedSettings[ modIdx ]!.Priority;
var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority;
Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath );
@ -270,6 +363,8 @@ public partial class ModCollection
{
switch( file.Extension.ToLowerInvariant() )
{
// We do not care for those file types
case ".scp" when !Penumbra.Config.DisableSoundStreaming:
case ".meta":
case ".rgsp":
return;
@ -279,10 +374,11 @@ public partial class ModCollection
}
}
private void AddPathsForOption( Option option, Mod.Mod mod, int modIdx, bool enabled )
private void AddPathsForOption( Option option, Mod mod, int modIdx, bool enabled )
{
foreach( var (file, paths) in option.OptionFiles )
{
// TODO: complete rework of options.
var fullPath = new FullPath( mod.BasePath, file );
var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
if( idx < 0 )
@ -309,7 +405,7 @@ public partial class ModCollection
}
}
private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod, int modIdx )
private void AddFilesForSingle( OptionGroup singleGroup, Mod mod, int modIdx )
{
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
var settings = ResolvedSettings[ modIdx ]!;
@ -324,7 +420,7 @@ public partial class ModCollection
}
}
private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod, int modIdx )
private void AddFilesForMulti( OptionGroup multiGroup, Mod mod, int modIdx )
{
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
var settings = ResolvedSettings[ modIdx ]!;
@ -340,7 +436,7 @@ public partial class ModCollection
}
}
private void AddRemainingFiles( Mod.Mod mod, int modIdx )
private void AddRemainingFiles( Mod mod, int modIdx )
{
for( var i = 0; i < mod.Resources.ModFiles.Count; ++i )
{
@ -431,81 +527,4 @@ public partial class ModCollection
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,18 +1,21 @@
using System;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Collections;
// Different types a mod setting can change:
public enum ModSettingChange
{
Inheritance,
EnableState,
Priority,
Setting,
Inheritance, // it was set to inherit from other collections or not inherit anymore
EnableState, // it was enabled or disabled
Priority, // its priority was changed
Setting, // a specific setting was changed
}
public partial class ModCollection
{
// If the change type is a bool, oldValue will be 1 for true and 0 for false.
// optionName will only be set for type == Setting.
public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited );
public event ModSettingChangeDelegate ModSettingChanged;
@ -99,13 +102,13 @@ public partial class ModCollection
private bool FixInheritance( int idx, bool inherit )
{
var settings = _settings[ idx ];
if( inherit != ( settings == null ) )
if( inherit == ( settings == null ) )
{
_settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta );
return true;
return false;
}
return false;
_settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta );
return true;
}
private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited )

View file

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
// File operations like saving, loading and deleting for a collection.
public partial class ModCollection
{
public static string CollectionDirectory
=> Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" );
// We need to remove all invalid path symbols from the collection name to be able to save it to file.
public FileInfo FileName
=> new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" ));
// Custom serialization due to shared mod information across managers.
public void Save()
{
try
{
var file = FileName;
file.Directory?.Create();
using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew );
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 ) );
// Write all used and unused settings by mod directory name.
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 (modDir, settings) in _unusedSettings )
{
j.WritePropertyName( modDir );
x.Serialize( j, settings );
}
j.WriteEndObject();
// Inherit by collection name.
j.WritePropertyName( nameof( Inheritance ) );
x.Serialize( j, Inheritance.Select( c => c.Name ) );
j.WriteEndObject();
}
catch( Exception e )
{
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
}
}
public void Delete()
{
if( Index == 0 )
{
return;
}
var file = FileName;
if( !file.Exists )
{
return;
}
try
{
file.Delete();
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete collection file {file.FullName} for {Name}:\n{e}" );
}
}
// Since inheritances depend on other collections existing,
// we return them as a list to be applied after reading all collections.
public static ModCollection? 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;
// Custom deserialization that is converted with the constructor.
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 ModCollection( name, version, settings );
}
catch( Exception e )
{
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
}
return null;
}
}

View file

@ -1,20 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
// ModCollections can inherit from an arbitrary number of other collections.
// This is transitive, so a collection A inheriting from B also inherits from everything B inherits.
// Circular dependencies are resolved by distinctness.
public partial class ModCollection
{
private readonly List< ModCollection > _inheritance = new();
// A change in inheritance usually requires complete recomputation.
public event Action< bool > InheritanceChanged;
private readonly List< ModCollection > _inheritance = new();
public IReadOnlyList< ModCollection > Inheritance
=> _inheritance;
// Iterate over all collections inherited from in depth-first order.
// Skip already visited collections to avoid circular dependencies.
public IEnumerable< ModCollection > GetFlattenedInheritance()
{
yield return this;
@ -27,6 +33,9 @@ public partial class ModCollection
}
}
// Add a new collection to the inheritance list.
// We do not check if this collection would be visited before,
// only that it is unique in the list itself.
public bool AddInheritance( ModCollection collection )
{
if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) )
@ -35,6 +44,7 @@ public partial class ModCollection
}
_inheritance.Add( collection );
// Changes in inherited collections may need to trigger further changes here.
collection.ModSettingChanged += OnInheritedModSettingChange;
collection.InheritanceChanged += OnInheritedInheritanceChange;
InheritanceChanged.Invoke( false );
@ -50,6 +60,7 @@ public partial class ModCollection
InheritanceChanged.Invoke( false );
}
// Order in the inheritance list is relevant.
public void MoveInheritance( int from, int to )
{
if( _inheritance.Move( from, to ) )
@ -58,6 +69,7 @@ public partial class ModCollection
}
}
// Carry changes in collections inherited from forward if they are relevant for this collection.
private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ )
{
if( _settings[ modIdx ] == null )
@ -69,13 +81,16 @@ public partial class ModCollection
private void OnInheritedInheritanceChange( bool _ )
=> InheritanceChanged.Invoke( true );
// Obtain the actual settings for a given mod via index.
// Also returns the collection the settings are taken from.
// If no collection provides settings for this mod, this collection is returned together with null.
public (ModSettings? Settings, ModCollection Collection) this[ Index idx ]
{
get
{
foreach( var collection in GetFlattenedInheritance() )
{
var settings = _settings[ idx ];
var settings = collection._settings[ idx ];
if( settings != null )
{
return ( settings, collection );

View file

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Collections;
public sealed partial class ModCollection
{
// Migration to convert ModCollections from older versions to newer.
private static class Migration
{
public static void Migrate( ModCollection collection )
@ -24,9 +26,10 @@ public sealed partial class ModCollection
}
collection.Version = 1;
// Remove all completely defaulted settings from active and inactive mods.
for( var i = 0; i < collection._settings.Count; ++i )
{
var setting = collection._settings[ i ];
if( SettingIsDefaultV0( collection._settings[ i ] ) )
{
collection._settings[ i ] = null;
@ -41,7 +44,11 @@ public sealed partial class ModCollection
return true;
}
// We treat every completely defaulted setting as inheritance-ready.
private static bool SettingIsDefaultV0( ModSettings? setting )
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 );
}
internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings )
=> new(name, 0, allSettings);
}

View file

@ -1,21 +1,28 @@
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;
using Penumbra.Mods;
namespace Penumbra.Collections;
public partial class ModCollection
{
// Create the always available Empty Collection that will always sit at index 0,
// can not be deleted and does never create a cache.
private static ModCollection CreateEmpty()
{
var collection = CreateNewEmpty( EmptyCollection );
collection.Index = 0;
collection._settings.Clear();
return collection;
}
}
// 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.
// Invariants:
// - Index is the collections index in the ModCollection.Manager
// - Settings has the same size as ModManager.Mods.
// - any change in settings or inheritance of the collection causes a Save.
public partial class ModCollection
{
public const int CurrentVersion = 1;
@ -24,29 +31,28 @@ public partial class ModCollection
public static readonly ModCollection Empty = CreateEmpty();
private static ModCollection CreateEmpty()
{
var collection = CreateNewEmpty( EmptyCollection );
collection.Index = 0;
collection._settings.Clear();
return collection;
}
// The collection name can contain invalid path characters,
// but after removing those and going to lower case it has to be unique.
public string Name { get; private init; }
public int Version { get; private set; }
public int Index { get; private set; } = -1;
// If a ModSetting is null, it can be inherited from other collections.
// If no collection provides a setting for the mod, it is just disabled.
private readonly List< ModSettings? > _settings;
public IReadOnlyList< ModSettings? > Settings
=> _settings;
// Evaluates the settings along the whole inheritance tree.
public IEnumerable< ModSettings? > ActualSettings
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings );
// Settings for deleted mods will be kept via directory name.
private readonly Dictionary< string, ModSettings > _unusedSettings;
// Constructor for duplication.
private ModCollection( string name, ModCollection duplicate )
{
Name = name;
@ -58,6 +64,7 @@ public partial class ModCollection
InheritanceChanged += SaveOnChange;
}
// Constructor for reading from files.
private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings )
{
Name = name;
@ -79,15 +86,15 @@ public partial class ModCollection
InheritanceChanged += SaveOnChange;
}
// Create a new, unique empty collection of a given name.
public static ModCollection CreateNewEmpty( string name )
=> new(name, CurrentVersion, new Dictionary< string, ModSettings >());
// Duplicate the calling collection to a new, unique collection of a given name.
public ModCollection Duplicate( string name )
=> new(name, this);
internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings )
=> new(name, 0, allSettings);
// Remove all settings for not currently-installed mods.
public void CleanUnavailableSettings()
{
var any = _unusedSettings.Count > 0;
@ -98,7 +105,8 @@ public partial class ModCollection
}
}
public void AddMod( Mod.Mod mod )
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
private void AddMod( Mods.Mod mod )
{
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
@ -111,7 +119,8 @@ public partial class ModCollection
}
}
public void RemoveMod( Mod.Mod mod, int idx )
// Move settings from the current mod list to the unused mod settings.
private void RemoveMod( Mods.Mod mod, int idx )
{
var settings = _settings[ idx ];
if( settings != null )
@ -121,104 +130,4 @@ public partial class ModCollection
_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()
{
if( Index == 0 )
{
return;
}
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 ModCollection? 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 ModCollection( name, version, settings );
}
catch( Exception e )
{
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
}
return null;
}
}

View file

@ -5,8 +5,9 @@ using Dalamud.Logging;
namespace Penumbra;
[Serializable]
public class Configuration : IPluginConfiguration
public partial class Configuration : IPluginConfiguration
{
private const int CurrentVersion = 1;
@ -36,7 +37,7 @@ public class Configuration : IPluginConfiguration
public string CurrentCollection { get; set; } = "Default";
public string DefaultCollection { get; set; } = "Default";
public string ForcedCollection { get; set; } = "";
public bool SortFoldersFirst { get; set; } = false;
public bool HasReadCharacterCollectionDesc { get; set; } = false;
@ -44,7 +45,6 @@ public class Configuration : IPluginConfiguration
public Dictionary< string, string > CharacterCollections { get; set; } = new();
public Dictionary< string, string > ModSortOrder { get; set; } = new();
public bool InvertModListOrder { internal get; set; }
public static Configuration Load()
{

View file

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Importer.Models
{

View file

@ -8,7 +8,7 @@ using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Importer.Models;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
using FileMode = System.IO.FileMode;

View file

@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using FFXIVClientStructs.STD;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Resolver;
namespace Penumbra.Interop.Loader;
@ -30,8 +29,7 @@ public unsafe partial class ResourceLoader
public ResourceType Extension;
}
private readonly SortedDictionary< FullPath, DebugData > _debugList = new();
private readonly List< (FullPath, DebugData?) > _deleteList = new();
private readonly SortedList< FullPath, DebugData > _debugList = new();
public IReadOnlyDictionary< FullPath, DebugData > DebugList
=> _debugList;
@ -161,35 +159,22 @@ public unsafe partial class ResourceLoader
public void UpdateDebugInfo()
{
var manager = *ResourceManager;
_deleteList.Clear();
foreach( var data in _debugList.Values )
for( var i = 0; i < _debugList.Count; ++i )
{
var data = _debugList.Values[ i ];
var regularResource = FindResource( data.Category, data.Extension, ( uint )data.OriginalPath.Path.Crc32 );
var modifiedResource = FindResource( data.Category, data.Extension, ( uint )data.ManipulatedPath.InternalName.Crc32 );
if( modifiedResource == null )
{
_deleteList.Add( ( data.ManipulatedPath, null ) );
_debugList.RemoveAt( i-- );
}
else if( regularResource != data.OriginalResource || modifiedResource != data.ManipulatedResource )
{
_deleteList.Add( ( data.ManipulatedPath, data with
_debugList[ _debugList.Keys[ i ] ] = data with
{
OriginalResource = ( Structs.ResourceHandle* )regularResource,
ManipulatedResource = ( Structs.ResourceHandle* )modifiedResource,
} ) );
}
}
foreach( var (path, data) in _deleteList )
{
if( data == null )
{
_debugList.Remove( path );
}
else
{
_debugList[ path ] = data.Value;
};
}
}
}

View file

@ -8,7 +8,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Mod;
using Penumbra.Mods;
using FileMode = Penumbra.Interop.Structs.FileMode;
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
@ -93,7 +92,7 @@ public unsafe partial class ResourceLoader
// Use the default method of path replacement.
public static (FullPath?, object?) DefaultResolver( Utf8GamePath path )
{
var resolved = Mod.Mod.Manager.ResolvePath( path );
var resolved = Mods.Mod.Manager.ResolvePath( path );
return ( resolved, null );
}

View file

@ -10,7 +10,6 @@ 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;
namespace Penumbra.Interop.Resolver;

View file

@ -7,7 +7,6 @@ using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Mods;
namespace Penumbra.Interop.Resolver;

View file

@ -5,7 +5,6 @@ using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Interop.Resolver;

View file

@ -5,7 +5,6 @@ using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader;
using Penumbra.Mods;
namespace Penumbra.Interop.Resolver;

View file

@ -7,7 +7,7 @@ using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Meta;

View file

@ -5,11 +5,16 @@ using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json.Linq;
using Penumbra.Collections;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra;
public partial class Configuration
{
public string ForcedCollection { internal get; set; } = "";
public bool InvertModListOrder { internal get; set; }
}
public static class MigrateConfiguration
{
public static void Version0To1( Configuration config )

View file

@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.IO;
using Penumbra.GameData.ByteString;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// A complete Mod containing settings (i.e. dependent on a collection)
// and the resulting cache.

View file

@ -4,7 +4,7 @@ using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
public enum SelectType
{

View file

@ -1,7 +1,6 @@
using System;
using Penumbra.Mods;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
public partial class Mod
{

View file

@ -3,9 +3,8 @@ using System.IO;
using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Mods;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// Mod contains all permanent information about a mod,
// and is independent of collections or settings.
@ -23,7 +22,7 @@ public partial class Mod
public FileInfo MetaFile { get; set; }
public int Index { get; private set; } = -1;
private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources )
private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources)
{
BasePath = basePath;
Meta = meta;

View file

@ -8,10 +8,9 @@ using System.Security.Cryptography;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Importer;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
public class ModCleanup
{

View file

@ -1,6 +1,5 @@
using System;
using System.Linq;
using Penumbra.Mod;
namespace Penumbra.Mods;
@ -37,7 +36,7 @@ public static partial class ModFileSystem
// Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes.
// Saves and returns true if anything changed.
public static bool Rename( this Mod.Mod mod, string newName )
public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName )
{
if( RenameNoSave( mod, newName ) )
{
@ -63,7 +62,7 @@ public static partial class ModFileSystem
// Move a single mod to the target folder.
// Returns true and saves if anything changed.
public static bool Move( this Mod.Mod mod, ModFolder target )
public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target )
{
if( MoveNoSave( mod, target ) )
{
@ -76,7 +75,7 @@ public static partial class ModFileSystem
// Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName.
// Creates all necessary Subfolders.
public static void Move( this Mod.Mod mod, string sortOrder )
public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder )
{
var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries );
var folder = Root;
@ -137,10 +136,10 @@ public static partial class ModFileSystem
}
// Sets and saves the sort order of a single mod, removing the entry if it is unnecessary.
private static void SaveMod( Mod.Mod mod )
private static void SaveMod( global::Penumbra.Mods.Mod mod )
{
if( ReferenceEquals( mod.Order.ParentFolder, Root )
&& string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) )
&& string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) )
{
Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name );
}
@ -184,7 +183,7 @@ public static partial class ModFileSystem
return true;
}
private static bool RenameNoSave( Mod.Mod mod, string newName )
private static bool RenameNoSave( global::Penumbra.Mods.Mod mod, string newName )
{
newName = newName.Replace( '/', '\\' );
if( mod.Order.SortOrderName == newName )
@ -193,12 +192,12 @@ public static partial class ModFileSystem
}
mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod );
mod.Order = new Mod.Mod.SortOrder( mod.Order.ParentFolder, newName );
mod.Order = new global::Penumbra.Mods.Mod.SortOrder( mod.Order.ParentFolder, newName );
mod.Order.ParentFolder.AddMod( mod );
return true;
}
private static bool MoveNoSave( Mod.Mod mod, ModFolder target )
private static bool MoveNoSave( global::Penumbra.Mods.Mod mod, ModFolder target )
{
var oldParent = mod.Order.ParentFolder;
if( ReferenceEquals( target, oldParent ) )
@ -207,7 +206,7 @@ public static partial class ModFileSystem
}
oldParent.RemoveMod( mod );
mod.Order = new Mod.Mod.SortOrder( target, mod.Order.SortOrderName );
mod.Order = new global::Penumbra.Mods.Mod.SortOrder( target, mod.Order.SortOrderName );
target.AddMod( mod );
return true;
}

View file

@ -1,247 +1,245 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Penumbra.Mod;
namespace Penumbra.Mods
namespace Penumbra.Mods;
public partial class ModFolder
{
public partial class ModFolder
public ModFolder? Parent;
public string FullName
{
public ModFolder? Parent;
public string FullName
get
{
get
{
var parentPath = Parent?.FullName ?? string.Empty;
return parentPath.Any() ? $"{parentPath}/{Name}" : Name;
}
}
private string _name = string.Empty;
public string Name
{
get => _name;
set => _name = value.Replace( '/', '\\' );
}
public List< ModFolder > SubFolders { get; } = new();
public List< Mod.Mod > Mods { get; } = new();
public ModFolder( ModFolder parent, string name )
{
Parent = parent;
Name = name;
}
public override string ToString()
=> FullName;
public int TotalDescendantMods()
=> Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() );
public int TotalDescendantFolders()
=> SubFolders.Sum( f => f.TotalDescendantFolders() );
// Return all descendant mods in the specified order.
public IEnumerable< Mod.Mod > AllMods( bool foldersFirst )
{
if( foldersFirst )
{
return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods );
}
return GetSortedEnumerator().SelectMany( f =>
{
if( f is ModFolder folder )
{
return folder.AllMods( false );
}
return new[] { ( Mod.Mod )f };
} );
}
// Return all descendant subfolders.
public IEnumerable< ModFolder > AllFolders()
=> SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this );
// Iterate through all descendants in the specified order, returning subfolders as well as mods.
public IEnumerable< object > GetItems( bool foldersFirst )
=> foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator();
// Find a subfolder by name. Returns true and sets folder to it if it exists.
public bool FindSubFolder( string name, out ModFolder folder )
{
var subFolder = new ModFolder( this, name );
var idx = SubFolders.BinarySearch( subFolder, FolderComparer );
folder = idx >= 0 ? SubFolders[ idx ] : this;
return idx >= 0;
}
// Checks if an equivalent subfolder as folder already exists and returns its index.
// If it does not exist, inserts folder as a subfolder and returns the new index.
// Also sets this as folders parent.
public int FindOrAddSubFolder( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx >= 0 )
{
return idx;
}
idx = ~idx;
SubFolders.Insert( idx, folder );
folder.Parent = this;
return idx;
}
// Checks if a subfolder with the given name already exists and returns it and its index.
// If it does not exists, creates and inserts it and returns the new subfolder and its index.
public (ModFolder, int) FindOrCreateSubFolder( string name )
{
var subFolder = new ModFolder( this, name );
var idx = FindOrAddSubFolder( subFolder );
return ( SubFolders[ idx ], idx );
}
// Remove folder as a subfolder if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveSubFolder( ModFolder folder )
{
RemoveFolderIgnoreEmpty( folder );
CheckEmpty();
}
// Add the given mod as a child, if it is not already a child.
// Returns the index of the found or inserted mod.
public int AddMod( Mod.Mod mod )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
return idx;
}
idx = ~idx;
Mods.Insert( idx, mod );
return idx;
}
// Remove mod as a child if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveMod( Mod.Mod mod )
{
RemoveModIgnoreEmpty( mod );
CheckEmpty();
var parentPath = Parent?.FullName ?? string.Empty;
return parentPath.Any() ? $"{parentPath}/{Name}" : Name;
}
}
// Internals
public partial class ModFolder
private string _name = string.Empty;
public string Name
{
// Create a Root folder without parent.
internal static ModFolder CreateRoot()
=> new( null!, string.Empty );
get => _name;
set => _name = value.Replace( '/', '\\' );
}
internal class ModFolderComparer : IComparer< ModFolder >
public List< ModFolder > SubFolders { get; } = new();
public List< Mod > Mods { get; } = new();
public ModFolder( ModFolder parent, string name )
{
Parent = parent;
Name = name;
}
public override string ToString()
=> FullName;
public int TotalDescendantMods()
=> Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() );
public int TotalDescendantFolders()
=> SubFolders.Sum( f => f.TotalDescendantFolders() );
// Return all descendant mods in the specified order.
public IEnumerable< Mod > AllMods( bool foldersFirst )
{
if( foldersFirst )
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder.
public int Compare( ModFolder? x, ModFolder? y )
=> ReferenceEquals( x, y )
? 0
: string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType );
return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods );
}
internal class ModDataComparer : IComparer< Mod.Mod >
return GetSortedEnumerator().SelectMany( f =>
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder.
// Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary.
public int Compare( Mod.Mod? x, Mod.Mod? y )
if( f is ModFolder folder )
{
if( ReferenceEquals( x, y ) )
{
return 0;
}
var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType );
if( cmp != 0 )
{
return cmp;
}
return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture );
return folder.AllMods( false );
}
return new[] { ( Mod )f };
} );
}
// Return all descendant subfolders.
public IEnumerable< ModFolder > AllFolders()
=> SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this );
// Iterate through all descendants in the specified order, returning subfolders as well as mods.
public IEnumerable< object > GetItems( bool foldersFirst )
=> foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator();
// Find a subfolder by name. Returns true and sets folder to it if it exists.
public bool FindSubFolder( string name, out ModFolder folder )
{
var subFolder = new ModFolder( this, name );
var idx = SubFolders.BinarySearch( subFolder, FolderComparer );
folder = idx >= 0 ? SubFolders[ idx ] : this;
return idx >= 0;
}
// Checks if an equivalent subfolder as folder already exists and returns its index.
// If it does not exist, inserts folder as a subfolder and returns the new index.
// Also sets this as folders parent.
public int FindOrAddSubFolder( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx >= 0 )
{
return idx;
}
internal static readonly ModFolderComparer FolderComparer = new();
internal static readonly ModDataComparer ModComparer = new();
idx = ~idx;
SubFolders.Insert( idx, folder );
folder.Parent = this;
return idx;
}
// Get an enumerator for actually sorted objects instead of folder-first objects.
private IEnumerable< object > GetSortedEnumerator()
// Checks if a subfolder with the given name already exists and returns it and its index.
// If it does not exists, creates and inserts it and returns the new subfolder and its index.
public (ModFolder, int) FindOrCreateSubFolder( string name )
{
var subFolder = new ModFolder( this, name );
var idx = FindOrAddSubFolder( subFolder );
return ( SubFolders[ idx ], idx );
}
// Remove folder as a subfolder if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveSubFolder( ModFolder folder )
{
RemoveFolderIgnoreEmpty( folder );
CheckEmpty();
}
// Add the given mod as a child, if it is not already a child.
// Returns the index of the found or inserted mod.
public int AddMod( Mod mod )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
var modIdx = 0;
foreach( var folder in SubFolders )
{
var folderString = folder.Name;
for( ; modIdx < Mods.Count; ++modIdx )
{
var mod = Mods[ modIdx ];
var modString = mod.Order.SortOrderName;
if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 )
{
yield return mod;
}
else
{
break;
}
}
return idx;
}
yield return folder;
idx = ~idx;
Mods.Insert( idx, mod );
return idx;
}
// Remove mod as a child if it exists.
// If this folder is empty afterwards, remove it from its parent.
public void RemoveMod( Mod mod )
{
RemoveModIgnoreEmpty( mod );
CheckEmpty();
}
}
// Internals
public partial class ModFolder
{
// Create a Root folder without parent.
internal static ModFolder CreateRoot()
=> new(null!, string.Empty);
internal class ModFolderComparer : IComparer< ModFolder >
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder.
public int Compare( ModFolder? x, ModFolder? y )
=> ReferenceEquals( x, y )
? 0
: string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType );
}
internal class ModDataComparer : IComparer< Mod >
{
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase;
// Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder.
// Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary.
public int Compare( Mod? x, Mod? y )
{
if( ReferenceEquals( x, y ) )
{
return 0;
}
var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType );
if( cmp != 0 )
{
return cmp;
}
return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture );
}
}
internal static readonly ModFolderComparer FolderComparer = new();
internal static readonly ModDataComparer ModComparer = new();
// Get an enumerator for actually sorted objects instead of folder-first objects.
private IEnumerable< object > GetSortedEnumerator()
{
var modIdx = 0;
foreach( var folder in SubFolders )
{
var folderString = folder.Name;
for( ; modIdx < Mods.Count; ++modIdx )
{
yield return Mods[ modIdx ];
var mod = Mods[ modIdx ];
var modString = mod.Order.SortOrderName;
if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 )
{
yield return mod;
}
else
{
break;
}
}
yield return folder;
}
private void CheckEmpty()
for( ; modIdx < Mods.Count; ++modIdx )
{
if( Mods.Count == 0 && SubFolders.Count == 0 )
{
Parent?.RemoveSubFolder( this );
}
yield return Mods[ modIdx ];
}
}
private void CheckEmpty()
{
if( Mods.Count == 0 && SubFolders.Count == 0 )
{
Parent?.RemoveSubFolder( this );
}
}
// Remove a subfolder but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveFolderIgnoreEmpty( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx < 0 )
{
return;
}
// Remove a subfolder but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveFolderIgnoreEmpty( ModFolder folder )
{
var idx = SubFolders.BinarySearch( folder, FolderComparer );
if( idx < 0 )
{
return;
}
SubFolders[ idx ].Parent = null;
SubFolders.RemoveAt( idx );
}
SubFolders[ idx ].Parent = null;
SubFolders.RemoveAt( idx );
}
// Remove a mod, but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveModIgnoreEmpty( Mod.Mod mod )
// Remove a mod, but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveModIgnoreEmpty( Mod mod )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 )
{
Mods.RemoveAt( idx );
}
Mods.RemoveAt( idx );
}
}
}

View file

@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// Functions that do not really depend on only one component of a mod.
public static class ModFunctions

View file

@ -2,16 +2,14 @@ using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Logging;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod;
namespace Penumbra.Mods;
public partial class ModManagerNew
{
private readonly List< Mod.Mod > _mods = new();
private readonly List< Mod > _mods = new();
public IReadOnlyList< Mod.Mod > Mods
public IReadOnlyList< Mod > Mods
=> _mods;
public void DiscoverMods()
@ -37,6 +35,7 @@ public partial class ModManagerNew
//Collections.RecreateCaches();
}
}
public partial class ModManagerNew
{
public DirectoryInfo BasePath { get; private set; } = null!;

View file

@ -6,11 +6,10 @@ using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
public partial class Mod
{

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using Dalamud.Logging;
using Penumbra.Mod;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -12,7 +11,7 @@ namespace Penumbra.Mods;
// Contains all change functions on a specific mod that also require corresponding changes to collections.
public static class ModManagerEditExtensions
{
public static bool RenameMod( this Mod.Mod.Manager manager, string newName, Mod.Mod mod )
public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod )
{
if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) )
{
@ -25,14 +24,14 @@ public static class ModManagerEditExtensions
return true;
}
public static bool ChangeSortOrder( this Mod.Mod.Manager manager, Mod.Mod mod, string newSortOrder )
public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder )
{
if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) )
{
return false;
}
var inRoot = new Mod.Mod.SortOrder( manager.StructuredMods, mod.Meta.Name );
var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name );
if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName )
{
mod.Order = inRoot;
@ -49,7 +48,7 @@ public static class ModManagerEditExtensions
return true;
}
public static bool RenameModFolder( this Mod.Mod.Manager manager, Mod.Mod mod, DirectoryInfo newDir, bool move = true )
public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true )
{
if( move )
{
@ -73,7 +72,7 @@ public static class ModManagerEditExtensions
var oldBasePath = mod.BasePath;
mod.BasePath = newDir;
mod.MetaFile = Mod.Mod.MetaFileInfo( newDir );
mod.MetaFile = Mod.MetaFileInfo( newDir );
manager.UpdateMod( mod );
if( manager.Config.ModSortOrder.ContainsKey( oldBasePath.Name ) )
@ -95,7 +94,7 @@ public static class ModManagerEditExtensions
return true;
}
public static bool ChangeModGroup( this Mod.Mod.Manager manager, string oldGroupName, string newGroupName, Mod.Mod mod,
public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod,
SelectType type = SelectType.Single )
{
if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) )
@ -157,7 +156,7 @@ public static class ModManagerEditExtensions
return true;
}
public static bool RemoveModOption( this Mod.Mod.Manager manager, int optionIdx, OptionGroup group, Mod.Mod mod )
public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod )
{
if( optionIdx < 0 || optionIdx >= group.Options.Count )
{

View file

@ -5,47 +5,20 @@ using System.Linq;
using Dalamud.Logging;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// Contains descriptive data about the mod as well as possible settings and fileswaps.
public class ModMeta
{
public uint FileVersion { get; set; }
public string Name
{
get => _name;
set
{
_name = value;
LowerName = value.ToLowerInvariant();
}
}
private string _name = "Mod";
[JsonIgnore]
public string LowerName { get; private set; } = "mod";
private string _author = "";
public string Author
{
get => _author;
set
{
_author = value;
LowerAuthor = value.ToLowerInvariant();
}
}
[JsonIgnore]
public string LowerAuthor { get; private set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
public LowerString Name { get; set; } = "Mod";
public LowerString Author { get; set; } = LowerString.Empty;
public string Description { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string Website { get; set; } = string.Empty;
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();

View file

@ -5,7 +5,7 @@ using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
[Flags]
public enum ResourceChange

View file

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// Contains the settings for a given mod.
public class ModSettings

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Mod;
namespace Penumbra.Mods;
// Contains settings with the option selections stored by names instead of index.
// This is meant to make them possibly more portable when we support importing collections from other users.

View file

@ -1,5 +1,4 @@
using System;
using System.IO;
using Dalamud.Game.Command;
using Dalamud.Logging;
using Dalamud.Plugin;
@ -10,13 +9,12 @@ using Lumina.Excel.GeneratedSheets;
using Penumbra.Api;
using Penumbra.GameData.Enums;
using Penumbra.Interop;
using Penumbra.Mods;
using Penumbra.UI;
using Penumbra.Util;
using Penumbra.Collections;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Resolver;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra;
@ -34,8 +32,9 @@ public class Penumbra : IDalamudPlugin
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static Mod.Mod.Manager ModManager { get; private set; } = null!;
public static Mod.Manager ModManager { get; private set; } = null!;
public static ModCollection.Manager CollectionManager { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; set; } = null!;
@ -65,9 +64,10 @@ public class Penumbra : IDalamudPlugin
ResidentResources = new ResidentResourceManager();
CharacterUtility = new CharacterUtility();
MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader( this );
ResourceLogger = new ResourceLogger( ResourceLoader );
ModManager = new Mod.Mod.Manager();
ModManager = new Mod.Manager();
ModManager.DiscoverMods();
CollectionManager = new ModCollection.Manager( ModManager );
ObjectReloader = new ObjectReloader();
@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin
PathResolver.Dispose();
ResourceLogger.Dispose();
MetaFileManager.Dispose();
ResourceLoader.Dispose();
CharacterUtility.Dispose();

View file

@ -7,7 +7,6 @@ using Dalamud.Interface.Components;
using Dalamud.Logging;
using ImGuiNET;
using Penumbra.Collections;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;
@ -22,7 +21,7 @@ public partial class SettingsInterface
private readonly Selector _selector;
private string _collectionNames = null!;
private string _collectionNamesWithNone = null!;
private ModCollection[] _collections = null!;
private ModCollection[] _collections = null!;
private int _currentCollectionIndex;
private int _currentDefaultIndex;
private readonly Dictionary< string, int > _currentCharacterIndices = new();
@ -192,6 +191,65 @@ public partial class SettingsInterface
}
}
private static void DrawInheritance( ModCollection collection )
{
ImGui.PushID( collection.Index );
if( ImGui.TreeNodeEx( collection.Name, ImGuiTreeNodeFlags.DefaultOpen ) )
{
foreach( var inheritance in collection.Inheritance )
{
DrawInheritance( inheritance );
}
}
ImGui.PopID();
}
private void DrawCurrentCollectionInheritance()
{
if( !ImGui.BeginListBox( "##inheritanceList",
new Vector2( SettingsMenu.InputTextWidth, ImGui.GetTextLineHeightWithSpacing() * 10 ) ) )
{
return;
}
using var end = ImGuiRaii.DeferredEnd( ImGui.EndListBox );
DrawInheritance( _collections[ _currentCollectionIndex + 1 ] );
}
private static int _newInheritanceIdx = 0;
private void DrawNewInheritanceSelection()
{
ImGui.SetNextItemWidth( SettingsMenu.InputTextWidth - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X );
if( ImGui.BeginCombo( "##newInheritance", Penumbra.CollectionManager[ _newInheritanceIdx ].Name ) )
{
using var end = ImGuiRaii.DeferredEnd( ImGui.EndCombo );
foreach( var collection in Penumbra.CollectionManager )
{
if( ImGui.Selectable( collection.Name, _newInheritanceIdx == collection.Index ) )
{
_newInheritanceIdx = collection.Index;
}
}
}
ImGui.SameLine();
var valid = _newInheritanceIdx > ModCollection.Empty.Index
&& _collections[ _currentCollectionIndex + 1 ].Index != _newInheritanceIdx
&& _collections[ _currentCollectionIndex + 1 ].Inheritance.All( c => c.Index != _newInheritanceIdx );
using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !valid );
using var font = ImGuiRaii.PushFont( UiBuilder.IconFont );
if( ImGui.Button( $"{FontAwesomeIcon.Plus.ToIconString()}##newInheritanceAdd", ImGui.GetFrameHeight() * Vector2.One ) && valid )
{
_collections[ _currentCollectionIndex + 1 ].AddInheritance( Penumbra.CollectionManager[ _newInheritanceIdx ] );
}
style.Pop();
font.Pop();
ImGuiComponents.HelpMarker( "Add a new inheritance to the collection." );
}
private void DrawDefaultCollectionSelector()
{
var index = _currentDefaultIndex;
@ -344,12 +402,14 @@ public partial class SettingsInterface
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem )
.Push( ImGui.EndChild );
if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 6 ), true ) )
if( ImGui.BeginChild( "##CollectionHandling", new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 17 ), true ) )
{
DrawCurrentCollectionSelector( true );
ImGuiHelpers.ScaledDummy( 0, 10 );
DrawNewCollectionInput();
ImGuiHelpers.ScaledDummy( 0, 10 );
DrawCurrentCollectionInheritance();
DrawNewInheritanceSelection();
}
raii.Pop();

View file

@ -6,8 +6,8 @@ using ImGuiNET;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;
namespace Penumbra.UI;
@ -17,10 +17,8 @@ public partial class SettingsInterface
{
private const string LabelTab = "Effective Changes";
private string _gamePathFilter = string.Empty;
private string _gamePathFilterLower = string.Empty;
private string _filePathFilter = string.Empty;
private string _filePathFilterLower = string.Empty;
private LowerString _gamePathFilter = LowerString.Empty;
private LowerString _filePathFilter = LowerString.Empty;
private const float LeftTextLength = 600;
@ -57,47 +55,49 @@ public partial class SettingsInterface
}
ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) )
var tmp = _gamePathFilter.Text;
if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref tmp, 256 ) )
{
_gamePathFilterLower = _gamePathFilter.ToLowerInvariant();
_gamePathFilter = tmp;
}
ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X );
ImGui.SetNextItemWidth( -1 );
if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) )
tmp = _filePathFilter.Text;
if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref tmp, 256 ) )
{
_filePathFilterLower = _filePathFilter.ToLowerInvariant();
_filePathFilter = tmp;
}
}
private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp )
{
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) )
{
return false;
}
return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower );
return _filePathFilter.Length == 0 || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilter.Lower );
}
private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp )
{
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
if( _gamePathFilter.Length > 0 && !kvp.Key.ToString().Contains( _gamePathFilter.Lower ) )
{
return false;
}
return !_filePathFilter.Any() || kvp.Value.ToString().Contains( _filePathFilterLower );
return _filePathFilter.Length == 0 || kvp.Value.ToString().Contains( _filePathFilter.Lower );
}
private bool CheckFilters( (string, string, string) kvp )
private bool CheckFilters( (string, LowerString) kvp )
{
if( _gamePathFilter.Any() && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilterLower ) )
if( _gamePathFilter.Length > 0 && !kvp.Item1.ToLowerInvariant().Contains( _gamePathFilter.Lower ) )
{
return false;
}
return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower );
return _filePathFilter.Length == 0 || kvp.Item2.Contains( _filePathFilter.Lower );
}
private void DrawFilteredRows( ModCollection active )
@ -113,49 +113,43 @@ public partial class SettingsInterface
return;
}
foreach( var (mp, mod, _) in cache.Cmp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Cmp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
foreach( var (mp, mod, _) in cache.Eqp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Eqp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
foreach( var (mp, mod, _) in cache.Eqdp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Eqdp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
foreach( var (mp, mod, _) in cache.Gmp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Gmp.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
foreach( var (mp, mod, _) in cache.Est.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Est.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );
}
foreach( var (mp, mod, _) in cache.Imc.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name,
Penumbra.ModManager.Mods[ p.Value ].Meta.LowerName ) )
foreach( var (mp, mod) in cache.Imc.Manipulations
.Select( p => ( p.Key.ToString(), Penumbra.ModManager.Mods[ p.Value ].Meta.Name ) )
.Where( CheckFilters ) )
{
DrawLine( mp, mod );

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Logging;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Util;
@ -15,19 +14,19 @@ public class ModListCache : IDisposable
public const uint ConflictingModColor = 0xFFAAAAFFu;
public const uint HandledConflictModColor = 0xFF88DDDDu;
private readonly Mod.Mod.Manager _manager;
private readonly Mods.Mod.Manager _manager;
private readonly List< FullMod > _modsInOrder = new();
private readonly List< (bool visible, uint color) > _visibleMods = new();
private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new();
private readonly IReadOnlySet< string > _newMods;
private string _modFilter = string.Empty;
private string _modFilterChanges = string.Empty;
private string _modFilterAuthor = string.Empty;
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
private bool _listResetNecessary;
private bool _filterResetNecessary;
private LowerString _modFilter = LowerString.Empty;
private LowerString _modFilterAuthor = LowerString.Empty;
private LowerString _modFilterChanges = LowerString.Empty;
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
private bool _listResetNecessary;
private bool _filterResetNecessary;
public ModFilter StateFilter
@ -44,7 +43,7 @@ public class ModListCache : IDisposable
}
}
public ModListCache( Mod.Mod.Manager manager, IReadOnlySet< string > newMods )
public ModListCache( Mods.Mod.Manager manager, IReadOnlySet< string > newMods )
{
_manager = manager;
_newMods = newMods;
@ -123,20 +122,20 @@ public class ModListCache : IDisposable
if( lower.StartsWith( "c:" ) )
{
_modFilterChanges = lower[ 2.. ];
_modFilter = string.Empty;
_modFilterAuthor = string.Empty;
_modFilter = LowerString.Empty;
_modFilterAuthor = LowerString.Empty;
}
else if( lower.StartsWith( "a:" ) )
{
_modFilterAuthor = lower[ 2.. ];
_modFilter = string.Empty;
_modFilterChanges = string.Empty;
_modFilter = LowerString.Empty;
_modFilterChanges = LowerString.Empty;
}
else
{
_modFilter = lower;
_modFilterAuthor = string.Empty;
_modFilterChanges = string.Empty;
_modFilterAuthor = LowerString.Empty;
_modFilterChanges = LowerString.Empty;
}
ResetFilters();
@ -233,12 +232,12 @@ public class ModListCache : IDisposable
{
var ret = ( false, 0u );
if( _modFilter.Length > 0 && !mod.Data.Meta.LowerName.Contains( _modFilter ) )
if( _modFilter.Length > 0 && !mod.Data.Meta.Name.Contains( _modFilter ) )
{
return ret;
}
if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) )
if( _modFilterAuthor.Length > 0 && !mod.Data.Meta.Author.Contains( _modFilterAuthor ) )
{
return ret;
}

View file

@ -10,7 +10,6 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;
@ -201,7 +200,7 @@ public partial class SettingsInterface
raii.Push( ImGui.EndListBox );
using var indent = ImGuiRaii.PushIndent( 0 );
Mod.Mod? oldBadMod = null;
Mods.Mod? oldBadMod = null;
foreach( var conflict in conflicts )
{
var badMod = Penumbra.ModManager[ conflict.Mod2 ];
@ -224,14 +223,14 @@ public partial class SettingsInterface
indent.Push( 30f );
}
if( conflict.Conflict is Utf8GamePath p )
if( conflict.Data is Utf8GamePath p )
{
unsafe
{
ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
}
}
else if( conflict.Conflict is MetaManipulation m )
else if( conflict.Data is MetaManipulation m )
{
ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty );
}

View file

@ -5,7 +5,6 @@ using Dalamud.Interface;
using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;

View file

@ -6,7 +6,6 @@ using System.Numerics;
using Dalamud.Interface;
using Dalamud.Logging;
using ImGuiNET;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;
@ -69,7 +68,7 @@ public partial class SettingsInterface
_currentWebsite = Meta?.Website ?? "";
}
private Mod.FullMod? Mod
private Mods.FullMod? Mod
=> _selector.Mod;
private ModMeta? Meta
@ -77,7 +76,7 @@ public partial class SettingsInterface
private void DrawName()
{
var name = Meta!.Name;
var name = Meta!.Name.Text;
var modManager = Penumbra.ModManager;
if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && modManager.RenameMod( name, Mod!.Data ) )
{
@ -122,7 +121,7 @@ public partial class SettingsInterface
ImGui.TextColored( GreyColor, "by" );
ImGui.SameLine();
var author = Meta!.Author;
var author = Meta!.Author.Text;
if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 )
&& author != Meta.Author )
{
@ -228,7 +227,7 @@ public partial class SettingsInterface
}
}
public static bool DrawSortOrder( Mod.Mod mod, Mod.Mod.Manager manager, Selector selector )
public static bool DrawSortOrder( Mods.Mod mod, Mods.Mod.Manager manager, Selector selector )
{
var currentSortOrder = mod.Order.FullPath;
ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale );

View file

@ -10,7 +10,6 @@ using Dalamud.Logging;
using ImGuiNET;
using Penumbra.Collections;
using Penumbra.Importer;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;
@ -410,11 +409,11 @@ public partial class SettingsInterface
// Selection
private partial class Selector
{
public Mod.FullMod? Mod { get; private set; }
public Mods.FullMod? Mod { get; private set; }
private int _index;
private string _nextDir = string.Empty;
private void SetSelection( int idx, Mod.FullMod? info )
private void SetSelection( int idx, Mods.FullMod? info )
{
Mod = info;
if( idx != _index )
@ -480,7 +479,7 @@ public partial class SettingsInterface
private partial class Selector
{
// === Mod ===
private void DrawModOrderPopup( string popupName, Mod.FullMod mod, bool firstOpen )
private void DrawModOrderPopup( string popupName, Mods.FullMod mod, bool firstOpen )
{
if( !ImGui.BeginPopup( popupName ) )
{
@ -664,7 +663,7 @@ public partial class SettingsInterface
idx += sub.TotalDescendantMods();
}
}
else if( item is Mod.Mod _ )
else if( item is Mods.Mod _ )
{
var (mod, visible, color) = Cache.GetMod( idx );
if( mod != null && visible )
@ -721,7 +720,7 @@ public partial class SettingsInterface
}
}
private void DrawMod( Mod.FullMod mod, int modIndex, uint color )
private void DrawMod( Mods.FullMod mod, int modIndex, uint color )
{
using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 );

View file

@ -10,7 +10,6 @@ using Dalamud.Logging;
using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.Interop;
using Penumbra.Mods;
using Penumbra.UI.Custom;
using Penumbra.Util;

View file

@ -0,0 +1,122 @@
using System;
using ImGuiNET;
using Newtonsoft.Json;
namespace Penumbra.Util;
[JsonConverter( typeof( Converter ) )]
public readonly struct LowerString : IEquatable< LowerString >, IComparable< LowerString >
{
public static readonly LowerString Empty = new(string.Empty);
public readonly string Text = string.Empty;
public readonly string Lower = string.Empty;
public LowerString( string text )
{
Text = string.Intern( text );
Lower = string.Intern( text.ToLowerInvariant() );
}
public int Length
=> Text.Length;
public int Count
=> Length;
public bool Equals( LowerString other )
=> string.Equals( Lower, other.Lower, StringComparison.InvariantCulture );
public bool Equals( string other )
=> string.Equals( Lower, other, StringComparison.InvariantCultureIgnoreCase );
public int CompareTo( LowerString other )
=> string.Compare( Lower, other.Lower, StringComparison.InvariantCulture );
public int CompareTo( string other )
=> string.Compare( Lower, other, StringComparison.InvariantCultureIgnoreCase );
public bool Contains( LowerString other )
=> Lower.Contains( other.Lower, StringComparison.InvariantCulture );
public bool Contains( string other )
=> Lower.Contains( other, StringComparison.InvariantCultureIgnoreCase );
public bool StartsWith( LowerString other )
=> Lower.StartsWith( other.Lower, StringComparison.InvariantCulture );
public bool StartsWith( string other )
=> Lower.StartsWith( other, StringComparison.InvariantCultureIgnoreCase );
public bool EndsWith( LowerString other )
=> Lower.EndsWith( other.Lower, StringComparison.InvariantCulture );
public bool EndsWith( string other )
=> Lower.EndsWith( other, StringComparison.InvariantCultureIgnoreCase );
public override string ToString()
=> Text;
public static implicit operator string( LowerString s )
=> s.Text;
public static implicit operator LowerString( string s )
=> new(s);
private class Converter : JsonConverter< LowerString >
{
public override void WriteJson( JsonWriter writer, LowerString value, JsonSerializer serializer )
{
writer.WriteValue( value.Text );
}
public override LowerString ReadJson( JsonReader reader, Type objectType, LowerString existingValue, bool hasExistingValue,
JsonSerializer serializer )
{
if( reader.Value is string text )
{
return new LowerString( text );
}
return existingValue;
}
}
public static bool InputWithHint( string label, string hint, ref LowerString s, uint maxLength = 128,
ImGuiInputTextFlags flags = ImGuiInputTextFlags.None )
{
var tmp = s.Text;
if( !ImGui.InputTextWithHint( label, hint, ref tmp, maxLength, flags ) || tmp == s.Text )
{
return false;
}
s = new LowerString( tmp );
return true;
}
public override bool Equals( object? obj )
=> obj is LowerString lowerString && Equals( lowerString );
public override int GetHashCode()
=> Text.GetHashCode();
public static bool operator ==( LowerString lhs, LowerString rhs )
=> lhs.Equals( rhs );
public static bool operator !=( LowerString lhs, LowerString rhs )
=> lhs.Equals( rhs );
public static bool operator ==( LowerString lhs, string rhs )
=> lhs.Equals( rhs );
public static bool operator !=( LowerString lhs, string rhs )
=> lhs.Equals( rhs );
public static bool operator ==( string lhs, LowerString rhs )
=> rhs.Equals( lhs );
public static bool operator !=( string lhs, LowerString rhs )
=> rhs.Equals( lhs );
}

View file

@ -7,7 +7,7 @@ using System.Text.RegularExpressions;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Files;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Util;
@ -74,7 +74,7 @@ public static class ModelChanger
}
}
public static bool ChangeModMaterials( Mod.Mod mod, string from, string to )
public static bool ChangeModMaterials( Mods.Mod mod, string from, string to )
{
if( ValidStrings( from, to ) )
{

View file

@ -1,5 +1,4 @@
using System.IO;
using System.Linq;
namespace Penumbra.Util;