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.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Mod;
using Penumbra.Mods;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -78,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_penumbra!.ObjectReloader.RedrawAll( setting ); _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 ) if( !Penumbra.Config.EnableMods )
{ {

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Mod; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -14,122 +13,39 @@ public partial class ModCollection
// Is invoked after the collections actually changed. // Is invoked after the collections actually changed.
public event CollectionChangeDelegate? CollectionChanged; public event CollectionChangeDelegate? CollectionChanged;
private int _currentIdx = 1; // The collection currently selected for changing settings.
private int _defaultIdx = 0; public ModCollection Current { get; private set; } = Empty;
private int _defaultNameIdx = 0;
public ModCollection Current // The collection used for general file redirections and all characters not specifically named.
=> this[ _currentIdx ]; public ModCollection Default { get; private set; } = Empty;
public ModCollection Default // A single collection that can not be deleted as a fallback for the current collection.
=> this[ _defaultIdx ]; 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 ) public ModCollection Character( string name )
=> _character.TryGetValue( name, out var idx ) ? this[ idx ] : Default; => _characters.TryGetValue( name, out var c ) ? c : Default;
public IEnumerable< (string, ModCollection) > Characters
=> _character.Select( kvp => ( kvp.Key, this[ kvp.Value ] ) );
public bool HasCharacterCollections public bool HasCharacterCollections
=> _character.Count > 0; => _characters.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 );
// Set a active collection, can be used to set Default, Current or Character collections.
public void SetCollection( int newIdx, Type type, string? characterName = null ) public void SetCollection( int newIdx, Type type, string? characterName = null )
{ {
var oldCollectionIdx = type switch var oldCollectionIdx = type switch
{ {
Type.Default => _defaultIdx, Type.Default => Default.Index,
Type.Current => _currentIdx, Type.Current => Current.Index,
Type.Character => characterName?.Length > 0 Type.Character => characterName?.Length > 0
? _character.TryGetValue( characterName, out var c ) ? _characters.TryGetValue( characterName, out var c )
? c ? c.Index
: _defaultIdx : Default.Index
: -1, : -1,
_ => -1, _ => -1,
}; };
@ -142,24 +58,24 @@ public partial class ModCollection
var newCollection = this[ newIdx ]; var newCollection = this[ newIdx ];
if( newIdx > Empty.Index ) if( newIdx > Empty.Index )
{ {
newCollection.CreateCache(false); newCollection.CreateCache( false );
} }
RemoveCache( oldCollectionIdx ); RemoveCache( oldCollectionIdx );
switch( type ) switch( type )
{ {
case Type.Default: case Type.Default:
_defaultIdx = newIdx; Default = newCollection;
Penumbra.Config.DefaultCollection = newCollection.Name; Penumbra.Config.DefaultCollection = newCollection.Name;
Penumbra.ResidentResources.Reload(); Penumbra.ResidentResources.Reload();
Default.SetFiles(); Default.SetFiles();
break; break;
case Type.Current: case Type.Current:
_currentIdx = newIdx; Current = newCollection;
Penumbra.Config.CurrentCollection = newCollection.Name; Penumbra.Config.CurrentCollection = newCollection.Name;
break; break;
case Type.Character: case Type.Character:
_character[ characterName! ] = newIdx; _characters[ characterName! ] = newCollection;
Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name; Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name;
break; break;
} }
@ -168,27 +84,32 @@ public partial class ModCollection
Penumbra.Config.Save(); 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 ) public bool CreateCharacterCollection( string characterName )
{ {
if( _character.ContainsKey( characterName ) ) if( _characters.ContainsKey( characterName ) )
{ {
return false; return false;
} }
_character[ characterName ] = Empty.Index; _characters[ characterName ] = Empty;
Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name; Penumbra.Config.CharacterCollections[ characterName ] = Empty.Name;
Penumbra.Config.Save(); Penumbra.Config.Save();
CollectionChanged?.Invoke( null, Empty, Type.Character, characterName ); CollectionChanged?.Invoke( null, Empty, Type.Character, characterName );
return true; return true;
} }
// Remove a character collection if it exists.
public void RemoveCharacterCollection( string characterName ) public void RemoveCharacterCollection( string characterName )
{ {
if( _character.TryGetValue( characterName, out var collection ) ) if( _characters.TryGetValue( characterName, out var collection ) )
{ {
RemoveCache( collection ); RemoveCache( collection.Index );
_character.Remove( characterName ); _characters.Remove( characterName );
CollectionChanged?.Invoke( this[ collection ], null, Type.Character, characterName ); CollectionChanged?.Invoke( collection, null, Type.Character, characterName );
} }
if( Penumbra.Config.CharacterCollections.Remove( 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 ) private int GetIndexForCollectionName( string name )
{ => name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name );
if( name.Length == 0 )
{
return Empty.Index;
}
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() public void LoadCollections()
{ {
var configChanged = false; var configChanged = false;
_defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection ); var defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection );
if( _defaultIdx < 0 ) if( defaultIdx < 0 )
{ {
PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." ); PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." );
_defaultIdx = Empty.Index; Default = Empty;
Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name; Penumbra.Config.DefaultCollection = Default.Name;
configChanged = true; configChanged = true;
} }
else
{
Default = this[ defaultIdx ];
}
_currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection ); var currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection );
if( _currentIdx < 0 ) if( currentIdx < 0 )
{ {
PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." ); PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." );
_currentIdx = _defaultNameIdx; Current = DefaultName;
Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name; Penumbra.Config.DefaultCollection = Current.Name;
configChanged = true; configChanged = true;
} }
else
{
Current = this[ currentIdx ];
}
if( LoadCharacterCollections() || configChanged ) if( LoadCharacterCollections() || configChanged )
{ {
@ -236,6 +162,7 @@ public partial class ModCollection
CreateNecessaryCaches(); CreateNecessaryCaches();
} }
// Load character collections. If a player name comes up multiple times, the last one is applied.
private bool LoadCharacterCollections() private bool LoadCharacterCollections()
{ {
var configChanged = false; var configChanged = false;
@ -245,17 +172,71 @@ public partial class ModCollection
if( idx < 0 ) if( idx < 0 )
{ {
PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." ); 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; Penumbra.Config.CharacterCollections[ player ] = Empty.Name;
configChanged = true; configChanged = true;
} }
else else
{ {
_character.Add( player, idx ); _characters.Add( player, this[ idx ] );
} }
} }
return configChanged; 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.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -13,19 +14,23 @@ public partial class ModCollection
{ {
public enum Type : byte public enum Type : byte
{ {
Inactive, Inactive, // A collection was added or removed
Default, Default, // The default collection was changed
Character, Character, // A character collection was changed
Current, Current, // The current collection was changed.
} }
public sealed partial class Manager : IDisposable, IEnumerable< ModCollection > 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, public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, Type type,
string? characterName = null ); 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() private readonly List< ModCollection > _collections = new()
{ {
Empty, Empty,
@ -34,25 +39,28 @@ public partial class ModCollection
public ModCollection this[ Index idx ] public ModCollection this[ Index idx ]
=> _collections[ idx ]; => _collections[ idx ];
public ModCollection this[ int idx ]
=> _collections[ idx ];
public ModCollection? this[ string name ] public ModCollection? this[ string name ]
=> ByName( name, out var c ) ? c : null; => 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 ) public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection )
=> _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection ); => _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection );
// Default enumeration skips the empty collection.
public IEnumerator< ModCollection > GetEnumerator() public IEnumerator< ModCollection > GetEnumerator()
=> _collections.Skip( 1 ).GetEnumerator(); => _collections.Skip( 1 ).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
public Manager( Mod.Mod.Manager manager ) public Manager( Mods.Mod.Manager manager )
{ {
_modManager = manager; _modManager = manager;
// The collection manager reacts to changes in mods by itself.
_modManager.ModsRediscovered += OnModsRediscovered; _modManager.ModsRediscovered += OnModsRediscovered;
_modManager.ModChange += OnModChanged; _modManager.ModChange += OnModChanged;
ReadCollections(); ReadCollections();
@ -65,27 +73,143 @@ public partial class ModCollection
_modManager.ModChange -= OnModChanged; _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() private void OnModsRediscovered()
{ {
// When mods are rediscovered, force all cache updates and set the files of the default collection.
ForceCacheUpdates(); ForceCacheUpdates();
Default.SetFiles(); 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() private void AddDefaultCollection()
{ {
var idx = _collections.IndexOf( c => c.Name == DefaultCollection ); var idx = GetIndexForCollectionName( DefaultCollection );
if( idx >= 0 ) if( idx >= 0 )
{ {
_defaultNameIdx = idx; DefaultName = this[ idx ];
return; return;
} }
var defaultCollection = CreateNewEmpty( DefaultCollection ); var defaultCollection = CreateNewEmpty( DefaultCollection );
defaultCollection.Save(); defaultCollection.Save();
_defaultNameIdx = _collections.Count; defaultCollection.Index = _collections.Count;
_collections.Add( defaultCollection ); _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 ) private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances )
{ {
foreach( var (collection, inheritance) in this.Zip( 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() private void ReadCollections()
{ {
var collectionDir = new DirectoryInfo( CollectionDirectory ); var collectionDir = new DirectoryInfo( CollectionDirectory );
@ -152,89 +279,5 @@ public partial class ModCollection
AddDefaultCollection(); AddDefaultCollection();
ApplyInheritancesAndFixSettings( inheritances ); 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 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 Mod1;
public readonly int Mod2; public readonly int Mod2;
public readonly bool Mod1Priority; public readonly bool Mod1Priority;
public readonly bool Solved; 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; Mod1 = modIdx1;
Mod2 = modIdx2; Mod2 = modIdx2;
Conflict = conflict; Data = data;
Mod1Priority = priority1 >= priority2; Mod1Priority = priority;
Solved = priority1 != priority2; 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 ); var idxComp = Mod1.CompareTo( other.Mod1 );
if( idxComp != 0 ) if( idxComp != 0 )
@ -44,55 +46,85 @@ public struct ConflictCache
return idxComp; return idxComp;
} }
return Conflict switch return Data switch
{ {
Utf8GamePath p when other.Conflict is Utf8GamePath q => p.CompareTo( q ), Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ),
Utf8GamePath => -1, Utf8GamePath => -1,
MetaManipulation m when other.Conflict is MetaManipulation n => m.CompareTo( n ), MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ),
MetaManipulation => 1, MetaManipulation => 1,
_ => 0, _ => 0,
}; };
} }
} }
private List< ModCacheStruct >? _conflicts; private readonly List< Conflict > _conflicts = new();
private bool _isSorted = true;
public IReadOnlyList< ModCacheStruct > Conflicts public ConflictCache()
=> _conflicts ?? ( IReadOnlyList< ModCacheStruct > )Array.Empty< ModCacheStruct >(); { }
public IEnumerable< ModCacheStruct > ModConflicts( int modIdx ) public IReadOnlyList< Conflict > Conflicts
{ {
return _conflicts?.SkipWhile( c => c.Mod1 < modIdx ).TakeWhile( c => c.Mod1 == modIdx ) get
?? Array.Empty< ModCacheStruct >(); {
Sort();
return _conflicts;
}
} }
public void Sort() // Find all mod conflicts concerning the specified mod (in both directions).
=> _conflicts?.Sort(); 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 ) public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath )
{ => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath );
_conflicts ??= new List< ModCacheStruct >( 2 );
_conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, gamePath ) );
_conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, gamePath ) );
}
public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation ) public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation )
{ => AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation );
_conflicts ??= new List< ModCacheStruct >( 2 );
_conflicts.Add( new ModCacheStruct( modIdx1, modIdx2, priority1, priority2, manipulation ) );
_conflicts.Add( new ModCacheStruct( modIdx2, modIdx1, priority2, priority1, manipulation ) );
}
public void ClearConflicts() public void ClearConflicts()
=> _conflicts?.Clear(); => _conflicts?.Clear();
public void ClearFileConflicts() public void ClearFileConflicts()
=> _conflicts?.RemoveAll( m => m.Conflict is Utf8GamePath ); => _conflicts?.RemoveAll( m => m.Data is Utf8GamePath );
public void ClearMetaConflicts() public void ClearMetaConflicts()
=> _conflicts?.RemoveAll( m => m.Conflict is MetaManipulation ); => _conflicts?.RemoveAll( m => m.Data is MetaManipulation );
public void ClearConflictsWithMod( int modIdx ) 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 Dalamud.Logging;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manager; using Penumbra.Meta.Manager;
using Penumbra.Mod; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public partial class ModCollection public partial class ModCollection
{ {
// Only active collections need to have a cache.
private Cache? _cache; private Cache? _cache;
public bool HasCache public bool HasCache
=> _cache != null; => _cache != null;
// Only create, do not update.
public void CreateCache( bool isDefault ) public void CreateCache( bool isDefault )
{ {
if( Index == 0 )
{
return;
}
if( _cache == null ) if( _cache == null )
{ {
CalculateEffectiveFileList( true, isDefault ); CalculateEffectiveFileList( true, isDefault );
} }
} }
// Force an update with metadata for this cache.
public void ForceCacheUpdate( bool isDefault ) public void ForceCacheUpdate( bool isDefault )
=> CalculateEffectiveFileList( true, isDefault ); => CalculateEffectiveFileList( true, isDefault );
// Clear the current cache.
public void ClearCache() public void ClearCache()
{ {
_cache?.Dispose(); _cache?.Dispose();
_cache = null; _cache = null;
} }
public FullPath? ResolvePath( Utf8GamePath path ) public FullPath? ResolvePath( Utf8GamePath path )
=> _cache?.ResolvePath( path ); => _cache?.ResolvePath( path );
// Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile( Utf8GamePath path, FullPath fullPath ) internal void ForceFile( Utf8GamePath path, FullPath fullPath )
=> _cache!.ResolvedFiles[ path ] = fullPath; => _cache!.ResolvedFiles[ path ] = fullPath;
// Force a file resolve to be removed.
internal void RemoveFile( Utf8GamePath path ) internal void RemoveFile( Utf8GamePath path )
=> _cache!.ResolvedFiles.Remove( path ); => _cache!.ResolvedFiles.Remove( path );
// Obtain data from the cache.
internal MetaManager? MetaCache internal MetaManager? MetaCache
=> _cache?.MetaManipulations; => _cache?.MetaManipulations;
@ -62,14 +66,17 @@ public partial class ModCollection
internal IReadOnlyDictionary< string, object? > ChangedItems internal IReadOnlyDictionary< string, object? > ChangedItems
=> _cache?.ChangedItems ?? new Dictionary< string, object? >(); => _cache?.ChangedItems ?? new Dictionary< string, object? >();
internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts internal IReadOnlyList< ConflictCache.Conflict > Conflicts
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >(); => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
internal IEnumerable< ConflictCache.ModCacheStruct > ModConflicts( int modIdx ) internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx )
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.ModCacheStruct >(); => _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 ) public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident )
{ {
// Skip the empty collection.
if( Index == 0 ) if( Index == 0 )
{ {
return; return;
@ -87,8 +94,84 @@ public partial class ModCollection
{ {
Penumbra.ResidentResources.Reload(); 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 Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new(); public readonly HashSet< FullPath > MissingFiles = new();
public readonly MetaManager MetaManipulations; 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 public IReadOnlyDictionary< string, object? > ChangedItems
{ {
get get
@ -117,6 +201,7 @@ public partial class ModCollection
} }
} }
// The cache reacts through events on its collection changing.
public Cache( ModCollection collection ) public Cache( ModCollection collection )
{ {
_collection = collection; _collection = collection;
@ -133,6 +218,8 @@ public partial class ModCollection
private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) 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() if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any()
|| type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled )
{ {
@ -143,9 +230,12 @@ public partial class ModCollection
_collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection );
} }
// Inheritance changes are too big to check for relevance,
// just recompute everything.
private void OnInheritanceChange( bool _ ) private void OnInheritanceChange( bool _ )
=> _collection.CalculateEffectiveFileList( true, true ); => _collection.CalculateEffectiveFileList( true, true );
// Reset the shared file-seen cache.
private static void ResetFileSeen( int size ) private static void ResetFileSeen( int size )
{ {
if( size < FileSeen.Length ) 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() private void ClearStorageAndPrepare()
{ {
ResolvedFiles.Clear(); ResolvedFiles.Clear();
@ -167,15 +259,15 @@ public partial class ModCollection
RegisteredFiles.Clear(); RegisteredFiles.Clear();
_changedItems.Clear(); _changedItems.Clear();
ResolvedSettings.Clear(); ResolvedSettings.Clear();
Conflicts.ClearFileConflicts();
// Obtains actual settings for this collection with all inheritances.
ResolvedSettings.AddRange( _collection.ActualSettings ); ResolvedSettings.AddRange( _collection.ActualSettings );
} }
public void CalculateEffectiveFileList() public void CalculateEffectiveFileList()
{ {
ClearStorageAndPrepare(); ClearStorageAndPrepare();
for( var i = 0; i < Penumbra.ModManager.Count; ++i )
Conflicts.ClearFileConflicts();
for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i )
{ {
if( ResolvedSettings[ i ]?.Enabled == true ) if( ResolvedSettings[ i ]?.Enabled == true )
{ {
@ -185,7 +277,6 @@ public partial class ModCollection
} }
AddMetaFiles(); AddMetaFiles();
Conflicts.Sort();
} }
private void SetChangedItems() private void SetChangedItems()
@ -204,6 +295,7 @@ public partial class ModCollection
{ {
identifier.Identify( _changedItems, resolved.ToGamePath() ); identifier.Identify( _changedItems, resolved.ToGamePath() );
} }
// TODO: Meta Manipulations
} }
catch( Exception e ) catch( Exception e )
{ {
@ -211,12 +303,12 @@ public partial class ModCollection
} }
} }
private void AddFiles( int idx ) private void AddFiles( int idx )
{ {
var mod = Penumbra.ModManager.Mods[ idx ]; var mod = Penumbra.ModManager.Mods[ idx ];
ResetFileSeen( mod.Resources.ModFiles.Count ); ResetFileSeen( mod.Resources.ModFiles.Count );
// Iterate in reverse so that later groups take precedence before earlier ones. // 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() ) foreach( var group in mod.Meta.Groups.Values.Reverse() )
{ {
switch( group.SelectionType ) switch( group.SelectionType )
@ -240,7 +332,6 @@ public partial class ModCollection
=> !Penumbra.Config.DisableSoundStreaming => !Penumbra.Config.DisableSoundStreaming
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); && gamePath.Path.EndsWith( '.', 's', 'c', 'd' );
private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file )
{ {
if( FilterFile( gamePath ) ) if( FilterFile( gamePath ) )
@ -250,11 +341,13 @@ public partial class ModCollection
if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) )
{ {
// No current conflict, just add.
RegisteredFiles.Add( gamePath, modIdx ); RegisteredFiles.Add( gamePath, modIdx );
ResolvedFiles[ gamePath ] = file; ResolvedFiles[ gamePath ] = file;
} }
else else
{ {
// Conflict, check which mod has higher priority, replace if necessary, add conflict.
var priority = ResolvedSettings[ modIdx ]!.Priority; var priority = ResolvedSettings[ modIdx ]!.Priority;
var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority;
Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath );
@ -270,6 +363,8 @@ public partial class ModCollection
{ {
switch( file.Extension.ToLowerInvariant() ) switch( file.Extension.ToLowerInvariant() )
{ {
// We do not care for those file types
case ".scp" when !Penumbra.Config.DisableSoundStreaming:
case ".meta": case ".meta":
case ".rgsp": case ".rgsp":
return; 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 ) foreach( var (file, paths) in option.OptionFiles )
{ {
// TODO: complete rework of options.
var fullPath = new FullPath( mod.BasePath, file ); var fullPath = new FullPath( mod.BasePath, file );
var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
if( idx < 0 ) 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 ); Debug.Assert( singleGroup.SelectionType == SelectType.Single );
var settings = ResolvedSettings[ modIdx ]!; 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 ); Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
var settings = ResolvedSettings[ modIdx ]!; 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 ) for( var i = 0; i < mod.Resources.ModFiles.Count; ++i )
{ {
@ -431,81 +527,4 @@ public partial class ModCollection
return candidate; 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 System;
using Penumbra.Mod; using Penumbra.Mods;
namespace Penumbra.Collections; namespace Penumbra.Collections;
// Different types a mod setting can change:
public enum ModSettingChange public enum ModSettingChange
{ {
Inheritance, Inheritance, // it was set to inherit from other collections or not inherit anymore
EnableState, EnableState, // it was enabled or disabled
Priority, Priority, // its priority was changed
Setting, Setting, // a specific setting was changed
} }
public partial class ModCollection 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 delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited );
public event ModSettingChangeDelegate ModSettingChanged; public event ModSettingChangeDelegate ModSettingChanged;
@ -99,13 +102,13 @@ public partial class ModCollection
private bool FixInheritance( int idx, bool inherit ) private bool FixInheritance( int idx, bool inherit )
{ {
var settings = _settings[ idx ]; 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 false;
return true;
} }
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 ) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Mod; using Penumbra.Mods;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Collections; 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 public partial class ModCollection
{ {
private readonly List< ModCollection > _inheritance = new(); // A change in inheritance usually requires complete recomputation.
public event Action< bool > InheritanceChanged; public event Action< bool > InheritanceChanged;
private readonly List< ModCollection > _inheritance = new();
public IReadOnlyList< ModCollection > Inheritance public IReadOnlyList< ModCollection > Inheritance
=> _inheritance; => _inheritance;
// Iterate over all collections inherited from in depth-first order.
// Skip already visited collections to avoid circular dependencies.
public IEnumerable< ModCollection > GetFlattenedInheritance() public IEnumerable< ModCollection > GetFlattenedInheritance()
{ {
yield return this; 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 ) public bool AddInheritance( ModCollection collection )
{ {
if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) ) if( ReferenceEquals( collection, this ) || _inheritance.Contains( collection ) )
@ -35,6 +44,7 @@ public partial class ModCollection
} }
_inheritance.Add( collection ); _inheritance.Add( collection );
// Changes in inherited collections may need to trigger further changes here.
collection.ModSettingChanged += OnInheritedModSettingChange; collection.ModSettingChanged += OnInheritedModSettingChange;
collection.InheritanceChanged += OnInheritedInheritanceChange; collection.InheritanceChanged += OnInheritedInheritanceChange;
InheritanceChanged.Invoke( false ); InheritanceChanged.Invoke( false );
@ -50,6 +60,7 @@ public partial class ModCollection
InheritanceChanged.Invoke( false ); InheritanceChanged.Invoke( false );
} }
// Order in the inheritance list is relevant.
public void MoveInheritance( int from, int to ) public void MoveInheritance( int from, int to )
{ {
if( _inheritance.Move( from, 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 _ ) private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ )
{ {
if( _settings[ modIdx ] == null ) if( _settings[ modIdx ] == null )
@ -69,13 +81,16 @@ public partial class ModCollection
private void OnInheritedInheritanceChange( bool _ ) private void OnInheritedInheritanceChange( bool _ )
=> InheritanceChanged.Invoke( true ); => 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 ] public (ModSettings? Settings, ModCollection Collection) this[ Index idx ]
{ {
get get
{ {
foreach( var collection in GetFlattenedInheritance() ) foreach( var collection in GetFlattenedInheritance() )
{ {
var settings = _settings[ idx ]; var settings = collection._settings[ idx ];
if( settings != null ) if( settings != null )
{ {
return ( settings, collection ); return ( settings, collection );

View file

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Mod; using Penumbra.Mods;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public sealed partial class ModCollection public sealed partial class ModCollection
{ {
// Migration to convert ModCollections from older versions to newer.
private static class Migration private static class Migration
{ {
public static void Migrate( ModCollection collection ) public static void Migrate( ModCollection collection )
@ -24,9 +26,10 @@ public sealed partial class ModCollection
} }
collection.Version = 1; collection.Version = 1;
// Remove all completely defaulted settings from active and inactive mods.
for( var i = 0; i < collection._settings.Count; ++i ) for( var i = 0; i < collection._settings.Count; ++i )
{ {
var setting = collection._settings[ i ];
if( SettingIsDefaultV0( collection._settings[ i ] ) ) if( SettingIsDefaultV0( collection._settings[ i ] ) )
{ {
collection._settings[ i ] = null; collection._settings[ i ] = null;
@ -41,7 +44,11 @@ public sealed partial class ModCollection
return true; return true;
} }
// We treat every completely defaulted setting as inheritance-ready.
private static bool SettingIsDefaultV0( ModSettings? setting ) private static bool SettingIsDefaultV0( ModSettings? setting )
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); => 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using Penumbra.Mods;
using Dalamud.Logging;
using Newtonsoft.Json.Linq;
using Penumbra.Mod;
using Penumbra.Util;
namespace Penumbra.Collections; 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. // 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. // 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 partial class ModCollection
{ {
public const int CurrentVersion = 1; public const int CurrentVersion = 1;
@ -24,29 +31,28 @@ public partial class ModCollection
public static readonly ModCollection Empty = CreateEmpty(); public static readonly ModCollection Empty = CreateEmpty();
private static ModCollection CreateEmpty() // The collection name can contain invalid path characters,
{ // but after removing those and going to lower case it has to be unique.
var collection = CreateNewEmpty( EmptyCollection );
collection.Index = 0;
collection._settings.Clear();
return collection;
}
public string Name { get; private init; } public string Name { get; private init; }
public int Version { get; private set; } public int Version { get; private set; }
public int Index { get; private set; } = -1; 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; private readonly List< ModSettings? > _settings;
public IReadOnlyList< ModSettings? > Settings public IReadOnlyList< ModSettings? > Settings
=> _settings; => _settings;
// Evaluates the settings along the whole inheritance tree.
public IEnumerable< ModSettings? > ActualSettings public IEnumerable< ModSettings? > ActualSettings
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); => 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; private readonly Dictionary< string, ModSettings > _unusedSettings;
// Constructor for duplication.
private ModCollection( string name, ModCollection duplicate ) private ModCollection( string name, ModCollection duplicate )
{ {
Name = name; Name = name;
@ -58,6 +64,7 @@ public partial class ModCollection
InheritanceChanged += SaveOnChange; InheritanceChanged += SaveOnChange;
} }
// Constructor for reading from files.
private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings )
{ {
Name = name; Name = name;
@ -79,15 +86,15 @@ public partial class ModCollection
InheritanceChanged += SaveOnChange; InheritanceChanged += SaveOnChange;
} }
// Create a new, unique empty collection of a given name.
public static ModCollection CreateNewEmpty( string name ) public static ModCollection CreateNewEmpty( string name )
=> new(name, CurrentVersion, new Dictionary< string, ModSettings >()); => 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 ) public ModCollection Duplicate( string name )
=> new(name, this); => new(name, this);
internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) // Remove all settings for not currently-installed mods.
=> new(name, 0, allSettings);
public void CleanUnavailableSettings() public void CleanUnavailableSettings()
{ {
var any = _unusedSettings.Count > 0; 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 ) ) 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 ]; var settings = _settings[ idx ];
if( settings != null ) if( settings != null )
@ -121,104 +130,4 @@ public partial class ModCollection
_settings.RemoveAt( idx ); _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; namespace Penumbra;
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public partial class Configuration : IPluginConfiguration
{ {
private const int CurrentVersion = 1; private const int CurrentVersion = 1;
@ -36,7 +37,7 @@ public class Configuration : IPluginConfiguration
public string CurrentCollection { get; set; } = "Default"; public string CurrentCollection { get; set; } = "Default";
public string DefaultCollection { get; set; } = "Default"; public string DefaultCollection { get; set; } = "Default";
public string ForcedCollection { get; set; } = "";
public bool SortFoldersFirst { get; set; } = false; public bool SortFoldersFirst { get; set; } = false;
public bool HasReadCharacterCollectionDesc { 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 > CharacterCollections { get; set; } = new();
public Dictionary< string, string > ModSortOrder { get; set; } = new(); public Dictionary< string, string > ModSortOrder { get; set; } = new();
public bool InvertModListOrder { internal get; set; }
public static Configuration Load() public static Configuration Load()
{ {

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Mods;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Linq; using System.Linq;
using Penumbra.Mod;
namespace Penumbra.Mods; namespace Penumbra.Mods;
@ -37,7 +36,7 @@ public static partial class ModFileSystem
// Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes.
// Saves and returns true if anything changed. // 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 ) ) if( RenameNoSave( mod, newName ) )
{ {
@ -63,7 +62,7 @@ public static partial class ModFileSystem
// Move a single mod to the target folder. // Move a single mod to the target folder.
// Returns true and saves if anything changed. // 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 ) ) 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. // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName.
// Creates all necessary Subfolders. // 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 split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries );
var folder = Root; 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. // 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 ) 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 ); Penumbra.Config.ModSortOrder.Remove( mod.BasePath.Name );
} }
@ -184,7 +183,7 @@ public static partial class ModFileSystem
return true; 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( '/', '\\' ); newName = newName.Replace( '/', '\\' );
if( mod.Order.SortOrderName == newName ) if( mod.Order.SortOrderName == newName )
@ -193,12 +192,12 @@ public static partial class ModFileSystem
} }
mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); 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 ); mod.Order.ParentFolder.AddMod( mod );
return true; 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; var oldParent = mod.Order.ParentFolder;
if( ReferenceEquals( target, oldParent ) ) if( ReferenceEquals( target, oldParent ) )
@ -207,7 +206,7 @@ public static partial class ModFileSystem
} }
oldParent.RemoveMod( mod ); 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 ); target.AddMod( mod );
return true; return true;
} }

View file

@ -1,12 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Mod;
namespace Penumbra.Mods namespace Penumbra.Mods;
public partial class ModFolder
{ {
public partial class ModFolder
{
public ModFolder? Parent; public ModFolder? Parent;
public string FullName public string FullName
@ -27,7 +26,7 @@ namespace Penumbra.Mods
} }
public List< ModFolder > SubFolders { get; } = new(); public List< ModFolder > SubFolders { get; } = new();
public List< Mod.Mod > Mods { get; } = new(); public List< Mod > Mods { get; } = new();
public ModFolder( ModFolder parent, string name ) public ModFolder( ModFolder parent, string name )
{ {
@ -45,7 +44,7 @@ namespace Penumbra.Mods
=> SubFolders.Sum( f => f.TotalDescendantFolders() ); => SubFolders.Sum( f => f.TotalDescendantFolders() );
// Return all descendant mods in the specified order. // Return all descendant mods in the specified order.
public IEnumerable< Mod.Mod > AllMods( bool foldersFirst ) public IEnumerable< Mod > AllMods( bool foldersFirst )
{ {
if( foldersFirst ) if( foldersFirst )
{ {
@ -59,7 +58,7 @@ namespace Penumbra.Mods
return folder.AllMods( false ); return folder.AllMods( false );
} }
return new[] { ( Mod.Mod )f }; return new[] { ( Mod )f };
} ); } );
} }
@ -116,7 +115,7 @@ namespace Penumbra.Mods
// Add the given mod as a child, if it is not already a child. // Add the given mod as a child, if it is not already a child.
// Returns the index of the found or inserted mod. // Returns the index of the found or inserted mod.
public int AddMod( Mod.Mod mod ) public int AddMod( Mod mod )
{ {
var idx = Mods.BinarySearch( mod, ModComparer ); var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 ) if( idx >= 0 )
@ -132,19 +131,19 @@ namespace Penumbra.Mods
// Remove mod as a child if it exists. // Remove mod as a child if it exists.
// If this folder is empty afterwards, remove it from its parent. // If this folder is empty afterwards, remove it from its parent.
public void RemoveMod( Mod.Mod mod ) public void RemoveMod( Mod mod )
{ {
RemoveModIgnoreEmpty( mod ); RemoveModIgnoreEmpty( mod );
CheckEmpty(); CheckEmpty();
} }
} }
// Internals // Internals
public partial class ModFolder public partial class ModFolder
{ {
// Create a Root folder without parent. // Create a Root folder without parent.
internal static ModFolder CreateRoot() internal static ModFolder CreateRoot()
=> new( null!, string.Empty ); => new(null!, string.Empty);
internal class ModFolderComparer : IComparer< ModFolder > internal class ModFolderComparer : IComparer< ModFolder >
{ {
@ -157,13 +156,13 @@ namespace Penumbra.Mods
: string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType );
} }
internal class ModDataComparer : IComparer< Mod.Mod > internal class ModDataComparer : IComparer< Mod >
{ {
public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; 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. // 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. // 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 ) public int Compare( Mod? x, Mod? y )
{ {
if( ReferenceEquals( x, y ) ) if( ReferenceEquals( x, y ) )
{ {
@ -235,7 +234,7 @@ namespace Penumbra.Mods
} }
// Remove a mod, but do not remove this folder from its parent if it is empty afterwards. // Remove a mod, but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveModIgnoreEmpty( Mod.Mod mod ) internal void RemoveModIgnoreEmpty( Mod mod )
{ {
var idx = Mods.BinarySearch( mod, ModComparer ); var idx = Mods.BinarySearch( mod, ModComparer );
if( idx >= 0 ) if( idx >= 0 )
@ -243,5 +242,4 @@ namespace Penumbra.Mods
Mods.RemoveAt( idx ); Mods.RemoveAt( idx );
} }
} }
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Penumbra.Mod; namespace Penumbra.Mods;
// Contains settings with the option selections stored by names instead of index. // 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. // 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;
using System.IO;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
@ -10,13 +9,12 @@ using Lumina.Excel.GeneratedSheets;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Mods;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.Util; using Penumbra.Util;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
using Penumbra.Interop.Resolver; using Penumbra.Interop.Resolver;
using Penumbra.Mod; using Penumbra.Mods;
namespace Penumbra; namespace Penumbra;
@ -34,8 +32,9 @@ public class Penumbra : IDalamudPlugin
public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { 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 ModCollection.Manager CollectionManager { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; set; } = null!; public static ResourceLoader ResourceLoader { get; set; } = null!;
@ -65,9 +64,10 @@ public class Penumbra : IDalamudPlugin
ResidentResources = new ResidentResourceManager(); ResidentResources = new ResidentResourceManager();
CharacterUtility = new CharacterUtility(); CharacterUtility = new CharacterUtility();
MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader( this ); ResourceLoader = new ResourceLoader( this );
ResourceLogger = new ResourceLogger( ResourceLoader ); ResourceLogger = new ResourceLogger( ResourceLoader );
ModManager = new Mod.Mod.Manager(); ModManager = new Mod.Manager();
ModManager.DiscoverMods(); ModManager.DiscoverMods();
CollectionManager = new ModCollection.Manager( ModManager ); CollectionManager = new ModCollection.Manager( ModManager );
ObjectReloader = new ObjectReloader(); ObjectReloader = new ObjectReloader();
@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin
PathResolver.Dispose(); PathResolver.Dispose();
ResourceLogger.Dispose(); ResourceLogger.Dispose();
MetaFileManager.Dispose();
ResourceLoader.Dispose(); ResourceLoader.Dispose();
CharacterUtility.Dispose(); CharacterUtility.Dispose();

View file

@ -7,7 +7,6 @@ using Dalamud.Interface.Components;
using Dalamud.Logging; using Dalamud.Logging;
using ImGuiNET; using ImGuiNET;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util; using Penumbra.Util;
@ -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() private void DrawDefaultCollectionSelector()
{ {
var index = _currentDefaultIndex; var index = _currentDefaultIndex;
@ -344,12 +402,14 @@ public partial class SettingsInterface
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ) using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem )
.Push( ImGui.EndChild ); .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 ); DrawCurrentCollectionSelector( true );
ImGuiHelpers.ScaledDummy( 0, 10 ); ImGuiHelpers.ScaledDummy( 0, 10 );
DrawNewCollectionInput(); DrawNewCollectionInput();
ImGuiHelpers.ScaledDummy( 0, 10 );
DrawCurrentCollectionInheritance();
DrawNewInheritanceSelection();
} }
raii.Pop(); raii.Pop();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ using Dalamud.Logging;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Mods;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util; 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 Dalamud.Logging;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Mod; using Penumbra.Mods;
namespace Penumbra.Util; 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 ) ) if( ValidStrings( from, to ) )
{ {

View file

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