mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add empty option for single select groups with empty options. More Editor stuff.
This commit is contained in:
parent
81e93e0664
commit
e2a6274b33
21 changed files with 937 additions and 336 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit cce4e9ed2cf5fa0068d6c8fadff5acd0d54f8359
|
||||
Subproject commit 5cb708ff692d397a9e71f3315d9d054f6558f42d
|
||||
|
|
@ -282,6 +282,7 @@ public partial class ModCollection
|
|||
|
||||
if( recomputeList )
|
||||
{
|
||||
// TODO: Does not check if the option that was changed is actually enabled.
|
||||
foreach( var collection in this.Where( c => c.HasCache ) )
|
||||
{
|
||||
if( collection[ mod.Index ].Settings is { Enabled: true } )
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ public partial class TexToolsImporter
|
|||
.Sum( page => page.ModGroups
|
||||
.Where( g => g.GroupName.Length > 0 && g.OptionList.Length > 0 )
|
||||
.Sum( group => group.OptionList
|
||||
.Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 ) ) );
|
||||
.Count( o => o.Name.Length > 0 && o.ModsJsons.Length > 0 )
|
||||
+ ( group.OptionList.Any( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 ) ? 1 : 0 ) ) );
|
||||
|
||||
// Extended V2 mod packs contain multiple options that need to be handled separately.
|
||||
private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw )
|
||||
|
|
@ -173,6 +174,18 @@ public partial class TexToolsImporter
|
|||
++_currentOptionIdx;
|
||||
}
|
||||
|
||||
// Handle empty options for single select groups without creating a folder for them.
|
||||
// We only want one of those at most, and it should usually be the first option.
|
||||
if( group.SelectionType == SelectType.Single )
|
||||
{
|
||||
var empty = group.OptionList.FirstOrDefault( o => o.Name.Length > 0 && o.ModsJsons.Length == 0 );
|
||||
if( empty != null )
|
||||
{
|
||||
_currentOptionName = empty.Name;
|
||||
options.Insert( 0, Mod.CreateEmptySubMod( empty.Name ) );
|
||||
}
|
||||
}
|
||||
|
||||
Mod.CreateOptionGroup( _currentModDirectory, group, groupPriority, groupPriority, description.ToString(), options );
|
||||
++groupPriority;
|
||||
}
|
||||
|
|
|
|||
207
Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs
Normal file
207
Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
{
|
||||
public partial class Editor
|
||||
{
|
||||
private readonly SHA256 _hasher = SHA256.Create();
|
||||
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
|
||||
|
||||
public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates
|
||||
=> _duplicates;
|
||||
|
||||
public long SavedSpace { get; private set; } = 0;
|
||||
|
||||
public bool DuplicatesFinished { get; private set; } = true;
|
||||
|
||||
public void DeleteDuplicates()
|
||||
{
|
||||
if( !DuplicatesFinished || _duplicates.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (set, _, _) in _duplicates )
|
||||
{
|
||||
if( set.Length < 2 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var remaining = set[ 0 ];
|
||||
foreach( var duplicate in set.Skip( 1 ) )
|
||||
{
|
||||
HandleDuplicate( duplicate, remaining );
|
||||
}
|
||||
}
|
||||
_availableFiles.RemoveAll( p => !p.Item1.Exists );
|
||||
_duplicates.Clear();
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
DuplicatesFinished = true;
|
||||
}
|
||||
|
||||
private void HandleDuplicate( FullPath duplicate, FullPath remaining )
|
||||
{
|
||||
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
|
||||
{
|
||||
var changes = false;
|
||||
var dict = subMod.Files.ToDictionary( kvp => kvp.Key,
|
||||
kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) );
|
||||
if( changes )
|
||||
{
|
||||
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict );
|
||||
}
|
||||
}
|
||||
|
||||
ApplyToAllOptions( _mod, HandleSubMod );
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete( duplicate.FullName );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private FullPath ChangeDuplicatePath( FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes )
|
||||
{
|
||||
if( !value.Equals( from ) )
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
changes = true;
|
||||
PluginLog.Debug( "[DeleteDuplicates] Changing {GamePath:l} for {Mod:d}\n : {Old:l}\n -> {New:l}", key, _mod.Name, from, to);
|
||||
return to;
|
||||
}
|
||||
|
||||
|
||||
public void StartDuplicateCheck()
|
||||
{
|
||||
DuplicatesFinished = false;
|
||||
Task.Run( CheckDuplicates );
|
||||
}
|
||||
|
||||
private void CheckDuplicates()
|
||||
{
|
||||
_duplicates.Clear();
|
||||
SavedSpace = 0;
|
||||
var list = new List< FullPath >();
|
||||
var lastSize = -1L;
|
||||
foreach( var (p, size) in AvailableFiles )
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( size == lastSize )
|
||||
{
|
||||
list.Add( p );
|
||||
continue;
|
||||
}
|
||||
|
||||
if( list.Count >= 2 )
|
||||
{
|
||||
CheckMultiDuplicates( list, lastSize );
|
||||
}
|
||||
lastSize = size;
|
||||
|
||||
list.Clear();
|
||||
list.Add( p );
|
||||
}
|
||||
if( list.Count >= 2 )
|
||||
{
|
||||
CheckMultiDuplicates( list, lastSize );
|
||||
}
|
||||
|
||||
DuplicatesFinished = true;
|
||||
}
|
||||
|
||||
private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size )
|
||||
{
|
||||
var hashes = list.Select( f => (f, ComputeHash(f)) ).ToList();
|
||||
while( hashes.Count > 0 )
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var set = new HashSet< FullPath > { hashes[ 0 ].Item1 };
|
||||
var hash = hashes[ 0 ];
|
||||
for( var j = 1; j < hashes.Count; ++j )
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( CompareHashes( hash.Item2, hashes[ j ].Item2 ) && CompareFilesDirectly( hashes[ 0 ].Item1, hashes[ j ].Item1 ) )
|
||||
{
|
||||
set.Add( hashes[ j ].Item1 );
|
||||
}
|
||||
}
|
||||
|
||||
hashes.RemoveAll( p => set.Contains(p.Item1) );
|
||||
if( set.Count > 1 )
|
||||
{
|
||||
_duplicates.Add( (set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2) );
|
||||
SavedSpace += ( set.Count - 1 ) * size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 )
|
||||
{
|
||||
if( !f1.Exists || !f2.Exists )
|
||||
return false;
|
||||
|
||||
using var s1 = File.OpenRead( f1.FullName );
|
||||
using var s2 = File.OpenRead( f2.FullName );
|
||||
var buffer1 = stackalloc byte[256];
|
||||
var buffer2 = stackalloc byte[256];
|
||||
var span1 = new Span< byte >( buffer1, 256 );
|
||||
var span2 = new Span< byte >( buffer2, 256 );
|
||||
|
||||
while( true )
|
||||
{
|
||||
var bytes1 = s1.Read( span1 );
|
||||
var bytes2 = s2.Read( span2 );
|
||||
if( bytes1 != bytes2 )
|
||||
return false;
|
||||
|
||||
if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) )
|
||||
return false;
|
||||
|
||||
if( bytes1 < 256 )
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CompareHashes( byte[] f1, byte[] f2 )
|
||||
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
|
||||
|
||||
public byte[] ComputeHash( FullPath f )
|
||||
{
|
||||
using var stream = File.OpenRead( f.FullName );
|
||||
return _hasher.ComputeHash( stream );
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Penumbra/Mods/Editor/Mod.Editor.Edit.cs
Normal file
43
Penumbra/Mods/Editor/Mod.Editor.Edit.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
{
|
||||
public partial class Editor
|
||||
{
|
||||
private int _groupIdx = -1;
|
||||
private int _optionIdx = 0;
|
||||
|
||||
private IModGroup? _modGroup;
|
||||
private SubMod _subMod;
|
||||
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
|
||||
public readonly HashSet< MetaManipulation > CurrentManipulations = new();
|
||||
|
||||
public void SetSubMod( int groupIdx, int optionIdx )
|
||||
{
|
||||
_groupIdx = groupIdx;
|
||||
_optionIdx = optionIdx;
|
||||
if( groupIdx >= 0 )
|
||||
{
|
||||
_modGroup = _mod.Groups[ groupIdx ];
|
||||
_subMod = ( SubMod )_modGroup![ optionIdx ];
|
||||
}
|
||||
else
|
||||
{
|
||||
_modGroup = null;
|
||||
_subMod = _mod._default;
|
||||
}
|
||||
|
||||
CurrentFiles.SetTo( _subMod.Files );
|
||||
CurrentSwaps.SetTo( _subMod.FileSwaps );
|
||||
CurrentManipulations.Clear();
|
||||
CurrentManipulations.UnionWith( _subMod.Manipulations );
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Penumbra/Mods/Editor/Mod.Editor.Files.cs
Normal file
112
Penumbra/Mods/Editor/Mod.Editor.Files.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
{
|
||||
public partial class Editor
|
||||
{
|
||||
// All files in subdirectories of the mod directory.
|
||||
public IReadOnlyList< (FullPath, long) > AvailableFiles
|
||||
=> _availableFiles;
|
||||
|
||||
private readonly List< (FullPath, long) > _availableFiles;
|
||||
|
||||
// All files that are available but not currently used in any option.
|
||||
private readonly SortedSet< FullPath > _unusedFiles;
|
||||
|
||||
public IReadOnlySet< FullPath > UnusedFiles
|
||||
=> _unusedFiles;
|
||||
|
||||
// All paths that are used in any option in the mod.
|
||||
private readonly SortedSet< FullPath > _usedPaths;
|
||||
|
||||
public IReadOnlySet< FullPath > UsedPaths
|
||||
=> _usedPaths;
|
||||
|
||||
// All paths that are used in
|
||||
private readonly SortedSet< FullPath > _missingPaths;
|
||||
|
||||
public IReadOnlySet< FullPath > MissingPaths
|
||||
=> _missingPaths;
|
||||
|
||||
// Adds all currently unused paths, relative to the mod directory, to the replacements.
|
||||
public void AddUnusedPathsToDefault()
|
||||
{
|
||||
var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count );
|
||||
foreach( var file in UnusedFiles )
|
||||
{
|
||||
var gamePath = file.ToGamePath( _mod.BasePath, out var g ) ? g : Utf8GamePath.Empty;
|
||||
if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) )
|
||||
{
|
||||
dict.Add( gamePath, file );
|
||||
PluginLog.Debug( "[AddUnusedPaths] Adding {GamePath} -> {File} to default option of {Mod}.", gamePath, file, _mod.Name );
|
||||
}
|
||||
}
|
||||
|
||||
Penumbra.ModManager.OptionAddFiles( _mod, -1, 0, dict );
|
||||
_usedPaths.UnionWith( _mod.Default.Files.Values );
|
||||
_unusedFiles.RemoveWhere( f => _mod.Default.Files.Values.Contains( f ) );
|
||||
}
|
||||
|
||||
// Delete all currently unused paths from your filesystem.
|
||||
public void DeleteUnusedPaths()
|
||||
{
|
||||
foreach( var file in UnusedFiles )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete( file.FullName );
|
||||
PluginLog.Debug( "[DeleteUnusedPaths] Deleted {File} from {Mod}.", file, _mod.Name );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error($"[DeleteUnusedPaths] Could not delete {file} from {_mod.Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
_unusedFiles.RemoveWhere( f => !f.Exists );
|
||||
_availableFiles.RemoveAll( p => !p.Item1.Exists );
|
||||
}
|
||||
|
||||
// Remove all path redirections where the pointed-to file does not exist.
|
||||
public void RemoveMissingPaths()
|
||||
{
|
||||
void HandleSubMod( ISubMod mod, int groupIdx, int optionIdx )
|
||||
{
|
||||
var newDict = mod.Files.Where( kvp => CheckAgainstMissing( kvp.Value, kvp.Key ) )
|
||||
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
|
||||
if( newDict.Count != mod.Files.Count )
|
||||
{
|
||||
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict );
|
||||
}
|
||||
}
|
||||
|
||||
ApplyToAllOptions( _mod, HandleSubMod );
|
||||
_usedPaths.RemoveWhere( _missingPaths.Contains );
|
||||
_missingPaths.Clear();
|
||||
}
|
||||
|
||||
private bool CheckAgainstMissing( FullPath file, Utf8GamePath key )
|
||||
{
|
||||
if( !_missingPaths.Contains( file ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
PluginLog.Debug( "[RemoveMissingPaths] Removing {GamePath} -> {File} from {Mod}.", key, file, _mod.Name );
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static List<(FullPath, long)> GetAvailablePaths( Mod mod )
|
||||
=> mod.BasePath.EnumerateDirectories()
|
||||
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) )
|
||||
.OrderBy( p => -p.Length ).ToList();
|
||||
}
|
||||
}
|
||||
73
Penumbra/Mods/Editor/Mod.Editor.Groups.cs
Normal file
73
Penumbra/Mods/Editor/Mod.Editor.Groups.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
using System.Collections.Generic;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
{
|
||||
public partial class Editor
|
||||
{
|
||||
public void Normalize()
|
||||
{}
|
||||
|
||||
public void AutoGenerateGroups()
|
||||
{
|
||||
//ClearEmptySubDirectories( _mod.BasePath );
|
||||
//for( var i = _mod.Groups.Count - 1; i >= 0; --i )
|
||||
//{
|
||||
// if (_mod.Groups.)
|
||||
// Penumbra.ModManager.DeleteModGroup( _mod, i );
|
||||
//}
|
||||
//Penumbra.ModManager.OptionSetFiles( _mod, -1, 0, new Dictionary< Utf8GamePath, FullPath >() );
|
||||
//
|
||||
//foreach( var groupDir in _mod.BasePath.EnumerateDirectories() )
|
||||
//{
|
||||
// var groupName = groupDir.Name;
|
||||
// foreach( var optionDir in groupDir.EnumerateDirectories() )
|
||||
// { }
|
||||
//}
|
||||
|
||||
//var group = new OptionGroup
|
||||
// {
|
||||
// GroupName = groupDir.Name,
|
||||
// SelectionType = SelectType.Single,
|
||||
// Options = new List< Option >(),
|
||||
// };
|
||||
//
|
||||
// foreach( var optionDir in groupDir.EnumerateDirectories() )
|
||||
// {
|
||||
// var option = new Option
|
||||
// {
|
||||
// OptionDesc = string.Empty,
|
||||
// OptionName = optionDir.Name,
|
||||
// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
// };
|
||||
// foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
// {
|
||||
// if( Utf8RelPath.FromFile( file, baseDir, out var rel )
|
||||
// && Utf8GamePath.FromFile( file, optionDir, out var game ) )
|
||||
// {
|
||||
// option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if( option.OptionFiles.Count > 0 )
|
||||
// {
|
||||
// group.Options.Add( option );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if( group.Options.Count > 0 )
|
||||
// {
|
||||
// meta.Groups.Add( groupDir.Name, group );
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta );
|
||||
//foreach( var collection in Penumbra.CollectionManager )
|
||||
//{
|
||||
// collection.Settings[ idx ]?.FixInvalidSettings( meta );
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
Penumbra/Mods/Editor/Mod.Editor.cs
Normal file
57
Penumbra/Mods/Editor/Mod.Editor.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
{
|
||||
public partial class Editor : IDisposable
|
||||
{
|
||||
private readonly Mod _mod;
|
||||
|
||||
public Editor( Mod mod )
|
||||
{
|
||||
_mod = mod;
|
||||
_availableFiles = GetAvailablePaths( mod );
|
||||
_usedPaths = new SortedSet< FullPath >( mod.AllFiles );
|
||||
_missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) );
|
||||
_unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) );
|
||||
_subMod = _mod._default;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DuplicatesFinished = true;
|
||||
}
|
||||
|
||||
// Does not delete the base directory itself even if it is completely empty at the end.
|
||||
private static void ClearEmptySubDirectories( DirectoryInfo baseDir )
|
||||
{
|
||||
foreach( var subDir in baseDir.GetDirectories() )
|
||||
{
|
||||
ClearEmptySubDirectories( subDir );
|
||||
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
|
||||
{
|
||||
subDir.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a option action to all available option in a mod, including the default option.
|
||||
private static void ApplyToAllOptions( Mod mod, Action< ISubMod, int, int > action )
|
||||
{
|
||||
action( mod.Default, -1, 0 );
|
||||
foreach( var (group, groupIdx) in mod.Groups.WithIndex() )
|
||||
{
|
||||
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
|
||||
{
|
||||
action( @group[ optionIdx ], groupIdx, optionIdx );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,3 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Import;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
|
|
@ -202,15 +188,10 @@ public partial class Mod
|
|||
//
|
||||
// private readonly DirectoryInfo _baseDir;
|
||||
// private readonly ModMeta _mod;
|
||||
// private SHA256? _hasher;
|
||||
|
||||
//
|
||||
// private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
|
||||
//
|
||||
// private SHA256 Sha()
|
||||
// {
|
||||
// _hasher ??= SHA256.Create();
|
||||
// return _hasher;
|
||||
// }
|
||||
//
|
||||
// private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
|
||||
// {
|
||||
|
|
@ -366,47 +347,6 @@ public partial class Mod
|
|||
// return meta.Groups[ Duplicates ].Options.First();
|
||||
// }
|
||||
//
|
||||
// public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
|
||||
// {
|
||||
// var dedup = new ModCleanup( baseDir, mod );
|
||||
// foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||
// {
|
||||
// if( value.Count == 2 )
|
||||
// {
|
||||
// if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
|
||||
// {
|
||||
// dedup.ReplaceFile( value[ 0 ], value[ 1 ] );
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var deleted = Enumerable.Repeat( false, value.Count ).ToArray();
|
||||
// var hashes = value.Select( dedup.ComputeHash ).ToArray();
|
||||
//
|
||||
// for( var i = 0; i < value.Count; ++i )
|
||||
// {
|
||||
// if( deleted[ i ] )
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// for( var j = i + 1; j < value.Count; ++j )
|
||||
// {
|
||||
// if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// dedup.ReplaceFile( value[ i ], value[ j ] );
|
||||
// deleted[ j ] = true;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// CleanUpDuplicates( mod );
|
||||
// ClearEmptySubDirectories( dedup._baseDir );
|
||||
// }
|
||||
//
|
||||
// private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||
// {
|
||||
|
|
@ -458,32 +398,8 @@ public partial class Mod
|
|||
// f2.Delete();
|
||||
// }
|
||||
//
|
||||
// public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
|
||||
// => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
|
||||
//
|
||||
// public static bool CompareHashes( byte[] f1, byte[] f2 )
|
||||
// => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
|
||||
//
|
||||
// public byte[] ComputeHash( FileInfo f )
|
||||
// {
|
||||
// var stream = File.OpenRead( f.FullName );
|
||||
// var ret = Sha().ComputeHash( stream );
|
||||
// stream.Dispose();
|
||||
// return ret;
|
||||
// }
|
||||
//
|
||||
// // Does not delete the base directory itself even if it is completely empty at the end.
|
||||
// public static void ClearEmptySubDirectories( DirectoryInfo baseDir )
|
||||
// {
|
||||
// foreach( var subDir in baseDir.GetDirectories() )
|
||||
// {
|
||||
// ClearEmptySubDirectories( subDir );
|
||||
// if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
|
||||
// {
|
||||
// subDir.Delete();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
//
|
||||
// private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false )
|
||||
// {
|
||||
|
|
@ -283,8 +283,7 @@ public sealed partial class Mod
|
|||
return;
|
||||
}
|
||||
|
||||
subMod.ManipulationData.Clear();
|
||||
subMod.ManipulationData.UnionWith( manipulations );
|
||||
subMod.ManipulationData = manipulations;
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 );
|
||||
}
|
||||
|
||||
|
|
@ -300,15 +299,26 @@ public sealed partial class Mod
|
|||
public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements )
|
||||
{
|
||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||
if( subMod.FileData.Equals( replacements ) )
|
||||
if( subMod.FileData.SetEquals( replacements ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subMod.FileData.SetTo( replacements );
|
||||
subMod.FileData = replacements;
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
|
||||
}
|
||||
|
||||
public void OptionAddFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > additions )
|
||||
{
|
||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||
var oldCount = subMod.FileData.Count;
|
||||
subMod.FileData.AddFrom( additions );
|
||||
if( oldCount != subMod.FileData.Count )
|
||||
{
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 );
|
||||
}
|
||||
}
|
||||
|
||||
public void OptionSetFileSwap( Mod mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath )
|
||||
{
|
||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||
|
|
@ -321,12 +331,12 @@ public sealed partial class Mod
|
|||
public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps )
|
||||
{
|
||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||
if( subMod.FileSwapData.Equals( swaps ) )
|
||||
if( subMod.FileSwapData.SetEquals( swaps ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
subMod.FileSwapData.SetTo( swaps );
|
||||
subMod.FileSwapData = swaps;
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 );
|
||||
}
|
||||
|
||||
|
|
@ -334,10 +344,9 @@ public sealed partial class Mod
|
|||
HashSet< MetaManipulation > manipulations, Dictionary< Utf8GamePath, FullPath > swaps )
|
||||
{
|
||||
var subMod = GetSubMod( mod, groupIdx, optionIdx );
|
||||
subMod.FileData.SetTo( replacements );
|
||||
subMod.ManipulationData.Clear();
|
||||
subMod.ManipulationData.UnionWith( manipulations );
|
||||
subMod.FileSwapData.SetTo( swaps );
|
||||
subMod.FileData = replacements;
|
||||
subMod.ManipulationData = manipulations;
|
||||
subMod.FileSwapData = swaps;
|
||||
ModOptionChanged.Invoke( ModOptionChangeType.OptionUpdated, mod, groupIdx, optionIdx, -1 );
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +370,11 @@ public sealed partial class Mod
|
|||
|
||||
private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx )
|
||||
{
|
||||
if( groupIdx == -1 && optionIdx == 0 )
|
||||
{
|
||||
return mod._default;
|
||||
}
|
||||
|
||||
return mod._groups[ groupIdx ] switch
|
||||
{
|
||||
SingleModGroup s => s.OptionData[ optionIdx ],
|
||||
|
|
@ -406,7 +420,14 @@ public sealed partial class Mod
|
|||
}
|
||||
else
|
||||
{
|
||||
IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx );
|
||||
if( groupIdx == -1 )
|
||||
{
|
||||
mod.SaveDefaultMod();
|
||||
}
|
||||
else
|
||||
{
|
||||
IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx );
|
||||
}
|
||||
}
|
||||
|
||||
// State can not change on adding groups, as they have no immediate options.
|
||||
|
|
|
|||
|
|
@ -110,6 +110,13 @@ public partial class Mod
|
|||
return mod;
|
||||
}
|
||||
|
||||
// Create an empty sub mod for single groups with None options.
|
||||
internal static ISubMod CreateEmptySubMod( string name )
|
||||
=> new SubMod()
|
||||
{
|
||||
Name = name,
|
||||
};
|
||||
|
||||
// Create the default data file from all unused files that were not handled before
|
||||
// and are used in sub mods.
|
||||
internal static void CreateDefaultFiles( DirectoryInfo directory )
|
||||
|
|
|
|||
|
|
@ -78,9 +78,6 @@ public partial class Mod
|
|||
=> !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' );
|
||||
|
||||
public List< FullPath > FindMissingFiles()
|
||||
=> AllFiles.Where( f => !f.Exists ).ToList();
|
||||
|
||||
private static IModGroup? LoadModGroup( FileInfo file, DirectoryInfo basePath )
|
||||
{
|
||||
if( !File.Exists( file.FullName ) )
|
||||
|
|
|
|||
|
|
@ -69,9 +69,9 @@ public partial class Mod
|
|||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > FileData = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
||||
public readonly HashSet< MetaManipulation > ManipulationData = new();
|
||||
public Dictionary< Utf8GamePath, FullPath > FileData = new();
|
||||
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
||||
public HashSet< MetaManipulation > ManipulationData = new();
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
|
||||
=> FileData;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ public class Penumbra : IDalamudPlugin
|
|||
btn = new LaunchButton( _configWindow );
|
||||
system = new WindowSystem( Name );
|
||||
system.AddWindow( _configWindow );
|
||||
system.AddWindow( cfg.SubModPopup );
|
||||
system.AddWindow( cfg.ModEditPopup );
|
||||
Dalamud.PluginInterface.UiBuilder.Draw += system.Draw;
|
||||
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mods_005Ceditor/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mods_005Cmanager/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mods_005Csubclasses/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
322
Penumbra/UI/Classes/ModEditWindow.cs
Normal file
322
Penumbra/UI/Classes/ModEditWindow.cs
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public class ModEditWindow : Window, IDisposable
|
||||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
private Mod.Editor? _editor;
|
||||
private Mod? _mod;
|
||||
|
||||
public void ChangeMod( Mod mod )
|
||||
{
|
||||
if( mod == _mod )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_editor?.Dispose();
|
||||
_editor = new Mod.Editor( mod );
|
||||
_mod = mod;
|
||||
WindowName = $"{mod.Name}{WindowBaseLabel}";
|
||||
}
|
||||
|
||||
public void ChangeOption( int groupIdx, int optionIdx )
|
||||
=> _editor?.SetSubMod( groupIdx, optionIdx );
|
||||
|
||||
public override bool DrawConditions()
|
||||
=> _editor != null;
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
using var tabBar = ImRaii.TabBar( "##tabs" );
|
||||
if( !tabBar )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawFileTab();
|
||||
DrawMetaTab();
|
||||
DrawSwapTab();
|
||||
DrawMissingFilesTab();
|
||||
DrawUnusedFilesTab();
|
||||
DrawDuplicatesTab();
|
||||
}
|
||||
|
||||
private void DrawMissingFilesTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Missing Files" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( _editor!.MissingPaths.Count == 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( "No missing files detected." );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( ImGui.Button( "Remove Missing Files from Mod" ) )
|
||||
{
|
||||
_editor.RemoveMissingPaths();
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var path in _editor.MissingPaths )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( path.FullName );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDuplicatesTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Duplicates" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton";
|
||||
if( ImGuiUtil.DrawDisabledButton( buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.",
|
||||
!_editor.DuplicatesFinished ) )
|
||||
{
|
||||
_editor.StartDuplicateCheck();
|
||||
}
|
||||
|
||||
if( !_editor.DuplicatesFinished )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Cancel" ) )
|
||||
{
|
||||
_editor.Cancel();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if( _editor.Duplicates.Count == 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( "No duplicates found." );
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Delete and Redirect Duplicates" ) )
|
||||
{
|
||||
_editor.DeleteDuplicates();
|
||||
}
|
||||
|
||||
if( _editor.SavedSpace > 0 )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( $"Frees up {Functions.HumanReadableSize( _editor.SavedSpace )} from your hard drive." );
|
||||
}
|
||||
|
||||
using var child = ImRaii.Child( "##duptable", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var width = ImGui.CalcTextSize( "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN " ).X;
|
||||
ImGui.TableSetupColumn( "file", ImGuiTableColumnFlags.WidthStretch );
|
||||
ImGui.TableSetupColumn( "size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "NNN.NNN " ).X );
|
||||
ImGui.TableSetupColumn( "hash", ImGuiTableColumnFlags.WidthFixed,
|
||||
ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize( "NNNNNNNN... " ).X );
|
||||
foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ],
|
||||
ImGuiTreeNodeFlags.NoTreePushOnOpen );
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) );
|
||||
ImGui.TableNextColumn();
|
||||
using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) )
|
||||
{
|
||||
if( ImGui.GetWindowWidth() > 2 * width )
|
||||
{
|
||||
ImGuiUtil.RightAlign( string.Concat( hash.Select( b => b.ToString( "X2" ) ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGuiUtil.RightAlign( string.Concat( hash.Take( 4 ).Select( b => b.ToString( "X2" ) ) ) + "..." );
|
||||
}
|
||||
}
|
||||
|
||||
if( !tree )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var indent = ImRaii.PushIndent();
|
||||
foreach( var duplicate in set.Skip( 1 ) )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 );
|
||||
using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawUnusedFilesTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Unused Files" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( _editor!.UnusedFiles.Count == 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( "No unused files detected." );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( ImGui.Button( "Add Unused Files to Default" ) )
|
||||
{
|
||||
_editor.AddUnusedPathsToDefault();
|
||||
}
|
||||
|
||||
if( ImGui.Button( "Delete Unused Files from Filesystem" ) )
|
||||
{
|
||||
_editor.DeleteUnusedPaths();
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##unusedFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var path in _editor.UnusedFiles )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( path.FullName );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawFileTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "File Redirections" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##files", 2 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (gamePath, file) in _editor!.CurrentFiles )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( gamePath.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( file.FullName );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMetaTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Meta Manipulations" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##meta", 3 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var manip in _editor!.CurrentManipulations )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType.ToString() );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType switch
|
||||
{
|
||||
MetaManipulation.Type.Imc => manip.Imc.ToString(),
|
||||
MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(),
|
||||
MetaManipulation.Type.Eqp => manip.Eqp.ToString(),
|
||||
MetaManipulation.Type.Est => manip.Est.ToString(),
|
||||
MetaManipulation.Type.Gmp => manip.Gmp.ToString(),
|
||||
MetaManipulation.Type.Rsp => manip.Rsp.ToString(),
|
||||
_ => string.Empty,
|
||||
} );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType switch
|
||||
{
|
||||
MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(),
|
||||
MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(),
|
||||
MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(),
|
||||
MetaManipulation.Type.Est => manip.Est.Entry.ToString(),
|
||||
MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(),
|
||||
MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(),
|
||||
_ => string.Empty,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSwapTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "File Swaps" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##swaps", 3 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (gamePath, file) in _editor!.CurrentSwaps )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( gamePath.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( file.FullName );
|
||||
}
|
||||
}
|
||||
|
||||
public ModEditWindow()
|
||||
: base( WindowBaseLabel )
|
||||
{ }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_editor?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public class SubModEditWindow : Window
|
||||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
private Mod? _mod;
|
||||
private int _groupIdx = -1;
|
||||
private int _optionIdx = -1;
|
||||
private IModGroup? _group;
|
||||
private ISubMod? _subMod;
|
||||
private readonly List< FilePathInfo > _availableFiles = new();
|
||||
|
||||
private readonly struct FilePathInfo
|
||||
{
|
||||
public readonly FullPath File;
|
||||
public readonly Utf8RelPath RelFile;
|
||||
public readonly long Size;
|
||||
public readonly List< (int, int, Utf8GamePath) > SubMods;
|
||||
|
||||
public FilePathInfo( FileInfo file, Mod mod )
|
||||
{
|
||||
File = new FullPath( file );
|
||||
RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty;
|
||||
Size = file.Length;
|
||||
SubMods = new List< (int, int, Utf8GamePath) >();
|
||||
var path = File;
|
||||
foreach( var (group, groupIdx) in mod.Groups.WithIndex() )
|
||||
{
|
||||
foreach( var (subMod, optionIdx) in group.WithIndex() )
|
||||
{
|
||||
SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) );
|
||||
}
|
||||
}
|
||||
SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) );
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HashSet< MetaManipulation > _manipulations = new();
|
||||
private readonly Dictionary< Utf8GamePath, FullPath > _files = new();
|
||||
private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new();
|
||||
|
||||
public void Activate( Mod mod, int groupIdx, int optionIdx )
|
||||
{
|
||||
IsOpen = true;
|
||||
_mod = mod;
|
||||
_groupIdx = groupIdx;
|
||||
_group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null;
|
||||
_optionIdx = optionIdx;
|
||||
_subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default;
|
||||
_availableFiles.Clear();
|
||||
_availableFiles.AddRange( mod.BasePath.EnumerateDirectories()
|
||||
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
.Select( f => new FilePathInfo( f, _mod ) ) );
|
||||
|
||||
_manipulations.Clear();
|
||||
_manipulations.UnionWith( _subMod.Manipulations );
|
||||
_files.SetTo( _subMod.Files );
|
||||
_fileSwaps.SetTo( _subMod.FileSwaps );
|
||||
|
||||
WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}";
|
||||
}
|
||||
|
||||
public override bool DrawConditions()
|
||||
=> _subMod != null;
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
using var tabBar = ImRaii.TabBar( "##tabs" );
|
||||
if( !tabBar )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawFileTab();
|
||||
DrawMetaTab();
|
||||
DrawSwapTab();
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
if( _mod != null )
|
||||
{
|
||||
Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps );
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
_subMod = null;
|
||||
}
|
||||
|
||||
private void DrawFileTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "File Redirections" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##files", 3 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var file in _availableFiles )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( file.RelFile.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( file.Size.ToString() );
|
||||
ImGui.TableNextColumn();
|
||||
if( file.SubMods.Count == 0 )
|
||||
{
|
||||
ImGui.TextUnformatted( "Unused" );
|
||||
}
|
||||
|
||||
foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null;
|
||||
var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default;
|
||||
var text = groupIdx >= 0
|
||||
? $"{group!.Name} - {option.Name}"
|
||||
: option.Name;
|
||||
ImGui.TextUnformatted( text );
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( gamePath.Path );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextRow();
|
||||
foreach( var (gamePath, fullPath) in _files )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( gamePath.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( fullPath.FullName );
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMetaTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Meta Manipulations" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##meta", 3 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var manip in _manipulations )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType.ToString() );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType switch
|
||||
{
|
||||
MetaManipulation.Type.Imc => manip.Imc.ToString(),
|
||||
MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(),
|
||||
MetaManipulation.Type.Eqp => manip.Eqp.ToString(),
|
||||
MetaManipulation.Type.Est => manip.Est.ToString(),
|
||||
MetaManipulation.Type.Gmp => manip.Gmp.ToString(),
|
||||
MetaManipulation.Type.Rsp => manip.Rsp.ToString(),
|
||||
_ => string.Empty,
|
||||
} );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( manip.ManipulationType switch
|
||||
{
|
||||
MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(),
|
||||
MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(),
|
||||
MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(),
|
||||
MetaManipulation.Type.Est => manip.Est.Entry.ToString(),
|
||||
MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(),
|
||||
MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(),
|
||||
_ => string.Empty,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSwapTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "File Swaps" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var list = ImRaii.Table( "##swaps", 3 );
|
||||
if( !list )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (from, to) in _fileSwaps )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ConfigWindow.Text( from.Path );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( to.FullName );
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
}
|
||||
|
||||
public SubModEditWindow()
|
||||
: base( WindowBaseLabel )
|
||||
{ }
|
||||
}
|
||||
|
|
@ -158,7 +158,9 @@ public partial class ConfigWindow
|
|||
|
||||
if( ImGui.Button( "Edit Default Mod", reducedSize ) )
|
||||
{
|
||||
_window.SubModPopup.Activate( _mod, -1, 0 );
|
||||
_window.ModEditPopup.ChangeMod( _mod );
|
||||
_window.ModEditPopup.ChangeOption( -1, 0 );
|
||||
_window.ModEditPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -180,6 +182,7 @@ public partial class ConfigWindow
|
|||
private int _currentField = -1;
|
||||
private int _optionIndex = -1;
|
||||
|
||||
private int _newOptionNameIdx = -1;
|
||||
private string _newGroupName = string.Empty;
|
||||
private string _newOptionName = string.Empty;
|
||||
private string _newDescription = string.Empty;
|
||||
|
|
@ -287,10 +290,15 @@ public partial class ConfigWindow
|
|||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 );
|
||||
var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty;
|
||||
if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) )
|
||||
{
|
||||
_newOptionName = tmp;
|
||||
_newOptionNameIdx = groupIdx;
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize,
|
||||
"Add a new option to this group.", _newOptionName.Length == 0, true ) )
|
||||
"Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) )
|
||||
{
|
||||
Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName );
|
||||
_newOptionName = string.Empty;
|
||||
|
|
@ -385,7 +393,9 @@ public partial class ConfigWindow
|
|||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize,
|
||||
"Edit this option.", false, true ) )
|
||||
{
|
||||
_window.SubModPopup.Activate( _mod, groupIdx, optionIdx );
|
||||
_window.ModEditPopup.ChangeMod( _mod );
|
||||
_window.ModEditPopup.ChangeOption( groupIdx, optionIdx );
|
||||
_window.ModEditPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ public partial class ConfigWindow
|
|||
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) )
|
||||
{
|
||||
|
||||
_currentPriority = priority;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
private readonly EffectiveTab _effectiveTab;
|
||||
private readonly DebugTab _debugTab;
|
||||
private readonly ResourceTab _resourceTab;
|
||||
public readonly SubModEditWindow SubModPopup = new();
|
||||
public readonly ModEditWindow ModEditPopup = new();
|
||||
|
||||
public ConfigWindow( Penumbra penumbra )
|
||||
: base( GetLabel() )
|
||||
|
|
@ -70,6 +70,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
{
|
||||
_selector.Dispose();
|
||||
_modPanel.Dispose();
|
||||
ModEditPopup.Dispose();
|
||||
}
|
||||
|
||||
private static string GetLabel()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
||||
|
|
@ -7,6 +10,12 @@ public static class DictionaryExtensions
|
|||
// Returns whether two dictionaries contain equal keys and values.
|
||||
public static bool SetEquals< TKey, TValue >( this IReadOnlyDictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs )
|
||||
{
|
||||
if( ReferenceEquals( lhs, rhs ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if( lhs.Count != rhs.Count )
|
||||
{
|
||||
return false;
|
||||
|
|
@ -42,6 +51,11 @@ public static class DictionaryExtensions
|
|||
public static void SetTo< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs )
|
||||
where TKey : notnull
|
||||
{
|
||||
if( ReferenceEquals( lhs, rhs ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lhs.Clear();
|
||||
lhs.EnsureCapacity( rhs.Count );
|
||||
foreach( var (key, value) in rhs )
|
||||
|
|
@ -49,4 +63,34 @@ public static class DictionaryExtensions
|
|||
lhs.Add( key, value );
|
||||
}
|
||||
}
|
||||
|
||||
// Add all entries from the other dictionary that would not overwrite current keys.
|
||||
public static void AddFrom< TKey, TValue >( this Dictionary< TKey, TValue > lhs, IReadOnlyDictionary< TKey, TValue > rhs )
|
||||
where TKey : notnull
|
||||
{
|
||||
if( ReferenceEquals( lhs, rhs ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lhs.EnsureCapacity( lhs.Count + rhs.Count );
|
||||
foreach( var (key, value) in rhs )
|
||||
{
|
||||
lhs.Add( key, value );
|
||||
}
|
||||
}
|
||||
|
||||
public static int ReplaceValue< TKey, TValue >( this Dictionary< TKey, TValue > dict, TValue from, TValue to )
|
||||
where TKey : notnull
|
||||
where TValue : IEquatable< TValue >
|
||||
{
|
||||
var count = 0;
|
||||
foreach( var (key, _) in dict.ToArray().Where( kvp => kvp.Value.Equals( from ) ) )
|
||||
{
|
||||
dict[ key ] = to;
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue