mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
tmp
This commit is contained in:
parent
bc47e08e08
commit
9a0b0bfa0f
35 changed files with 1365 additions and 1997 deletions
|
|
@ -16,15 +16,15 @@ public class ModsController : WebApiController
|
|||
[Route( HttpVerbs.Get, "/mods" )]
|
||||
public object? GetMods()
|
||||
{
|
||||
return Penumbra.CollectionManager.CurrentCollection.Cache?.AvailableMods.Values.Select( x => new
|
||||
{
|
||||
x.Settings.Enabled,
|
||||
x.Settings.Priority,
|
||||
x.Data.BasePath.Name,
|
||||
x.Data.Meta,
|
||||
BasePath = x.Data.BasePath.FullName,
|
||||
Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ),
|
||||
} );
|
||||
return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new
|
||||
{
|
||||
x.Second?.Enabled,
|
||||
x.Second?.Priority,
|
||||
x.First.BasePath.Name,
|
||||
x.First.Meta,
|
||||
BasePath = x.First.BasePath.FullName,
|
||||
Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ),
|
||||
} );
|
||||
}
|
||||
|
||||
[Route( HttpVerbs.Post, "/mods" )]
|
||||
|
|
@ -34,7 +34,7 @@ public class ModsController : WebApiController
|
|||
[Route( HttpVerbs.Get, "/files" )]
|
||||
public object GetFiles()
|
||||
{
|
||||
return Penumbra.CollectionManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
|
||||
return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary(
|
||||
o => o.Key.ToString(),
|
||||
o => o.Value.FullName
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Reflection;
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Lumina.Data;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -76,7 +77,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
_penumbra!.ObjectReloader.RedrawAll( setting );
|
||||
}
|
||||
|
||||
private static string ResolvePath( string path, ModManager _, ModCollection collection )
|
||||
private static string ResolvePath( string path, ModManager _, ModCollection2 collection )
|
||||
{
|
||||
if( !Penumbra.Config.EnableMods )
|
||||
{
|
||||
|
|
@ -84,24 +85,21 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
}
|
||||
|
||||
var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty;
|
||||
var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
ret ??= Penumbra.CollectionManager.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
var ret = collection.ResolvePath( gamePath );
|
||||
return ret?.ToString() ?? path;
|
||||
}
|
||||
|
||||
public string ResolvePath( string path )
|
||||
{
|
||||
CheckInitialized();
|
||||
return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.DefaultCollection );
|
||||
return ResolvePath( path, Penumbra.ModManager, Penumbra.CollectionManager.Default );
|
||||
}
|
||||
|
||||
public string ResolvePath( string path, string characterName )
|
||||
{
|
||||
CheckInitialized();
|
||||
return ResolvePath( path, Penumbra.ModManager,
|
||||
Penumbra.CollectionManager.CharacterCollection.TryGetValue( characterName, out var collection )
|
||||
? collection
|
||||
: ModCollection.Empty );
|
||||
Penumbra.CollectionManager.Character( characterName ) );
|
||||
}
|
||||
|
||||
private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource
|
||||
|
|
@ -136,12 +134,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
{
|
||||
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
|
||||
{
|
||||
collection = ModCollection.Empty;
|
||||
collection = ModCollection2.Empty;
|
||||
}
|
||||
|
||||
if( collection.Cache != null )
|
||||
if( collection.HasCache )
|
||||
{
|
||||
return collection.Cache.ChangedItems;
|
||||
return collection.ChangedItems;
|
||||
}
|
||||
|
||||
PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." );
|
||||
|
|
|
|||
261
Penumbra/Collections/CollectionManager.Active.cs
Normal file
261
Penumbra/Collections/CollectionManager.Active.cs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public sealed partial class CollectionManager2
|
||||
{
|
||||
// Is invoked after the collections actually changed.
|
||||
public event CollectionChangeDelegate? CollectionChanged;
|
||||
|
||||
private int _currentIdx = -1;
|
||||
private int _defaultIdx = -1;
|
||||
private int _defaultNameIdx = 0;
|
||||
|
||||
public ModCollection2 Current
|
||||
=> this[ _currentIdx ];
|
||||
|
||||
public ModCollection2 Default
|
||||
=> this[ _defaultIdx ];
|
||||
|
||||
private readonly Dictionary< string, int > _character = new();
|
||||
|
||||
public ModCollection2 Character( string name )
|
||||
=> _character.TryGetValue( name, out var idx ) ? _collections[ idx ] : Default;
|
||||
|
||||
public bool HasCharacterCollections
|
||||
=> _character.Count > 0;
|
||||
|
||||
private void OnModChanged( ModChangeType type, int idx, ModData mod )
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case ModChangeType.Added:
|
||||
foreach( var collection in _collections )
|
||||
{
|
||||
collection.AddMod( mod );
|
||||
}
|
||||
|
||||
foreach( var collection in _collections.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) )
|
||||
{
|
||||
collection.UpdateCache();
|
||||
}
|
||||
|
||||
break;
|
||||
case ModChangeType.Removed:
|
||||
var list = new List< ModSettings? >( _collections.Count );
|
||||
foreach( var collection in _collections )
|
||||
{
|
||||
list.Add( collection[ idx ].Settings );
|
||||
collection.RemoveMod( mod, idx );
|
||||
}
|
||||
|
||||
foreach( var (collection, _) in _collections.Zip( list ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) )
|
||||
{
|
||||
collection.UpdateCache();
|
||||
}
|
||||
|
||||
break;
|
||||
case ModChangeType.Changed:
|
||||
foreach( var collection in _collections.Where(
|
||||
collection => collection.Settings[ idx ]?.FixInvalidSettings( mod.Meta ) ?? false ) )
|
||||
{
|
||||
collection.Save();
|
||||
}
|
||||
|
||||
foreach( var collection in _collections.Where( c => c.HasCache && c[ idx ].Settings?.Enabled == true ) )
|
||||
{
|
||||
collection.UpdateCache();
|
||||
}
|
||||
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateNecessaryCaches()
|
||||
{
|
||||
if( _defaultIdx >= 0 )
|
||||
{
|
||||
Default.CreateCache();
|
||||
}
|
||||
|
||||
if( _currentIdx >= 0 )
|
||||
{
|
||||
Current.CreateCache();
|
||||
}
|
||||
|
||||
foreach( var idx in _character.Values.Where( i => i >= 0 ) )
|
||||
{
|
||||
_collections[ idx ].CreateCache();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateCaches()
|
||||
{
|
||||
foreach( var collection in _collections )
|
||||
{
|
||||
collection.UpdateCache();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCache( int idx )
|
||||
{
|
||||
if( idx != _defaultIdx && idx != _currentIdx && _character.All( kvp => kvp.Value != idx ) )
|
||||
{
|
||||
_collections[ idx ].ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCollection( string name, CollectionType type, string? characterName = null )
|
||||
=> SetCollection( GetIndexForCollectionName( name ), type, characterName );
|
||||
|
||||
public void SetCollection( ModCollection2 collection, CollectionType type, string? characterName = null )
|
||||
=> SetCollection( GetIndexForCollectionName( collection.Name ), type, characterName );
|
||||
|
||||
public void SetCollection( int newIdx, CollectionType type, string? characterName = null )
|
||||
{
|
||||
var oldCollectionIdx = type switch
|
||||
{
|
||||
CollectionType.Default => _defaultIdx,
|
||||
CollectionType.Current => _currentIdx,
|
||||
CollectionType.Character => characterName?.Length > 0
|
||||
? _character.TryGetValue( characterName, out var c )
|
||||
? c
|
||||
: _defaultIdx
|
||||
: -2,
|
||||
_ => -2,
|
||||
};
|
||||
|
||||
if( oldCollectionIdx == -2 || newIdx == oldCollectionIdx )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newCollection = this[ newIdx ];
|
||||
if( newIdx >= 0 )
|
||||
{
|
||||
newCollection.CreateCache();
|
||||
}
|
||||
|
||||
RemoveCache( oldCollectionIdx );
|
||||
switch( type )
|
||||
{
|
||||
case CollectionType.Default:
|
||||
_defaultIdx = newIdx;
|
||||
Penumbra.Config.DefaultCollection = newCollection.Name;
|
||||
Penumbra.ResidentResources.Reload();
|
||||
Default.SetFiles();
|
||||
break;
|
||||
case CollectionType.Current:
|
||||
_currentIdx = newIdx;
|
||||
Penumbra.Config.CurrentCollection = newCollection.Name;
|
||||
break;
|
||||
case CollectionType.Character:
|
||||
_character[ characterName! ] = newIdx;
|
||||
Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name;
|
||||
break;
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke( this[ oldCollectionIdx ], newCollection, type, characterName );
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
|
||||
public bool CreateCharacterCollection( string characterName )
|
||||
{
|
||||
if( _character.ContainsKey( characterName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_character[ characterName ] = -1;
|
||||
Penumbra.Config.CharacterCollections[ characterName ] = ModCollection2.Empty.Name;
|
||||
Penumbra.Config.Save();
|
||||
CollectionChanged?.Invoke( null, ModCollection2.Empty, CollectionType.Character, characterName );
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveCharacterCollection( string characterName )
|
||||
{
|
||||
if( _character.TryGetValue( characterName, out var collection ) )
|
||||
{
|
||||
RemoveCache( collection );
|
||||
_character.Remove( characterName );
|
||||
CollectionChanged?.Invoke( this[ collection ], null, CollectionType.Character, characterName );
|
||||
}
|
||||
|
||||
if( Penumbra.Config.CharacterCollections.Remove( characterName ) )
|
||||
{
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private int GetIndexForCollectionName( string name )
|
||||
{
|
||||
if( name.Length == 0 || name == ModCollection2.DefaultCollection )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var idx = _collections.IndexOf( c => c.Name == Penumbra.Config.DefaultCollection );
|
||||
return idx < 0 ? -2 : idx;
|
||||
}
|
||||
|
||||
public void LoadCollections()
|
||||
{
|
||||
var configChanged = false;
|
||||
_defaultIdx = GetIndexForCollectionName( Penumbra.Config.DefaultCollection );
|
||||
if( _defaultIdx == -2 )
|
||||
{
|
||||
PluginLog.Error( $"Last choice of Default Collection {Penumbra.Config.DefaultCollection} is not available, reset to None." );
|
||||
_defaultIdx = -1;
|
||||
Penumbra.Config.DefaultCollection = this[ _defaultIdx ].Name;
|
||||
configChanged = true;
|
||||
}
|
||||
|
||||
_currentIdx = GetIndexForCollectionName( Penumbra.Config.CurrentCollection );
|
||||
if( _currentIdx == -2 )
|
||||
{
|
||||
PluginLog.Error( $"Last choice of Current Collection {Penumbra.Config.CurrentCollection} is not available, reset to Default." );
|
||||
_currentIdx = _defaultNameIdx;
|
||||
Penumbra.Config.DefaultCollection = this[ _currentIdx ].Name;
|
||||
configChanged = true;
|
||||
}
|
||||
|
||||
if( LoadCharacterCollections() || configChanged )
|
||||
{
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
|
||||
CreateNecessaryCaches();
|
||||
}
|
||||
|
||||
private bool LoadCharacterCollections()
|
||||
{
|
||||
var configChanged = false;
|
||||
foreach( var (player, collectionName) in Penumbra.Config.CharacterCollections.ToArray() )
|
||||
{
|
||||
var idx = GetIndexForCollectionName( collectionName );
|
||||
if( idx == -2 )
|
||||
{
|
||||
PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." );
|
||||
_character.Add( player, -1 );
|
||||
Penumbra.Config.CharacterCollections[ player ] = ModCollection2.Empty.Name;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_character.Add( player, idx );
|
||||
}
|
||||
}
|
||||
|
||||
return configChanged;
|
||||
}
|
||||
}
|
||||
223
Penumbra/Collections/CollectionManager.cs
Normal file
223
Penumbra/Collections/CollectionManager.cs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public enum CollectionType : byte
|
||||
{
|
||||
Inactive,
|
||||
Default,
|
||||
Character,
|
||||
Current,
|
||||
}
|
||||
|
||||
public sealed partial class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 >
|
||||
{
|
||||
public delegate void CollectionChangeDelegate( ModCollection2? oldCollection, ModCollection2? newCollection, CollectionType type,
|
||||
string? characterName = null );
|
||||
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
private readonly List< ModCollection2 > _collections = new();
|
||||
|
||||
public ModCollection2 this[ Index idx ]
|
||||
=> idx.Value == -1 ? ModCollection2.Empty : _collections[ idx ];
|
||||
|
||||
public ModCollection2? this[ string name ]
|
||||
=> ByName( name, out var c ) ? c : null;
|
||||
|
||||
public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection )
|
||||
=> _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection );
|
||||
|
||||
public IEnumerator< ModCollection2 > GetEnumerator()
|
||||
=> _collections.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public CollectionManager2( ModManager manager )
|
||||
{
|
||||
_modManager = manager;
|
||||
|
||||
_modManager.ModsRediscovered += OnModsRediscovered;
|
||||
_modManager.ModChange += OnModChanged;
|
||||
ReadCollections();
|
||||
LoadCollections();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_modManager.ModsRediscovered -= OnModsRediscovered;
|
||||
_modManager.ModChange -= OnModChanged;
|
||||
}
|
||||
|
||||
private void OnModsRediscovered()
|
||||
{
|
||||
UpdateCaches();
|
||||
Default.SetFiles();
|
||||
}
|
||||
|
||||
private void AddDefaultCollection()
|
||||
{
|
||||
var idx = _collections.IndexOf( c => c.Name == ModCollection2.DefaultCollection );
|
||||
if( idx >= 0 )
|
||||
{
|
||||
_defaultNameIdx = idx;
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection );
|
||||
defaultCollection.Save();
|
||||
_defaultNameIdx = _collections.Count;
|
||||
_collections.Add( defaultCollection );
|
||||
}
|
||||
|
||||
private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances )
|
||||
{
|
||||
foreach( var (collection, inheritance) in this.Zip( inheritances ) )
|
||||
{
|
||||
var changes = false;
|
||||
foreach( var subCollectionName in inheritance )
|
||||
{
|
||||
if( !ByName( subCollectionName, out var subCollection ) )
|
||||
{
|
||||
changes = true;
|
||||
PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." );
|
||||
}
|
||||
else if( !collection.AddInheritance( subCollection ) )
|
||||
{
|
||||
changes = true;
|
||||
PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) )
|
||||
{
|
||||
changes |= setting!.FixInvalidSettings( mod.Meta );
|
||||
}
|
||||
|
||||
if( changes )
|
||||
{
|
||||
collection.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadCollections()
|
||||
{
|
||||
var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory );
|
||||
var inheritances = new List< IReadOnlyList< string > >();
|
||||
if( collectionDir.Exists )
|
||||
{
|
||||
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
|
||||
{
|
||||
var collection = ModCollection2.LoadFromFile( file, out var inheritance );
|
||||
if( collection == null || collection.Name.Length == 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" )
|
||||
{
|
||||
PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
|
||||
}
|
||||
|
||||
if( this[ collection.Name ] != null )
|
||||
{
|
||||
PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." );
|
||||
}
|
||||
else
|
||||
{
|
||||
inheritances.Add( inheritance );
|
||||
_collections.Add( collection );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddDefaultCollection();
|
||||
ApplyInheritancesAndFixSettings( inheritances );
|
||||
}
|
||||
|
||||
public bool AddCollection( string name, ModCollection2? duplicate )
|
||||
{
|
||||
var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||
if( nameFixed.Length == 0
|
||||
|| nameFixed == ModCollection2.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 ) ?? ModCollection2.CreateNewEmpty( name );
|
||||
_collections.Add( newCollection );
|
||||
newCollection.Save();
|
||||
CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive );
|
||||
SetCollection( _collections.Count - 1, CollectionType.Current );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveCollection( int idx )
|
||||
{
|
||||
if( idx < 0 || 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, CollectionType.Current );
|
||||
}
|
||||
else if( _currentIdx > idx )
|
||||
{
|
||||
--_currentIdx;
|
||||
}
|
||||
|
||||
if( idx == _defaultIdx )
|
||||
{
|
||||
SetCollection( -1, CollectionType.Default );
|
||||
}
|
||||
else if( _defaultIdx > idx )
|
||||
{
|
||||
--_defaultIdx;
|
||||
}
|
||||
|
||||
if( _defaultNameIdx > idx )
|
||||
{
|
||||
--_defaultNameIdx;
|
||||
}
|
||||
|
||||
foreach( var (characterName, characterIdx) in _character.ToList() )
|
||||
{
|
||||
if( idx == characterIdx )
|
||||
{
|
||||
SetCollection( -1, CollectionType.Character, characterName );
|
||||
}
|
||||
else if( characterIdx > idx )
|
||||
{
|
||||
_character[ characterName ] = characterIdx - 1;
|
||||
}
|
||||
}
|
||||
|
||||
var collection = _collections[ idx ];
|
||||
collection.Delete();
|
||||
_collections.RemoveAt( idx );
|
||||
CollectionChanged?.Invoke( collection, null, CollectionType.Inactive );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
472
Penumbra/Collections/ModCollection.Cache.cs
Normal file
472
Penumbra/Collections/ModCollection.Cache.cs
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection2
|
||||
{
|
||||
private Cache? _cache;
|
||||
|
||||
public bool HasCache
|
||||
=> _cache != null;
|
||||
|
||||
public void CreateCache()
|
||||
{
|
||||
if( _cache == null )
|
||||
{
|
||||
_cache = new Cache( this );
|
||||
_cache.CalculateEffectiveFileList();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateCache()
|
||||
=> _cache?.CalculateEffectiveFileList();
|
||||
|
||||
public void ClearCache()
|
||||
=> _cache = null;
|
||||
|
||||
public FullPath? ResolvePath( Utf8GamePath path )
|
||||
=> _cache?.ResolvePath( path );
|
||||
|
||||
internal void ForceFile( Utf8GamePath path, FullPath fullPath )
|
||||
=> _cache!.ResolvedFiles[ path ] = fullPath;
|
||||
|
||||
internal void RemoveFile( Utf8GamePath path )
|
||||
=> _cache!.ResolvedFiles.Remove( path );
|
||||
|
||||
internal MetaManager? MetaCache
|
||||
=> _cache?.MetaManipulations;
|
||||
|
||||
internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles
|
||||
=> _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >();
|
||||
|
||||
internal IReadOnlySet< FullPath > MissingFiles
|
||||
=> _cache?.MissingFiles ?? new HashSet< FullPath >();
|
||||
|
||||
internal IReadOnlyDictionary< string, object? > ChangedItems
|
||||
=> _cache?.ChangedItems ?? new Dictionary< string, object? >();
|
||||
|
||||
internal IReadOnlyList< ConflictCache.ModCacheStruct > Conflicts
|
||||
=> _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.ModCacheStruct >();
|
||||
|
||||
public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident )
|
||||
{
|
||||
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations );
|
||||
_cache ??= new Cache( this );
|
||||
_cache.CalculateEffectiveFileList();
|
||||
if( withMetaManipulations )
|
||||
{
|
||||
_cache.UpdateMetaManipulations();
|
||||
}
|
||||
|
||||
if( reloadResident )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The ModCollectionCache 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
|
||||
{
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256);
|
||||
private static readonly List< ModSettings? > ResolvedSettings = new(128);
|
||||
|
||||
private readonly ModCollection2 _collection;
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
public ConflictCache Conflicts;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
public Cache( ModCollection2 collection )
|
||||
{
|
||||
_collection = collection;
|
||||
MetaManipulations = new MetaManager( collection );
|
||||
}
|
||||
|
||||
private static void ResetFileSeen( int size )
|
||||
{
|
||||
if( size < FileSeen.Length )
|
||||
{
|
||||
FileSeen.Length = size;
|
||||
FileSeen.SetAll( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSeen.SetAll( false );
|
||||
FileSeen.Length = size;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearStorageAndPrepare()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
ResolvedSettings.Clear();
|
||||
ResolvedSettings.AddRange( _collection.ActualSettings );
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ClearStorageAndPrepare();
|
||||
|
||||
for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i )
|
||||
{
|
||||
if( ResolvedSettings[ i ]?.Enabled == true )
|
||||
{
|
||||
AddFiles( i );
|
||||
AddSwaps( i );
|
||||
}
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
Conflicts.Sort();
|
||||
}
|
||||
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 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' ) ) )
|
||||
{
|
||||
identifier.Identify( _changedItems, resolved.ToGamePath() );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Unknown Error:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AddFiles( int idx )
|
||||
{
|
||||
var mod = Penumbra.ModManager.Mods[ idx ];
|
||||
ResetFileSeen( mod.Resources.ModFiles.Count );
|
||||
// Iterate in reverse so that later groups take precedence before earlier ones.
|
||||
foreach( var group in mod.Meta.Groups.Values.Reverse() )
|
||||
{
|
||||
switch( group.SelectionType )
|
||||
{
|
||||
case SelectType.Single:
|
||||
AddFilesForSingle( group, mod, idx );
|
||||
break;
|
||||
case SelectType.Multi:
|
||||
AddFilesForMulti( group, mod, idx );
|
||||
break;
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
AddRemainingFiles( mod, idx );
|
||||
}
|
||||
|
||||
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||
// so only add those files if it is disabled.
|
||||
private static bool FilterFile( Utf8GamePath gamePath )
|
||||
=> !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' );
|
||||
|
||||
|
||||
private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file )
|
||||
{
|
||||
if( FilterFile( gamePath ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) )
|
||||
{
|
||||
RegisteredFiles.Add( gamePath, modIdx );
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
else
|
||||
{
|
||||
var priority = ResolvedSettings[ modIdx ]!.Priority;
|
||||
var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority;
|
||||
Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath );
|
||||
if( priority > oldPriority )
|
||||
{
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
RegisteredFiles[ gamePath ] = modIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMissingFile( FullPath file )
|
||||
{
|
||||
switch( file.Extension.ToLowerInvariant() )
|
||||
{
|
||||
case ".meta":
|
||||
case ".rgsp":
|
||||
return;
|
||||
default:
|
||||
MissingFiles.Add( file );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled )
|
||||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath( mod.BasePath, file );
|
||||
var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
continue;
|
||||
}
|
||||
|
||||
var registeredFile = mod.Resources.ModFiles[ idx ];
|
||||
if( !registeredFile.Exists )
|
||||
{
|
||||
AddMissingFile( registeredFile );
|
||||
continue;
|
||||
}
|
||||
|
||||
FileSeen.Set( idx, true );
|
||||
if( enabled )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
AddFile( modIdx, path, registeredFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx )
|
||||
{
|
||||
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
|
||||
var settings = ResolvedSettings[ modIdx ]!;
|
||||
if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) )
|
||||
{
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
for( var i = 0; i < singleGroup.Options.Count; ++i )
|
||||
{
|
||||
AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx )
|
||||
{
|
||||
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
|
||||
var settings = ResolvedSettings[ modIdx ]!;
|
||||
if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also iterate options in reverse so that later options take precedence before earlier ones.
|
||||
for( var i = multiGroup.Options.Count - 1; i >= 0; --i )
|
||||
{
|
||||
AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRemainingFiles( ModData mod, int modIdx )
|
||||
{
|
||||
for( var i = 0; i < mod.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
if( FileSeen.Get( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var file = mod.Resources.ModFiles[ i ];
|
||||
if( file.Exists )
|
||||
{
|
||||
if( file.ToGamePath( mod.BasePath, out var gamePath ) )
|
||||
{
|
||||
AddFile( modIdx, gamePath, file );
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MissingFiles.Add( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMetaFiles()
|
||||
=> MetaManipulations.Imc.SetFiles();
|
||||
|
||||
private void AddSwaps( int modIdx )
|
||||
{
|
||||
var mod = Penumbra.ModManager.Mods[ modIdx ];
|
||||
foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) )
|
||||
{
|
||||
AddFile( modIdx, gamePath, swapPath );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddManipulations( int modIdx )
|
||||
{
|
||||
var mod = Penumbra.ModManager.Mods[ modIdx ];
|
||||
foreach( var manip in mod.Resources.MetaManipulations.GetManipulationsForConfig( ResolvedSettings[ modIdx ]!, mod.Meta ) )
|
||||
{
|
||||
if( !MetaManipulations.TryGetValue( manip, out var oldModIdx ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, modIdx );
|
||||
}
|
||||
else
|
||||
{
|
||||
var priority = ResolvedSettings[ modIdx ]!.Priority;
|
||||
var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority;
|
||||
Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, manip );
|
||||
if( priority > oldPriority )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, modIdx );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMetaManipulations()
|
||||
{
|
||||
MetaManipulations.Reset();
|
||||
Conflicts.ClearMetaConflicts();
|
||||
|
||||
foreach( var mod in Penumbra.ModManager.Mods.Zip( ResolvedSettings )
|
||||
.Select( ( m, i ) => ( m.First, m.Second, i ) )
|
||||
.Where( m => m.Second?.Enabled == true && m.First.Resources.MetaManipulations.Count > 0 ) )
|
||||
{
|
||||
AddManipulations( mod.i );
|
||||
}
|
||||
}
|
||||
|
||||
public FullPath? ResolvePath( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.IsRooted && !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public enum ModSettingChange
|
||||
{
|
||||
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection2
|
||||
{
|
||||
|
|
@ -53,7 +53,7 @@ public partial class ModCollection2
|
|||
}
|
||||
}
|
||||
|
||||
public (ModSettings? Settings, ModCollection2 Collection) this[ int idx ]
|
||||
public (ModSettings? Settings, ModCollection2 Collection) this[ Index idx ]
|
||||
{
|
||||
get
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Linq;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection2
|
||||
public sealed partial class ModCollection2
|
||||
{
|
||||
private static class Migration
|
||||
{
|
||||
209
Penumbra/Collections/ModCollection.cs
Normal file
209
Penumbra/Collections/ModCollection.cs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
// A ModCollection is a named set of ModSettings to all of the users' installed mods.
|
||||
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
|
||||
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made.
|
||||
// Active ModCollections build a cache of currently relevant data.
|
||||
public partial class ModCollection2
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
public const string DefaultCollection = "Default";
|
||||
|
||||
public static readonly ModCollection2 Empty = CreateNewEmpty( "None" );
|
||||
|
||||
public string Name { get; private init; }
|
||||
public int Version { get; private set; }
|
||||
|
||||
private readonly List< ModSettings? > _settings;
|
||||
|
||||
public IReadOnlyList< ModSettings? > Settings
|
||||
=> _settings;
|
||||
|
||||
public IEnumerable< ModSettings? > ActualSettings
|
||||
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings );
|
||||
|
||||
private readonly Dictionary< string, ModSettings > _unusedSettings;
|
||||
|
||||
|
||||
private ModCollection2( string name, ModCollection2 duplicate )
|
||||
{
|
||||
Name = name;
|
||||
Version = duplicate.Version;
|
||||
_settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() );
|
||||
_unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
||||
_inheritance = duplicate._inheritance.ToList();
|
||||
ModSettingChanged += SaveOnChange;
|
||||
InheritanceChanged += Save;
|
||||
}
|
||||
|
||||
private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings )
|
||||
{
|
||||
Name = name;
|
||||
Version = version;
|
||||
_unusedSettings = allSettings;
|
||||
_settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList();
|
||||
for( var i = 0; i < Penumbra.ModManager.Count; ++i )
|
||||
{
|
||||
var modName = Penumbra.ModManager[ i ].BasePath.Name;
|
||||
if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) )
|
||||
{
|
||||
_unusedSettings.Remove( modName );
|
||||
_settings[ i ] = settings;
|
||||
}
|
||||
}
|
||||
|
||||
Migration.Migrate( this );
|
||||
ModSettingChanged += SaveOnChange;
|
||||
InheritanceChanged += Save;
|
||||
}
|
||||
|
||||
public static ModCollection2 CreateNewEmpty( string name )
|
||||
=> new(name, CurrentVersion, new Dictionary< string, ModSettings >());
|
||||
|
||||
public ModCollection2 Duplicate( string name )
|
||||
=> new(name, this);
|
||||
|
||||
internal static ModCollection2 MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings )
|
||||
=> new(name, 0, allSettings);
|
||||
|
||||
private void CleanUnavailableSettings()
|
||||
{
|
||||
var any = _unusedSettings.Count > 0;
|
||||
_unusedSettings.Clear();
|
||||
if( any )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModData mod )
|
||||
{
|
||||
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
_settings.Add( settings );
|
||||
_unusedSettings.Remove( mod.BasePath.Name );
|
||||
}
|
||||
else
|
||||
{
|
||||
_settings.Add( null );
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveMod( ModData mod, int idx )
|
||||
{
|
||||
var settings = _settings[ idx ];
|
||||
if( settings != null )
|
||||
{
|
||||
_unusedSettings.Add( mod.BasePath.Name, settings );
|
||||
}
|
||||
|
||||
_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()
|
||||
{
|
||||
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 ModCollection2? 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 ModCollection2( name, version, settings );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
|||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Mods;
|
||||
using FileMode = Penumbra.Interop.Structs.FileMode;
|
||||
using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ public unsafe partial class ResourceLoader
|
|||
// Use the default method of path replacement.
|
||||
public static (FullPath?, object?) DefaultResolver( Utf8GamePath path )
|
||||
{
|
||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
|
||||
var resolved = ModManager.ResolvePath( path );
|
||||
return ( resolved, null );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Mods;
|
||||
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
|
|
@ -90,10 +91,10 @@ public unsafe partial class PathResolver
|
|||
|
||||
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
|
||||
// It contains any DrawObjects that correspond to a human actor, even those without specific collections.
|
||||
internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new();
|
||||
internal readonly Dictionary< IntPtr, (ModCollection2, int) > DrawObjectToObject = new();
|
||||
|
||||
// This map links files to their corresponding collection, if it is non-default.
|
||||
internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new();
|
||||
internal readonly ConcurrentDictionary< Utf8String, ModCollection2 > PathCollections = new();
|
||||
|
||||
internal GameObject* LastGameObject = null;
|
||||
|
||||
|
|
@ -158,11 +159,11 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
// Identify the correct collection for a GameObject by index and name.
|
||||
private static ModCollection IdentifyCollection( GameObject* gameObject )
|
||||
private static ModCollection2 IdentifyCollection( GameObject* gameObject )
|
||||
{
|
||||
if( gameObject == null )
|
||||
{
|
||||
return Penumbra.CollectionManager.DefaultCollection;
|
||||
return Penumbra.CollectionManager.Default;
|
||||
}
|
||||
|
||||
var name = gameObject->ObjectIndex switch
|
||||
|
|
@ -175,13 +176,11 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
?? new Utf8String( gameObject->Name ).ToString();
|
||||
|
||||
return Penumbra.CollectionManager.CharacterCollection.TryGetValue( name, out var col )
|
||||
? col
|
||||
: Penumbra.CollectionManager.DefaultCollection;
|
||||
return Penumbra.CollectionManager.Character( name );
|
||||
}
|
||||
|
||||
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
|
||||
private void CheckCollections( ModCollection? _1, ModCollection? _2, CollectionType type, string? name )
|
||||
private void CheckCollections( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? name )
|
||||
{
|
||||
if( type is not (CollectionType.Character or CollectionType.Default) )
|
||||
{
|
||||
|
|
@ -201,7 +200,7 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
// Use the stored information to find the GameObject and Collection linked to a DrawObject.
|
||||
private GameObject* FindParent( IntPtr drawObject, out ModCollection collection )
|
||||
private GameObject* FindParent( IntPtr drawObject, out ModCollection2 collection )
|
||||
{
|
||||
if( DrawObjectToObject.TryGetValue( drawObject, out var data ) )
|
||||
{
|
||||
|
|
@ -226,7 +225,7 @@ public unsafe partial class PathResolver
|
|||
|
||||
|
||||
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
|
||||
private void SetCollection( Utf8String path, ModCollection collection )
|
||||
private void SetCollection( Utf8String path, ModCollection2 collection )
|
||||
{
|
||||
if( PathCollections.ContainsKey( path ) || path.IsOwned )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Dalamud.Hooking;
|
|||
using Dalamud.Logging;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
|
@ -40,7 +41,7 @@ public unsafe partial class PathResolver
|
|||
return ret;
|
||||
}
|
||||
|
||||
private ModCollection? _mtrlCollection;
|
||||
private ModCollection2? _mtrlCollection;
|
||||
|
||||
private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
|
||||
{
|
||||
|
|
@ -55,7 +56,7 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
// Check specifically for shpk and tex files whether we are currently in a material load.
|
||||
private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection )
|
||||
private bool HandleMaterialSubFiles( ResourceType type, out ModCollection2? collection )
|
||||
{
|
||||
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
|
||||
{
|
||||
|
|
@ -95,7 +96,7 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
// Materials need to be set per collection so they can load their textures independently from each other.
|
||||
private void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved,
|
||||
private static void HandleMtrlCollection( ModCollection2 collection, string path, bool nonDefault, ResourceType type, FullPath? resolved,
|
||||
out (FullPath?, object?) data )
|
||||
{
|
||||
if( nonDefault && type == ResourceType.Mtrl )
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -160,15 +161,15 @@ public unsafe partial class PathResolver
|
|||
RspSetupCharacterHook?.Dispose();
|
||||
}
|
||||
|
||||
private ModCollection? GetCollection( IntPtr drawObject )
|
||||
private ModCollection2? GetCollection( IntPtr drawObject )
|
||||
{
|
||||
var parent = FindParent( drawObject, out var collection );
|
||||
if( parent == null || collection == Penumbra.CollectionManager.DefaultCollection )
|
||||
if( parent == null || collection == Penumbra.CollectionManager.Default )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return collection.Cache == null ? Penumbra.CollectionManager.ForcedCollection : collection;
|
||||
return collection.HasCache ? collection : null;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -194,7 +195,7 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
}
|
||||
|
||||
public static MetaChanger ChangeEqp( ModCollection collection )
|
||||
public static MetaChanger ChangeEqp( ModCollection2 collection )
|
||||
{
|
||||
#if USE_EQP
|
||||
collection.SetEqpFiles();
|
||||
|
|
@ -232,7 +233,7 @@ public unsafe partial class PathResolver
|
|||
return new MetaChanger( MetaManipulation.Type.Unknown );
|
||||
}
|
||||
|
||||
public static MetaChanger ChangeEqdp( ModCollection collection )
|
||||
public static MetaChanger ChangeEqdp( ModCollection2 collection )
|
||||
{
|
||||
#if USE_EQDP
|
||||
collection.SetEqdpFiles();
|
||||
|
|
@ -268,13 +269,13 @@ public unsafe partial class PathResolver
|
|||
return new MetaChanger( MetaManipulation.Type.Unknown );
|
||||
}
|
||||
|
||||
public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection )
|
||||
public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection2? collection )
|
||||
{
|
||||
if( resolver.LastGameObject != null )
|
||||
{
|
||||
collection = IdentifyCollection( resolver.LastGameObject );
|
||||
#if USE_CMP
|
||||
if( collection != Penumbra.CollectionManager.DefaultCollection && collection.Cache != null )
|
||||
if( collection != Penumbra.CollectionManager.Default && collection.HasCache )
|
||||
{
|
||||
collection.SetCmpFiles();
|
||||
return new MetaChanger( MetaManipulation.Type.Rsp );
|
||||
|
|
@ -309,25 +310,25 @@ public unsafe partial class PathResolver
|
|||
case MetaManipulation.Type.Eqdp:
|
||||
if( --_eqdpCounter == 0 )
|
||||
{
|
||||
Penumbra.CollectionManager.DefaultCollection.SetEqdpFiles();
|
||||
Penumbra.CollectionManager.Default.SetEqdpFiles();
|
||||
}
|
||||
|
||||
break;
|
||||
case MetaManipulation.Type.Eqp:
|
||||
if( --_eqpCounter == 0 )
|
||||
{
|
||||
Penumbra.CollectionManager.DefaultCollection.SetEqpFiles();
|
||||
Penumbra.CollectionManager.Default.SetEqpFiles();
|
||||
}
|
||||
|
||||
break;
|
||||
case MetaManipulation.Type.Est:
|
||||
Penumbra.CollectionManager.DefaultCollection.SetEstFiles();
|
||||
Penumbra.CollectionManager.Default.SetEstFiles();
|
||||
break;
|
||||
case MetaManipulation.Type.Gmp:
|
||||
Penumbra.CollectionManager.DefaultCollection.SetGmpFiles();
|
||||
Penumbra.CollectionManager.Default.SetGmpFiles();
|
||||
break;
|
||||
case MetaManipulation.Type.Rsp:
|
||||
Penumbra.CollectionManager.DefaultCollection.SetCmpFiles();
|
||||
Penumbra.CollectionManager.Default.SetCmpFiles();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Interop.Resolver;
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ public unsafe partial class PathResolver
|
|||
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
|
||||
private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path )
|
||||
=> ResolvePathDetour( FindParent( drawObject, out var collection ) == null
|
||||
? Penumbra.CollectionManager.DefaultCollection
|
||||
? Penumbra.CollectionManager.Default
|
||||
: collection, path );
|
||||
|
||||
// Weapons have the characters DrawObject as a parent,
|
||||
|
|
@ -123,14 +123,14 @@ public unsafe partial class PathResolver
|
|||
{
|
||||
var parent = FindParent( ( IntPtr )parentObject, out var collection );
|
||||
return ResolvePathDetour( parent == null
|
||||
? Penumbra.CollectionManager.DefaultCollection
|
||||
? Penumbra.CollectionManager.Default
|
||||
: collection, path );
|
||||
}
|
||||
}
|
||||
|
||||
// Just add or remove the resolved path.
|
||||
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
|
||||
private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path )
|
||||
private IntPtr ResolvePathDetour( ModCollection2 collection, IntPtr path )
|
||||
{
|
||||
if( path == IntPtr.Zero )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Loader;
|
||||
|
|
@ -39,27 +40,17 @@ public partial class PathResolver : IDisposable
|
|||
var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection );
|
||||
if( !nonDefault )
|
||||
{
|
||||
collection = Penumbra.CollectionManager.DefaultCollection;
|
||||
collection = Penumbra.CollectionManager.Default;
|
||||
}
|
||||
|
||||
// Resolve using character/default collection first, otherwise forced, as usual.
|
||||
var resolved = collection!.ResolveSwappedOrReplacementPath( gamePath );
|
||||
if( resolved == null )
|
||||
{
|
||||
resolved = Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gamePath );
|
||||
if( resolved == null )
|
||||
{
|
||||
// We also need to handle defaulted materials against a non-default collection.
|
||||
HandleMtrlCollection( collection, gamePath.Path.ToString(), nonDefault, type, resolved, out data );
|
||||
return true;
|
||||
}
|
||||
|
||||
collection = Penumbra.CollectionManager.ForcedCollection;
|
||||
}
|
||||
var resolved = collection!.ResolvePath( gamePath );
|
||||
|
||||
// Since mtrl files load their files separately, we need to add the new, resolved path
|
||||
// so that the functions loading tex and shpk can find that path and use its collection.
|
||||
HandleMtrlCollection( collection, resolved.Value.FullName, nonDefault, type, resolved, out data );
|
||||
// We also need to handle defaulted materials against a non-default collection.
|
||||
var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName;
|
||||
HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -113,14 +104,14 @@ public partial class PathResolver : IDisposable
|
|||
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
|
||||
}
|
||||
|
||||
private void OnCollectionChange( ModCollection? _1, ModCollection? _2, CollectionType type, string? characterName )
|
||||
private void OnCollectionChange( ModCollection2? _1, ModCollection2? _2, CollectionType type, string? characterName )
|
||||
{
|
||||
if( type != CollectionType.Character )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( Penumbra.CollectionManager.CharacterCollection.Count > 0 )
|
||||
if( Penumbra.CollectionManager.HasCharacterCollections )
|
||||
{
|
||||
Enable();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ public partial class MetaManager
|
|||
{
|
||||
public struct MetaManagerCmp : IDisposable
|
||||
{
|
||||
public CmpFile? File = null;
|
||||
public readonly Dictionary< RspManipulation, Mod.Mod > Manipulations = new();
|
||||
public CmpFile? File = null;
|
||||
public readonly Dictionary< RspManipulation, int > Manipulations = new();
|
||||
|
||||
public MetaManagerCmp()
|
||||
{ }
|
||||
|
|
@ -38,14 +38,10 @@ public partial class MetaManager
|
|||
Manipulations.Clear();
|
||||
}
|
||||
|
||||
public bool ApplyMod( RspManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( RspManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_CMP
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Manipulations[ m ] = modIdx;
|
||||
File ??= new CmpFile();
|
||||
return m.Apply( File );
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public partial class MetaManager
|
|||
{
|
||||
public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 1]; // TODO: female Hrothgar
|
||||
|
||||
public readonly Dictionary< EqdpManipulation, Mod.Mod > Manipulations = new();
|
||||
public readonly Dictionary< EqdpManipulation, int > Manipulations = new();
|
||||
|
||||
public MetaManagerEqdp()
|
||||
{ }
|
||||
|
|
@ -50,14 +50,10 @@ public partial class MetaManager
|
|||
Manipulations.Clear();
|
||||
}
|
||||
|
||||
public bool ApplyMod( EqdpManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( EqdpManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_EQDP
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Manipulations[ m ] = modIdx;
|
||||
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 );
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ public partial class MetaManager
|
|||
{
|
||||
public struct MetaManagerEqp : IDisposable
|
||||
{
|
||||
public ExpandedEqpFile? File = null;
|
||||
public readonly Dictionary< EqpManipulation, Mod.Mod > Manipulations = new();
|
||||
public ExpandedEqpFile? File = null;
|
||||
public readonly Dictionary< EqpManipulation, int > Manipulations = new();
|
||||
|
||||
public MetaManagerEqp()
|
||||
{ }
|
||||
|
|
@ -38,14 +38,10 @@ public partial class MetaManager
|
|||
Manipulations.Clear();
|
||||
}
|
||||
|
||||
public bool ApplyMod( EqpManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( EqpManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_EQP
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Manipulations[ m ] = modIdx;
|
||||
File ??= new ExpandedEqpFile();
|
||||
return m.Apply( File );
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public partial class MetaManager
|
|||
public EstFile? BodyFile = null;
|
||||
public EstFile? HeadFile = null;
|
||||
|
||||
public readonly Dictionary< EstManipulation, Mod.Mod > Manipulations = new();
|
||||
public readonly Dictionary< EstManipulation, int > Manipulations = new();
|
||||
|
||||
public MetaManagerEst()
|
||||
{ }
|
||||
|
|
@ -49,14 +49,10 @@ public partial class MetaManager
|
|||
Manipulations.Clear();
|
||||
}
|
||||
|
||||
public bool ApplyMod( EstManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( EstManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_EST
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Manipulations[ m ] = modIdx;
|
||||
var file = m.Slot switch
|
||||
{
|
||||
EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ),
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ public partial class MetaManager
|
|||
{
|
||||
public struct MetaManagerGmp : IDisposable
|
||||
{
|
||||
public ExpandedGmpFile? File = null;
|
||||
public readonly Dictionary< GmpManipulation, Mod.Mod > Manipulations = new();
|
||||
public ExpandedGmpFile? File = null;
|
||||
public readonly Dictionary< GmpManipulation, int > Manipulations = new();
|
||||
|
||||
public MetaManagerGmp()
|
||||
{ }
|
||||
|
|
@ -37,15 +37,11 @@ public partial class MetaManager
|
|||
}
|
||||
}
|
||||
|
||||
public bool ApplyMod( GmpManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( GmpManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_GMP
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
File ??= new ExpandedGmpFile();
|
||||
Manipulations[ m ] = modIdx;
|
||||
File ??= new ExpandedGmpFile();
|
||||
return m.Apply( File );
|
||||
#else
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using Dalamud.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Loader;
|
||||
using Penumbra.Interop.Resolver;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Meta.Manager;
|
||||
|
||||
|
|
@ -18,14 +16,14 @@ public partial class MetaManager
|
|||
{
|
||||
public readonly struct MetaManagerImc : IDisposable
|
||||
{
|
||||
public readonly Dictionary< Utf8GamePath, ImcFile > Files = new();
|
||||
public readonly Dictionary< ImcManipulation, Mod.Mod > Manipulations = new();
|
||||
public readonly Dictionary< Utf8GamePath, ImcFile > Files = new();
|
||||
public readonly Dictionary< ImcManipulation, int > Manipulations = new();
|
||||
|
||||
private readonly ModCollection _collection;
|
||||
private static int _imcManagerCount;
|
||||
private readonly ModCollection2 _collection;
|
||||
private static int _imcManagerCount;
|
||||
|
||||
|
||||
public MetaManagerImc( ModCollection collection )
|
||||
public MetaManagerImc( ModCollection2 collection )
|
||||
{
|
||||
_collection = collection;
|
||||
SetupDelegate();
|
||||
|
|
@ -34,37 +32,43 @@ public partial class MetaManager
|
|||
[Conditional( "USE_IMC" )]
|
||||
public void SetFiles()
|
||||
{
|
||||
if( _collection.Cache == null )
|
||||
if( !_collection.HasCache )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var path in Files.Keys )
|
||||
{
|
||||
_collection.Cache.ResolvedFiles[ path ] = CreateImcPath( path );
|
||||
_collection.ForceFile( path, CreateImcPath( path ) );
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional( "USE_IMC" )]
|
||||
public void Reset()
|
||||
{
|
||||
foreach( var (path, file) in Files )
|
||||
if( _collection.HasCache )
|
||||
{
|
||||
_collection.Cache?.ResolvedFiles.Remove( path );
|
||||
file.Reset();
|
||||
foreach( var (path, file) in Files )
|
||||
{
|
||||
_collection.RemoveFile( path );
|
||||
file.Reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var (_, file) in Files )
|
||||
{
|
||||
file.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
Manipulations.Clear();
|
||||
}
|
||||
|
||||
public bool ApplyMod( ImcManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( ImcManipulation m, int modIdx )
|
||||
{
|
||||
#if USE_IMC
|
||||
if( !Manipulations.TryAdd( m, mod ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Manipulations[ m ] = modIdx;
|
||||
var path = m.GamePath();
|
||||
if( !Files.TryGetValue( path, out var file ) )
|
||||
{
|
||||
|
|
@ -78,9 +82,9 @@ public partial class MetaManager
|
|||
|
||||
Files[ path ] = file;
|
||||
var fullPath = CreateImcPath( path );
|
||||
if( _collection.Cache != null )
|
||||
if( _collection.HasCache )
|
||||
{
|
||||
_collection.Cache.ResolvedFiles[ path ] = fullPath;
|
||||
_collection.ForceFile( path, fullPath );
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -135,8 +139,8 @@ public partial class MetaManager
|
|||
PluginLog.Verbose( "Using ImcLoadHandler for path {$Path:l}.", path );
|
||||
ret = Penumbra.ResourceLoader.ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||
if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection )
|
||||
&& collection.Cache != null
|
||||
&& collection.Cache.MetaManipulations.Imc.Files.TryGetValue(
|
||||
&& collection.HasCache
|
||||
&& collection.MetaCache!.Imc.Files.TryGetValue(
|
||||
Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) )
|
||||
{
|
||||
PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path,
|
||||
|
|
@ -152,9 +156,8 @@ public partial class MetaManager
|
|||
{
|
||||
// Only check imcs.
|
||||
if( resource->FileType != ResourceType.Imc
|
||||
|| resolveData is not ModCollection collection
|
||||
|| collection.Cache == null
|
||||
|| !collection.Cache.MetaManipulations.Imc.Files.TryGetValue( gamePath, out var file )
|
||||
|| resolveData is not ModCollection2 { HasCache: true } collection
|
||||
|| !collection.MetaCache!.Imc.Files.TryGetValue( gamePath, out var file )
|
||||
|| !file.ChangesSinceLoad )
|
||||
{
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Meta.Manager;
|
||||
|
||||
|
|
@ -28,19 +28,19 @@ public partial class MetaManager : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public bool TryGetValue( MetaManipulation manip, out Mod.Mod? mod )
|
||||
public bool TryGetValue( MetaManipulation manip, out int modIdx )
|
||||
{
|
||||
mod = manip.ManipulationType switch
|
||||
modIdx = manip.ManipulationType switch
|
||||
{
|
||||
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,
|
||||
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,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
return mod != null;
|
||||
return modIdx != -1;
|
||||
}
|
||||
|
||||
public int Count
|
||||
|
|
@ -51,7 +51,7 @@ public partial class MetaManager : IDisposable
|
|||
+ Est.Manipulations.Count
|
||||
+ Eqp.Manipulations.Count;
|
||||
|
||||
public MetaManager( ModCollection collection )
|
||||
public MetaManager( ModCollection2 collection )
|
||||
=> Imc = new MetaManagerImc( collection );
|
||||
|
||||
public void SetFiles()
|
||||
|
|
@ -84,16 +84,16 @@ public partial class MetaManager : IDisposable
|
|||
Imc.Dispose();
|
||||
}
|
||||
|
||||
public bool ApplyMod( MetaManipulation m, Mod.Mod mod )
|
||||
public bool ApplyMod( MetaManipulation m, int modIdx )
|
||||
{
|
||||
return m.ManipulationType switch
|
||||
{
|
||||
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.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.Unknown => false,
|
||||
_ => false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
|
||||
|
|
@ -33,8 +34,8 @@ public static class MigrateConfiguration
|
|||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = new ModCollection();
|
||||
var defaultCollectionFile = defaultCollection.FileName();
|
||||
var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection );
|
||||
var defaultCollectionFile = defaultCollection.FileName;
|
||||
if( defaultCollectionFile.Exists )
|
||||
{
|
||||
return;
|
||||
|
|
@ -46,6 +47,7 @@ public static class MigrateConfiguration
|
|||
var data = JArray.Parse( text );
|
||||
|
||||
var maxPriority = 0;
|
||||
var dict = new Dictionary< string, ModSettings >();
|
||||
foreach( var setting in data.Cast< JObject >() )
|
||||
{
|
||||
var modName = ( string )setting[ "FolderName" ]!;
|
||||
|
|
@ -54,24 +56,25 @@ public static class MigrateConfiguration
|
|||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
|
||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
|
||||
|
||||
var save = new ModSettings()
|
||||
dict[ modName ] = new ModSettings()
|
||||
{
|
||||
Enabled = enabled,
|
||||
Priority = priority,
|
||||
Settings = settings!,
|
||||
};
|
||||
defaultCollection.Settings.Add( modName, save );
|
||||
;
|
||||
maxPriority = Math.Max( maxPriority, priority );
|
||||
}
|
||||
|
||||
if( !config.InvertModListOrder )
|
||||
{
|
||||
foreach( var setting in defaultCollection.Settings.Values )
|
||||
foreach( var setting in dict.Values )
|
||||
{
|
||||
setting.Priority = maxPriority - setting.Priority;
|
||||
}
|
||||
}
|
||||
|
||||
defaultCollection = ModCollection2.MigrateFromV0( ModCollection2.DefaultCollection, dict );
|
||||
defaultCollection.Save();
|
||||
}
|
||||
catch( Exception e )
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations;
|
|||
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
public struct ModCache2
|
||||
public struct ConflictCache
|
||||
{
|
||||
public readonly struct ModCacheStruct : IComparable< ModCacheStruct >
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Dalamud.Logging;
|
|||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
|
|
@ -60,8 +61,8 @@ public class ModCleanup
|
|||
|
||||
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
||||
{
|
||||
var idx = Penumbra.ModManager.AddMod( newDir );
|
||||
var newMod = Penumbra.ModManager.Mods[idx];
|
||||
var idx = Penumbra.ModManager.AddMod( newDir );
|
||||
var newMod = Penumbra.ModManager.Mods[ idx ];
|
||||
newMod.Move( newSortOrder );
|
||||
newMod.ComputeChangedItems();
|
||||
ModFileSystem.InvokeChange();
|
||||
|
|
@ -509,21 +510,23 @@ public class ModCleanup
|
|||
}
|
||||
}
|
||||
|
||||
if( option.OptionFiles.Any() )
|
||||
if( option.OptionFiles.Count > 0 )
|
||||
{
|
||||
group.Options.Add( option );
|
||||
}
|
||||
}
|
||||
|
||||
if( group.Options.Any() )
|
||||
if( group.Options.Count > 0 )
|
||||
{
|
||||
meta.Groups.Add( groupDir.Name, group );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var collection in Penumbra.CollectionManager.Collections )
|
||||
// TODO
|
||||
var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta );
|
||||
foreach( var collection in Penumbra.CollectionManager )
|
||||
{
|
||||
collection.UpdateSetting( baseDir, meta, true );
|
||||
collection.Settings[ idx ]?.FixInvalidSettings( meta );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,574 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed class CollectionManager2 : IDisposable, IEnumerable< ModCollection2 >
|
||||
{
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
private readonly List< ModCollection2 > _collections = new();
|
||||
|
||||
public ModCollection2 this[ int idx ]
|
||||
=> _collections[ idx ];
|
||||
|
||||
public ModCollection2? this[ string name ]
|
||||
=> ByName( name, out var c ) ? c : null;
|
||||
|
||||
public ModCollection2 Default
|
||||
=> this[ ModCollection2.DefaultCollection ]!;
|
||||
|
||||
public bool ByName( string name, [NotNullWhen( true )] out ModCollection2? collection )
|
||||
=> _collections.FindFirst( c => c.Name == name, out collection );
|
||||
|
||||
public IEnumerator< ModCollection2 > GetEnumerator()
|
||||
=> _collections.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public CollectionManager2( ModManager manager )
|
||||
{
|
||||
_modManager = manager;
|
||||
|
||||
//_modManager.ModsRediscovered += OnModsRediscovered;
|
||||
//_modManager.ModChange += OnModChanged;
|
||||
ReadCollections();
|
||||
//LoadConfigCollections( Penumbra.Config );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{ }
|
||||
|
||||
private void AddDefaultCollection()
|
||||
{
|
||||
if( this[ ModCollection.DefaultCollection ] != null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = ModCollection2.CreateNewEmpty( ModCollection2.DefaultCollection );
|
||||
defaultCollection.Save();
|
||||
_collections.Add( defaultCollection );
|
||||
}
|
||||
|
||||
private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances )
|
||||
{
|
||||
foreach( var (collection, inheritance) in this.Zip( inheritances ) )
|
||||
{
|
||||
var changes = false;
|
||||
foreach( var subCollectionName in inheritance )
|
||||
{
|
||||
if( !ByName( subCollectionName, out var subCollection ) )
|
||||
{
|
||||
changes = true;
|
||||
PluginLog.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." );
|
||||
}
|
||||
else if( !collection.AddInheritance( subCollection ) )
|
||||
{
|
||||
changes = true;
|
||||
PluginLog.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var (setting, mod) in collection.Settings.Zip( Penumbra.ModManager.Mods ).Where( s => s.First != null ) )
|
||||
{
|
||||
changes |= setting!.FixInvalidSettings( mod.Meta );
|
||||
}
|
||||
|
||||
if( changes )
|
||||
{
|
||||
collection.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadCollections()
|
||||
{
|
||||
var collectionDir = new DirectoryInfo( ModCollection2.CollectionDirectory );
|
||||
var inheritances = new List< IReadOnlyList< string > >();
|
||||
if( collectionDir.Exists )
|
||||
{
|
||||
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
|
||||
{
|
||||
var collection = ModCollection2.LoadFromFile( file, out var inheritance );
|
||||
if( collection == null || collection.Name.Length == 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" )
|
||||
{
|
||||
PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
|
||||
}
|
||||
|
||||
if( this[ collection.Name ] != null )
|
||||
{
|
||||
PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." );
|
||||
}
|
||||
else
|
||||
{
|
||||
inheritances.Add( inheritance );
|
||||
_collections.Add( collection );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddDefaultCollection();
|
||||
ApplyInheritancesAndFixSettings( inheritances );
|
||||
}
|
||||
}
|
||||
|
||||
public enum CollectionType : byte
|
||||
{
|
||||
Inactive,
|
||||
Default,
|
||||
Forced,
|
||||
Character,
|
||||
Current,
|
||||
}
|
||||
|
||||
public delegate void CollectionChangeDelegate( ModCollection? oldCollection, ModCollection? newCollection, CollectionType type,
|
||||
string? characterName = null );
|
||||
|
||||
// Contains all collections and respective functions, as well as the collection settings.
|
||||
public sealed class CollectionManager : IDisposable
|
||||
{
|
||||
private readonly ModManager _manager;
|
||||
|
||||
public List< ModCollection > Collections { get; } = new();
|
||||
public Dictionary< string, ModCollection > CharacterCollection { get; } = new();
|
||||
|
||||
public ModCollection CurrentCollection { get; private set; } = ModCollection.Empty;
|
||||
public ModCollection DefaultCollection { get; private set; } = ModCollection.Empty;
|
||||
public ModCollection ForcedCollection { get; private set; } = ModCollection.Empty;
|
||||
|
||||
public bool IsActive( ModCollection collection )
|
||||
=> ReferenceEquals( collection, DefaultCollection ) || ReferenceEquals( collection, ForcedCollection );
|
||||
|
||||
public ModCollection Default
|
||||
=> ByName( ModCollection.DefaultCollection )!;
|
||||
|
||||
public ModCollection? ByName( string name )
|
||||
=> name.Length > 0
|
||||
? Collections.Find( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ) )
|
||||
: ModCollection.Empty;
|
||||
|
||||
public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection )
|
||||
{
|
||||
if( name.Length > 0 )
|
||||
{
|
||||
return Collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.InvariantCultureIgnoreCase ), out collection );
|
||||
}
|
||||
|
||||
collection = ModCollection.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is invoked after the collections actually changed.
|
||||
public event CollectionChangeDelegate? CollectionChanged;
|
||||
|
||||
public CollectionManager( ModManager manager )
|
||||
{
|
||||
_manager = manager;
|
||||
|
||||
_manager.ModsRediscovered += OnModsRediscovered;
|
||||
_manager.ModChange += OnModChanged;
|
||||
ReadCollections();
|
||||
LoadConfigCollections( Penumbra.Config );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_manager.ModsRediscovered -= OnModsRediscovered;
|
||||
_manager.ModChange -= OnModChanged;
|
||||
}
|
||||
|
||||
private void OnModsRediscovered()
|
||||
{
|
||||
RecreateCaches();
|
||||
DefaultCollection.SetFiles();
|
||||
}
|
||||
|
||||
private void OnModChanged( ModChangeType type, int idx, ModData mod )
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case ModChangeType.Added:
|
||||
foreach( var collection in Collections )
|
||||
{
|
||||
collection.AddMod( mod );
|
||||
}
|
||||
|
||||
break;
|
||||
case ModChangeType.Removed:
|
||||
RemoveModFromCaches( mod.BasePath );
|
||||
break;
|
||||
case ModChangeType.Changed:
|
||||
// TODO
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateNecessaryCaches()
|
||||
{
|
||||
AddCache( DefaultCollection );
|
||||
AddCache( ForcedCollection );
|
||||
foreach( var (_, collection) in CharacterCollection )
|
||||
{
|
||||
AddCache( collection );
|
||||
}
|
||||
}
|
||||
|
||||
public void RecreateCaches()
|
||||
{
|
||||
foreach( var collection in Collections.Where( c => c.Cache != null ) )
|
||||
{
|
||||
collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) );
|
||||
}
|
||||
|
||||
CreateNecessaryCaches();
|
||||
}
|
||||
|
||||
public void RemoveModFromCaches( DirectoryInfo modDir )
|
||||
{
|
||||
foreach( var collection in Collections )
|
||||
{
|
||||
collection.Cache?.RemoveMod( modDir );
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateCollections( ModData mod, bool metaChanges, ResourceChange fileChanges, bool nameChange, bool reloadMeta )
|
||||
{
|
||||
foreach( var collection in Collections )
|
||||
{
|
||||
if( metaChanges )
|
||||
{
|
||||
collection.UpdateSetting( mod );
|
||||
}
|
||||
|
||||
if( fileChanges.HasFlag( ResourceChange.Files )
|
||||
&& collection.Settings.TryGetValue( mod.BasePath.Name, out var settings )
|
||||
&& settings.Enabled )
|
||||
{
|
||||
collection.Cache?.CalculateEffectiveFileList();
|
||||
}
|
||||
|
||||
if( reloadMeta )
|
||||
{
|
||||
collection.Cache?.UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
|
||||
if( reloadMeta && DefaultCollection.Settings.TryGetValue( mod.BasePath.Name, out var config ) && config.Enabled )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddCollection( string name, Dictionary< string, ModSettings > settings )
|
||||
{
|
||||
var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||
if( nameFixed.Length == 0 || 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 = new ModCollection( name, settings );
|
||||
Collections.Add( newCollection );
|
||||
newCollection.Save();
|
||||
CollectionChanged?.Invoke( null, newCollection, CollectionType.Inactive );
|
||||
SetCollection( newCollection, CollectionType.Current );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveCollection( string name )
|
||||
{
|
||||
if( name == ModCollection.DefaultCollection )
|
||||
{
|
||||
PluginLog.Error( "Can not remove the default collection." );
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = Collections.IndexOf( c => c.Name == name );
|
||||
if( idx < 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var collection = Collections[ idx ];
|
||||
|
||||
if( CurrentCollection == collection )
|
||||
{
|
||||
SetCollection( Default, CollectionType.Current );
|
||||
}
|
||||
|
||||
if( ForcedCollection == collection )
|
||||
{
|
||||
SetCollection( ModCollection.Empty, CollectionType.Forced );
|
||||
}
|
||||
|
||||
if( DefaultCollection == collection )
|
||||
{
|
||||
SetCollection( ModCollection.Empty, CollectionType.Default );
|
||||
}
|
||||
|
||||
foreach( var (characterName, characterCollection) in CharacterCollection.ToArray() )
|
||||
{
|
||||
if( characterCollection == collection )
|
||||
{
|
||||
SetCollection( ModCollection.Empty, CollectionType.Character, characterName );
|
||||
}
|
||||
}
|
||||
|
||||
collection.Delete();
|
||||
Collections.RemoveAt( idx );
|
||||
CollectionChanged?.Invoke( collection, null, CollectionType.Inactive );
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddCache( ModCollection collection )
|
||||
{
|
||||
if( collection.Cache == null && collection.Name != string.Empty )
|
||||
{
|
||||
collection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) );
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCache( ModCollection collection )
|
||||
{
|
||||
if( collection.Name != ForcedCollection.Name
|
||||
&& collection.Name != CurrentCollection.Name
|
||||
&& collection.Name != DefaultCollection.Name
|
||||
&& CharacterCollection.All( kvp => kvp.Value.Name != collection.Name ) )
|
||||
{
|
||||
collection.ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCollection( ModCollection newCollection, CollectionType type, string? characterName = null )
|
||||
{
|
||||
var oldCollection = type switch
|
||||
{
|
||||
CollectionType.Default => DefaultCollection,
|
||||
CollectionType.Forced => ForcedCollection,
|
||||
CollectionType.Current => CurrentCollection,
|
||||
CollectionType.Character => characterName?.Length > 0
|
||||
? CharacterCollection.TryGetValue( characterName, out var c )
|
||||
? c
|
||||
: ModCollection.Empty
|
||||
: null,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if( oldCollection == null || newCollection.Name == oldCollection.Name )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddCache( newCollection );
|
||||
RemoveCache( oldCollection );
|
||||
switch( type )
|
||||
{
|
||||
case CollectionType.Default:
|
||||
DefaultCollection = newCollection;
|
||||
Penumbra.Config.DefaultCollection = newCollection.Name;
|
||||
Penumbra.ResidentResources.Reload();
|
||||
DefaultCollection.SetFiles();
|
||||
break;
|
||||
case CollectionType.Forced:
|
||||
ForcedCollection = newCollection;
|
||||
Penumbra.Config.ForcedCollection = newCollection.Name;
|
||||
Penumbra.ResidentResources.Reload();
|
||||
break;
|
||||
case CollectionType.Current:
|
||||
CurrentCollection = newCollection;
|
||||
Penumbra.Config.CurrentCollection = newCollection.Name;
|
||||
break;
|
||||
case CollectionType.Character:
|
||||
CharacterCollection[ characterName! ] = newCollection;
|
||||
Penumbra.Config.CharacterCollections[ characterName! ] = newCollection.Name;
|
||||
break;
|
||||
}
|
||||
|
||||
CollectionChanged?.Invoke( oldCollection, newCollection, type, characterName );
|
||||
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
|
||||
public bool CreateCharacterCollection( string characterName )
|
||||
{
|
||||
if( CharacterCollection.ContainsKey( characterName ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CharacterCollection[ characterName ] = ModCollection.Empty;
|
||||
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
|
||||
Penumbra.Config.Save();
|
||||
CollectionChanged?.Invoke( null, ModCollection.Empty, CollectionType.Character, characterName );
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveCharacterCollection( string characterName )
|
||||
{
|
||||
if( CharacterCollection.TryGetValue( characterName, out var collection ) )
|
||||
{
|
||||
RemoveCache( collection );
|
||||
CharacterCollection.Remove( characterName );
|
||||
CollectionChanged?.Invoke( collection, null, CollectionType.Character, characterName );
|
||||
}
|
||||
|
||||
if( Penumbra.Config.CharacterCollections.Remove( characterName ) )
|
||||
{
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private bool LoadCurrentCollection( Configuration config )
|
||||
{
|
||||
if( ByName( config.CurrentCollection, out var currentCollection ) )
|
||||
{
|
||||
CurrentCollection = currentCollection;
|
||||
AddCache( CurrentCollection );
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginLog.Error( $"Last choice of CurrentCollection {config.CurrentCollection} is not available, reset to Default." );
|
||||
CurrentCollection = Default;
|
||||
if( CurrentCollection.Cache == null )
|
||||
{
|
||||
CurrentCollection.CreateCache( _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) );
|
||||
}
|
||||
|
||||
config.CurrentCollection = ModCollection.DefaultCollection;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool LoadForcedCollection( Configuration config )
|
||||
{
|
||||
if( config.ForcedCollection.Length == 0 )
|
||||
{
|
||||
ForcedCollection = ModCollection.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ByName( config.ForcedCollection, out var forcedCollection ) )
|
||||
{
|
||||
ForcedCollection = forcedCollection;
|
||||
AddCache( ForcedCollection );
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginLog.Error( $"Last choice of ForcedCollection {config.ForcedCollection} is not available, reset to None." );
|
||||
ForcedCollection = ModCollection.Empty;
|
||||
config.ForcedCollection = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool LoadDefaultCollection( Configuration config )
|
||||
{
|
||||
if( config.DefaultCollection.Length == 0 )
|
||||
{
|
||||
DefaultCollection = ModCollection.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ByName( config.DefaultCollection, out var defaultCollection ) )
|
||||
{
|
||||
DefaultCollection = defaultCollection;
|
||||
AddCache( DefaultCollection );
|
||||
return false;
|
||||
}
|
||||
|
||||
PluginLog.Error( $"Last choice of DefaultCollection {config.DefaultCollection} is not available, reset to None." );
|
||||
DefaultCollection = ModCollection.Empty;
|
||||
config.DefaultCollection = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool LoadCharacterCollections( Configuration config )
|
||||
{
|
||||
var configChanged = false;
|
||||
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
|
||||
{
|
||||
if( collectionName.Length == 0 )
|
||||
{
|
||||
CharacterCollection.Add( player, ModCollection.Empty );
|
||||
}
|
||||
else if( ByName( collectionName, out var charCollection ) )
|
||||
{
|
||||
AddCache( charCollection );
|
||||
CharacterCollection.Add( player, charCollection );
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to None." );
|
||||
CharacterCollection.Add( player, ModCollection.Empty );
|
||||
config.CharacterCollections[ player ] = string.Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return configChanged;
|
||||
}
|
||||
|
||||
private void LoadConfigCollections( Configuration config )
|
||||
{
|
||||
var configChanged = LoadCurrentCollection( config );
|
||||
configChanged |= LoadDefaultCollection( config );
|
||||
configChanged |= LoadForcedCollection( config );
|
||||
configChanged |= LoadCharacterCollections( config );
|
||||
|
||||
if( configChanged )
|
||||
{
|
||||
config.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadCollections()
|
||||
{
|
||||
var collectionDir = ModCollection.CollectionDir();
|
||||
if( collectionDir.Exists )
|
||||
{
|
||||
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
|
||||
{
|
||||
var collection = ModCollection.LoadFromFile( file );
|
||||
if( collection == null || collection.Name == string.Empty )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" )
|
||||
{
|
||||
PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
|
||||
}
|
||||
|
||||
if( ByName( collection.Name ) != null )
|
||||
{
|
||||
PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." );
|
||||
}
|
||||
else
|
||||
{
|
||||
Collections.Add( collection );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( ByName( ModCollection.DefaultCollection ) == null )
|
||||
{
|
||||
var defaultCollection = new ModCollection();
|
||||
defaultCollection.Save();
|
||||
Collections.Add( defaultCollection );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,523 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class ModCollection2
|
||||
{
|
||||
public const int CurrentVersion = 1;
|
||||
public const string DefaultCollection = "Default";
|
||||
|
||||
public string Name { get; private init; }
|
||||
public int Version { get; private set; }
|
||||
|
||||
private readonly List< ModSettings? > _settings;
|
||||
|
||||
public IReadOnlyList< ModSettings? > Settings
|
||||
=> _settings;
|
||||
|
||||
public IEnumerable< ModSettings? > ActualSettings
|
||||
=> Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings );
|
||||
|
||||
private readonly Dictionary< string, ModSettings > _unusedSettings;
|
||||
|
||||
private ModCollection2( string name, ModCollection2 duplicate )
|
||||
{
|
||||
Name = name;
|
||||
Version = duplicate.Version;
|
||||
_settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() );
|
||||
_unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
||||
_inheritance = duplicate._inheritance.ToList();
|
||||
ModSettingChanged += SaveOnChange;
|
||||
InheritanceChanged += Save;
|
||||
}
|
||||
|
||||
private ModCollection2( string name, int version, Dictionary< string, ModSettings > allSettings )
|
||||
{
|
||||
Name = name;
|
||||
Version = version;
|
||||
_unusedSettings = allSettings;
|
||||
_settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList();
|
||||
for( var i = 0; i < Penumbra.ModManager.Count; ++i )
|
||||
{
|
||||
var modName = Penumbra.ModManager[ i ].BasePath.Name;
|
||||
if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) )
|
||||
{
|
||||
_unusedSettings.Remove( modName );
|
||||
_settings[ i ] = settings;
|
||||
}
|
||||
}
|
||||
|
||||
Migration.Migrate( this );
|
||||
ModSettingChanged += SaveOnChange;
|
||||
InheritanceChanged += Save;
|
||||
}
|
||||
|
||||
public static ModCollection2 CreateNewEmpty( string name )
|
||||
=> new(name, CurrentVersion, new Dictionary< string, ModSettings >());
|
||||
|
||||
public ModCollection2 Duplicate( string name )
|
||||
=> new(name, this);
|
||||
|
||||
private void CleanUnavailableSettings()
|
||||
{
|
||||
var any = _unusedSettings.Count > 0;
|
||||
_unusedSettings.Clear();
|
||||
if( any )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModData mod )
|
||||
{
|
||||
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
_settings.Add( settings );
|
||||
_unusedSettings.Remove( mod.BasePath.Name );
|
||||
}
|
||||
else
|
||||
{
|
||||
_settings.Add( null );
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveMod( ModData mod, int idx )
|
||||
{
|
||||
var settings = _settings[ idx ];
|
||||
if( settings != null )
|
||||
{
|
||||
_unusedSettings.Add( mod.BasePath.Name, settings );
|
||||
}
|
||||
|
||||
_settings.RemoveAt( idx );
|
||||
}
|
||||
|
||||
public static string CollectionDirectory
|
||||
=> Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" );
|
||||
|
||||
private 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()
|
||||
{
|
||||
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 ModCollection2? 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 ModCollection2( name, version, settings );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// A ModCollection is a named set of ModSettings to all of the users' installed mods.
|
||||
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
|
||||
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made.
|
||||
// Active ModCollections build a cache of currently relevant data.
|
||||
public class ModCollection
|
||||
{
|
||||
public const string DefaultCollection = "Default";
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public Dictionary< string, ModSettings > Settings { get; }
|
||||
|
||||
public ModCollection()
|
||||
{
|
||||
Name = DefaultCollection;
|
||||
Settings = new Dictionary< string, ModSettings >();
|
||||
}
|
||||
|
||||
public ModCollection( string name, Dictionary< string, ModSettings > settings )
|
||||
{
|
||||
Name = name;
|
||||
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
||||
}
|
||||
|
||||
public Mod.Mod GetMod( ModData mod )
|
||||
{
|
||||
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
return new Mod.Mod( settings, mod );
|
||||
}
|
||||
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
Save();
|
||||
return new Mod.Mod( newSettings, mod );
|
||||
}
|
||||
|
||||
private bool CleanUnavailableSettings( Dictionary< string, ModData > data )
|
||||
{
|
||||
var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray();
|
||||
|
||||
foreach( var s in removeList )
|
||||
{
|
||||
Settings.Remove( s.Key );
|
||||
}
|
||||
|
||||
return removeList.Length > 0;
|
||||
}
|
||||
|
||||
public void CreateCache( IEnumerable< ModData > data )
|
||||
{
|
||||
Cache = new ModCollectionCache( this );
|
||||
var changedSettings = false;
|
||||
foreach( var mod in data )
|
||||
{
|
||||
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
Cache.AddMod( settings, mod, false );
|
||||
}
|
||||
else
|
||||
{
|
||||
changedSettings = true;
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
Cache.AddMod( newSettings, mod, false );
|
||||
}
|
||||
}
|
||||
|
||||
if( changedSettings )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList( true, false );
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
=> Cache = null;
|
||||
|
||||
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
|
||||
{
|
||||
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( clear )
|
||||
{
|
||||
settings.Settings.Clear();
|
||||
}
|
||||
|
||||
if( settings.FixInvalidSettings( meta ) )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSetting( ModData mod )
|
||||
=> UpdateSetting( mod.BasePath, mod.Meta, false );
|
||||
|
||||
public void UpdateSettings( bool forceSave )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var changes = false;
|
||||
foreach( var mod in Cache.AvailableMods.Values )
|
||||
{
|
||||
changes |= mod.FixSettings();
|
||||
}
|
||||
|
||||
if( forceSave || changes )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident )
|
||||
{
|
||||
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations );
|
||||
Cache ??= new ModCollectionCache( this );
|
||||
UpdateSettings( false );
|
||||
Cache.CalculateEffectiveFileList();
|
||||
if( withMetaManipulations )
|
||||
{
|
||||
Cache.UpdateMetaManipulations();
|
||||
}
|
||||
|
||||
if( reloadResident )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public ModCollectionCache? Cache { get; private set; }
|
||||
|
||||
public static ModCollection? LoadFromFile( FileInfo file )
|
||||
{
|
||||
if( !file.Exists )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
|
||||
return collection;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SaveToFile( FileInfo file )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static DirectoryInfo CollectionDir()
|
||||
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ));
|
||||
|
||||
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
|
||||
=> new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ));
|
||||
|
||||
public FileInfo FileName()
|
||||
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
|
||||
$"{Name.RemoveInvalidPathSymbols()}.json" ));
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = CollectionDir();
|
||||
dir.Create();
|
||||
var file = FileName( dir, Name );
|
||||
SaveToFile( file );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static ModCollection? Load( string name )
|
||||
{
|
||||
var file = FileName( CollectionDir(), name );
|
||||
return file.Exists ? LoadFromFile( file ) : null;
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
var file = FileName( CollectionDir(), Name );
|
||||
if( file.Exists )
|
||||
{
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModData data )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings )
|
||||
? settings
|
||||
: ModSettings.DefaultSettings( data.Meta ),
|
||||
data );
|
||||
}
|
||||
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly ModCollection Empty = new() { Name = "" };
|
||||
}
|
||||
|
|
@ -1,662 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
// The ModCollectionCache contains all required temporary data to use a collection.
|
||||
// It will only be setup if a collection gets activated in any way.
|
||||
public class ModCollectionCache2
|
||||
{
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256);
|
||||
private static readonly List< ModSettings? > ResolvedSettings = new(128);
|
||||
|
||||
private readonly ModCollection2 _collection;
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
private ModCache2 _cache;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
public ModCollectionCache2( ModCollection2 collection )
|
||||
=> _collection = collection;
|
||||
|
||||
//MetaManipulations = new MetaManager( collection );
|
||||
private static void ResetFileSeen( int size )
|
||||
{
|
||||
if( size < FileSeen.Length )
|
||||
{
|
||||
FileSeen.Length = size;
|
||||
FileSeen.SetAll( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSeen.SetAll( false );
|
||||
FileSeen.Length = size;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearStorageAndPrepare()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
_cache.ClearFileConflicts();
|
||||
|
||||
ResolvedSettings.Clear();
|
||||
ResolvedSettings.AddRange( _collection.ActualSettings );
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ClearStorageAndPrepare();
|
||||
|
||||
for( var i = 0; i < Penumbra.ModManager.Mods.Count; ++i )
|
||||
{
|
||||
if( ResolvedSettings[ i ]?.Enabled == true )
|
||||
{
|
||||
AddFiles( i );
|
||||
AddSwaps( i );
|
||||
}
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
}
|
||||
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 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' ) ) )
|
||||
{
|
||||
identifier.Identify( _changedItems, resolved.ToGamePath() );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Unknown Error:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AddFiles( int idx )
|
||||
{
|
||||
var mod = Penumbra.ModManager.Mods[ idx ];
|
||||
ResetFileSeen( mod.Resources.ModFiles.Count );
|
||||
// Iterate in reverse so that later groups take precedence before earlier ones.
|
||||
foreach( var group in mod.Meta.Groups.Values.Reverse() )
|
||||
{
|
||||
switch( group.SelectionType )
|
||||
{
|
||||
case SelectType.Single:
|
||||
AddFilesForSingle( group, mod, idx );
|
||||
break;
|
||||
case SelectType.Multi:
|
||||
AddFilesForMulti( group, mod, idx );
|
||||
break;
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
AddRemainingFiles( mod, idx );
|
||||
}
|
||||
|
||||
private static bool FilterFile( Utf8GamePath gamePath )
|
||||
{
|
||||
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||
// so only add those files if it is disabled.
|
||||
if( !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file )
|
||||
{
|
||||
if( FilterFile( gamePath ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) )
|
||||
{
|
||||
RegisteredFiles.Add( gamePath, modIdx );
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
else
|
||||
{
|
||||
var priority = ResolvedSettings[ modIdx ]!.Priority;
|
||||
var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority;
|
||||
_cache.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath );
|
||||
if( priority > oldPriority )
|
||||
{
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
RegisteredFiles[ gamePath ] = modIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMissingFile( FullPath file )
|
||||
{
|
||||
switch( file.Extension.ToLowerInvariant() )
|
||||
{
|
||||
case ".meta":
|
||||
case ".rgsp":
|
||||
return;
|
||||
default:
|
||||
MissingFiles.Add( file );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPathsForOption( Option option, ModData mod, int modIdx, bool enabled )
|
||||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath( mod.BasePath, file );
|
||||
var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
continue;
|
||||
}
|
||||
|
||||
var registeredFile = mod.Resources.ModFiles[ idx ];
|
||||
if( !registeredFile.Exists )
|
||||
{
|
||||
AddMissingFile( registeredFile );
|
||||
continue;
|
||||
}
|
||||
|
||||
FileSeen.Set( idx, true );
|
||||
if( enabled )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
AddFile( modIdx, path, registeredFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForSingle( OptionGroup singleGroup, ModData mod, int modIdx )
|
||||
{
|
||||
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
|
||||
var settings = ResolvedSettings[ modIdx ]!;
|
||||
if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) )
|
||||
{
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
for( var i = 0; i < singleGroup.Options.Count; ++i )
|
||||
{
|
||||
AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForMulti( OptionGroup multiGroup, ModData mod, int modIdx )
|
||||
{
|
||||
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
|
||||
var settings = ResolvedSettings[ modIdx ]!;
|
||||
if( !settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also iterate options in reverse so that later options take precedence before earlier ones.
|
||||
for( var i = multiGroup.Options.Count - 1; i >= 0; --i )
|
||||
{
|
||||
AddPathsForOption( multiGroup.Options[ i ], mod, modIdx, ( setting & ( 1 << i ) ) != 0 );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRemainingFiles( ModData mod, int modIdx )
|
||||
{
|
||||
for( var i = 0; i < mod.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
if( FileSeen.Get( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var file = mod.Resources.ModFiles[ i ];
|
||||
if( file.Exists )
|
||||
{
|
||||
if( file.ToGamePath( mod.BasePath, out var gamePath ) )
|
||||
{
|
||||
AddFile( modIdx, gamePath, file );
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MissingFiles.Add( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMetaFiles()
|
||||
=> MetaManipulations.Imc.SetFiles();
|
||||
|
||||
private void AddSwaps( int modIdx )
|
||||
{
|
||||
var mod = Penumbra.ModManager.Mods[ modIdx ];
|
||||
foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) )
|
||||
{
|
||||
AddFile( modIdx, gamePath, swapPath );
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Manipulations
|
||||
public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.IsRooted && !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath );
|
||||
}
|
||||
|
||||
// The ModCollectionCache contains all required temporary data to use a collection.
|
||||
// It will only be setup if a collection gets activated in any way.
|
||||
public class ModCollectionCache
|
||||
{
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256);
|
||||
|
||||
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
||||
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
public ModCollectionCache( ModCollection collection )
|
||||
=> MetaManipulations = new MetaManager( collection );
|
||||
|
||||
private static void ResetFileSeen( int size )
|
||||
{
|
||||
if( size < FileSeen.Length )
|
||||
{
|
||||
FileSeen.Length = size;
|
||||
FileSeen.SetAll( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
FileSeen.SetAll( false );
|
||||
FileSeen.Length = size;
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
|
||||
foreach( var mod in AvailableMods.Values
|
||||
.Where( m => m.Settings.Enabled )
|
||||
.OrderByDescending( m => m.Settings.Priority ) )
|
||||
{
|
||||
mod.Cache.ClearFileConflicts();
|
||||
AddFiles( mod );
|
||||
AddSwaps( mod );
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
}
|
||||
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 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' ) ) )
|
||||
{
|
||||
identifier.Identify( _changedItems, resolved.ToGamePath() );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Unknown Error:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void AddFiles( Mod.Mod mod )
|
||||
{
|
||||
ResetFileSeen( mod.Data.Resources.ModFiles.Count );
|
||||
// Iterate in reverse so that later groups take precedence before earlier ones.
|
||||
foreach( var group in mod.Data.Meta.Groups.Values.Reverse() )
|
||||
{
|
||||
switch( group.SelectionType )
|
||||
{
|
||||
case SelectType.Single:
|
||||
AddFilesForSingle( group, mod );
|
||||
break;
|
||||
case SelectType.Multi:
|
||||
AddFilesForMulti( group, mod );
|
||||
break;
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
AddRemainingFiles( mod );
|
||||
}
|
||||
|
||||
private static bool FilterFile( Utf8GamePath gamePath )
|
||||
{
|
||||
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||
// so only add those files if it is disabled.
|
||||
if( !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file )
|
||||
{
|
||||
if( FilterFile( gamePath ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) )
|
||||
{
|
||||
RegisteredFiles.Add( gamePath, mod );
|
||||
ResolvedFiles[ gamePath ] = file;
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, gamePath );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, gamePath );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMissingFile( FullPath file )
|
||||
{
|
||||
switch( file.Extension.ToLowerInvariant() )
|
||||
{
|
||||
case ".meta":
|
||||
case ".rgsp":
|
||||
return;
|
||||
default:
|
||||
MissingFiles.Add( file );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled )
|
||||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath( mod.Data.BasePath, file );
|
||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
continue;
|
||||
}
|
||||
|
||||
var registeredFile = mod.Data.Resources.ModFiles[ idx ];
|
||||
if( !registeredFile.Exists )
|
||||
{
|
||||
AddMissingFile( registeredFile );
|
||||
continue;
|
||||
}
|
||||
|
||||
FileSeen.Set( idx, true );
|
||||
if( enabled )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
AddFile( mod, path, registeredFile );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( singleGroup.SelectionType == SelectType.Single );
|
||||
|
||||
if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) )
|
||||
{
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
for( var i = 0; i < singleGroup.Options.Count; ++i )
|
||||
{
|
||||
AddPathsForOption( singleGroup.Options[ i ], mod, setting == i );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod )
|
||||
{
|
||||
Debug.Assert( multiGroup.SelectionType == SelectType.Multi );
|
||||
|
||||
if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Also iterate options in reverse so that later options take precedence before earlier ones.
|
||||
for( var i = multiGroup.Options.Count - 1; i >= 0; --i )
|
||||
{
|
||||
AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRemainingFiles( Mod.Mod mod )
|
||||
{
|
||||
for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
if( FileSeen.Get( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var file = mod.Data.Resources.ModFiles[ i ];
|
||||
if( file.Exists )
|
||||
{
|
||||
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
|
||||
{
|
||||
AddFile( mod, gamePath, file );
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Warning( $"Could not convert {file} in {mod.Data.BasePath.FullName} to GamePath." );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MissingFiles.Add( file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMetaFiles()
|
||||
=> MetaManipulations.Imc.SetFiles();
|
||||
|
||||
private void AddSwaps( Mod.Mod mod )
|
||||
{
|
||||
foreach( var (key, value) in mod.Data.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) )
|
||||
{
|
||||
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
|
||||
{
|
||||
RegisteredFiles.Add( key, mod );
|
||||
ResolvedFiles.Add( key, value );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod, key );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, key );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddManipulations( Mod.Mod mod )
|
||||
{
|
||||
foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) )
|
||||
{
|
||||
if( !MetaManipulations.TryGetValue( manip, out var oldMod ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
mod.Cache.AddConflict( oldMod!, manip );
|
||||
if( !ReferenceEquals( mod, oldMod ) && mod.Settings.Priority == oldMod!.Settings.Priority )
|
||||
{
|
||||
oldMod.Cache.AddConflict( mod, manip );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMetaManipulations()
|
||||
{
|
||||
MetaManipulations.Reset();
|
||||
|
||||
foreach( var mod in AvailableMods.Values.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) )
|
||||
{
|
||||
mod.Cache.ClearMetaConflicts();
|
||||
AddManipulations( mod );
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveMod( DirectoryInfo basePath )
|
||||
{
|
||||
if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AvailableMods.Remove( basePath.Name );
|
||||
if( !mod.Settings.Enabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList();
|
||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModSettings settings, ModData data, bool updateFileList = true )
|
||||
{
|
||||
if( AvailableMods.ContainsKey( data.BasePath.Name ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data );
|
||||
|
||||
if( !updateFileList || !settings.Enabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList();
|
||||
if( data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
|
||||
public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.IsRooted && !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath );
|
||||
}
|
||||
|
|
@ -263,7 +263,7 @@ public class ModManager : IEnumerable< ModData >
|
|||
mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) );
|
||||
}
|
||||
|
||||
Penumbra.CollectionManager.UpdateCollections( mod, metaChanges, fileChanges, nameChange, reloadMeta ); // TODO
|
||||
// TODO: more specific mod changes?
|
||||
ModChange?.Invoke( ModChangeType.Changed, idx, mod );
|
||||
return true;
|
||||
}
|
||||
|
|
@ -271,10 +271,6 @@ public class ModManager : IEnumerable< ModData >
|
|||
public bool UpdateMod( ModData mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false )
|
||||
=> UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force );
|
||||
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
var ret = Penumbra.CollectionManager.DefaultCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
ret ??= Penumbra.CollectionManager.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
return ret;
|
||||
}
|
||||
public static FullPath? ResolvePath( Utf8GamePath gameResourcePath )
|
||||
=> Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath );
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using System.ComponentModel;
|
|||
using System.IO;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
|
@ -82,20 +83,13 @@ public static class ModManagerEditExtensions
|
|||
manager.Config.Save();
|
||||
}
|
||||
|
||||
foreach( var collection in Penumbra.CollectionManager.Collections )
|
||||
var idx = manager.Mods.IndexOf( mod );
|
||||
foreach( var collection in Penumbra.CollectionManager )
|
||||
{
|
||||
if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) )
|
||||
if( collection.Settings[ idx ] != null )
|
||||
{
|
||||
collection.Settings[ newDir.Name ] = settings;
|
||||
collection.Settings.Remove( oldBasePath.Name );
|
||||
collection.Save();
|
||||
}
|
||||
|
||||
if( collection.Cache != null )
|
||||
{
|
||||
collection.Cache.RemoveMod( newDir );
|
||||
collection.AddMod( mod );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -140,9 +134,13 @@ public static class ModManagerEditExtensions
|
|||
|
||||
mod.SaveMeta();
|
||||
|
||||
foreach( var collection in Penumbra.CollectionManager.Collections )
|
||||
// TODO to indices
|
||||
var idx = Penumbra.ModManager.Mods.IndexOf( mod );
|
||||
|
||||
foreach( var collection in Penumbra.CollectionManager )
|
||||
{
|
||||
if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
var settings = collection.Settings[ idx ];
|
||||
if( settings == null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -176,9 +174,11 @@ public static class ModManagerEditExtensions
|
|||
return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 );
|
||||
}
|
||||
|
||||
foreach( var collection in Penumbra.CollectionManager.Collections )
|
||||
var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO
|
||||
foreach( var collection in Penumbra.CollectionManager )
|
||||
{
|
||||
if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
var settings = collection.Settings[ idx ];
|
||||
if( settings == null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -199,10 +199,10 @@ public static class ModManagerEditExtensions
|
|||
{
|
||||
settings.Settings[ group.GroupName ] = newSetting;
|
||||
collection.Save();
|
||||
if( collection.Cache != null && settings.Enabled )
|
||||
if( collection.HasCache && settings.Enabled )
|
||||
{
|
||||
collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0,
|
||||
Penumbra.CollectionManager.IsActive( collection ) );
|
||||
Penumbra.CollectionManager.Default == collection );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
|
|
@ -12,7 +13,7 @@ using Penumbra.Interop;
|
|||
using Penumbra.Mods;
|
||||
using Penumbra.UI;
|
||||
using Penumbra.Util;
|
||||
using System.Linq;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Interop.Loader;
|
||||
using Penumbra.Interop.Resolver;
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ public class Penumbra : IDalamudPlugin
|
|||
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
||||
|
||||
public static ModManager ModManager { get; private set; } = null!;
|
||||
public static CollectionManager CollectionManager { get; private set; } = null!;
|
||||
public static CollectionManager2 CollectionManager { get; private set; } = null!;
|
||||
|
||||
public static ResourceLoader ResourceLoader { get; set; } = null!;
|
||||
public ResourceLogger ResourceLogger { get; }
|
||||
|
|
@ -67,7 +68,7 @@ public class Penumbra : IDalamudPlugin
|
|||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||
ModManager = new ModManager();
|
||||
ModManager.DiscoverMods();
|
||||
CollectionManager = new CollectionManager( ModManager );
|
||||
CollectionManager = new CollectionManager2( ModManager );
|
||||
ObjectReloader = new ObjectReloader();
|
||||
PathResolver = new PathResolver( ResourceLoader );
|
||||
|
||||
|
|
@ -110,10 +111,15 @@ public class Penumbra : IDalamudPlugin
|
|||
ResourceLoader.EnableFullLogging();
|
||||
}
|
||||
|
||||
if (CollectionManager.CharacterCollection.Count > 0)
|
||||
if( CollectionManager.HasCharacterCollections )
|
||||
{
|
||||
PathResolver.Enable();
|
||||
}
|
||||
|
||||
ResidentResources.Reload();
|
||||
//var c = ModCollection2.LoadFromFile( new FileInfo(@"C:\Users\Ozy\AppData\Roaming\XIVLauncher\pluginConfigs\Penumbra\collections\Rayla.json"),
|
||||
// out var inheritance );
|
||||
//c?.Save();
|
||||
}
|
||||
|
||||
public bool Enable()
|
||||
|
|
@ -217,10 +223,9 @@ public class Penumbra : IDalamudPlugin
|
|||
type = type.ToLowerInvariant();
|
||||
collectionName = collectionName.ToLowerInvariant();
|
||||
|
||||
var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.InvariantCultureIgnoreCase )
|
||||
? ModCollection.Empty
|
||||
: CollectionManager.Collections.FirstOrDefault( c
|
||||
=> string.Equals( c.Name, collectionName, StringComparison.InvariantCultureIgnoreCase ) );
|
||||
var collection = string.Equals( collectionName, ModCollection2.Empty.Name, StringComparison.InvariantCultureIgnoreCase )
|
||||
? ModCollection2.Empty
|
||||
: CollectionManager[collectionName];
|
||||
if( collection == null )
|
||||
{
|
||||
Dalamud.Chat.Print( $"The collection {collection} does not exist." );
|
||||
|
|
@ -230,7 +235,7 @@ public class Penumbra : IDalamudPlugin
|
|||
switch( type )
|
||||
{
|
||||
case "default":
|
||||
if( collection == CollectionManager.DefaultCollection )
|
||||
if( collection == CollectionManager.Default )
|
||||
{
|
||||
Dalamud.Chat.Print( $"{collection.Name} already is the default collection." );
|
||||
return false;
|
||||
|
|
@ -240,20 +245,9 @@ public class Penumbra : IDalamudPlugin
|
|||
Dalamud.Chat.Print( $"Set {collection.Name} as default collection." );
|
||||
SettingsInterface.ResetDefaultCollection();
|
||||
return true;
|
||||
case "forced":
|
||||
if( collection == CollectionManager.ForcedCollection )
|
||||
{
|
||||
Dalamud.Chat.Print( $"{collection.Name} already is the forced collection." );
|
||||
return false;
|
||||
}
|
||||
|
||||
CollectionManager.SetCollection( collection, CollectionType.Forced );
|
||||
Dalamud.Chat.Print( $"Set {collection.Name} as forced collection." );
|
||||
SettingsInterface.ResetForcedCollection();
|
||||
return true;
|
||||
default:
|
||||
Dalamud.Chat.Print(
|
||||
"Second command argument is not default or forced, the correct command format is: /penumbra collection {default|forced} <collectionName>" );
|
||||
"Second command argument is not default, the correct command format is: /penumbra collection default <collectionName>" );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Dalamud.Interface;
|
|||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Logging;
|
||||
using ImGuiNET;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI.Custom;
|
||||
|
|
@ -21,7 +22,7 @@ public partial class SettingsInterface
|
|||
private readonly Selector _selector;
|
||||
private string _collectionNames = null!;
|
||||
private string _collectionNamesWithNone = null!;
|
||||
private ModCollection[] _collections = null!;
|
||||
private ModCollection2[] _collections = null!;
|
||||
private int _currentCollectionIndex;
|
||||
private int _currentForcedIndex;
|
||||
private int _currentDefaultIndex;
|
||||
|
|
@ -31,14 +32,14 @@ public partial class SettingsInterface
|
|||
|
||||
private void UpdateNames()
|
||||
{
|
||||
_collections = Penumbra.CollectionManager.Collections.Prepend( ModCollection.Empty ).ToArray();
|
||||
_collections = Penumbra.CollectionManager.Prepend( ModCollection2.Empty ).ToArray();
|
||||
_collectionNames = string.Join( "\0", _collections.Skip( 1 ).Select( c => c.Name ) ) + '\0';
|
||||
_collectionNamesWithNone = "None\0" + _collectionNames;
|
||||
UpdateIndices();
|
||||
}
|
||||
|
||||
|
||||
private int GetIndex( ModCollection collection )
|
||||
private int GetIndex( ModCollection2 collection )
|
||||
{
|
||||
var ret = _collections.IndexOf( c => c.Name == collection.Name );
|
||||
if( ret < 0 )
|
||||
|
|
@ -175,7 +176,7 @@ public partial class SettingsInterface
|
|||
}
|
||||
}
|
||||
|
||||
public void SetCurrentCollection( ModCollection collection, bool force = false )
|
||||
public void SetCurrentCollection( ModCollection2 collection, bool force = false )
|
||||
{
|
||||
var idx = Array.IndexOf( _collections, collection ) - 1;
|
||||
if( idx >= 0 )
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -99,9 +100,9 @@ public partial class SettingsInterface
|
|||
return !_filePathFilter.Any() || kvp.Item3.Contains( _filePathFilterLower );
|
||||
}
|
||||
|
||||
private void DrawFilteredRows( ModCollectionCache? active, ModCollectionCache? forced )
|
||||
private void DrawFilteredRows( ModCollection2 active )
|
||||
{
|
||||
void DrawFileLines( ModCollectionCache cache )
|
||||
void DrawFileLines( ModCollection2.Cache cache )
|
||||
{
|
||||
foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) )
|
||||
{
|
||||
|
|
@ -116,15 +117,7 @@ public partial class SettingsInterface
|
|||
//}
|
||||
}
|
||||
|
||||
if( active != null )
|
||||
{
|
||||
DrawFileLines( active );
|
||||
}
|
||||
|
||||
if( forced != null )
|
||||
{
|
||||
DrawFileLines( forced );
|
||||
}
|
||||
DrawFileLines( active );
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ namespace Penumbra.UI
|
|||
{
|
||||
foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) )
|
||||
{
|
||||
var mod = Penumbra.CollectionManager.CurrentCollection.GetMod( modData );
|
||||
var mod = Penumbra.CollectionManager.Current.GetMod( modData );
|
||||
_modsInOrder.Add( mod );
|
||||
_visibleMods.Add( CheckFilters( mod ) );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Windows.Forms.VisualStyles;
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Logging;
|
||||
using ImGuiNET;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -606,10 +607,10 @@ public partial class SettingsInterface
|
|||
Cache = new ModListCache( Penumbra.ModManager, newMods );
|
||||
}
|
||||
|
||||
private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection )
|
||||
private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection2 collection )
|
||||
{
|
||||
if( collection == ModCollection.Empty
|
||||
|| collection == Penumbra.CollectionManager.CurrentCollection )
|
||||
if( collection == ModCollection2.Empty
|
||||
|| collection == Penumbra.CollectionManager.Current )
|
||||
{
|
||||
using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f );
|
||||
ImGui.Button( label, Vector2.UnitX * size );
|
||||
|
|
@ -632,16 +633,13 @@ public partial class SettingsInterface
|
|||
var comboSize = size * ImGui.GetIO().FontGlobalScale;
|
||||
var offset = comboSize + textSize;
|
||||
|
||||
var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth()
|
||||
- offset
|
||||
- SelectorPanelWidth * _selectorScalingFactor
|
||||
- 4 * ImGui.GetStyle().ItemSpacing.X )
|
||||
/ 2, 5f );
|
||||
var buttonSize = Math.Max( ImGui.GetWindowContentRegionWidth()
|
||||
- offset
|
||||
- SelectorPanelWidth * _selectorScalingFactor
|
||||
- 3 * ImGui.GetStyle().ItemSpacing.X, 5f );
|
||||
ImGui.SameLine();
|
||||
DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.DefaultCollection );
|
||||
DrawCollectionButton( "Default", "default", buttonSize, Penumbra.CollectionManager.Default );
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawCollectionButton( "Forced", "forced", buttonSize, Penumbra.CollectionManager.ForcedCollection );
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( comboSize );
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue