Change cache reloading and conflicts to actually keep the effective mod and not force full recalculations on every change.

This commit is contained in:
Ottermandias 2022-05-29 19:00:34 +02:00
parent ee87098386
commit 4b036c6c26
29 changed files with 778 additions and 457 deletions

@ -1 +1 @@
Subproject commit d6fcf1f53888d5eec8196eaba2dbb3853534d3bf
Subproject commit 3679cb37d5cc04351c064b1372a6eac51c7375a8

View file

@ -7,45 +7,43 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
namespace Penumbra.GameData
namespace Penumbra.GameData;
public static class GameData
{
public static class GameData
{
internal static ObjectIdentification? Identification;
internal static readonly GamePathParser GamePathParser = new();
internal static ObjectIdentification? Identification;
internal static readonly GamePathParser GamePathParser = new();
public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage )
public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage )
{
Identification ??= new ObjectIdentification( dataManager, clientLanguage );
return Identification;
}
public static IObjectIdentifier GetIdentifier()
{
if( Identification == null )
{
Identification ??= new ObjectIdentification( dataManager, clientLanguage );
return Identification;
throw new Exception( "Object Identification was not initialized." );
}
public static IObjectIdentifier GetIdentifier()
{
if( Identification == null )
{
throw new Exception( "Object Identification was not initialized." );
}
return Identification;
}
public static IGamePathParser GetGamePathParser()
=> GamePathParser;
return Identification;
}
public interface IObjectIdentifier
{
public void Identify( IDictionary< string, object? > set, GamePath path );
public Dictionary< string, object? > Identify( GamePath path );
public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot );
}
public interface IGamePathParser
{
public ObjectType PathToObjectType( GamePath path );
public GameObjectInfo GetFileInfo( GamePath path );
public string VfxToKey( GamePath path );
}
public static IGamePathParser GetGamePathParser()
=> GamePathParser;
}
public interface IObjectIdentifier
{
public void Identify( IDictionary< string, object? > set, GamePath path );
public Dictionary< string, object? > Identify( GamePath path );
public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot );
}
public interface IGamePathParser
{
public ObjectType PathToObjectType( GamePath path );
public GameObjectInfo GetFileInfo( GamePath path );
public string VfxToKey( GamePath path );
}

View file

@ -36,7 +36,7 @@ public class ModsController : WebApiController
{
return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary(
o => o.Key.ToString(),
o => o.Value.FullName
o => o.Value.Path.FullName
)
?? new Dictionary< string, string >();
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
@ -138,7 +139,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if( collection.HasCache )
{
return collection.ChangedItems;
return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 );
}
PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." );

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Dalamud.Logging;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.Mods;
@ -22,13 +23,13 @@ public enum RedirectResult
public class SimpleRedirectManager
{
internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new();
public readonly HashSet< string > AllowedTags = new();
public readonly HashSet< string > AllowedTags = new();
public void Apply( IDictionary< Utf8GamePath, FullPath > dict )
public void Apply( IDictionary< Utf8GamePath, ModPath > dict )
{
foreach( var (gamePath, (file, _)) in Replacements )
{
dict.TryAdd( gamePath, file );
dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) );
}
}

View file

@ -59,7 +59,7 @@ public partial class ModCollection
var newCollection = this[ newIdx ];
if( newIdx > Empty.Index )
{
newCollection.CreateCache( false );
newCollection.CreateCache();
}
RemoveCache( oldCollectionIdx );
@ -122,7 +122,7 @@ public partial class ModCollection
var configChanged = !ReadActiveCollections( out var jObject );
// Load the default collection.
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? (configChanged ? DefaultCollection : Empty.Name);
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name );
var defaultIdx = GetIndexForCollectionName( defaultName );
if( defaultIdx < 0 )
{
@ -250,12 +250,12 @@ public partial class ModCollection
// Cache handling.
private void CreateNecessaryCaches()
{
Default.CreateCache( true );
Current.CreateCache( false );
Default.CreateCache();
Current.CreateCache();
foreach( var collection in _characters.Values )
{
collection.CreateCache( false );
collection.CreateCache();
}
}
@ -268,27 +268,27 @@ public partial class ModCollection
}
// Recalculate effective files for active collections on events.
private void OnModAddedActive( bool meta )
private void OnModAddedActive( Mod mod )
{
foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) )
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
collection._cache!.AddMod( mod, true );
}
}
private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings )
private void OnModRemovedActive( Mod mod )
{
foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
collection._cache!.RemoveMod( mod, true );
}
}
private void OnModChangedActive( bool meta, int modIdx )
private void OnModMovedActive( Mod mod )
{
foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) )
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) )
{
collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default );
collection._cache!.ReloadMod( mod, true );
}
}
}

View file

@ -204,7 +204,7 @@ public partial class ModCollection
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach( var collection in this )
{
collection.ForceCacheUpdate( collection == Default );
collection.ForceCacheUpdate();
}
}
@ -221,27 +221,29 @@ public partial class ModCollection
collection.AddMod( mod );
}
OnModAddedActive( mod.TotalManipulations > 0 );
OnModAddedActive( mod );
break;
case ModPathChangeType.Deleted:
var settings = this.Select( c => c[mod.Index].Settings ).ToList();
OnModRemovedActive( mod );
foreach( var collection in this )
{
collection.RemoveMod( mod, mod.Index );
}
OnModRemovedActive( mod.TotalManipulations > 0, settings );
break;
case ModPathChangeType.Moved:
OnModMovedActive( mod );
foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) )
{
collection.Save();
}
OnModChangedActive( mod.TotalManipulations > 0, mod.Index );
break;
case ModPathChangeType.StartingReload:
OnModRemovedActive( mod );
break;
case ModPathChangeType.Reloaded:
OnModChangedActive( mod.TotalManipulations > 0, mod.Index );
OnModAddedActive( mod );
break;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
}
@ -252,25 +254,24 @@ public partial class ModCollection
// And also updating effective file and meta manipulation lists if necessary.
private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
{
var (handleChanges, recomputeList, withMeta) = type switch
// Handle changes that break revertability.
if( type == ModOptionChangeType.PrepareChange )
{
ModOptionChangeType.GroupRenamed => ( true, false, false ),
ModOptionChangeType.GroupAdded => ( true, false, false ),
ModOptionChangeType.GroupDeleted => ( true, true, true ),
ModOptionChangeType.GroupMoved => ( true, false, false ),
ModOptionChangeType.GroupTypeChanged => ( true, true, true ),
ModOptionChangeType.PriorityChanged => ( true, true, true ),
ModOptionChangeType.OptionAdded => ( true, true, true ),
ModOptionChangeType.OptionDeleted => ( true, true, true ),
ModOptionChangeType.OptionMoved => ( true, false, false ),
ModOptionChangeType.OptionFilesChanged => ( false, true, false ),
ModOptionChangeType.OptionSwapsChanged => ( false, true, false ),
ModOptionChangeType.OptionMetaChanged => ( false, true, true ),
ModOptionChangeType.DisplayChange => ( false, false, false ),
_ => ( false, false, false ),
};
foreach( var collection in this.Where( c => c.HasCache ) )
{
if( collection[ mod.Index ].Settings is { Enabled: true } )
{
collection._cache!.RemoveMod( mod, false );
}
}
if( handleChanges )
return;
}
type.HandlingInfo( out var requiresSaving, out var recomputeList, out var reload );
// Handle changes that require overwriting the collection.
if( requiresSaving )
{
foreach( var collection in this )
{
@ -281,14 +282,22 @@ public partial class ModCollection
}
}
// Handle changes that reload the mod if the changes did not need to be prepared,
// or re-add the mod if they were prepared.
if( recomputeList )
{
// TODO: Does not check if the option that was changed is actually enabled.
foreach( var collection in this.Where( c => c.HasCache ) )
{
if( collection[ mod.Index ].Settings is { Enabled: true } )
{
collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default );
if( reload )
{
collection._cache!.ReloadMod( mod, true );
}
else
{
collection._cache!.AddMod( mod, true );
}
}
}
}

View file

@ -6,6 +6,7 @@ using Dalamud.Logging;
using OtterGui.Classes;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manager;
using Penumbra.Mods;
namespace Penumbra.Collections;
@ -18,21 +19,21 @@ public partial class ModCollection
=> _cache != null;
public int RecomputeCounter
=> _cache?.RecomputeCounter ?? 0;
=> _cache?.ChangeCounter ?? 0;
// Only create, do not update.
private void CreateCache( bool isDefault )
private void CreateCache()
{
if( _cache == null )
{
CalculateEffectiveFileList( true, isDefault );
CalculateEffectiveFileList();
PluginLog.Verbose( "Created new cache for collection {Name:l}.", Name );
}
}
// Force an update with metadata for this cache.
private void ForceCacheUpdate( bool isDefault )
=> CalculateEffectiveFileList( true, isDefault );
private void ForceCacheUpdate()
=> CalculateEffectiveFileList();
// Clear the current cache.
@ -49,7 +50,7 @@ public partial class ModCollection
// Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile( Utf8GamePath path, FullPath fullPath )
=> _cache!.ResolvedFiles[ path ] = fullPath;
=> _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath );
// Force a file resolve to be removed.
internal void RemoveFile( Utf8GamePath path )
@ -59,25 +60,25 @@ public partial class ModCollection
internal MetaManager? MetaCache
=> _cache?.MetaManipulations;
internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles
=> _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >();
internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles
=> _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >();
internal IReadOnlyDictionary< string, object? > ChangedItems
=> _cache?.ChangedItems ?? new Dictionary< string, object? >();
internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems
=> _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >();
internal IReadOnlyList< ConflictCache.Conflict > Conflicts
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >();
internal IEnumerable< SingleArray< ModConflicts > > AllConflicts
=> _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >();
internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx )
=> _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty;
internal SingleArray< ModConflicts > Conflicts( Mod mod )
=> _cache?.Conflicts( mod ) ?? new SingleArray< ModConflicts >();
// Update the effective file list for the given cache.
// Creates a cache if necessary.
public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault )
public void CalculateEffectiveFileList()
=> Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name,
() => CalculateEffectiveFileListInternal( withMetaManipulations, reloadDefault ) );
CalculateEffectiveFileListInternal );
private void CalculateEffectiveFileListInternal( bool withMetaManipulations, bool reloadDefault )
private void CalculateEffectiveFileListInternal()
{
// Skip the empty collection.
if( Index == 0 )
@ -85,16 +86,13 @@ public partial class ModCollection
return;
}
PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Thread.CurrentThread.ManagedThreadId, Name,
withMetaManipulations, reloadDefault );
PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l}",
Thread.CurrentThread.ManagedThreadId, Name );
_cache ??= new Cache( this );
_cache.CalculateEffectiveFileList( withMetaManipulations );
if( reloadDefault )
{
SetFiles();
Penumbra.ResidentResources.Reload();
}
PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", Thread.CurrentThread.ManagedThreadId, Name);
_cache.FullRecalculation();
PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.",
Thread.CurrentThread.ManagedThreadId, Name );
}
// Set Metadata files.

View file

@ -1,38 +1,44 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Dalamud.Logging;
using Dalamud.Utility;
using OtterGui.Classes;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Collections;
public record struct ModPath( Mod Mod, FullPath Path );
public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved );
public partial class ModCollection
{
// The Cache contains all required temporary data to use a collection.
// It will only be setup if a collection gets activated in any way.
private class Cache : IDisposable
{
private readonly Dictionary< Utf8GamePath, FileRegister > _registeredFiles = new();
private readonly Dictionary< MetaManipulation, FileRegister > _registeredManipulations = new();
private readonly ModCollection _collection;
private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new();
public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new();
public readonly MetaManager MetaManipulations;
private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new();
private readonly ModCollection _collection;
private readonly SortedList< string, object? > _changedItems = new();
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly MetaManager MetaManipulations;
public ConflictCache Conflicts = new();
public IEnumerable< SingleArray< ModConflicts > > AllConflicts
=> _conflicts.Values;
// Count the number of recalculations of the effective file list.
public SingleArray< ModConflicts > Conflicts( Mod mod )
=> _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >();
// Count the number of changes of the effective file list.
// This is used for material and imc changes.
public int RecomputeCounter { get; private set; } = 0;
public int ChangeCounter { get; private set; }
private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary< string, object? > ChangedItems
public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems
{
get
{
@ -64,85 +70,376 @@ public partial class ModCollection
return null;
}
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.IsRooted && !candidate.Exists )
if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.Path.IsRooted && !candidate.Path.Exists )
{
return null;
}
return candidate;
return candidate.Path;
}
private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ )
{
// Recompute the file list if it was not just a non-conflicting priority change
// or a setting change for a disabled mod.
if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any()
|| type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled )
var mod = Penumbra.ModManager[ modIdx ];
switch( type )
{
return;
}
case ModSettingChange.Inheritance:
ReloadMod( mod, true );
break;
case ModSettingChange.EnableState:
if( oldValue != 1 )
{
AddMod( mod, true );
}
else
{
RemoveMod( mod, true );
}
var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance
|| Penumbra.ModManager[ modIdx ].AllManipulations.Any();
_collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection );
break;
case ModSettingChange.Priority:
if( Conflicts( mod ).Count > 0 )
{
ReloadMod( mod, true );
}
break;
case ModSettingChange.Setting:
if( _collection[ modIdx ].Settings?.Enabled == true )
{
ReloadMod( mod, true );
}
break;
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
FullRecalculation();
break;
}
}
// Inheritance changes are too big to check for relevance,
// just recompute everything.
private void OnInheritanceChange( bool _ )
=> _collection.CalculateEffectiveFileList( true, true );
=> FullRecalculation();
// Clear all local and global caches to prepare for recomputation.
private void ClearStorageAndPrepare()
public void FullRecalculation()
{
_changedItems.Clear();
_registeredFiles.EnsureCapacity( 2 * ResolvedFiles.Count );
ResolvedFiles.Clear();
Conflicts.ClearFileConflicts();
}
MetaManipulations.Reset();
_conflicts.Clear();
// Recalculate all file changes from current settings. Include all fixed custom redirects.
// Recalculate meta manipulations only if withManipulations is true.
public void CalculateEffectiveFileList( bool withManipulations )
{
ClearStorageAndPrepare();
if( withManipulations )
{
_registeredManipulations.EnsureCapacity( 2 * MetaManipulations.Count );
MetaManipulations.Reset();
}
// Add all forced redirects.
Penumbra.Redirects.Apply( ResolvedFiles );
AddCustomRedirects();
for( var i = 0; i < Penumbra.ModManager.Count; ++i )
foreach( var mod in Penumbra.ModManager )
{
AddMod( i, withManipulations );
AddMod( mod, false );
}
AddMetaFiles();
++RecomputeCounter;
_registeredFiles.Clear();
_registeredFiles.TrimExcess();
_registeredManipulations.Clear();
_registeredManipulations.TrimExcess();
++ChangeCounter;
if( _collection == Penumbra.CollectionManager.Default )
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
}
}
public void ReloadMod( Mod mod, bool addMetaChanges )
{
RemoveMod( mod, addMetaChanges );
AddMod( mod, addMetaChanges );
}
public void RemoveMod( Mod mod, bool addMetaChanges )
{
var conflicts = Conflicts( mod );
foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) )
{
if( !ResolvedFiles.TryGetValue( path, out var modPath ) )
{
continue;
}
if( modPath.Mod == mod )
{
ResolvedFiles.Remove( path );
}
}
foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) )
{
if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod )
{
MetaManipulations.RevertMod( manipulation );
}
}
_conflicts.Remove( mod );
foreach( var conflict in conflicts )
{
if( conflict.HasPriority )
{
ReloadMod( conflict.Mod2, false );
}
else
{
var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod );
if( newConflicts.Count > 0 )
{
_conflicts[ conflict.Mod2 ] = newConflicts;
}
else
{
_conflicts.Remove( conflict.Mod2 );
}
}
}
if( addMetaChanges )
{
++ChangeCounter;
if( _collection == Penumbra.CollectionManager.Default )
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
}
}
}
// Add all files and possibly manipulations of a given mod according to its settings in this collection.
public void AddMod( Mod mod, bool addMetaChanges )
{
var settings = _collection[ mod.Index ].Settings;
if( settings is not { Enabled: true } )
{
return;
}
AddSubMod( mod.Default, mod );
foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) )
{
if( group.Count == 0 )
{
continue;
}
var config = settings.Settings[ groupIndex ];
switch( group.Type )
{
case SelectType.Single:
AddSubMod( group[ ( int )config ], mod );
break;
case SelectType.Multi:
{
foreach( var (option, _) in group.WithIndex()
.OrderByDescending( p => group.OptionPriority( p.Item2 ) )
.Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) )
{
AddSubMod( option, mod );
}
break;
}
}
}
if( addMetaChanges )
{
++ChangeCounter;
if( _collection == Penumbra.CollectionManager.Default )
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
}
if( mod.TotalManipulations > 0 )
{
AddMetaFiles();
}
}
}
// Add all files and possibly manipulations of a specific submod
private void AddSubMod( ISubMod subMod, Mod parentMod )
{
foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) )
{
// Skip all filtered files
if( Mod.FilterFile( path ) )
{
continue;
}
AddFile( path, file, parentMod );
}
foreach( var manip in subMod.Manipulations )
{
AddManipulation( manip, parentMod );
}
}
// Add a specific file redirection, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddFile( Utf8GamePath path, FullPath file, Mod mod )
{
if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) )
{
return;
}
var modPath = ResolvedFiles[ path ];
// Lower prioritized option in the same mod.
if( mod == modPath.Mod )
{
return;
}
if( AddConflict( path, mod, modPath.Mod ) )
{
ResolvedFiles[ path ] = new ModPath( mod, file );
}
}
// Remove all empty conflict sets for a given mod with the given conflicts.
// If transitive is true, also removes the corresponding version of the other mod.
private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive )
{
var changedConflicts = oldConflicts.Remove( c =>
{
if( c.Conflicts.Count == 0 )
{
if( transitive )
{
RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false );
}
return true;
}
return false;
} );
if( changedConflicts.Count == 0 )
{
_conflicts.Remove( mod );
}
else
{
_conflicts[ mod ] = changedConflicts;
}
}
// Add a new conflict between the added mod and the existing mod.
// Update all other existing conflicts between the existing mod and other mods if necessary.
// Returns if the added mod takes priority before the existing mod.
private bool AddConflict( object data, Mod addedMod, Mod existingMod )
{
var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue;
var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue;
if( existingPriority < addedPriority )
{
var tmpConflicts = Conflicts( existingMod );
foreach( var conflict in tmpConflicts )
{
if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals(path) ) > 0
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals(meta) ) > 0 )
{
AddConflict( data, addedMod, conflict.Mod2 );
}
}
RemoveEmptyConflicts( existingMod, tmpConflicts, true );
}
var addedConflicts = Conflicts( addedMod );
var existingConflicts = Conflicts( existingMod );
if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) )
{
// Only need to change one list since both conflict lists refer to the same list.
oldConflicts.Conflicts.Add( data );
}
else
{
// Add the same conflict list to both conflict directions.
var conflictList = new List< object > { data };
_conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority,
existingPriority != addedPriority ) );
_conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList,
existingPriority >= addedPriority,
existingPriority != addedPriority ) );
}
return existingPriority < addedPriority;
}
// Add a specific manipulation, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation( MetaManipulation manip, Mod mod )
{
if( !MetaManipulations.TryGetValue( manip, out var existingMod ) )
{
MetaManipulations.ApplyMod( manip, mod );
return;
}
// Lower prioritized option in the same mod.
if( mod == existingMod )
{
return;
}
if( AddConflict( manip, mod, existingMod ) )
{
MetaManipulations.ApplyMod( manip, mod );
}
}
// Add all necessary meta file redirects.
private void AddMetaFiles()
=> MetaManipulations.Imc.SetFiles();
// Identify and record all manipulated objects for this entire collection.
private void SetChangedItems()
{
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
if( _changedItemsSaveCounter == ChangeCounter )
{
return;
}
try
{
_changedItemsSaveCounter = ChangeCounter;
_changedItems.Clear();
// Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant.
var identifier = GameData.GameData.GetIdentifier();
foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) )
foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( 'i', 'm', 'c' ) ) )
{
identifier.Identify( _changedItems, resolved.ToGamePath() );
foreach( var (name, obj) in identifier.Identify( resolved.ToGamePath() ) )
{
if( !_changedItems.TryGetValue( name, out var data ) )
{
_changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) );
}
else if( !data.Item1.Contains( modPath.Mod ) )
{
_changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj );
}
}
}
// TODO: Meta Manipulations
}
@ -151,186 +448,5 @@ public partial class ModCollection
PluginLog.Error( $"Unknown Error:\n{e}" );
}
}
// Add a specific file redirection, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddFile( Utf8GamePath path, FullPath file, FileRegister priority )
{
if( _registeredFiles.TryGetValue( path, out var register ) )
{
if( register.SameMod( priority, out var less ) )
{
Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path );
if( less )
{
_registeredFiles[ path ] = priority;
ResolvedFiles[ path ] = file;
}
}
else
{
// File seen before in the same mod:
// use higher priority or earlier recurrences in case of same priority.
// Do not add conflicts.
if( less )
{
_registeredFiles[ path ] = priority;
ResolvedFiles[ path ] = file;
}
}
}
else // File not seen before, just add it.
{
_registeredFiles.Add( path, priority );
ResolvedFiles.Add( path, file );
}
}
// Add a specific manipulation, handling potential conflicts.
// For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded.
private void AddManipulation( MetaManipulation manip, FileRegister priority )
{
if( _registeredManipulations.TryGetValue( manip, out var register ) )
{
if( register.SameMod( priority, out var less ) )
{
Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, manip );
if( less )
{
_registeredManipulations[ manip ] = priority;
MetaManipulations.ApplyMod( manip, priority.ModIdx );
}
}
else
{
// Manipulation seen before in the same mod:
// use higher priority or earlier occurrences in case of same priority.
// Do not add conflicts.
if( less )
{
_registeredManipulations[ manip ] = priority;
MetaManipulations.ApplyMod( manip, priority.ModIdx );
}
}
}
else // Manipulation not seen before, just add it.
{
_registeredManipulations[ manip ] = priority;
MetaManipulations.ApplyMod( manip, priority.ModIdx );
}
}
// Add all files and possibly manipulations of a specific submod with the given priorities.
private void AddSubMod( ISubMod mod, FileRegister priority, bool withManipulations )
{
foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) )
{
// Skip all filtered files
if( Mod.FilterFile( path ) )
{
continue;
}
AddFile( path, file, priority );
}
if( withManipulations )
{
foreach( var manip in mod.Manipulations )
{
AddManipulation( manip, priority );
}
}
}
// Add all files and possibly manipulations of a given mod according to its settings in this collection.
private void AddMod( int modIdx, bool withManipulations )
{
var settings = _collection[ modIdx ].Settings;
if( settings is not { Enabled: true } )
{
return;
}
var mod = Penumbra.ModManager[ modIdx ];
AddSubMod( mod.Default, new FileRegister( modIdx, settings.Priority, 0, 0 ), withManipulations );
for( var idx = 0; idx < mod.Groups.Count; ++idx )
{
var config = settings.Settings[ idx ];
var group = mod.Groups[ idx ];
if( group.Count == 0 )
{
continue;
}
switch( group.Type )
{
case SelectType.Single:
var singlePriority = new FileRegister( modIdx, settings.Priority, group.Priority, group.Priority );
AddSubMod( group[ ( int )config ], singlePriority, withManipulations );
break;
case SelectType.Multi:
{
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{
if( ( ( 1 << optionIdx ) & config ) != 0 )
{
var priority = new FileRegister( modIdx, settings.Priority, group.Priority, group.OptionPriority( optionIdx ) );
AddSubMod( group[ optionIdx ], priority, withManipulations );
}
}
break;
}
}
}
}
// Add all necessary meta file redirects.
private void AddMetaFiles()
=> MetaManipulations.Imc.SetFiles();
// Add all API redirects.
private void AddCustomRedirects()
{
Penumbra.Redirects.Apply( ResolvedFiles );
foreach( var gamePath in ResolvedFiles.Keys )
{
_registeredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) );
}
}
// Struct to keep track of all priorities involved in a mod and register and compare accordingly.
private readonly record struct FileRegister( int ModIdx, int ModPriority, int GroupPriority, int OptionPriority )
{
public bool SameMod( FileRegister other, out bool less )
{
if( ModIdx != other.ModIdx )
{
less = ModPriority < other.ModPriority;
return true;
}
if( GroupPriority < other.GroupPriority )
{
less = true;
}
else if( GroupPriority == other.GroupPriority )
{
less = OptionPriority < other.OptionPriority;
}
else
{
less = false;
}
return false;
}
};
}
}

View file

@ -5,6 +5,7 @@ using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -13,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerCmp : IDisposable
{
public CmpFile? File = null;
public readonly Dictionary< RspManipulation, int > Manipulations = new();
public readonly Dictionary< RspManipulation, Mod > Manipulations = new();
public MetaManagerCmp()
{ }
@ -38,10 +39,10 @@ public partial class MetaManager
Manipulations.Clear();
}
public bool ApplyMod( RspManipulation m, int modIdx )
public bool ApplyMod( RspManipulation m, Mod mod )
{
#if USE_CMP
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
File ??= new CmpFile();
return m.Apply( File );
#else
@ -49,6 +50,19 @@ public partial class MetaManager
#endif
}
public bool RevertMod( RspManipulation m )
{
#if USE_CMP
if( Manipulations.Remove( m ) )
{
var def = CmpFile.GetDefault( m.SubRace, m.Attribute );
var manip = new RspManipulation( m.SubRace, m.Attribute, def );
return manip.Apply( File! );
}
#endif
return false;
}
public void Dispose()
{
File?.Dispose();

View file

@ -6,6 +6,7 @@ using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Meta.Manager;
@ -14,9 +15,9 @@ public partial class MetaManager
{
public struct MetaManagerEqdp : IDisposable
{
public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar
public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar
public readonly Dictionary< EqdpManipulation, int > Manipulations = new();
public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new();
public MetaManagerEqdp()
{ }
@ -50,10 +51,10 @@ public partial class MetaManager
Manipulations.Clear();
}
public bool ApplyMod( EqdpManipulation m, int modIdx )
public bool ApplyMod( EqdpManipulation m, Mod mod )
{
#if USE_EQDP
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??=
new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar
return m.Apply( file );
@ -62,6 +63,20 @@ public partial class MetaManager
#endif
}
public bool RevertMod( EqdpManipulation m )
{
#if USE_EQDP
if( Manipulations.Remove( m ) )
{
var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory(), m.SetId );
var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ]!;
var manip = new EqdpManipulation( def, m.Slot, m.Gender, m.Race, m.SetId );
return manip.Apply( file );
}
#endif
return false;
}
public ExpandedEqdpFile? File( GenderRace race, bool accessory )
=> Files[ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar

View file

@ -5,6 +5,7 @@ using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -13,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerEqp : IDisposable
{
public ExpandedEqpFile? File = null;
public readonly Dictionary< EqpManipulation, int > Manipulations = new();
public readonly Dictionary< EqpManipulation, Mod > Manipulations = new();
public MetaManagerEqp()
{ }
@ -38,10 +39,10 @@ public partial class MetaManager
Manipulations.Clear();
}
public bool ApplyMod( EqpManipulation m, int modIdx )
public bool ApplyMod( EqpManipulation m, Mod mod )
{
#if USE_EQP
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
File ??= new ExpandedEqpFile();
return m.Apply( File );
#else
@ -49,6 +50,19 @@ public partial class MetaManager
#endif
}
public bool RevertMod( EqpManipulation m )
{
#if USE_EQP
if( Manipulations.Remove( m ) )
{
var def = ExpandedEqpFile.GetDefault( m.SetId );
var manip = new EqpManipulation( def, m.Slot, m.SetId );
return manip.Apply( File! );
}
#endif
return false;
}
public void Dispose()
{
File?.Dispose();

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -16,7 +18,7 @@ public partial class MetaManager
public EstFile? BodyFile = null;
public EstFile? HeadFile = null;
public readonly Dictionary< EstManipulation, int > Manipulations = new();
public readonly Dictionary< EstManipulation, Mod > Manipulations = new();
public MetaManagerEst()
{ }
@ -49,10 +51,10 @@ public partial class MetaManager
Manipulations.Clear();
}
public bool ApplyMod( EstManipulation m, int modIdx )
public bool ApplyMod( EstManipulation m, Mod mod )
{
#if USE_EST
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
var file = m.Slot switch
{
EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ),
@ -67,6 +69,27 @@ public partial class MetaManager
#endif
}
public bool RevertMod( EstManipulation m )
{
#if USE_EST
if( Manipulations.Remove( m ) )
{
var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId );
var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def );
var file = m.Slot switch
{
EstManipulation.EstType.Hair => HairFile!,
EstManipulation.EstType.Face => FaceFile!,
EstManipulation.EstType.Body => BodyFile!,
EstManipulation.EstType.Head => HeadFile!,
_ => throw new ArgumentOutOfRangeException(),
};
return manip.Apply( file );
}
#endif
return false;
}
public void Dispose()
{
FaceFile?.Dispose();

View file

@ -5,6 +5,7 @@ using System.Linq;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -13,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerGmp : IDisposable
{
public ExpandedGmpFile? File = null;
public readonly Dictionary< GmpManipulation, int > Manipulations = new();
public readonly Dictionary< GmpManipulation, Mod > Manipulations = new();
public MetaManagerGmp()
{ }
@ -37,10 +38,10 @@ public partial class MetaManager
}
}
public bool ApplyMod( GmpManipulation m, int modIdx )
public bool ApplyMod( GmpManipulation m, Mod mod )
{
#if USE_GMP
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
File ??= new ExpandedGmpFile();
return m.Apply( File );
#else
@ -48,6 +49,19 @@ public partial class MetaManager
#endif
}
public bool RevertMod( GmpManipulation m )
{
#if USE_GMP
if( Manipulations.Remove( m ) )
{
var def = ExpandedGmpFile.GetDefault( m.SetId );
var manip = new GmpManipulation( def, m.SetId );
return manip.Apply( File! );
}
#endif
return false;
}
public void Dispose()
{
File?.Dispose();

View file

@ -9,6 +9,7 @@ using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -17,7 +18,7 @@ public partial class MetaManager
public readonly struct MetaManagerImc : IDisposable
{
public readonly Dictionary< Utf8GamePath, ImcFile > Files = new();
public readonly Dictionary< ImcManipulation, int > Manipulations = new();
public readonly Dictionary< ImcManipulation, Mod > Manipulations = new();
private readonly ModCollection _collection;
private static int _imcManagerCount;
@ -64,10 +65,10 @@ public partial class MetaManager
Manipulations.Clear();
}
public bool ApplyMod( ImcManipulation m, int modIdx )
public bool ApplyMod( ImcManipulation m, Mod mod )
{
#if USE_IMC
Manipulations[ m ] = modIdx;
Manipulations[ m ] = mod;
var path = m.GamePath();
if( !Files.TryGetValue( path, out var file ) )
{
@ -92,6 +93,36 @@ public partial class MetaManager
#endif
}
public bool RevertMod( ImcManipulation m )
{
#if USE_IMC
if( Manipulations.Remove( m ) )
{
var path = m.GamePath();
if( !Files.TryGetValue( path, out var file ) )
{
return false;
}
var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant );
var manip = m with { Entry = def };
if( !manip.Apply( file ) )
{
return false;
}
var fullPath = CreateImcPath( path );
if( _collection.HasCache )
{
_collection.ForceFile( path, fullPath );
}
return true;
}
#endif
return false;
}
public void Dispose()
{
foreach( var file in Files.Values )

View file

@ -1,8 +1,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Penumbra.Collections;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Meta.Manager;
@ -28,19 +30,19 @@ public partial class MetaManager : IDisposable
}
}
public bool TryGetValue( MetaManipulation manip, out int modIdx )
public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod )
{
modIdx = manip.ManipulationType switch
mod = manip.ManipulationType switch
{
MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : -1,
MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : -1,
MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : -1,
MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : -1,
MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : -1,
MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : -1,
MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null,
MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null,
MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null,
MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null,
MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null,
MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null,
_ => throw new ArgumentOutOfRangeException(),
};
return modIdx != -1;
return mod != null;
}
public int Count
@ -84,16 +86,31 @@ public partial class MetaManager : IDisposable
Imc.Dispose();
}
public bool ApplyMod( MetaManipulation m, int modIdx )
public bool ApplyMod( MetaManipulation m, Mod mod )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, modIdx ),
MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, modIdx ),
MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, modIdx ),
MetaManipulation.Type.Est => Est.ApplyMod( m.Est, modIdx ),
MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, modIdx ),
MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, modIdx ),
MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ),
MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ),
MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ),
MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ),
MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ),
MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ),
MetaManipulation.Type.Unknown => false,
_ => false,
};
}
public bool RevertMod( MetaManipulation m )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Eqp => Eqp.RevertMod( m.Eqp ),
MetaManipulation.Type.Gmp => Gmp.RevertMod( m.Gmp ),
MetaManipulation.Type.Eqdp => Eqdp.RevertMod( m.Eqdp ),
MetaManipulation.Type.Est => Est.RevertMod( m.Est ),
MetaManipulation.Type.Rsp => Cmp.RevertMod( m.Rsp ),
MetaManipulation.Type.Imc => Imc.RevertMod( m.Imc ),
MetaManipulation.Type.Unknown => false,
_ => false,
};

View file

@ -81,6 +81,7 @@ public partial class Mod
var mod = this[ idx ];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath );
if( !mod.Reload( out var metaChange ) )
{
PluginLog.Warning( mod.Name.Length == 0

View file

@ -9,23 +9,6 @@ using Penumbra.Util;
namespace Penumbra.Mods;
public enum ModOptionChangeType
{
GroupRenamed,
GroupAdded,
GroupDeleted,
GroupMoved,
GroupTypeChanged,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionMoved,
OptionFilesChanged,
OptionSwapsChanged,
OptionMetaChanged,
DisplayChange,
}
public sealed partial class Mod
{
public sealed partial class Manager
@ -84,6 +67,7 @@ public sealed partial class Mod
public void DeleteModGroup( Mod mod, int groupIdx )
{
var group = mod._groups[ groupIdx ];
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 );
mod._groups.RemoveAt( groupIdx );
group.DeleteFile( mod.ModPath, groupIdx );
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 );
@ -221,6 +205,7 @@ public sealed partial class Mod
public void DeleteOption( Mod mod, int groupIdx, int optionIdx )
{
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 );
switch( mod._groups[ groupIdx ] )
{
case SingleModGroup s:
@ -252,6 +237,7 @@ public sealed partial class Mod
return;
}
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 );
subMod.ManipulationData = manipulations;
ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 );
}
@ -264,6 +250,7 @@ public sealed partial class Mod
return;
}
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 );
subMod.FileData = replacements;
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
}
@ -275,7 +262,7 @@ public sealed partial class Mod
subMod.FileData.AddFrom( additions );
if( oldCount != subMod.FileData.Count )
{
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1 );
}
}
@ -287,6 +274,7 @@ public sealed partial class Mod
return;
}
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 );
subMod.FileSwapData = swaps;
ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 );
}

View file

@ -0,0 +1,50 @@
namespace Penumbra.Mods;
public enum ModOptionChangeType
{
GroupRenamed,
GroupAdded,
GroupDeleted,
GroupMoved,
GroupTypeChanged,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionMoved,
OptionFilesChanged,
OptionFilesAdded,
OptionSwapsChanged,
OptionMetaChanged,
DisplayChange,
PrepareChange,
}
public static class ModOptionChangeTypeExtension
{
// Give information for each type of change.
// If requiresSaving, collections need to be re-saved after this change.
// If requiresReloading, caches need to be manipulated after this change.
// If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired.
// Otherwise, caches need to reload the mod itself.
public static void HandlingInfo( this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared )
{
( requiresSaving, requiresReloading, wasPrepared ) = type switch
{
ModOptionChangeType.GroupRenamed => ( true, false, false ),
ModOptionChangeType.GroupAdded => ( true, false, false ),
ModOptionChangeType.GroupDeleted => ( true, true, false ),
ModOptionChangeType.GroupMoved => ( true, false, false ),
ModOptionChangeType.GroupTypeChanged => ( true, true, true ),
ModOptionChangeType.PriorityChanged => ( true, true, true ),
ModOptionChangeType.OptionAdded => ( true, true, true ),
ModOptionChangeType.OptionDeleted => ( true, true, false ),
ModOptionChangeType.OptionMoved => ( true, false, false ),
ModOptionChangeType.OptionFilesChanged => ( false, true, false ),
ModOptionChangeType.OptionFilesAdded => ( false, true, true ),
ModOptionChangeType.OptionSwapsChanged => ( false, true, false ),
ModOptionChangeType.OptionMetaChanged => ( false, true, false ),
ModOptionChangeType.DisplayChange => ( false, false, false ),
_ => ( false, false, false ),
};
}
}

View file

@ -9,6 +9,7 @@ public enum ModPathChangeType
Deleted,
Moved,
Reloaded,
StartingReload,
}
public partial class Mod

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Logging;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;

View file

@ -24,6 +24,12 @@ public enum MetaChangeType : ushort
public sealed partial class Mod
{
public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." ))
{
Name = "Forced Files",
Index = -1,
};
public const uint CurrentFileVersion = 1;
public uint FileVersion { get; private set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "New Mod";
@ -138,4 +144,7 @@ public sealed partial class Mod
PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" );
}
}
public override string ToString()
=> Name.Text;
}

View file

@ -432,8 +432,9 @@ public class Penumbra : IDisposable
+ "> **`Enabled Mods: `** {3}\n"
+ "> **`Total Conflicts: `** {4}\n"
+ "> **`Solved Conflicts: `** {5}\n",
CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), c.Conflicts.Count,
c.Conflicts.Count( con => con.Solved ) );
CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ),
c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count ),
c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) );
sb.AppendLine( "**Collections**" );
sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count );

View file

@ -119,7 +119,7 @@ public partial class ModFileSystemSelector
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value();
}
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod );
if( conflicts.Count == 0 )
{
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value();
@ -188,7 +188,7 @@ public partial class ModFileSystemSelector
}
// Conflicts can only be relevant if the mod is enabled.
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod );
if( conflicts.Count > 0 )
{
if( conflicts.Any( c => !c.Solved ) )

View file

@ -1,28 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using Lumina.Data.Parsing;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private LowerString _changedItemFilter = LowerString.Empty;
private LowerString _changedItemFilter = LowerString.Empty;
private LowerString _changedItemModFilter = LowerString.Empty;
// Draw a simple clipped table containing all changed items.
private void DrawChangedItemTab()
{
// Functions in here for less pollution.
bool FilterChangedItem( KeyValuePair< string, object? > item )
=> item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase );
bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item )
=> ( _changedItemFilter.IsEmpty || item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) )
&& ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) );
void DrawChangedItemColumn( KeyValuePair< string, object? > item )
void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item )
{
ImGui.TableNextColumn();
DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize );
DrawChangedItem( item.Key, item.Value.Item2, false );
ImGui.TableNextColumn();
if( item.Value.Item1.Count > 0 )
{
ImGui.TextUnformatted( item.Value.Item1[ 0 ].Name );
if( item.Value.Item1.Count > 1 )
{
ImGuiUtil.HoverTooltip( string.Join( "\n", item.Value.Item1.Skip( 1 ).Select( m => m.Name ) ) );
}
}
ImGui.TableNextColumn();
if( item.Value.Item2 is Item it )
{
using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() );
ImGuiUtil.RightAlign( $"({( ( Quad )it.ModelMain ).A})" );
}
}
using var tab = ImRaii.TabItem( "Changed Items" );
@ -32,8 +56,14 @@ public partial class ConfigWindow
}
// Draw filters.
ImGui.SetNextItemWidth( -1 );
LowerString.InputWithHint( "##changedItemsFilter", "Filter...", ref _changedItemFilter, 64 );
var varWidth = ImGui.GetContentRegionAvail().X
- 400 * ImGuiHelpers.GlobalScale
- ImGui.GetStyle().ItemSpacing.X;
ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale );
LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 );
ImGui.SameLine();
ImGui.SetNextItemWidth( varWidth );
LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 );
using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One );
if( !child )
@ -44,14 +74,19 @@ public partial class ConfigWindow
// Draw table of changed items.
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
var skips = ImGuiClip.GetNecessarySkips( height );
using var list = ImRaii.Table( "##changedItems", 1, ImGuiTableFlags.RowBg, -Vector2.One );
using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One );
if( !list )
{
return;
}
const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed;
ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "id", flags, 100 * ImGuiHelpers.GlobalScale );
var items = Penumbra.CollectionManager.Default.ChangedItems;
var rest = _changedItemFilter.IsEmpty
var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty
? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count )
: ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn );
ImGuiClip.DrawEndDummy( rest, height );

View file

@ -8,6 +8,7 @@ using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.Mods;
namespace Penumbra.UI;
@ -111,7 +112,7 @@ public partial class ConfigWindow
{
// We can treat all meta manipulations the same,
// we are only really interested in their ToString function here.
static (object, int) Convert< T >( KeyValuePair< T, int > kvp )
static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp )
=> ( kvp.Key!, kvp.Value );
var it = m.Cmp.Manipulations.Select( Convert )
@ -124,7 +125,7 @@ public partial class ConfigWindow
// Filters mean we can not use the known counts.
if( hasFilters )
{
var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager[ p.Item2 ].Name ) );
var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, p.Item2.Name ) );
if( stop >= 0 )
{
ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height );
@ -155,7 +156,7 @@ public partial class ConfigWindow
}
// Draw a line for a game path and its redirected file.
private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair )
private static void DrawLine( KeyValuePair< Utf8GamePath, ModPath > pair )
{
var (path, name) = pair;
ImGui.TableNextColumn();
@ -164,7 +165,8 @@ public partial class ConfigWindow
ImGui.TableNextColumn();
ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft );
ImGui.TableNextColumn();
CopyOnClickSelectable( name.InternalName );
CopyOnClickSelectable( name.Path.InternalName );
ImGuiUtil.HoverTooltip( $"\nChanged by {name.Mod.Name}." );
}
// Draw a line for a path and its name.
@ -181,20 +183,20 @@ public partial class ConfigWindow
}
// Draw a line for a unfiltered/unconverted manipulation and mod-index pair.
private static void DrawLine( (object, int) pair )
private static void DrawLine( (object, Mod) pair )
{
var (manipulation, modIdx) = pair;
var (manipulation, mod) = pair;
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty );
ImGui.TableNextColumn();
ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft );
ImGui.TableNextColumn();
ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager[ modIdx ].Name );
ImGuiUtil.CopyOnClickSelectable( mod.Name );
}
// Check filters for file replacements.
private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp )
private bool CheckFilters( KeyValuePair< Utf8GamePath, ModPath > kvp )
{
var (gamePath, fullPath) = kvp;
if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) )
@ -202,7 +204,7 @@ public partial class ConfigWindow
return false;
}
return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower );
return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower );
}
// Check filters for meta manipulations.

View file

@ -29,8 +29,8 @@ public partial class ConfigWindow
=> Text( resource->FileName(), resource->FileNameLength );
// Draw a changed item, invoking the Api-Events for clicks and tooltips.
// Also draw the item Id in grey
private void DrawChangedItem( string name, object? data, float itemIdOffset = 0 )
// Also draw the item Id in grey if requested
private void DrawChangedItem( string name, object? data, bool drawId )
{
var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None;
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
@ -55,7 +55,7 @@ public partial class ConfigWindow
}
}
if( data is Item it )
if( data is Item it && drawId )
{
ImGui.SameLine( ImGui.GetContentRegionAvail().X );
ImGuiUtil.RightJustify( $"({( ( Quad )it.ModelMain ).A})", ColorId.ItemId.Value() );

View file

@ -15,11 +15,11 @@ public partial class ConfigWindow
{
private partial class ModPanel
{
private ModSettings _settings = null!;
private ModCollection _collection = null!;
private bool _emptySetting;
private bool _inherited;
private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty;
private ModSettings _settings = null!;
private ModCollection _collection = null!;
private bool _emptySetting;
private bool _inherited;
private SingleArray< ModConflicts > _conflicts = new();
private int? _currentPriority;
@ -29,7 +29,7 @@ public partial class ConfigWindow
_collection = selector.SelectedSettingCollection;
_emptySetting = _settings == ModSettings.Empty;
_inherited = _collection != Penumbra.CollectionManager.Current;
_conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
_conflicts = Penumbra.CollectionManager.Current.Conflicts( _mod );
}
// Draw the whole settings tab as well as its contents.
@ -105,7 +105,6 @@ public partial class ConfigWindow
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) )
{
_currentPriority = priority;
}

View file

@ -93,11 +93,11 @@ public partial class ConfigWindow
var zipList = ZipList.FromSortedList( _mod.ChangedItems );
var height = ImGui.GetTextLineHeight();
ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height );
ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2, true ), height );
}
// If any conflicts exist, show them in this tab.
private void DrawConflictsTab()
private unsafe void DrawConflictsTab()
{
using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts );
if( !tab )
@ -111,45 +111,30 @@ public partial class ConfigWindow
return;
}
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
Mod? oldBadMod = null;
using var indent = ImRaii.PushIndent( 0f );
foreach( var conflict in conflicts )
foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) )
{
var badMod = Penumbra.ModManager[ conflict.Mod2 ];
if( badMod != oldBadMod )
if( ImGui.Selectable( conflict.Mod2.Name ) )
{
if( oldBadMod != null )
{
indent.Pop( 30f );
}
if( ImGui.Selectable( badMod.Name ) )
{
_window._selector.SelectByValue( badMod );
}
ImGui.SameLine();
using var color = ImRaii.PushColor( ImGuiCol.Text,
conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() );
ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" );
indent.Push( 30f );
_window._selector.SelectByValue( conflict.Mod2 );
}
if( conflict.Data is Utf8GamePath p )
ImGui.SameLine();
using( var color = ImRaii.PushColor( ImGuiCol.Text,
conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) )
{
unsafe
{
ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
}
}
else if( conflict.Data is MetaManipulation m )
{
ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty );
ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" );
}
oldBadMod = badMod;
using var indent = ImRaii.PushIndent( 30f );
foreach( var data in conflict.Conflicts )
{
var _ = data switch
{
Utf8GamePath p => ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) > 0,
MetaManipulation m => ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ),
_ => false,
};
}
}
}