Why is this so much work?

This commit is contained in:
Ottermandias 2023-03-20 17:30:09 +01:00
parent 651c7410ac
commit b92a3161b5
53 changed files with 3054 additions and 2936 deletions

View file

@ -32,16 +32,21 @@ public partial class TexToolsImporter : IDisposable
public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods;
public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files,
Action< FileInfo, DirectoryInfo?, Exception? > handler )
: this( baseDirectory, files.Count, files, handler )
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor)
: this( baseDirectory, files.Count, files, handler, config, editor)
{ }
private readonly Configuration _config;
private readonly ModEditor _editor;
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
Action< FileInfo, DirectoryInfo?, Exception? > handler )
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor)
{
_baseDirectory = baseDirectory;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
_modPackFiles = modPackFiles;
_config = config;
_editor = editor;
_modPackCount = count;
ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count );
_token = _cancellation.Token;
@ -95,10 +100,10 @@ public partial class TexToolsImporter : IDisposable
{
var directory = VerifyVersionAndImport( file );
ExtractedMods.Add( ( file, directory, null ) );
if( Penumbra.Config.AutoDeduplicateOnImport )
if( _config.AutoDeduplicateOnImport )
{
State = ImporterState.DeduplicatingFiles;
Mod.Editor.DeduplicateMod( directory );
_editor.Duplicates.DeduplicateMod( directory );
}
}
catch( Exception e )

View file

@ -0,0 +1,258 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public class DuplicateManager
{
private readonly Mod.Manager _modManager;
private readonly SHA256 _hasher = SHA256.Create();
private readonly ModFileCollection _files;
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
public DuplicateManager(ModFileCollection files, Mod.Manager modManager)
{
_files = files;
_modManager = modManager;
}
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
=> _duplicates;
public long SavedSpace { get; private set; } = 0;
public bool Finished { get; private set; } = true;
public void StartDuplicateCheck(IEnumerable<FileRegistry> files)
{
if (!Finished)
return;
Finished = false;
var filesTmp = files.OrderByDescending(f => f.FileSize).ToArray();
Task.Run(() => CheckDuplicates(filesTmp));
}
public void DeleteDuplicates(Mod mod, ISubMod option, bool useModManager)
{
if (!Finished || _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(mod, duplicate, remaining, useModManager);
}
_duplicates.Clear();
DeleteEmptyDirectories(mod.ModPath);
_files.UpdateAll(mod, option);
}
public void Clear()
{
Finished = true;
SavedSpace = 0;
}
private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager)
{
void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx)
{
var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
kvp => ChangeDuplicatePath(mod, kvp.Value, duplicate, remaining, kvp.Key, ref changes));
if (!changes)
return;
if (useModManager)
{
_modManager.OptionSetFiles(mod, groupIdx, optionIdx, dict);
}
else
{
var sub = (Mod.SubMod)subMod;
sub.FileData = dict;
if (groupIdx == -1)
mod.SaveDefaultMod();
else
IModGroup.Save(mod.Groups[groupIdx], mod.ModPath, groupIdx);
}
}
ModEditor.ApplyToAllOptions(mod, HandleSubMod);
try
{
File.Delete(duplicate.FullName);
}
catch (Exception e)
{
Penumbra.Log.Error($"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}");
}
}
private static FullPath ChangeDuplicatePath(Mod mod, FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes)
{
if (!value.Equals(from))
return value;
changes = true;
Penumbra.Log.Debug($"[DeleteDuplicates] Changing {key} for {mod.Name}\n : {from}\n -> {to}");
return to;
}
private void CheckDuplicates(IReadOnlyList<FileRegistry> files)
{
_duplicates.Clear();
SavedSpace = 0;
var list = new List<FullPath>();
var lastSize = -1L;
foreach (var file in files)
{
// Skip any UI Files because deduplication causes weird crashes for those.
if (file.SubModUsage.Any(f => f.Item2.Path.StartsWith("ui/"u8)))
continue;
if (Finished)
return;
if (file.FileSize == lastSize)
{
list.Add(file.File);
continue;
}
if (list.Count >= 2)
CheckMultiDuplicates(list, lastSize);
lastSize = file.FileSize;
list.Clear();
list.Add(file.File);
}
if (list.Count >= 2)
CheckMultiDuplicates(list, lastSize);
_duplicates.Sort((a, b) => a.Size != b.Size ? b.Size.CompareTo(a.Size) : a.Paths[0].CompareTo(b.Paths[0]));
Finished = true;
}
private void CheckMultiDuplicates(IReadOnlyList<FullPath> list, long size)
{
var hashes = list.Select(f => (f, ComputeHash(f))).ToList();
while (hashes.Count > 0)
{
if (Finished)
return;
var set = new HashSet<FullPath> { hashes[0].Item1 };
var hash = hashes[0];
for (var j = 1; j < hashes.Count; ++j)
{
if (Finished)
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);
}
/// <summary>
/// Recursively delete all empty directories starting from the given directory.
/// Deletes inner directories first, so that a tree of empty directories is actually deleted.
/// </summary>
private static void DeleteEmptyDirectories(DirectoryInfo baseDir)
{
try
{
if (!baseDir.Exists)
return;
foreach (var dir in baseDir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly))
DeleteEmptyDirectories(dir);
baseDir.Refresh();
if (!baseDir.EnumerateFileSystemInfos().Any())
Directory.Delete(baseDir.FullName, false);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete empty directories in {baseDir.FullName}:\n{e}");
}
}
/// <summary> Deduplicate a mod simply by its directory without any confirmation or waiting time. </summary>
internal void DeduplicateMod(DirectoryInfo modDirectory)
{
try
{
var mod = new Mod(modDirectory);
mod.Reload(true, out _);
Finished = false;
_files.UpdateAll(mod, mod.Default);
CheckDuplicates(_files.Available.OrderByDescending(f => f.FileSize).ToArray());
DeleteDuplicates(mod, mod.Default, false);
}
catch (Exception e)
{
Penumbra.Log.Warning($"Could not deduplicate mod {modDirectory.Name}:\n{e}");
}
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public class FileRegistry : IEquatable<FileRegistry>
{
public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = new();
public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }
public int CurrentUsage;
public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry)
{
var fullPath = new FullPath(file.FullName);
if (!fullPath.ToRelPath(modPath, out var relPath))
{
registry = null;
return false;
}
registry = new FileRegistry
{
File = fullPath,
RelPath = relPath,
FileSize = file.Length,
CurrentUsage = 0,
};
return true;
}
public bool Equals(FileRegistry? other)
{
if (other is null)
return false;
return ReferenceEquals(this, other) || File.Equals(other.File);
}
public override bool Equals(object? obj)
{
if (obj is null)
return false;
if (ReferenceEquals(this, obj))
return true;
return obj.GetType() == GetType() && Equals((FileRegistry)obj);
}
public override int GetHashCode()
=> File.GetHashCode();
}

View file

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
namespace Penumbra.Mods;
public partial class MdlMaterialEditor
{
[GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)]
private static partial Regex MaterialRegex();
private readonly ModFileCollection _files;
private readonly List<ModelMaterialInfo> _modelFiles = new();
public IReadOnlyList<ModelMaterialInfo> ModelFiles
=> _modelFiles;
public MdlMaterialEditor(ModFileCollection files)
=> _files = files;
public void SaveAllModels()
{
foreach (var info in _modelFiles)
info.Save();
}
public void RestoreAllModels()
{
foreach (var info in _modelFiles)
info.Restore();
}
public void Clear()
{
_modelFiles.Clear();
}
/// <summary>
/// Go through the currently loaded files and replace all appropriate suffices.
/// Does nothing if toSuffix is invalid.
/// If raceCode is Unknown, apply to all raceCodes.
/// If fromSuffix is empty, apply to all suffices.
/// </summary>
public void ReplaceAllMaterials(string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown)
{
if (!ValidString(toSuffix))
return;
foreach (var info in _modelFiles)
{
for (var i = 0; i < info.Count; ++i)
{
var (_, def) = info[i];
var match = MaterialRegex().Match(def);
if (match.Success
&& (raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups["RaceCode"].Value)
&& (fromSuffix.Length == 0 || fromSuffix == match.Groups["Suffix"].Value))
info.SetMaterial($"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i);
}
}
}
/// Non-ASCII encoding is not supported.
public static bool ValidString(string to)
=> to.Length != 0
&& to.Length < 16
&& Encoding.UTF8.GetByteCount(to) == to.Length;
/// <summary> Find all model files in the mod that contain skin materials. </summary>
public void ScanModels(Mod mod)
{
_modelFiles.Clear();
foreach (var file in _files.Mdl)
{
try
{
var bytes = File.ReadAllBytes(file.File.FullName);
var mdlFile = new MdlFile(bytes);
var materials = mdlFile.Materials.WithIndex().Where(p => MaterialRegex().IsMatch((string)p.Item1))
.Select(p => p.Item2).ToArray();
if (materials.Length > 0)
_modelFiles.Add(new ModelMaterialInfo(file.File, mdlFile, materials));
}
catch (Exception e)
{
Penumbra.Log.Error($"Unexpected error scanning {mod.Name}'s {file.File.FullName} for materials:\n{e}");
}
}
}
}

View file

@ -1,288 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Penumbra.String.Classes;
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( bool useModManager = true )
{
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, useModManager );
}
}
_duplicates.Clear();
DeleteEmptyDirectories( _mod.ModPath );
UpdateFiles();
}
private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager )
{
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 )
{
if( useModManager )
{
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict );
}
else
{
var sub = ( SubMod )subMod;
sub.FileData = dict;
if( groupIdx == -1 )
{
_mod.SaveDefaultMod();
}
else
{
IModGroup.Save( _mod.Groups[ groupIdx ], _mod.ModPath, groupIdx );
}
}
}
}
ApplyToAllOptions( _mod, HandleSubMod );
try
{
File.Delete( duplicate.FullName );
}
catch( Exception e )
{
Penumbra.Log.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;
Penumbra.Log.Debug( $"[DeleteDuplicates] Changing {key} for {_mod.Name}\n : {from}\n -> {to}" );
return to;
}
public void StartDuplicateCheck()
{
if( DuplicatesFinished )
{
DuplicatesFinished = false;
UpdateFiles();
var files = _availableFiles.OrderByDescending( f => f.FileSize ).ToArray();
Task.Run( () => CheckDuplicates( files ) );
}
}
private void CheckDuplicates( IReadOnlyList< FileRegistry > files )
{
_duplicates.Clear();
SavedSpace = 0;
var list = new List< FullPath >();
var lastSize = -1L;
foreach( var file in files )
{
// Skip any UI Files because deduplication causes weird crashes for those.
if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( "ui/"u8 ) ) )
{
continue;
}
if( DuplicatesFinished )
{
return;
}
if( file.FileSize == lastSize )
{
list.Add( file.File );
continue;
}
if( list.Count >= 2 )
{
CheckMultiDuplicates( list, lastSize );
}
lastSize = file.FileSize;
list.Clear();
list.Add( file.File );
}
if( list.Count >= 2 )
{
CheckMultiDuplicates( list, lastSize );
}
_duplicates.Sort( ( a, b ) => a.Size != b.Size ? b.Size.CompareTo( a.Size ) : a.Paths[ 0 ].CompareTo( b.Paths[ 0 ] ) );
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 );
}
// Recursively delete all empty directories starting from the given directory.
// Deletes inner directories first, so that a tree of empty directories is actually deleted.
private static void DeleteEmptyDirectories( DirectoryInfo baseDir )
{
try
{
if( !baseDir.Exists )
{
return;
}
foreach( var dir in baseDir.EnumerateDirectories( "*", SearchOption.TopDirectoryOnly ) )
{
DeleteEmptyDirectories( dir );
}
baseDir.Refresh();
if( !baseDir.EnumerateFileSystemInfos().Any() )
{
Directory.Delete( baseDir.FullName, false );
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" );
}
}
// Deduplicate a mod simply by its directory without any confirmation or waiting time.
internal static void DeduplicateMod( DirectoryInfo modDirectory )
{
try
{
var mod = new Mod( modDirectory );
mod.Reload( true, out _ );
var editor = new Editor( mod, mod.Default );
editor.DuplicatesFinished = false;
editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() );
editor.DeleteDuplicates( false );
}
catch( Exception e )
{
Penumbra.Log.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" );
}
}
}
}

View file

@ -1,58 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
private SubMod _subMod;
public ISubMod CurrentOption
=> _subMod;
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
public void SetSubMod( ISubMod? subMod )
{
_subMod = subMod as SubMod ?? _mod._default;
UpdateFiles();
RevertSwaps();
RevertManipulations();
}
public int ApplyFiles()
{
var dict = new Dictionary< Utf8GamePath, FullPath >();
var num = 0;
foreach( var file in _availableFiles )
{
foreach( var path in file.SubModUsage.Where( p => p.Item1 == CurrentOption ) )
{
num += dict.TryAdd( path.Item2, file.File ) ? 0 : 1;
}
}
Penumbra.ModManager.OptionSetFiles( _mod, _subMod.GroupIdx, _subMod.OptionIdx, dict );
UpdateFiles();
return num;
}
public void RevertFiles()
=> UpdateFiles();
public void ApplySwaps()
{
Penumbra.ModManager.OptionSetFileSwaps( _mod, _subMod.GroupIdx, _subMod.OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) );
}
public void RevertSwaps()
{
CurrentSwaps.SetTo( _subMod.FileSwaps );
}
}
}

View file

@ -1,290 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
public class FileRegistry : IEquatable< FileRegistry >
{
public readonly List< (ISubMod, Utf8GamePath) > SubModUsage = new();
public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }
public int CurrentUsage;
public static bool FromFile( Mod mod, FileInfo file, [NotNullWhen( true )] out FileRegistry? registry )
{
var fullPath = new FullPath( file.FullName );
if( !fullPath.ToRelPath( mod.ModPath, out var relPath ) )
{
registry = null;
return false;
}
registry = new FileRegistry
{
File = fullPath,
RelPath = relPath,
FileSize = file.Length,
CurrentUsage = 0,
};
return true;
}
public bool Equals( FileRegistry? other )
{
if( other is null )
{
return false;
}
return ReferenceEquals( this, other ) || File.Equals( other.File );
}
public override bool Equals( object? obj )
{
if( obj is null )
{
return false;
}
if( ReferenceEquals( this, obj ) )
{
return true;
}
return obj.GetType() == GetType() && Equals( ( FileRegistry )obj );
}
public override int GetHashCode()
=> File.GetHashCode();
}
// All files in subdirectories of the mod directory.
public IReadOnlyList< FileRegistry > AvailableFiles
=> _availableFiles;
public bool FileChanges { get; private set; }
private List< FileRegistry > _availableFiles = null!;
private List< FileRegistry > _mtrlFiles = null!;
private List< FileRegistry > _mdlFiles = null!;
private List< FileRegistry > _texFiles = null!;
private List< FileRegistry > _shpkFiles = null!;
private readonly HashSet< Utf8GamePath > _usedPaths = new();
// All paths that are used in
private readonly SortedSet< FullPath > _missingFiles = new();
public IReadOnlySet< FullPath > MissingFiles
=> _missingFiles;
public IReadOnlyList< FileRegistry > MtrlFiles
=> _mtrlFiles;
public IReadOnlyList< FileRegistry > MdlFiles
=> _mdlFiles;
public IReadOnlyList< FileRegistry > TexFiles
=> _texFiles;
public IReadOnlyList< FileRegistry > ShpkFiles
=> _shpkFiles;
// 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, mod == _subMod ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
if( newDict.Count != mod.Files.Count )
{
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, newDict );
}
}
ApplyToAllOptions( _mod, HandleSubMod );
_missingFiles.Clear();
}
private bool CheckAgainstMissing( FullPath file, Utf8GamePath key, bool removeUsed )
{
if( !_missingFiles.Contains( file ) )
{
return true;
}
if( removeUsed )
{
_usedPaths.Remove( key );
}
Penumbra.Log.Debug( $"[RemoveMissingPaths] Removing {key} -> {file} from {_mod.Name}." );
return false;
}
// Fetch all files inside subdirectories of the main mod directory.
// Then check which options use them and how often.
private void UpdateFiles()
{
_availableFiles = _mod.ModPath.EnumerateDirectories()
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories )
.Select( f => FileRegistry.FromFile( _mod, f, out var r ) ? r : null )
.OfType< FileRegistry >() )
.ToList();
_usedPaths.Clear();
_mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList();
_mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList();
_texFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".tex", StringComparison.OrdinalIgnoreCase ) ).ToList();
_shpkFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".shpk", StringComparison.OrdinalIgnoreCase ) ).ToList();
FileChanges = false;
foreach( var subMod in _mod.AllSubMods )
{
foreach( var (gamePath, file) in subMod.Files )
{
if( !file.Exists )
{
_missingFiles.Add( file );
if( subMod == _subMod )
{
_usedPaths.Add( gamePath );
}
}
else
{
var registry = _availableFiles.Find( x => x.File.Equals( file ) );
if( registry != null )
{
if( subMod == _subMod )
{
++registry.CurrentUsage;
_usedPaths.Add( gamePath );
}
registry.SubModUsage.Add( ( subMod, gamePath ) );
}
}
}
}
}
// Return whether the given path is already used in the current option.
public bool CanAddGamePath( Utf8GamePath path )
=> !_usedPaths.Contains( path );
// Try to set a given path for a given file.
// Returns false if this is not possible.
// If path is empty, it will be deleted instead.
// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
public bool SetGamePath( int fileIdx, int pathIdx, Utf8GamePath path )
{
if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count )
{
return false;
}
var registry = _availableFiles[ fileIdx ];
if( pathIdx > registry.SubModUsage.Count )
{
return false;
}
if( ( pathIdx == -1 || pathIdx == registry.SubModUsage.Count ) && !path.IsEmpty )
{
registry.SubModUsage.Add( ( CurrentOption, path ) );
++registry.CurrentUsage;
_usedPaths.Add( path );
}
else
{
_usedPaths.Remove( registry.SubModUsage[ pathIdx ].Item2 );
if( path.IsEmpty )
{
registry.SubModUsage.RemoveAt( pathIdx );
--registry.CurrentUsage;
}
else
{
registry.SubModUsage[ pathIdx ] = ( registry.SubModUsage[ pathIdx ].Item1, path );
}
}
FileChanges = true;
return true;
}
// Transform a set of files to the appropriate game paths with the given number of folders skipped,
// and add them to the given option.
public int AddPathsToSelected( IEnumerable< FileRegistry > files, int skipFolders = 0 )
{
var failed = 0;
foreach( var file in files )
{
var gamePath = file.RelPath.ToGamePath( skipFolders );
if( gamePath.IsEmpty )
{
++failed;
continue;
}
if( CanAddGamePath( gamePath ) )
{
++file.CurrentUsage;
file.SubModUsage.Add( ( CurrentOption, gamePath ) );
_usedPaths.Add( gamePath );
FileChanges = true;
}
else
{
++failed;
}
}
return failed;
}
// Remove all paths in the current option from the given files.
public void RemovePathsFromSelected( IEnumerable< FileRegistry > files )
{
foreach( var file in files )
{
file.CurrentUsage = 0;
FileChanges |= file.SubModUsage.RemoveAll( p => p.Item1 == CurrentOption && _usedPaths.Remove( p.Item2 ) ) > 0;
}
}
// Delete all given files from your filesystem
public void DeleteFiles( IEnumerable< FileRegistry > files )
{
var deletions = 0;
foreach( var file in files )
{
try
{
File.Delete( file.File.FullName );
Penumbra.Log.Debug( $"[DeleteFiles] Deleted {file.File.FullName} from {_mod.Name}." );
++deletions;
}
catch( Exception e )
{
Penumbra.Log.Error( $"[DeleteFiles] Could not delete {file.File.FullName} from {_mod.Name}:\n{e}" );
}
}
if( deletions > 0 )
{
_mod.Reload( false, out _ );
UpdateFiles();
}
}
}
}

View file

@ -1,183 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled);
private readonly List< ModelMaterialInfo > _modelFiles = new();
public IReadOnlyList< ModelMaterialInfo > ModelFiles
=> _modelFiles;
// Non-ASCII encoding can not be used.
public static bool ValidString( string to )
=> to.Length != 0
&& to.Length < 16
&& Encoding.UTF8.GetByteCount( to ) == to.Length;
public void SaveAllModels()
{
foreach( var info in _modelFiles )
{
info.Save();
}
}
public void RestoreAllModels()
{
foreach( var info in _modelFiles )
{
info.Restore();
}
}
// Go through the currently loaded files and replace all appropriate suffices.
// Does nothing if toSuffix is invalid.
// If raceCode is Unknown, apply to all raceCodes.
// If fromSuffix is empty, apply to all suffices.
public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown )
{
if( !ValidString( toSuffix ) )
{
return;
}
foreach( var info in _modelFiles )
{
for( var i = 0; i < info.Count; ++i )
{
var (_, def) = info[ i ];
var match = MaterialRegex.Match( def );
if( match.Success
&& ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value )
&& ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) )
{
info.SetMaterial( $"/mt_c{match.Groups[ "RaceCode" ].Value}b0001_{toSuffix}.mtrl", i );
}
}
}
}
// Find all model files in the mod that contain skin materials.
public void ScanModels()
{
_modelFiles.Clear();
foreach( var file in _mdlFiles.Where( f => f.File.Extension == ".mdl" ) )
{
try
{
var bytes = File.ReadAllBytes( file.File.FullName );
var mdlFile = new MdlFile( bytes );
var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) )
.Select( p => p.Item2 ).ToArray();
if( materials.Length > 0 )
{
_modelFiles.Add( new ModelMaterialInfo( file.File, mdlFile, materials ) );
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Unexpected error scanning {_mod.Name}'s {file.File.FullName} for materials:\n{e}" );
}
}
}
// A class that collects information about skin materials in a model file and handle changes on them.
public class ModelMaterialInfo
{
public readonly FullPath Path;
public readonly MdlFile File;
private readonly string[] _currentMaterials;
private readonly IReadOnlyList< int > _materialIndices;
public bool Changed { get; private set; }
public IReadOnlyList< string > CurrentMaterials
=> _currentMaterials;
private IEnumerable< string > DefaultMaterials
=> _materialIndices.Select( i => File.Materials[ i ] );
public (string Current, string Default) this[ int idx ]
=> ( _currentMaterials[ idx ], File.Materials[ _materialIndices[ idx ] ] );
public int Count
=> _materialIndices.Count;
// Set the skin material to a new value and flag changes appropriately.
public void SetMaterial( string value, int materialIdx )
{
var mat = File.Materials[ _materialIndices[ materialIdx ] ];
_currentMaterials[ materialIdx ] = value;
if( mat != value )
{
Changed = true;
}
else
{
Changed = !_currentMaterials.SequenceEqual( DefaultMaterials );
}
}
// Save a changed .mdl file.
public void Save()
{
if( !Changed )
{
return;
}
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
File.Materials[ idx ] = _currentMaterials[ i ];
}
try
{
System.IO.File.WriteAllBytes( Path.FullName, File.Write() );
Changed = false;
}
catch( Exception e )
{
Restore();
Penumbra.Log.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" );
}
}
// Revert all current changes.
public void Restore()
{
if( !Changed )
{
return;
}
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
_currentMaterials[ i ] = File.Materials[ idx ];
}
Changed = false;
}
public ModelMaterialInfo( FullPath path, MdlFile file, IReadOnlyList< int > indices )
{
Path = path;
File = file;
_materialIndices = indices;
_currentMaterials = DefaultMaterials.ToArray();
}
}
}
}

View file

@ -1,165 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
public struct Manipulations
{
private readonly HashSet< ImcManipulation > _imc = new();
private readonly HashSet< EqpManipulation > _eqp = new();
private readonly HashSet< EqdpManipulation > _eqdp = new();
private readonly HashSet< GmpManipulation > _gmp = new();
private readonly HashSet< EstManipulation > _est = new();
private readonly HashSet< RspManipulation > _rsp = new();
public bool Changes { get; private set; } = false;
public IReadOnlySet< ImcManipulation > Imc
=> _imc;
public IReadOnlySet< EqpManipulation > Eqp
=> _eqp;
public IReadOnlySet< EqdpManipulation > Eqdp
=> _eqdp;
public IReadOnlySet< GmpManipulation > Gmp
=> _gmp;
public IReadOnlySet< EstManipulation > Est
=> _est;
public IReadOnlySet< RspManipulation > Rsp
=> _rsp;
public Manipulations()
{ }
public bool CanAdd( MetaManipulation m )
{
return m.ManipulationType switch
{
MetaManipulation.Type.Imc => !_imc.Contains( m.Imc ),
MetaManipulation.Type.Eqdp => !_eqdp.Contains( m.Eqdp ),
MetaManipulation.Type.Eqp => !_eqp.Contains( m.Eqp ),
MetaManipulation.Type.Est => !_est.Contains( m.Est ),
MetaManipulation.Type.Gmp => !_gmp.Contains( m.Gmp ),
MetaManipulation.Type.Rsp => !_rsp.Contains( m.Rsp ),
_ => false,
};
}
public bool Add( MetaManipulation m )
{
var added = m.ManipulationType switch
{
MetaManipulation.Type.Imc => _imc.Add( m.Imc ),
MetaManipulation.Type.Eqdp => _eqdp.Add( m.Eqdp ),
MetaManipulation.Type.Eqp => _eqp.Add( m.Eqp ),
MetaManipulation.Type.Est => _est.Add( m.Est ),
MetaManipulation.Type.Gmp => _gmp.Add( m.Gmp ),
MetaManipulation.Type.Rsp => _rsp.Add( m.Rsp ),
_ => false,
};
Changes |= added;
return added;
}
public bool Delete( MetaManipulation m )
{
var deleted = m.ManipulationType switch
{
MetaManipulation.Type.Imc => _imc.Remove( m.Imc ),
MetaManipulation.Type.Eqdp => _eqdp.Remove( m.Eqdp ),
MetaManipulation.Type.Eqp => _eqp.Remove( m.Eqp ),
MetaManipulation.Type.Est => _est.Remove( m.Est ),
MetaManipulation.Type.Gmp => _gmp.Remove( m.Gmp ),
MetaManipulation.Type.Rsp => _rsp.Remove( m.Rsp ),
_ => false,
};
Changes |= deleted;
return deleted;
}
public bool Change( MetaManipulation m )
=> Delete( m ) && Add( m );
public bool Set( MetaManipulation m )
=> Delete( m ) | Add( m );
public void Clear()
{
_imc.Clear();
_eqp.Clear();
_eqdp.Clear();
_gmp.Clear();
_est.Clear();
_rsp.Clear();
Changes = true;
}
public void Split( IEnumerable< MetaManipulation > manips )
{
Clear();
foreach( var manip in manips )
{
switch( manip.ManipulationType )
{
case MetaManipulation.Type.Imc:
_imc.Add( manip.Imc );
break;
case MetaManipulation.Type.Eqdp:
_eqdp.Add( manip.Eqdp );
break;
case MetaManipulation.Type.Eqp:
_eqp.Add( manip.Eqp );
break;
case MetaManipulation.Type.Est:
_est.Add( manip.Est );
break;
case MetaManipulation.Type.Gmp:
_gmp.Add( manip.Gmp );
break;
case MetaManipulation.Type.Rsp:
_rsp.Add( manip.Rsp );
break;
}
}
Changes = false;
}
public IEnumerable< MetaManipulation > Recombine()
=> _imc.Select( m => ( MetaManipulation )m )
.Concat( _eqdp.Select( m => ( MetaManipulation )m ) )
.Concat( _eqp.Select( m => ( MetaManipulation )m ) )
.Concat( _est.Select( m => ( MetaManipulation )m ) )
.Concat( _gmp.Select( m => ( MetaManipulation )m ) )
.Concat( _rsp.Select( m => ( MetaManipulation )m ) );
public void Apply( Mod mod, int groupIdx, int optionIdx )
{
if( Changes )
{
Penumbra.ModManager.OptionSetManipulations( mod, groupIdx, optionIdx, Recombine().ToHashSet() );
Changes = false;
}
}
}
public Manipulations Meta = new();
public void RevertManipulations()
=> Meta.Split( _subMod.Manipulations );
public void ApplyManipulations()
{
Meta.Apply( _mod, _subMod.GroupIdx, _subMod.OptionIdx );
}
}
}

View file

@ -1,56 +0,0 @@
using System;
using System.IO;
using OtterGui;
namespace Penumbra.Mods;
public partial class Mod : IMod
{
public partial class Editor : IDisposable
{
private readonly Mod _mod;
public Editor( Mod mod, ISubMod? option )
{
_mod = mod;
_subMod = null!;
SetSubMod( option );
UpdateFiles();
ScanModels();
}
public void Cancel()
{
DuplicatesFinished = true;
}
public void Dispose()
=> Cancel();
// 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 );
}
}
}
}
}

View file

@ -1,262 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class Mod
{
public void Normalize( Manager manager )
=> ModNormalizer.Normalize( manager, this );
private struct ModNormalizer
{
private readonly Mod _mod;
private readonly string _normalizationDirName;
private readonly string _oldDirName;
private Dictionary< Utf8GamePath, FullPath >[][]? _redirections = null;
private ModNormalizer( Mod mod )
{
_mod = mod;
_normalizationDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalization" );
_oldDirName = Path.Combine( _mod.ModPath.FullName, "TmpNormalizationOld" );
}
public static void Normalize( Manager manager, Mod mod )
{
var normalizer = new ModNormalizer( mod );
try
{
Penumbra.Log.Debug( $"[Normalization] Starting Normalization of {mod.ModPath.Name}..." );
if( !normalizer.CheckDirectories() )
{
return;
}
Penumbra.Log.Debug( "[Normalization] Copying files to temporary directory structure..." );
if( !normalizer.CopyNewFiles() )
{
return;
}
Penumbra.Log.Debug( "[Normalization] Moving old files out of the way..." );
if( !normalizer.MoveOldFiles() )
{
return;
}
Penumbra.Log.Debug( "[Normalization] Moving new directory structure in place..." );
if( !normalizer.MoveNewFiles() )
{
return;
}
Penumbra.Log.Debug( "[Normalization] Applying new redirections..." );
normalizer.ApplyRedirections( manager );
}
catch( Exception e )
{
Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error );
}
finally
{
Penumbra.Log.Debug( "[Normalization] Cleaning up remaining directories..." );
normalizer.Cleanup();
}
}
private bool CheckDirectories()
{
if( Directory.Exists( _normalizationDirName ) )
{
Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n"
+ "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure",
NotificationType.Error );
return false;
}
if( Directory.Exists( _oldDirName ) )
{
Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n"
+ "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure",
NotificationType.Error );
return false;
}
return true;
}
private void Cleanup()
{
if( Directory.Exists( _normalizationDirName ) )
{
try
{
Directory.Delete( _normalizationDirName, true );
}
catch
{
// ignored
}
}
if( Directory.Exists( _oldDirName ) )
{
try
{
foreach( var dir in new DirectoryInfo( _oldDirName ).EnumerateDirectories() )
{
dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) );
}
Directory.Delete( _oldDirName, true );
}
catch
{
// ignored
}
}
}
private bool CopyNewFiles()
{
// We copy all files to a temporary folder to ensure that we can revert the operation on failure.
try
{
var directory = Directory.CreateDirectory( _normalizationDirName );
_redirections = new Dictionary< Utf8GamePath, FullPath >[_mod.Groups.Count + 1][];
_redirections[ 0 ] = new Dictionary< Utf8GamePath, FullPath >[] { new(_mod.Default.Files.Count) };
// Normalize the default option.
var newDict = new Dictionary< Utf8GamePath, FullPath >( _mod.Default.Files.Count );
_redirections[ 0 ][ 0 ] = newDict;
foreach( var (gamePath, fullPath) in _mod._default.FileData )
{
var relPath = new Utf8RelPath( gamePath ).ToString();
var newFullPath = Path.Combine( directory.FullName, relPath );
var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, relPath ) );
Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! );
File.Copy( fullPath.FullName, newFullPath, true );
newDict.Add( gamePath, redirectPath );
}
// Normalize all other options.
foreach( var (group, groupIdx) in _mod.Groups.WithIndex() )
{
_redirections[ groupIdx + 1 ] = new Dictionary< Utf8GamePath, FullPath >[group.Count];
var groupDir = Creator.CreateModFolder( directory, group.Name );
foreach( var option in group.OfType< SubMod >() )
{
var optionDir = Creator.CreateModFolder( groupDir, option.Name );
newDict = new Dictionary< Utf8GamePath, FullPath >( option.FileData.Count );
_redirections[ groupIdx + 1 ][ option.OptionIdx ] = newDict;
foreach( var (gamePath, fullPath) in option.FileData )
{
var relPath = new Utf8RelPath( gamePath ).ToString();
var newFullPath = Path.Combine( optionDir.FullName, relPath );
var redirectPath = new FullPath( Path.Combine( _mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath ) );
Directory.CreateDirectory( Path.GetDirectoryName( newFullPath )! );
File.Copy( fullPath.FullName, newFullPath, true );
newDict.Add( gamePath, redirectPath );
}
}
}
return true;
}
catch( Exception e )
{
Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error );
_redirections = null;
}
return false;
}
private bool MoveOldFiles()
{
try
{
// Clean old directories and files.
var oldDirectory = Directory.CreateDirectory( _oldDirName );
foreach( var dir in _mod.ModPath.EnumerateDirectories() )
{
if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase )
|| dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) )
{
continue;
}
dir.MoveTo( Path.Combine( oldDirectory.FullName, dir.Name ) );
}
return true;
}
catch( Exception e )
{
Penumbra.ChatService.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error );
}
return false;
}
private bool MoveNewFiles()
{
try
{
var mainDir = new DirectoryInfo( _normalizationDirName );
foreach( var dir in mainDir.EnumerateDirectories() )
{
dir.MoveTo( Path.Combine( _mod.ModPath.FullName, dir.Name ) );
}
mainDir.Delete();
Directory.Delete( _oldDirName, true );
return true;
}
catch( Exception e )
{
Penumbra.ChatService.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error );
foreach( var dir in _mod.ModPath.EnumerateDirectories() )
{
if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase )
|| dir.FullName.Equals( _normalizationDirName, StringComparison.OrdinalIgnoreCase ) )
{
continue;
}
try
{
dir.Delete( true );
}
catch
{
// ignored
}
}
}
return false;
}
private void ApplyRedirections( Manager manager )
{
if( _redirections == null )
{
return;
}
foreach( var option in _mod.AllSubMods.OfType< SubMod >() )
{
manager.OptionSetFiles( _mod, option.GroupIdx, option.OptionIdx, _redirections[ option.GroupIdx + 1 ][ option.OptionIdx ] );
}
}
}
}

View file

@ -14,10 +14,10 @@ public class ModBackup
public readonly string Name;
public readonly bool Exists;
public ModBackup( Mod mod )
public ModBackup( Mod.Manager modManager, Mod mod )
{
_mod = mod;
Name = Path.Combine( Penumbra.ModManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp";
Name = Path.Combine( modManager.ExportDirectory.FullName, _mod.ModPath.Name ) + ".pmp";
Exists = File.Exists( Name );
}

View file

@ -0,0 +1,128 @@
using System;
using System.IO;
using OtterGui;
namespace Penumbra.Mods;
public class ModEditor : IDisposable
{
public readonly ModNormalizer ModNormalizer;
public readonly ModMetaEditor MetaEditor;
public readonly ModFileEditor FileEditor;
public readonly DuplicateManager Duplicates;
public readonly ModFileCollection Files;
public readonly ModSwapEditor SwapEditor;
public readonly MdlMaterialEditor MdlMaterialEditor;
public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; }
public int OptionIdx { get; private set; }
public IModGroup? Group { get; private set; }
public ISubMod? Option { get; private set; }
public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files,
ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor)
{
ModNormalizer = modNormalizer;
MetaEditor = metaEditor;
Files = files;
FileEditor = fileEditor;
Duplicates = duplicates;
SwapEditor = swapEditor;
MdlMaterialEditor = mdlMaterialEditor;
}
public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0);
public void LoadMod(Mod mod, int groupIdx, int optionIdx)
{
Mod = mod;
LoadOption(groupIdx, optionIdx, true);
Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!);
MetaEditor.Load(Option!);
Duplicates.Clear();
}
public void LoadOption(int groupIdx, int optionIdx)
{
LoadOption(groupIdx, optionIdx, true);
SwapEditor.Revert(Option!);
Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Option!);
FileEditor.Clear();
Duplicates.Clear();
}
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
private void LoadOption(int groupIdx, int optionIdx, bool message)
{
if (Mod != null && Mod.Groups.Count > groupIdx)
{
if (groupIdx == -1 && optionIdx == 0)
{
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
}
if (groupIdx >= 0)
{
Group = Mod.Groups[groupIdx];
if (optionIdx >= 0 && optionIdx < Group.Count)
{
Option = Group[optionIdx];
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
}
}
}
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
OptionIdx = 0;
if (message)
global::Penumbra.Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}.");
}
public void Clear()
{
Duplicates.Clear();
FileEditor.Clear();
Files.Clear();
MetaEditor.Clear();
Mod = null;
LoadOption(0, 0, false);
}
public void Dispose()
=> Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public 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);
}
}
// 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();
}
}
}

View file

@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Win32;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public class ModFileCollection : IDisposable
{
private readonly List<FileRegistry> _available = new();
private readonly List<FileRegistry> _mtrl = new();
private readonly List<FileRegistry> _mdl = new();
private readonly List<FileRegistry> _tex = new();
private readonly List<FileRegistry> _shpk = new();
private readonly SortedSet<FullPath> _missing = new();
private readonly HashSet<Utf8GamePath> _usedPaths = new();
public IReadOnlySet<FullPath> Missing
=> Ready ? _missing : new HashSet<FullPath>();
public IReadOnlySet<Utf8GamePath> UsedPaths
=> Ready ? _usedPaths : new HashSet<Utf8GamePath>();
public IReadOnlyList<FileRegistry> Available
=> Ready ? _available : Array.Empty<FileRegistry>();
public IReadOnlyList<FileRegistry> Mtrl
=> Ready ? _mtrl : Array.Empty<FileRegistry>();
public IReadOnlyList<FileRegistry> Mdl
=> Ready ? _mdl : Array.Empty<FileRegistry>();
public IReadOnlyList<FileRegistry> Tex
=> Ready ? _tex : Array.Empty<FileRegistry>();
public IReadOnlyList<FileRegistry> Shpk
=> Ready ? _shpk : Array.Empty<FileRegistry>();
public bool Ready { get; private set; } = true;
public ModFileCollection()
{ }
public void UpdateAll(Mod mod, ISubMod option)
{
UpdateFiles(mod, new CancellationToken());
UpdatePaths(mod, option, false, new CancellationToken());
}
public void UpdatePaths(Mod mod, ISubMod option)
=> UpdatePaths(mod, option, true, new CancellationToken());
public void Clear()
{
ClearFiles();
ClearPaths(false, new CancellationToken());
}
public void Dispose()
=> Clear();
public void ClearMissingFiles()
=> _missing.Clear();
public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Remove(gamePath);
if (file != null)
{
--file.CurrentUsage;
file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath));
}
}
public void RemoveUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath)
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void AddUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Add(gamePath);
if (file == null)
return;
++file.CurrentUsage;
file.SubModUsage.Add((option, gamePath));
}
public void AddUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath)
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
{
var oldPath = file.SubModUsage[pathIdx];
_usedPaths.Remove(oldPath.Item2);
if (!gamePath.IsEmpty)
{
_usedPaths.Add(gamePath);
}
else
{
--file.CurrentUsage;
file.SubModUsage.RemoveAt(pathIdx);
}
}
private void UpdateFiles(Mod mod, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearFiles();
foreach (var file in mod.ModPath.EnumerateDirectories().SelectMany(d => d.EnumerateFiles("*.*", SearchOption.AllDirectories)))
{
tok.ThrowIfCancellationRequested();
if (!FileRegistry.FromFile(mod.ModPath, file, out var registry))
continue;
_available.Add(registry);
switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant())
{
case ".mtrl":
_mtrl.Add(registry);
break;
case ".mdl":
_mdl.Add(registry);
break;
case ".tex":
_tex.Add(registry);
break;
case ".shpk":
_shpk.Add(registry);
break;
}
}
}
private void ClearFiles()
{
_available.Clear();
_mtrl.Clear();
_mdl.Clear();
_tex.Clear();
_shpk.Clear();
}
private void ClearPaths(bool clearRegistries, CancellationToken tok)
{
if (clearRegistries)
foreach (var reg in _available)
{
tok.ThrowIfCancellationRequested();
reg.CurrentUsage = 0;
reg.SubModUsage.Clear();
}
_missing.Clear();
_usedPaths.Clear();
}
private void UpdatePaths(Mod mod, ISubMod option, bool clearRegistries, CancellationToken tok)
{
tok.ThrowIfCancellationRequested();
ClearPaths(clearRegistries, tok);
tok.ThrowIfCancellationRequested();
foreach (var subMod in mod.AllSubMods)
{
foreach (var (gamePath, file) in subMod.Files)
{
tok.ThrowIfCancellationRequested();
if (!file.Exists)
{
_missing.Add(file);
if (subMod == option)
_usedPaths.Add(gamePath);
}
else
{
var registry = _available.Find(x => x.File.Equals(file));
if (registry == null)
continue;
if (subMod == option)
{
++registry.CurrentUsage;
_usedPaths.Add(gamePath);
}
registry.SubModUsage.Add((subMod, gamePath));
}
}
}
}
}

View file

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public class ModFileEditor
{
private readonly ModFileCollection _files;
private readonly Mod.Manager _modManager;
public bool Changes { get; private set; }
public ModFileEditor(ModFileCollection files, Mod.Manager modManager)
{
_files = files;
_modManager = modManager;
}
public void Clear()
{
Changes = false;
}
public int Apply(Mod mod, Mod.SubMod option)
{
var dict = new Dictionary<Utf8GamePath, FullPath>();
var num = 0;
foreach (var file in _files.Available)
{
foreach (var path in file.SubModUsage.Where(p => p.Item1 == option))
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
}
Penumbra.ModManager.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict);
_files.UpdatePaths(mod, option);
return num;
}
public void RevertFiles(Mod mod, ISubMod option)
{
_files.UpdatePaths(mod, option);
Changes = false;
}
/// <summary> Remove all path redirections where the pointed-to file does not exist. </summary>
public void RemoveMissingPaths(Mod mod, ISubMod option)
{
void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx)
{
var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (newDict.Count != subMod.Files.Count)
_modManager.OptionSetFiles(mod, groupIdx, optionIdx, newDict);
}
ModEditor.ApplyToAllOptions(mod, HandleSubMod);
_files.ClearMissingFiles();
}
/// <summary> Return whether the given path is already used in the current option. </summary>
public bool CanAddGamePath(Utf8GamePath path)
=> !_files.UsedPaths.Contains(path);
/// <summary>
/// Try to set a given path for a given file.
/// Returns false if this is not possible.
/// If path is empty, it will be deleted instead.
/// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
/// </summary>
public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path)
{
if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > _files.Available.Count)
return false;
var registry = _files.Available[fileIdx];
if (pathIdx > registry.SubModUsage.Count)
return false;
if ((pathIdx == -1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty)
_files.AddUsedPath(option, registry, path);
else
_files.ChangeUsedPath(registry, pathIdx, path);
Changes = true;
return true;
}
/// <summary>
/// Transform a set of files to the appropriate game paths with the given number of folders skipped,
/// and add them to the given option.
/// </summary>
public int AddPathsToSelected(ISubMod option, IEnumerable<FileRegistry> files, int skipFolders = 0)
{
var failed = 0;
foreach (var file in files)
{
var gamePath = file.RelPath.ToGamePath(skipFolders);
if (gamePath.IsEmpty)
{
++failed;
continue;
}
if (CanAddGamePath(gamePath))
{
_files.AddUsedPath(option, file, gamePath);
Changes = true;
}
else
{
++failed;
}
}
return failed;
}
/// <summary> Remove all paths in the current option from the given files. </summary>
public void RemovePathsFromSelected(ISubMod option, IEnumerable<FileRegistry> files)
{
foreach (var file in files)
{
foreach (var (_, path) in file.SubModUsage.Where(p => p.Item1 == option))
{
_files.RemoveUsedPath(option, file, path);
Changes = true;
}
}
}
/// <summary> Delete all given files from your filesystem </summary>
public void DeleteFiles(Mod mod, ISubMod option, IEnumerable<FileRegistry> files)
{
var deletions = 0;
foreach (var file in files)
{
try
{
File.Delete(file.File.FullName);
Penumbra.Log.Debug($"[DeleteFiles] Deleted {file.File.FullName} from {mod.Name}.");
++deletions;
}
catch (Exception e)
{
Penumbra.Log.Error($"[DeleteFiles] Could not delete {file.File.FullName} from {mod.Name}:\n{e}");
}
}
if (deletions <= 0)
return;
mod.Reload(false, out _);
_files.UpdateAll(mod, option);
}
private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed)
{
if (!_files.Missing.Contains(file))
return true;
if (removeUsed)
_files.RemoveUsedPath(option, file, key);
Penumbra.Log.Debug($"[RemoveMissingPaths] Removing {key} -> {file} from {mod.Name}.");
return false;
}
}

View file

@ -0,0 +1,154 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mods;
public class ModMetaEditor
{
private readonly Mod.Manager _modManager;
private readonly HashSet<ImcManipulation> _imc = new();
private readonly HashSet<EqpManipulation> _eqp = new();
private readonly HashSet<EqdpManipulation> _eqdp = new();
private readonly HashSet<GmpManipulation> _gmp = new();
private readonly HashSet<EstManipulation> _est = new();
private readonly HashSet<RspManipulation> _rsp = new();
public ModMetaEditor(Mod.Manager modManager)
=> _modManager = modManager;
public bool Changes { get; private set; } = false;
public IReadOnlySet<ImcManipulation> Imc
=> _imc;
public IReadOnlySet<EqpManipulation> Eqp
=> _eqp;
public IReadOnlySet<EqdpManipulation> Eqdp
=> _eqdp;
public IReadOnlySet<GmpManipulation> Gmp
=> _gmp;
public IReadOnlySet<EstManipulation> Est
=> _est;
public IReadOnlySet<RspManipulation> Rsp
=> _rsp;
public bool CanAdd(MetaManipulation m)
{
return m.ManipulationType switch
{
MetaManipulation.Type.Imc => !_imc.Contains(m.Imc),
MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp),
MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp),
MetaManipulation.Type.Est => !_est.Contains(m.Est),
MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp),
MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp),
_ => false,
};
}
public bool Add(MetaManipulation m)
{
var added = m.ManipulationType switch
{
MetaManipulation.Type.Imc => _imc.Add(m.Imc),
MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp),
MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp),
MetaManipulation.Type.Est => _est.Add(m.Est),
MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp),
MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp),
_ => false,
};
Changes |= added;
return added;
}
public bool Delete(MetaManipulation m)
{
var deleted = m.ManipulationType switch
{
MetaManipulation.Type.Imc => _imc.Remove(m.Imc),
MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp),
MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp),
MetaManipulation.Type.Est => _est.Remove(m.Est),
MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp),
MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp),
_ => false,
};
Changes |= deleted;
return deleted;
}
public bool Change(MetaManipulation m)
=> Delete(m) && Add(m);
public bool Set(MetaManipulation m)
=> Delete(m) | Add(m);
public void Clear()
{
_imc.Clear();
_eqp.Clear();
_eqdp.Clear();
_gmp.Clear();
_est.Clear();
_rsp.Clear();
Changes = true;
}
public void Load(ISubMod mod)
=> Split(mod.Manipulations);
public void Apply(Mod mod, int groupIdx, int optionIdx)
{
if (!Changes)
return;
_modManager.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet());
Changes = false;
}
private void Split(IEnumerable<MetaManipulation> manips)
{
Clear();
foreach (var manip in manips)
{
switch (manip.ManipulationType)
{
case MetaManipulation.Type.Imc:
_imc.Add(manip.Imc);
break;
case MetaManipulation.Type.Eqdp:
_eqdp.Add(manip.Eqdp);
break;
case MetaManipulation.Type.Eqp:
_eqp.Add(manip.Eqp);
break;
case MetaManipulation.Type.Est:
_est.Add(manip.Est);
break;
case MetaManipulation.Type.Gmp:
_gmp.Add(manip.Gmp);
break;
case MetaManipulation.Type.Rsp:
_rsp.Add(manip.Rsp);
break;
}
}
Changes = false;
}
public IEnumerable<MetaManipulation> Recombine()
=> _imc.Select(m => (MetaManipulation)m)
.Concat(_eqdp.Select(m => (MetaManipulation)m))
.Concat(_eqp.Select(m => (MetaManipulation)m))
.Concat(_est.Select(m => (MetaManipulation)m))
.Concat(_gmp.Select(m => (MetaManipulation)m))
.Concat(_rsp.Select(m => (MetaManipulation)m));
}

View file

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public class ModNormalizer
{
private readonly Mod.Manager _modManager;
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
public Mod Mod { get; private set; } = null!;
private string _normalizationDirName = null!;
private string _oldDirName = null!;
public int Step { get; private set; }
public int TotalSteps { get; private set; }
public bool Running
=> Step < TotalSteps;
public ModNormalizer(Mod.Manager modManager)
=> _modManager = modManager;
public void Normalize(Mod mod)
{
if (Step < TotalSteps)
return;
Mod = mod;
_normalizationDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalization");
_oldDirName = Path.Combine(Mod.ModPath.FullName, "TmpNormalizationOld");
Step = 0;
TotalSteps = mod.TotalFileCount + 5;
Task.Run(NormalizeSync);
}
private void NormalizeSync()
{
try
{
Penumbra.Log.Debug($"[Normalization] Starting Normalization of {Mod.ModPath.Name}...");
if (!CheckDirectories())
{
return;
}
Penumbra.Log.Debug("[Normalization] Copying files to temporary directory structure...");
if (!CopyNewFiles())
{
return;
}
Penumbra.Log.Debug("[Normalization] Moving old files out of the way...");
if (!MoveOldFiles())
{
return;
}
Penumbra.Log.Debug("[Normalization] Moving new directory structure in place...");
if (!MoveNewFiles())
{
return;
}
Penumbra.Log.Debug("[Normalization] Applying new redirections...");
ApplyRedirections();
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error);
}
finally
{
Penumbra.Log.Debug("[Normalization] Cleaning up remaining directories...");
Cleanup();
}
}
private bool CheckDirectories()
{
if (Directory.Exists(_normalizationDirName))
{
Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n"
+ "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure",
NotificationType.Error);
return false;
}
if (Directory.Exists(_oldDirName))
{
Penumbra.ChatService.NotificationMessage("Could not normalize mod:\n"
+ "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure",
NotificationType.Error);
return false;
}
++Step;
return true;
}
private void Cleanup()
{
if (Directory.Exists(_normalizationDirName))
{
try
{
Directory.Delete(_normalizationDirName, true);
}
catch
{
// ignored
}
}
if (Directory.Exists(_oldDirName))
{
try
{
foreach (var dir in new DirectoryInfo(_oldDirName).EnumerateDirectories())
{
dir.MoveTo(Path.Combine(Mod.ModPath.FullName, dir.Name));
}
Directory.Delete(_oldDirName, true);
}
catch
{
// ignored
}
}
Step = TotalSteps;
}
private bool CopyNewFiles()
{
// We copy all files to a temporary folder to ensure that we can revert the operation on failure.
try
{
var directory = Directory.CreateDirectory(_normalizationDirName);
for (var i = _redirections.Count; i < Mod.Groups.Count + 1; ++i)
_redirections.Add(new List<Dictionary<Utf8GamePath, FullPath>>());
if (_redirections[0].Count == 0)
_redirections[0].Add(new Dictionary<Utf8GamePath, FullPath>(Mod.Default.Files.Count));
else
{
_redirections[0][0].Clear();
_redirections[0][0].EnsureCapacity(Mod.Default.Files.Count);
}
// Normalize the default option.
var newDict = _redirections[0][0];
foreach (var (gamePath, fullPath) in Mod.Default.Files)
{
var relPath = new Utf8RelPath(gamePath).ToString();
var newFullPath = Path.Combine(directory.FullName, relPath);
var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, relPath));
Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!);
File.Copy(fullPath.FullName, newFullPath, true);
newDict.Add(gamePath, redirectPath);
++Step;
}
// Normalize all other options.
foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
{
_redirections[groupIdx + 1].EnsureCapacity(group.Count);
for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i)
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
foreach (var option in group.OfType<Mod.SubMod>())
{
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
newDict = _redirections[groupIdx + 1][option.OptionIdx];
newDict.Clear();
newDict.EnsureCapacity(option.FileData.Count);
foreach (var (gamePath, fullPath) in option.FileData)
{
var relPath = new Utf8RelPath(gamePath).ToString();
var newFullPath = Path.Combine(optionDir.FullName, relPath);
var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath));
Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!);
File.Copy(fullPath.FullName, newFullPath, true);
newDict.Add(gamePath, redirectPath);
++Step;
}
}
}
return true;
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not normalize mod:\n{e}", "Failure", NotificationType.Error);
}
return false;
}
private bool MoveOldFiles()
{
try
{
// Clean old directories and files.
var oldDirectory = Directory.CreateDirectory(_oldDirName);
foreach (var dir in Mod.ModPath.EnumerateDirectories())
{
if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase)
|| dir.FullName.Equals(_normalizationDirName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
dir.MoveTo(Path.Combine(oldDirectory.FullName, dir.Name));
}
++Step;
return true;
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure",
NotificationType.Error);
}
return false;
}
private bool MoveNewFiles()
{
try
{
var mainDir = new DirectoryInfo(_normalizationDirName);
foreach (var dir in mainDir.EnumerateDirectories())
{
dir.MoveTo(Path.Combine(Mod.ModPath.FullName, dir.Name));
}
mainDir.Delete();
Directory.Delete(_oldDirName, true);
++Step;
return true;
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure",
NotificationType.Error);
foreach (var dir in Mod.ModPath.EnumerateDirectories())
{
if (dir.FullName.Equals(_oldDirName, StringComparison.OrdinalIgnoreCase)
|| dir.FullName.Equals(_normalizationDirName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
try
{
dir.Delete(true);
}
catch
{
// ignored
}
}
}
return false;
}
private void ApplyRedirections()
{
foreach (var option in Mod.AllSubMods.OfType<Mod.SubMod>())
{
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]);
}
++Step;
}
}

View file

@ -0,0 +1,51 @@
using System.Collections.Generic;
using Penumbra.Mods;
using Penumbra.String.Classes;
using Penumbra.Util;
public class ModSwapEditor
{
private readonly Mod.Manager _modManager;
private readonly Dictionary<Utf8GamePath, FullPath> _swaps = new();
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
=> _swaps;
public ModSwapEditor(Mod.Manager modManager)
=> _modManager = modManager;
public void Revert(ISubMod option)
{
_swaps.SetTo(option.FileSwaps);
Changes = false;
}
public void Apply(Mod mod, int groupIdx, int optionIdx)
{
if (Changes)
{
_modManager.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps);
Changes = false;
}
}
public bool Changes { get; private set; }
public void Remove(Utf8GamePath path)
=> Changes |= _swaps.Remove(path);
public void Add(Utf8GamePath path, FullPath file)
=> Changes |= _swaps.TryAdd(path, file);
public void Change(Utf8GamePath path, Utf8GamePath newPath)
{
if (_swaps.Remove(path, out var file))
Add(newPath, file);
}
public void Change(Utf8GamePath path, FullPath file)
{
_swaps[path] = file;
Changes = true;
}
}

View file

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OtterGui;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
/// <summary> A class that collects information about skin materials in a model file and handle changes on them. </summary>
public class ModelMaterialInfo
{
public readonly FullPath Path;
public readonly MdlFile File;
private readonly string[] _currentMaterials;
private readonly IReadOnlyList<int> _materialIndices;
public bool Changed { get; private set; }
public IReadOnlyList<string> CurrentMaterials
=> _currentMaterials;
private IEnumerable<string> DefaultMaterials
=> _materialIndices.Select(i => File.Materials[i]);
public (string Current, string Default) this[int idx]
=> (_currentMaterials[idx], File.Materials[_materialIndices[idx]]);
public int Count
=> _materialIndices.Count;
// Set the skin material to a new value and flag changes appropriately.
public void SetMaterial(string value, int materialIdx)
{
var mat = File.Materials[_materialIndices[materialIdx]];
_currentMaterials[materialIdx] = value;
if (mat != value)
Changed = true;
else
Changed = !_currentMaterials.SequenceEqual(DefaultMaterials);
}
// Save a changed .mdl file.
public void Save()
{
if (!Changed)
return;
foreach (var (idx, i) in _materialIndices.WithIndex())
File.Materials[idx] = _currentMaterials[i];
try
{
System.IO.File.WriteAllBytes(Path.FullName, File.Write());
Changed = false;
}
catch (Exception e)
{
Restore();
Penumbra.Log.Error($"Could not write manipulated .mdl file {Path.FullName}:\n{e}");
}
}
// Revert all current changes.
public void Restore()
{
if (!Changed)
return;
foreach (var (idx, i) in _materialIndices.WithIndex())
_currentMaterials[i] = File.Materials[idx];
Changed = false;
}
public ModelMaterialInfo(FullPath path, MdlFile file, IReadOnlyList<int> indices)
{
Path = path;
File = file;
_materialIndices = indices;
_currentMaterials = DefaultMaterials.ToArray();
}
}

View file

@ -59,7 +59,7 @@ public partial class Mod
}
MoveDataFile( oldDirectory, dir );
new ModBackup( mod ).Move( null, dir.Name );
new ModBackup( this, mod ).Move( null, dir.Name );
dir.Refresh();
mod.ModPath = dir;

View file

@ -158,7 +158,7 @@ public sealed partial class Mod
{
foreach( var mod in _mods )
{
new ModBackup( mod ).Move( dir.FullName );
new ModBackup( this, mod ).Move( dir.FullName );
}
}

View file

@ -25,7 +25,7 @@ public partial class Mod
public int Priority
=> 0;
private Mod( DirectoryInfo modPath )
internal Mod( DirectoryInfo modPath )
{
ModPath = modPath;
_default = new SubMod( this );
@ -51,7 +51,7 @@ public partial class Mod
return mod;
}
private bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange )
internal bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{
modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh();

View file

@ -26,7 +26,7 @@ public enum ModDataChangeType : ushort
Note = 0x0800,
}
public sealed partial class Mod
public sealed partial class Mod : IMod
{
public static readonly TemporaryMod ForcedFiles = new()
{

View file

@ -18,7 +18,7 @@ public partial class Mod
// The default mod contains setting-independent sets of file replacements, file swaps and meta changes.
// Every mod has an default mod, though it may be empty.
private void SaveDefaultMod()
public void SaveDefaultMod()
{
var defaultFile = DefaultFile;
@ -100,7 +100,7 @@ public partial class Mod
// It can be loaded and reloaded from Json.
// Nothing is checked for existence or validity when loading.
// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
private sealed class SubMod : ISubMod
public sealed class SubMod : ISubMod
{
public string Name { get; set; } = "Default";

View file

@ -16,10 +16,11 @@ using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.ModTab;
using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.ModsTab;
using Penumbra.UI.Tabs;
using Penumbra.Util;
using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
namespace Penumbra;
@ -121,7 +122,18 @@ public class PenumbraNew
.AddSingleton<DebugTab>()
.AddSingleton<ResourceTab>()
.AddSingleton<ConfigTabBar>()
.AddSingleton<ResourceWatcher>();
.AddSingleton<ResourceWatcher>()
.AddSingleton<ItemSwapTab>();
// Add Mod Editor
services.AddSingleton<ModFileCollection>()
.AddSingleton<DuplicateManager>()
.AddSingleton<MdlMaterialEditor>()
.AddSingleton<ModFileEditor>()
.AddSingleton<ModMetaEditor>()
.AddSingleton<ModSwapEditor>()
.AddSingleton<ModNormalizer>()
.AddSingleton<ModEditor>();
// Add API
services.AddSingleton<PenumbraApi>()

View file

@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Data;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.Mods;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public class FileEditor<T> where T : class, IWritable
{
private readonly Configuration _config;
private readonly FileDialogService _fileDialog;
private readonly DataManager _gameData;
public FileEditor(DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType,
Func<IReadOnlyList<FileRegistry>> getFiles, Func<T, bool, bool> drawEdit, Func<string> getInitialPath,
Func<byte[], T?>? parseFile)
{
_gameData = gameData;
_config = config;
_fileDialog = fileDialog;
_tabName = tabName;
_fileType = fileType;
_getFiles = getFiles;
_drawEdit = drawEdit;
_getInitialPath = getInitialPath;
_parseFile = parseFile ?? DefaultParseFile;
}
public void Draw()
{
_list = _getFiles();
using var tab = ImRaii.TabItem(_tabName);
if (!tab)
return;
ImGui.NewLine();
DrawFileSelectCombo();
SaveButton();
ImGui.SameLine();
ResetButton();
ImGui.SameLine();
DefaultInput();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawFilePanel();
}
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<IReadOnlyList<FileRegistry>> _getFiles;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], T?> _parseFile;
private FileRegistry? _currentPath;
private T? _currentFile;
private Exception? _currentException;
private bool _changed;
private string _defaultPath = string.Empty;
private bool _inInput;
private T? _defaultFile;
private Exception? _defaultException;
private IReadOnlyList<FileRegistry> _list = null!;
private void DefaultInput()
{
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale });
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight());
ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength);
_inInput = ImGui.IsItemActive();
if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0)
{
_fileDialog.Reset();
try
{
var file = _gameData.GetFile(_defaultPath);
if (file != null)
{
_defaultException = null;
_defaultFile = _parseFile(file.Data);
}
else
{
_defaultFile = null;
_defaultException = new Exception("File does not exist.");
}
}
catch (Exception e)
{
_defaultFile = null;
_defaultException = e;
}
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.",
_defaultFile == null, true))
_fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType,
(success, name) =>
{
if (!success)
return;
try
{
File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid."));
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error);
}
}, _getInitialPath(), false);
_fileDialog.Draw();
}
public void Reset()
{
_currentException = null;
_currentPath = null;
_currentFile = null;
_changed = false;
}
private void DrawFileSelectCombo()
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...");
if (!combo)
return;
foreach (var file in _list)
{
if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath)))
UpdateCurrentFile(file);
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted("All Game Paths");
ImGui.Separator();
using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit);
foreach (var (option, gamePath) in file.SubModUsage)
{
ImGui.TableNextColumn();
UiHelpers.Text(gamePath.Path);
ImGui.TableNextColumn();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config));
ImGui.TextUnformatted(option.FullName);
}
}
if (file.SubModUsage.Count > 0)
{
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(_config));
ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString());
}
}
}
private static T? DefaultParseFile(byte[] bytes)
=> Activator.CreateInstance(typeof(T), bytes) as T;
private void UpdateCurrentFile(FileRegistry path)
{
if (ReferenceEquals(_currentPath, path))
return;
_changed = false;
_currentPath = path;
_currentException = null;
try
{
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
_currentFile = _parseFile(bytes);
}
catch (Exception e)
{
_currentFile = null;
_currentException = e;
}
}
private void SaveButton()
{
if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero,
$"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed))
{
File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
_changed = false;
}
}
private void ResetButton()
{
if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero,
$"Reset all changes made to the {_fileType} file.", !_changed))
{
var tmp = _currentPath;
_currentPath = null;
UpdateCurrentFile(tmp!);
}
}
private void DrawFilePanel()
{
using var child = ImRaii.Child("##filePanel", -Vector2.One, true);
if (!child)
return;
if (_currentPath != null)
{
if (_currentFile == null)
{
ImGui.TextUnformatted($"Could not parse selected {_fileType} file.");
if (_currentException != null)
{
using var tab = ImRaii.PushIndent();
ImGuiUtil.TextWrapped(_currentException.ToString());
}
}
else
{
using var id = ImRaii.PushId(0);
_changed |= _drawEdit(_currentFile, false);
}
}
if (!_inInput && _defaultPath.Length > 0)
{
if (_currentPath != null)
{
ImGui.NewLine();
ImGui.NewLine();
ImGui.TextUnformatted($"Preview of {_defaultPath}:");
ImGui.Separator();
}
if (_defaultFile == null)
{
ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n");
if (_defaultException != null)
{
using var tab = ImRaii.PushIndent();
ImGuiUtil.TextWrapped(_defaultException.ToString());
}
}
else
{
using var id = ImRaii.PushId(1);
_drawEdit(_defaultFile, true);
}
}
}
}

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility;
using ImGuiNET;
@ -18,12 +17,90 @@ using Penumbra.GameData.Structs;
using Penumbra.Mods;
using Penumbra.Mods.ItemSwap;
using Penumbra.Services;
using Penumbra.Util;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public class ItemSwapWindow : IDisposable
public class ItemSwapTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly ItemService _itemService;
private readonly ModCollection.Manager _collectionManager;
private readonly Mod.Manager _modManager;
private readonly Configuration _config;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager,
Mod.Manager modManager, Configuration config)
{
_communicator = communicator;
_itemService = itemService;
_collectionManager = collectionManager;
_modManager = modManager;
_config = config;
_selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)>
{
// @formatter:off
[SwapType.Hat] = (new ItemSelector(_itemService, FullEquipType.Head), new ItemSelector(_itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ),
[SwapType.Top] = (new ItemSelector(_itemService, FullEquipType.Body), new ItemSelector(_itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ),
[SwapType.Gloves] = (new ItemSelector(_itemService, FullEquipType.Hands), new ItemSelector(_itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ),
[SwapType.Pants] = (new ItemSelector(_itemService, FullEquipType.Legs), new ItemSelector(_itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ),
[SwapType.Shoes] = (new ItemSelector(_itemService, FullEquipType.Feet), new ItemSelector(_itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ),
[SwapType.Earrings] = (new ItemSelector(_itemService, FullEquipType.Ears), new ItemSelector(_itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ),
[SwapType.Necklace] = (new ItemSelector(_itemService, FullEquipType.Neck), new ItemSelector(_itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ),
[SwapType.Bracelet] = (new ItemSelector(_itemService, FullEquipType.Wrists), new ItemSelector(_itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ),
[SwapType.Ring] = (new ItemSelector(_itemService, FullEquipType.Finger), new ItemSelector(_itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ),
// @formatter:on
};
_communicator.CollectionChange.Event += OnCollectionChange;
_collectionManager.Current.ModSettingChanged += OnSettingChange;
}
/// <summary> Update the currently selected mod or its settings. </summary>
public void UpdateMod(Mod mod, ModSettings? settings)
{
if (mod == _mod && settings == _modSettings)
return;
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
if (_newModName.Length == 0 || oldDefaultName == _newModName)
_newModName = $"{mod.Name.Text} (Swapped)";
_mod = mod;
_modSettings = settings;
_swapData.LoadMod(_mod, _modSettings);
UpdateOption();
_dirty = true;
}
public ReadOnlySpan<byte> Label
=> "Item Swap (WIP)"u8;
public void DrawContent()
{
ImGui.NewLine();
DrawHeaderLine(300 * UiHelpers.Scale);
ImGui.NewLine();
DrawSwapBar();
using var table = ImRaii.ListBox("##swaps", -Vector2.One);
if (_loadException != null)
ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
else if (_swapData.Loaded)
foreach (var swap in _swapData.Swaps)
DrawSwap(swap);
else
ImGui.TextUnformatted(NonExistentText());
}
public void Dispose()
{
_communicator.CollectionChange.Event -= OnCollectionChange;
_collectionManager.Current.ModSettingChanged -= OnSettingChange;
}
private enum SwapType
{
Hat,
@ -45,8 +122,8 @@ public class ItemSwapWindow : IDisposable
private class ItemSelector : FilterComboCache<(string, Item)>
{
public ItemSelector(FullEquipType type)
: base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray())
public ItemSelector(ItemService data, FullEquipType type)
: base(() => data.AwaitedService[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray())
{ }
protected override string ToString((string, Item) obj)
@ -63,45 +140,10 @@ public class ItemSwapWindow : IDisposable
=> type.ToName();
}
private readonly CommunicatorService _communicator;
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors;
public ItemSwapWindow(CommunicatorService communicator)
{
_communicator = communicator;
_communicator.CollectionChange.Event += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
}
public void Dispose()
{
_communicator.CollectionChange.Event -= OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
}
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new()
{
[SwapType.Hat] =
(new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"),
[SwapType.Top] =
(new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"),
[SwapType.Gloves] =
(new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"),
[SwapType.Pants] =
(new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"),
[SwapType.Shoes] =
(new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"),
[SwapType.Earrings] =
(new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"),
[SwapType.Necklace] =
(new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"),
[SwapType.Bracelet] =
(new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"),
[SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring",
"and put it on this one"),
};
private ItemSelector? _weaponSource = null;
private ItemSelector? _weaponTarget = null;
private ItemSelector? _weaponSource;
private ItemSelector? _weaponTarget;
private readonly WeaponSelector _slotSelector = new();
private readonly ItemSwapContainer _swapData = new();
@ -112,40 +154,24 @@ public class ItemSwapWindow : IDisposable
private SwapType _lastTab = SwapType.Hair;
private Gender _currentGender = Gender.Male;
private ModelRace _currentRace = ModelRace.Midlander;
private int _targetId = 0;
private int _sourceId = 0;
private Exception? _loadException = null;
private int _targetId;
private int _sourceId;
private Exception? _loadException;
private EquipSlot _slotFrom = EquipSlot.Head;
private EquipSlot _slotTo = EquipSlot.Ears;
private string _newModName = string.Empty;
private string _newGroupName = "Swaps";
private string _newOptionName = string.Empty;
private IModGroup? _selectedGroup = null;
private bool _subModValid = false;
private IModGroup? _selectedGroup;
private bool _subModValid;
private bool _useFileSwaps = true;
private bool _useCurrentCollection = false;
private bool _useCurrentCollection;
private bool _useLeftRing = true;
private bool _useRightRing = true;
private Item[]? _affectedItems;
public void UpdateMod(Mod mod, ModSettings? settings)
{
if (mod == _mod && settings == _modSettings)
return;
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
if (_newModName.Length == 0 || oldDefaultName == _newModName)
_newModName = $"{mod.Name.Text} (Swapped)";
_mod = mod;
_modSettings = settings;
_swapData.LoadMod(_mod, _modSettings);
UpdateOption();
_dirty = true;
}
private void UpdateState()
{
if (!_dirty)
@ -167,42 +193,39 @@ public class ItemSwapWindow : IDisposable
case SwapType.Necklace:
case SwapType.Bracelet:
case SwapType.Ring:
var values = _selectors[ _lastTab ];
if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null )
{
_affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing );
}
var values = _selectors[_lastTab];
if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null)
_affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2,
_useCurrentCollection ? _collectionManager.Current : null, _useRightRing, _useLeftRing);
break;
case SwapType.BetweenSlots:
var (_, _, selectorFrom) = GetAccessorySelector( _slotFrom, true );
var (_, _, selectorTo) = GetAccessorySelector( _slotTo, false );
if( selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null )
{
_affectedItems = _swapData.LoadTypeSwap( _slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
}
var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true);
var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false);
if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null)
_affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom,
selectorFrom.CurrentSelection.Item2,
_useCurrentCollection ? _collectionManager.Current : null);
break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Current : null);
break;
case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Current : null);
break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Current : null);
break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
_useCurrentCollection ? _collectionManager.Current : null);
break;
case SwapType.Weapon: break;
}
@ -243,13 +266,13 @@ public class ItemSwapWindow : IDisposable
private void CreateMod()
{
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod(newDir);
if (!_swapData.WriteMod(Penumbra.ModManager.Last(),
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(),
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1);
_modManager.DeleteMod(_modManager.Count - 1);
}
private void CreateOption()
@ -273,12 +296,12 @@ public class ItemSwapWindow : IDisposable
{
if (_selectedGroup == null)
{
Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_selectedGroup = _mod.Groups.Last();
groupCreated = true;
}
Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
_modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true;
@ -294,11 +317,11 @@ public class ItemSwapWindow : IDisposable
try
{
if (optionCreated && _selectedGroup != null)
Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
_modManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
if (groupCreated)
{
Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_selectedGroup = null;
}
@ -365,17 +388,17 @@ public class ItemSwapWindow : IDisposable
private void DrawSwapBar()
{
using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None );
using var bar = ImRaii.TabBar("##swapBar", ImGuiTabBarFlags.None);
DrawEquipmentSwap( SwapType.Hat );
DrawEquipmentSwap( SwapType.Top );
DrawEquipmentSwap( SwapType.Gloves );
DrawEquipmentSwap( SwapType.Pants );
DrawEquipmentSwap( SwapType.Shoes );
DrawEquipmentSwap( SwapType.Earrings );
DrawEquipmentSwap( SwapType.Necklace );
DrawEquipmentSwap( SwapType.Bracelet );
DrawEquipmentSwap( SwapType.Ring );
DrawEquipmentSwap(SwapType.Hat);
DrawEquipmentSwap(SwapType.Top);
DrawEquipmentSwap(SwapType.Gloves);
DrawEquipmentSwap(SwapType.Pants);
DrawEquipmentSwap(SwapType.Shoes);
DrawEquipmentSwap(SwapType.Earrings);
DrawEquipmentSwap(SwapType.Necklace);
DrawEquipmentSwap(SwapType.Bracelet);
DrawEquipmentSwap(SwapType.Ring);
DrawAccessorySwap();
DrawHairSwap();
DrawFaceSwap();
@ -384,10 +407,10 @@ public class ItemSwapWindow : IDisposable
DrawWeaponSwap();
}
private ImRaii.IEndObject DrawTab( SwapType newTab )
private ImRaii.IEndObject DrawTab(SwapType newTab)
{
using var tab = ImRaii.TabItem( newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString() );
if( tab )
using var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString());
if (tab)
{
_dirty |= _lastTab != newTab;
_lastTab = newTab;
@ -400,82 +423,75 @@ public class ItemSwapWindow : IDisposable
private void DrawAccessorySwap()
{
using var tab = DrawTab( SwapType.BetweenSlots );
if( !tab )
{
using var tab = DrawTab(SwapType.BetweenSlots);
if (!tab)
return;
}
using var table = ImRaii.Table( "##settings", 3, ImGuiTableFlags.SizingFixedFit );
ImGui.TableSetupColumn( "##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "and put them on these" ).X );
using var table = ImRaii.Table("##settings", 3, ImGuiTableFlags.SizingFixedFit);
ImGui.TableSetupColumn("##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("and put them on these").X);
var (article1, article2, selector) = GetAccessorySelector( _slotFrom, true );
var (article1, article2, selector) = GetAccessorySelector(_slotFrom, true);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( $"Take {article1}" );
ImGui.TextUnformatted($"Take {article1}");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( 100 * UiHelpers.Scale );
using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) )
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName()))
{
if( combo )
{
foreach( var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head) )
{
if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom )
if (combo)
foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head))
{
if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom)
continue;
_dirty = true;
_slotFrom = slot;
if( slot == _slotTo )
{
_slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s );
}
}
}
if (slot == _slotTo)
_slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s);
}
}
ImGui.TableNextColumn();
_dirty |= selector.Draw( "##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
_dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
(article1, _, selector) = GetAccessorySelector( _slotTo, false );
(article1, _, selector) = GetAccessorySelector(_slotTo, false);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( $"and put {article2} on {article1}" );
ImGui.TextUnformatted($"and put {article2} on {article1}");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( 100 * UiHelpers.Scale );
using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) )
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
using (var combo = ImRaii.Combo("##toType", _slotTo.ToName()))
{
if( combo )
{
foreach( var slot in EquipSlotExtensions.AccessorySlots.Where( s => s != _slotFrom ) )
{
if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo )
if (combo)
foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom))
{
if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo)
continue;
_dirty = true;
_slotTo = slot;
}
}
}
}
ImGui.TableNextColumn();
_dirty |= selector.Draw( "##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
if( _affectedItems is { Length: > 1 } )
{
_dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
if (_affectedItems is not { Length: > 1 })
return;
ImGui.SameLine();
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg );
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, selector.CurrentSelection.Item2 ) )
.Select( i => i.Name.ToDalamudString().TextValue ) ) );
}
}
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2))
.Select(i => i.Name.ToDalamudString().TextValue)));
}
private (string, string, ItemSelector) GetAccessorySelector( EquipSlot slot, bool source )
private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source)
{
var (type, article1, article2) = slot switch
{
@ -487,8 +503,8 @@ public class ItemSwapWindow : IDisposable
EquipSlot.LFinger => (SwapType.Ring, "this", "it"),
_ => (SwapType.Ring, "this", "it"),
};
var tuple = _selectors[ type ];
return (article1, article2, source ? tuple.Source : tuple.Target);
var (itemSelector, target, _, _) = _selectors[type];
return (article1, article2, source ? itemSelector : target);
}
private void DrawEquipmentSwap(SwapType type)
@ -524,8 +540,9 @@ public class ItemSwapWindow : IDisposable
_dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing);
}
if (_affectedItems is { Length: > 1 })
{
if (_affectedItems is not { Length: > 1 })
return;
ImGui.SameLine();
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg);
@ -533,7 +550,6 @@ public class ItemSwapWindow : IDisposable
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
.Select(i => i.Name.ToDalamudString().TextValue)));
}
}
private void DrawHairSwap()
{
@ -602,14 +618,14 @@ public class ItemSwapWindow : IDisposable
ImGui.GetTextLineHeightWithSpacing()))
{
_dirty = true;
_weaponSource = new ItemSelector(_slotSelector.CurrentSelection);
_weaponTarget = new ItemSelector(_slotSelector.CurrentSelection);
_weaponSource = new ItemSelector(_itemService, _slotSelector.CurrentSelection);
_weaponTarget = new ItemSelector(_itemService, _slotSelector.CurrentSelection);
}
else
{
_dirty = _weaponSource == null || _weaponTarget == null;
_weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection);
_weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection);
_weaponSource ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection);
_weaponTarget ??= new ItemSelector(_itemService, _slotSelector.CurrentSelection);
}
ImGui.TableNextColumn();
@ -706,29 +722,6 @@ public class ItemSwapWindow : IDisposable
_ => string.Empty,
};
public void DrawItemSwapPanel()
{
using var tab = ImRaii.TabItem("Item Swap (WIP)");
if (!tab)
return;
ImGui.NewLine();
DrawHeaderLine(300 * UiHelpers.Scale);
ImGui.NewLine();
DrawSwapBar();
using var table = ImRaii.ListBox("##swaps", -Vector2.One);
if (_loadException != null)
ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
else if (_swapData.Loaded)
foreach (var swap in _swapData.Swaps)
DrawSwap(swap);
else
ImGui.TextUnformatted(NonExistentText());
}
private static void DrawSwap(Swap swap)
{
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
@ -754,10 +747,10 @@ public class ItemSwapWindow : IDisposable
private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
{
if (modIdx == _mod?.Index)
{
if (modIdx != _mod?.Index)
return;
_swapData.LoadMod(_mod, _modSettings);
_dirty = true;
}
}
}

View file

@ -9,27 +9,29 @@ using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Mods;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private readonly HashSet<Mod.Editor.FileRegistry> _selectedFiles = new(256);
private readonly HashSet<FileRegistry> _selectedFiles = new(256);
private LowerString _fileFilter = LowerString.Empty;
private bool _showGamePaths = true;
private string _gamePathEdit = string.Empty;
private int _fileIdx = -1;
private int _pathIdx = -1;
private int _folderSkip = 0;
private bool _overviewMode = false;
private int _folderSkip;
private bool _overviewMode;
private LowerString _fileOverviewFilter1 = LowerString.Empty;
private LowerString _fileOverviewFilter2 = LowerString.Empty;
private LowerString _fileOverviewFilter3 = LowerString.Empty;
private bool CheckFilter(Mod.Editor.FileRegistry registry)
private bool CheckFilter(FileRegistry registry)
=> _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase);
private bool CheckFilter((Mod.Editor.FileRegistry, int) p)
private bool CheckFilter((FileRegistry, int) p)
=> CheckFilter(p.Item1);
private void DrawFileTab()
@ -74,13 +76,13 @@ public partial class ModEditWindow
var idx = 0;
var files = _editor!.AvailableFiles.SelectMany(f =>
var files = _editor.Files.Available.SelectMany(f =>
{
var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName,
_editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u));
_editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u));
});
void DrawLine((string, string, string, uint) data)
@ -119,7 +121,7 @@ public partial class ModEditWindow
if (!list)
return;
foreach (var (registry, i) in _editor!.AvailableFiles.WithIndex().Where(CheckFilter))
foreach (var (registry, i) in _editor.Files.Available.WithIndex().Where(CheckFilter))
{
using var id = ImRaii.PushId(i);
ImGui.TableNextColumn();
@ -133,17 +135,17 @@ public partial class ModEditWindow
for (var j = 0; j < registry.SubModUsage.Count; ++j)
{
var (subMod, gamePath) = registry.SubModUsage[j];
if (subMod != _editor.CurrentOption)
if (subMod != _editor.Option)
continue;
PrintGamePath(i, j, registry, subMod, gamePath);
}
PrintNewGamePath(i, registry, _editor.CurrentOption);
PrintNewGamePath(i, registry, _editor.Option!);
}
}
private static string DrawFileTooltip(Mod.Editor.FileRegistry registry, ColorId color)
private static string DrawFileTooltip(FileRegistry registry, ColorId color)
{
(string, int) GetMulti()
{
@ -172,7 +174,7 @@ public partial class ModEditWindow
};
}
private void DrawSelectable(Mod.Editor.FileRegistry registry)
private void DrawSelectable(FileRegistry registry)
{
var selected = _selectedFiles.Contains(registry);
var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod :
@ -192,7 +194,7 @@ public partial class ModEditWindow
ImGuiUtil.RightAlign(rightText);
}
private void PrintGamePath(int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath)
private void PrintGamePath(int i, int j, FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath)
{
using var id = ImRaii.PushId(j);
ImGui.TableNextColumn();
@ -211,7 +213,7 @@ public partial class ModEditWindow
if (ImGui.IsItemDeactivatedAfterEdit())
{
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false))
_editor!.SetGamePath(_fileIdx, _pathIdx, path);
_editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path);
_fileIdx = -1;
_pathIdx = -1;
@ -219,7 +221,7 @@ public partial class ModEditWindow
else if (_fileIdx == i
&& _pathIdx == j
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false)
|| !path.IsEmpty && !path.Equals(gamePath) && !_editor!.CanAddGamePath(path)))
|| !path.IsEmpty && !path.Equals(gamePath) && !_editor.FileEditor.CanAddGamePath(path)))
{
ImGui.SameLine();
ImGui.SetCursorPosX(pos);
@ -228,7 +230,7 @@ public partial class ModEditWindow
}
}
private void PrintNewGamePath(int i, Mod.Editor.FileRegistry registry, ISubMod subMod)
private void PrintNewGamePath(int i, FileRegistry registry, ISubMod subMod)
{
var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();
@ -243,7 +245,7 @@ public partial class ModEditWindow
if (ImGui.IsItemDeactivatedAfterEdit())
{
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty)
_editor!.SetGamePath(_fileIdx, _pathIdx, path);
_editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path);
_fileIdx = -1;
_pathIdx = -1;
@ -251,7 +253,7 @@ public partial class ModEditWindow
else if (_fileIdx == i
&& _pathIdx == -1
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false)
|| !path.IsEmpty && !_editor!.CanAddGamePath(path)))
|| !path.IsEmpty && !_editor.FileEditor.CanAddGamePath(path)))
{
ImGui.SameLine();
ImGui.SetCursorPosX(pos);
@ -271,7 +273,7 @@ public partial class ModEditWindow
ImGui.SameLine();
spacing.Pop();
if (ImGui.Button("Add Paths"))
_editor!.AddPathsToSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains), _folderSkip);
_editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip);
ImGuiUtil.HoverTooltip(
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders.");
@ -279,25 +281,25 @@ public partial class ModEditWindow
ImGui.SameLine();
if (ImGui.Button("Remove Paths"))
_editor!.RemovePathsFromSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains));
_editor.FileEditor.RemovePathsFromSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains));
ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option.");
ImGui.SameLine();
if (ImGui.Button("Delete Selected Files"))
_editor!.DeleteFiles(_editor!.AvailableFiles.Where(_selectedFiles.Contains));
_editor.FileEditor.DeleteFiles(_editor.Mod!, _editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains));
ImGuiUtil.HoverTooltip(
"Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!");
ImGui.SameLine();
var changes = _editor!.FileChanges;
var changes = _editor.FileEditor.Changes;
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
{
var failedFiles = _editor!.ApplyFiles();
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!);
if (failedFiles > 0)
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}.");
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}.");
}
@ -305,7 +307,7 @@ public partial class ModEditWindow
var label = changes ? "Revert Changes" : "Reload Files";
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
if (ImGui.Button(label, length))
_editor!.RevertFiles();
_editor.FileEditor.RevertFiles(_editor.Mod!, _editor.Option!);
ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh.");
@ -325,19 +327,19 @@ public partial class ModEditWindow
ImGui.SameLine();
if (ImGui.Button("Select Visible"))
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter));
_selectedFiles.UnionWith(_editor.Files.Available.Where(CheckFilter));
ImGui.SameLine();
if (ImGui.Button("Select Unused"))
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.SubModUsage.Count == 0));
_selectedFiles.UnionWith(_editor.Files.Available.Where(f => f.SubModUsage.Count == 0));
ImGui.SameLine();
if (ImGui.Button("Select Used Here"))
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.CurrentUsage > 0));
_selectedFiles.UnionWith(_editor.Files.Available.Where(f => f.CurrentUsage > 0));
ImGui.SameLine();
ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected");
ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
}
private void DrawFileManagementOverview()

View file

@ -9,7 +9,7 @@ using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Functions;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -15,7 +15,7 @@ using Penumbra.String.Classes;
using Penumbra.Util;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -13,7 +13,7 @@ using Penumbra.GameData;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -6,8 +6,9 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
@ -121,7 +122,7 @@ public partial class ModEditWindow
private void DrawMaterialReassignmentTab()
{
if( _editor!.ModelFiles.Count == 0 )
if( _editor.Files.Mdl.Count == 0 )
{
return;
}
@ -149,7 +150,7 @@ public partial class ModEditWindow
}
var iconSize = ImGui.GetFrameHeight() * Vector2.One;
foreach( var (info, idx) in _editor.ModelFiles.WithIndex() )
foreach( var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex() )
{
using var id = ImRaii.PushId( idx );
ImGui.TableNextColumn();

View file

@ -0,0 +1,886 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private const string ModelSetIdTooltip =
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
private const string PrimaryIdTooltip =
"Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
private const string ModelSetIdTooltipShort = "Model Set ID";
private const string EquipSlotTooltip = "Equip Slot";
private const string ModelRaceTooltip = "Model Race";
private const string GenderTooltip = "Gender";
private const string ObjectTypeTooltip = "Object Type";
private const string SecondaryIdTooltip = "Secondary ID";
private const string VariantIdTooltip = "Variant ID";
private const string EstTypeTooltip = "EST Type";
private const string RacialTribeTooltip = "Racial Tribe";
private const string ScalingTypeTooltip = "Scaling Type";
private void DrawMetaTab()
{
using var tab = ImRaii.TabItem("Meta Manipulations");
if (!tab)
return;
DrawOptionSelectHeader();
var setsEqual = !_editor.MetaEditor.Changes;
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx);
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
_editor.MetaEditor.Load(_editor.Option!);
ImGui.SameLine();
AddFromClipboardButton();
ImGui.SameLine();
SetFromClipboardButton();
ImGui.SameLine();
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine());
ImGui.SameLine();
if (ImGui.Button("Write as TexTools Files"))
_mod!.WriteAllTexToolsMeta();
using var child = ImRaii.Child("##meta", -Vector2.One, true);
if (!child)
return;
DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew);
DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew);
DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew);
DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew);
DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew);
DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew);
}
// The headers for the different meta changes all have basically the same structure for different types.
private void DrawEditHeader<T>(IReadOnlyCollection<T> items, string label, int numColumns, Action<T, ModEditor, Vector2> draw,
Action<ModEditor, Vector2> drawNew)
{
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
if (!ImGui.CollapsingHeader($"{items.Count} {label}"))
return;
using (var table = ImRaii.Table(label, numColumns, flags))
{
if (table)
{
drawNew(_editor, _iconSize);
foreach (var (item, index) in items.ToArray().WithIndex())
{
using var id = ImRaii.PushId(index);
draw(item, _editor, _iconSize);
}
}
}
ImGui.NewLine();
}
private static class EqpRow
{
private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize,
editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedEqpFile.GetDefault(_new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), _new.Slot, setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), slot, _new.SetId);
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
foreach (var flag in Eqp.EqpAttributes[_new.Slot])
{
var value = defaultEntry.HasFlag(flag);
Checkmark("##eqp", flag.ToLocalName(), value, value, out _);
ImGui.SameLine();
}
ImGui.NewLine();
}
public static void Draw(EqpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
var idx = 0;
foreach (var flag in Eqp.EqpAttributes[meta.Slot])
{
using var id = ImRaii.PushId(idx++);
var defaultValue = defaultEntry.HasFlag(flag);
var currentValue = meta.Entry.HasFlag(flag);
if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value))
editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag));
ImGui.SameLine();
}
ImGui.NewLine();
}
}
private static class EqdpRow
{
private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize,
editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var raceCode = Names.CombinedRace(_new.Gender, _new.Race);
var validRaceCode = CharacterUtility.EqdpIdx(raceCode, false) >= 0;
var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." :
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
var defaultEntry = validRaceCode
? ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId)
: 0;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId);
}
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.Race("##eqdpRace", _new.Race, out var race))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId);
}
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
if (Combos.Gender("##eqdpGender", _new.Gender, out var gender))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId);
}
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot))
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId);
}
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
var (bit1, bit2) = defaultEntry.ToBits(_new.Slot);
Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _);
ImGui.SameLine();
Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _);
}
public static void Draw(EqdpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Race.ToName());
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Gender.ToName());
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values
var defaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId);
var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot);
var (bit1, bit2) = meta.Entry.ToBits(meta.Slot);
ImGui.TableNextColumn();
if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1))
editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2)));
ImGui.SameLine();
if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2))
editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2)));
}
}
private static class ImcRow
{
private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry());
private static float IdWidth
=> 80 * UiHelpers.Scale;
private static float SmallIdWidth
=> 45 * UiHelpers.Scale;
// Convert throwing to null-return if the file does not exist.
private static ImcEntry? GetDefault(ImcManipulation imc)
{
try
{
return ImcFile.GetDefault(imc.GamePath(), imc.EquipSlot, imc.Variant, out _);
}
catch
{
return null;
}
}
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize,
editor.MetaEditor.Imc.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var defaultEntry = GetDefault(_new);
var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited.";
defaultEntry ??= new ImcEntry();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry.Value));
// Identifier
ImGui.TableNextColumn();
if (Combos.ImcType("##imcType", _new.ObjectType, out var type))
{
var equipSlot = type switch
{
ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears,
_ => EquipSlot.Unknown,
};
_new = new ImcManipulation(type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? (ushort)1 : _new.SecondaryId,
_new.Variant, equipSlot, _new.Entry);
}
ImGuiUtil.HoverTooltip(ObjectTypeTooltip);
ImGui.TableNextColumn();
if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry)
.Copy(GetDefault(_new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(PrimaryIdTooltip);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
ImGui.TableNextColumn();
// Equipment and accessories are slightly different imcs than other types.
if (_new.ObjectType is ObjectType.Equipment)
{
if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
else if (_new.ObjectType is ObjectType.Accessory)
{
if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
else
{
if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry)
.Copy(GetDefault(_new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(SecondaryIdTooltip);
}
ImGui.TableNextColumn();
if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot,
_new.Entry).Copy(GetDefault(_new)
?? new ImcEntry());
ImGui.TableNextColumn();
if (_new.ObjectType is ObjectType.DemiHuman)
{
if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
else
{
ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0));
}
ImGuiUtil.HoverTooltip(VariantIdTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput("##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _,
1, byte.MaxValue, 0f);
ImGui.SameLine();
IntDragInput("##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId,
defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f);
ImGui.TableNextColumn();
IntDragInput("##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0,
byte.MaxValue, 0f);
ImGui.SameLine();
IntDragInput("##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue,
0f);
ImGui.SameLine();
IntDragInput("##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111,
0f);
ImGui.TableNextColumn();
for (var i = 0; i < 10; ++i)
{
using var id = ImRaii.PushId(i);
var flag = 1 << i;
Checkmark("##attribute", $"{(char)('A' + i)}", (defaultEntry.Value.AttributeMask & flag) != 0,
(defaultEntry.Value.AttributeMask & flag) != 0, out _);
ImGui.SameLine();
}
ImGui.NewLine();
}
public static void Draw(ImcManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.ObjectType.ToName());
ImGuiUtil.HoverTooltip(ObjectTypeTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.PrimaryId.ToString());
ImGuiUtil.HoverTooltip("Primary ID");
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory)
{
ImGui.TextUnformatted(meta.EquipSlot.ToName());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
}
else
{
ImGui.TextUnformatted(meta.SecondaryId.ToString());
ImGuiUtil.HoverTooltip(SecondaryIdTooltip);
}
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Variant.ToString());
ImGuiUtil.HoverTooltip(VariantIdTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
if (meta.ObjectType is ObjectType.DemiHuman)
ImGui.TextUnformatted(meta.EquipSlot.ToName());
// Values
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
ImGui.TableNextColumn();
var defaultEntry = GetDefault(meta) ?? new ImcEntry();
if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId,
defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId }));
ImGui.SameLine();
if (IntDragInput("##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth,
meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialAnimationId = (byte)materialAnimId }));
ImGui.TableNextColumn();
if (IntDragInput("##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId,
defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { DecalId = (byte)decalId }));
ImGui.SameLine();
if (IntDragInput("##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId,
out var vfxId, 0, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { VfxId = (byte)vfxId }));
ImGui.SameLine();
if (IntDragInput("##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId,
defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { SoundId = (byte)soundId }));
ImGui.TableNextColumn();
for (var i = 0; i < 10; ++i)
{
using var id = ImRaii.PushId(i);
var flag = 1 << i;
if (Checkmark("##attribute", $"{(char)('A' + i)}", (meta.Entry.AttributeMask & flag) != 0,
(defaultEntry.AttributeMask & flag) != 0, out var val))
{
var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag;
editor.MetaEditor.Change(meta.Copy(meta.Entry with { AttributeMask = (ushort)attributes }));
}
ImGui.SameLine();
}
ImGui.NewLine();
}
}
private static class EstRow
{
private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize,
editor.MetaEditor.Est.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId);
_new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn();
if (Combos.Race("##estRace", _new.Race, out var race))
{
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId);
_new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
if (Combos.Gender("##estGender", _new.Gender, out var gender))
{
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId);
_new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
if (Combos.EstSlot("##estSlot", _new.Slot, out var slot))
{
var newDefaultEntry = EstFile.GetDefault(slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
_new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry);
}
ImGuiUtil.HoverTooltip(EstTypeTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f);
}
public static void Draw(EstManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Race.ToName());
ImGuiUtil.HoverTooltip(ModelRaceTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Gender.ToName());
ImGuiUtil.HoverTooltip(GenderTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Slot.ToString());
ImGuiUtil.HoverTooltip(EstTypeTooltip);
// Values
var defaultEntry = EstFile.GetDefault(meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId);
ImGui.TableNextColumn();
if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
out var entry, 0, ushort.MaxValue, 0.05f))
editor.MetaEditor.Change(meta.Copy((ushort)entry));
}
}
private static class GmpRow
{
private static GmpManipulation _new = new(GmpEntry.Default, 1);
private static float RotationWidth
=> 75 * UiHelpers.Scale;
private static float UnkWidth
=> 50 * UiHelpers.Scale;
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize,
editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedGmpFile.GetDefault(_new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new GmpManipulation(ExpandedGmpFile.GetDefault(setId), setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _);
ImGui.TableNextColumn();
Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _);
ImGui.TableNextColumn();
IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0,
360, 0f);
ImGui.SameLine();
IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0,
360, 0f);
ImGui.SameLine();
IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0,
360, 0f);
ImGui.TableNextColumn();
IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f);
ImGui.SameLine();
IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f);
}
public static void Draw(GmpManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
// Values
var defaultEntry = ExpandedGmpFile.GetDefault(meta.SetId);
ImGui.TableNextColumn();
if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled }));
ImGui.TableNextColumn();
if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated }));
ImGui.TableNextColumn();
if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth,
meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA }));
ImGui.SameLine();
if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth,
meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB }));
ImGui.SameLine();
if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth,
meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC }));
ImGui.TableNextColumn();
if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA,
defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA }));
ImGui.SameLine();
if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB,
defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkB }));
}
}
private static class RspRow
{
private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f);
private static float FloatWidth
=> 150 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize,
editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = CmpFile.GetDefault(_new.SubRace, _new.Attribute);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace))
_new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(subRace, _new.Attribute));
ImGuiUtil.HoverTooltip(RacialTribeTooltip);
ImGui.TableNextColumn();
if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute))
_new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(subRace, attribute));
ImGuiUtil.HoverTooltip(ScalingTypeTooltip);
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(FloatWidth);
ImGui.DragFloat("##rspValue", ref defaultEntry, 0f);
}
public static void Draw(RspManipulation meta, ModEditor editor, Vector2 iconSize)
{
DrawMetaButtons(meta, editor, iconSize);
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SubRace.ToName());
ImGuiUtil.HoverTooltip(RacialTribeTooltip);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.Attribute.ToFullString());
ImGuiUtil.HoverTooltip(ScalingTypeTooltip);
ImGui.TableNextColumn();
// Values
var def = CmpFile.GetDefault(meta.SubRace, meta.Attribute);
var value = meta.Entry;
ImGui.SetNextItemWidth(FloatWidth);
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config),
def != value);
if (ImGui.DragFloat("##rspValue", ref value, 0.001f, 0.01f, 8f) && value is >= 0.01f and <= 8f)
editor.MetaEditor.Change(meta.Copy(value));
ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}");
}
}
// A number input for ids with a optional max id of given width.
// Returns true if newId changed against currentId.
private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border)
{
int tmp = currentId;
ImGui.SetNextItemWidth(width);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border);
using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border);
if (ImGui.InputInt(label, ref tmp, 0))
tmp = Math.Clamp(tmp, minId, maxId);
newId = (ushort)tmp;
return newId != currentId;
}
// A checkmark that compares against a default value and shows a tooltip.
// Returns true if newValue is changed against currentValue.
private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue)
{
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config),
defaultValue != currentValue);
newValue = currentValue;
ImGui.Checkbox(label, ref newValue);
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
return newValue != currentValue;
}
// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
// Returns true if newValue changed against currentValue.
private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue,
int minValue, int maxValue, float speed)
{
newValue = currentValue;
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config),
defaultValue != currentValue);
ImGui.SetNextItemWidth(width);
if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue))
newValue = Math.Clamp(newValue, minValue, maxValue);
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
return newValue != currentValue;
}
private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, IEnumerable<MetaManipulation> manipulations)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true))
return;
var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion);
if (text.Length > 0)
ImGui.SetClipboardText(text);
}
private void AddFromClipboardButton()
{
if (ImGui.Button("Add from Clipboard"))
{
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaManipulation[]>(clipboard, out var manips);
if (version == MetaManipulation.CurrentVersion && manips != null)
foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
_editor.MetaEditor.Set(manip);
}
ImGuiUtil.HoverTooltip(
"Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations.");
}
private void SetFromClipboardButton()
{
if (ImGui.Button("Set from Clipboard"))
{
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64<MetaManipulation[]>(clipboard, out var manips);
if (version == MetaManipulation.CurrentVersion && manips != null)
{
_editor.MetaEditor.Clear();
foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
_editor.MetaEditor.Set(manip);
}
}
ImGuiUtil.HoverTooltip(
"Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations.");
}
private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty<MetaManipulation>().Append(meta));
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true))
editor.MetaEditor.Delete(meta);
}
}

View file

@ -5,8 +5,9 @@ using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using System.Globalization;
using System.Linq;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -14,10 +14,10 @@ using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.String;
using Penumbra.Util;
using Penumbra.UI.AdvancedWindow;
using static Penumbra.GameData.Files.ShpkFile;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -6,7 +6,7 @@ using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{

View file

@ -7,7 +7,7 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.Import.Textures;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
@ -43,7 +43,7 @@ public partial class ModEditWindow
tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName,
_fileDialog);
var files = _editor!.TexFiles.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
var files = _editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
.Prepend((f.File.FullName, false)));
tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.",
files, _mod.ModPath.FullName.Length + 1);

View file

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Data;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
@ -12,30 +13,31 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.Util;
using static Penumbra.Mods.Mod;
namespace Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow : Window, IDisposable
{
private const string WindowBaseLabel = "###SubModEdit";
internal readonly ItemSwapWindow _swapWindow;
private Editor? _editor;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
private bool _allowReduplicate = false;
private bool _allowReduplicate;
public void ChangeMod(Mod mod)
{
if (mod == _mod)
return;
_editor?.Dispose();
_editor = new Editor(mod, mod.Default);
_editor.LoadMod(mod, -1, 0);
_mod = mod;
SizeConstraints = new WindowSizeConstraints
@ -47,17 +49,20 @@ public partial class ModEditWindow : Window, IDisposable
_modelTab.Reset();
_materialTab.Reset();
_shaderPackageTab.Reset();
_swapWindow.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings);
_itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings);
}
public void ChangeOption(ISubMod? subMod)
=> _editor?.SetSubMod(subMod);
public void ChangeOption(SubMod? subMod)
=> _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.GroupIdx ?? 0);
public void UpdateModels()
=> _editor?.ScanModels();
{
if (_mod != null)
_editor.MdlMaterialEditor.ScanModels(_mod);
}
public override bool DrawConditions()
=> _editor != null;
=> _mod != null;
public override void PreDraw()
{
@ -67,7 +72,7 @@ public partial class ModEditWindow : Window, IDisposable
var redirections = 0;
var unused = 0;
var size = _editor!.AvailableFiles.Sum(f =>
var size = _editor.Files.Available.Sum(f =>
{
if (f.SubModUsage.Count > 0)
redirections += f.SubModUsage.Count;
@ -89,13 +94,13 @@ public partial class ModEditWindow : Window, IDisposable
sb.Append($" | {subMods} Options");
if (size > 0)
sb.Append($" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize(size)})");
sb.Append($" | {_editor.Files.Available.Count} Files ({Functions.HumanReadableSize(size)})");
if (unused > 0)
sb.Append($" | {unused} Unused Files");
if (_editor.MissingFiles.Count > 0)
sb.Append($" | {_editor.MissingFiles.Count} Missing Files");
if (_editor.Files.Missing.Count > 0)
sb.Append($" | {_editor.Files.Available.Count} Missing Files");
if (redirections > 0)
sb.Append($" | {redirections} Redirections");
@ -106,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable
if (swaps > 0)
sb.Append($" | {swaps} Swaps");
_allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0;
_allowReduplicate = redirections != _editor.Files.Available.Count || _editor.Files.Available.Count > 0;
sb.Append(WindowBaseLabel);
WindowName = sb.ToString();
}
@ -136,7 +141,7 @@ public partial class ModEditWindow : Window, IDisposable
_materialTab.Draw();
DrawTextureTab();
_shaderPackageTab.Draw();
_swapWindow.DrawItemSwapPanel();
_itemSwapTab.DrawContent();
}
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
@ -169,7 +174,7 @@ public partial class ModEditWindow : Window, IDisposable
}
}
public static void Draw(Editor editor, Vector2 buttonSize)
public static void Draw(ModEditor editor, Vector2 buttonSize)
{
DrawRaceCodeCombo(buttonSize);
ImGui.SameLine();
@ -179,7 +184,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SetNextItemWidth(buttonSize.X);
ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32);
ImGui.SameLine();
var disabled = !Editor.ValidString(_materialSuffixTo);
var disabled = !MdlMaterialEditor.ValidString(_materialSuffixTo);
var tt = _materialSuffixTo.Length == 0
? "Please enter a target suffix."
: _materialSuffixFrom == _materialSuffixTo
@ -194,17 +199,17 @@ public partial class ModEditWindow : Window, IDisposable
? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."
: $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'.";
if (ImGuiUtil.DrawDisabledButton("Change Material Suffix", buttonSize, tt, disabled))
editor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode);
editor.MdlMaterialEditor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode);
var anyChanges = editor.ModelFiles.Any(m => m.Changed);
var anyChanges = editor.MdlMaterialEditor.ModelFiles.Any(m => m.Changed);
if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize,
anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges))
editor.SaveAllModels();
editor.MdlMaterialEditor.SaveAllModels();
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize,
anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges))
editor.RestoreAllModels();
editor.MdlMaterialEditor.RestoreAllModels();
ImGui.SameLine();
ImGuiComponents.HelpMarker(
@ -216,7 +221,7 @@ public partial class ModEditWindow : Window, IDisposable
private void DrawMissingFilesTab()
{
if (_editor!.MissingFiles.Count == 0)
if (_editor.Files.Missing.Count == 0)
return;
using var tab = ImRaii.TabItem("Missing Files");
@ -225,7 +230,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.NewLine();
if (ImGui.Button("Remove Missing Files from Mod"))
_editor.RemoveMissingPaths();
_editor.FileEditor.RemoveMissingPaths(_mod!, _editor.Option!);
using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true);
if (!child)
@ -235,7 +240,7 @@ public partial class ModEditWindow : Window, IDisposable
if (!table)
return;
foreach (var path in _editor.MissingFiles)
foreach (var path in _editor.Files.Missing)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(path.FullName);
@ -248,37 +253,44 @@ public partial class ModEditWindow : Window, IDisposable
if (!tab)
return;
var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton";
var buttonText = _editor.Duplicates.Finished ? "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();
!_editor.Duplicates.Finished))
_editor.Duplicates.StartDuplicateCheck(_editor.Files.Available);
const string desc =
"Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n"
+ "This will also delete all unused files and directories if it succeeds.\n"
+ "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway.";
var modifier = Penumbra.Config.DeleteModModifier.IsActive();
var modifier = _config.DeleteModModifier.IsActive();
var tt = _allowReduplicate ? desc :
modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway.";
if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier))
{
_mod!.Normalize(Penumbra.ModManager);
_editor.RevertFiles();
_editor.ModNormalizer.Normalize(_mod!);
_editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx);
}
if (!_editor.DuplicatesFinished)
if (_editor.ModNormalizer.Running)
{
using var popup = ImRaii.Popup("Normalization", ImGuiWindowFlags.Modal);
ImGui.ProgressBar((float)_editor.ModNormalizer.Step / _editor.ModNormalizer.TotalSteps,
new Vector2(300 * UiHelpers.Scale, ImGui.GetFrameHeight()),
$"{_editor.ModNormalizer.Step} / {_editor.ModNormalizer.TotalSteps}");
}
if (!_editor.Duplicates.Finished)
{
ImGui.SameLine();
if (ImGui.Button("Cancel"))
_editor.Cancel();
_editor.Duplicates.Clear();
return;
}
if (_editor.Duplicates.Count == 0)
if (_editor.Duplicates.Duplicates.Count == 0)
{
ImGui.NewLine();
ImGui.TextUnformatted("No duplicates found.");
@ -286,12 +298,12 @@ public partial class ModEditWindow : Window, IDisposable
}
if (ImGui.Button("Delete and Redirect Duplicates"))
_editor.DeleteDuplicates();
_editor.Duplicates.DeleteDuplicates(_editor.Mod!, _editor.Option!, true);
if (_editor.SavedSpace > 0)
if (_editor.Duplicates.SavedSpace > 0)
{
ImGui.SameLine();
ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.SavedSpace)} from your hard drive.");
ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.Duplicates.SavedSpace)} from your hard drive.");
}
using var child = ImRaii.Child("##duptable", -Vector2.One, true);
@ -307,7 +319,7 @@ public partial class ModEditWindow : Window, IDisposable
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))
foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1))
{
ImGui.TableNextColumn();
using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..],
@ -346,23 +358,23 @@ public partial class ModEditWindow : Window, IDisposable
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0);
var width = new Vector2(ImGui.GetWindowWidth() / 3, 0);
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
_editor!.CurrentOption.IsDefault))
_editor.SetSubMod(_mod!.Default);
_editor!.Option!.IsDefault))
_editor.LoadOption(-1, 0);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
_editor.SetSubMod(_editor.CurrentOption);
_editor.LoadOption(_editor.GroupIdx, _editor.OptionIdx);
ImGui.SameLine();
using var combo = ImRaii.Combo("##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton);
using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName, ImGuiComboFlags.NoArrowButton);
if (!combo)
return;
foreach (var option in _mod!.AllSubMods)
foreach (var option in _mod!.AllSubMods.Cast<SubMod>())
{
if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption))
_editor.SetSubMod(option);
if (ImGui.Selectable(option.FullName, option == _editor.Option))
_editor.LoadOption(option.GroupIdx, option.OptionIdx);
}
}
@ -377,16 +389,16 @@ public partial class ModEditWindow : Window, IDisposable
DrawOptionSelectHeader();
var setsEqual = _editor!.CurrentSwaps.SetEquals(_editor.CurrentOption.FileSwaps);
var setsEqual = !_editor!.SwapEditor.Changes;
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.ApplySwaps();
_editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx);
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
_editor.RevertSwaps();
_editor.SwapEditor.Revert(_editor.Option!);
using var child = ImRaii.Child("##swaps", -Vector2.One, true);
if (!child)
@ -403,30 +415,26 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize);
ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthFixed, pathSize);
foreach (var (gamePath, file) in _editor!.CurrentSwaps.ToList())
foreach (var (gamePath, file) in _editor.SwapEditor.Swaps.ToList())
{
using var id = ImRaii.PushId(idx++);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true))
_editor.CurrentSwaps.Remove(gamePath);
_editor.SwapEditor.Remove(gamePath);
ImGui.TableNextColumn();
var tmp = gamePath.Path.ToString();
ImGui.SetNextItemWidth(-1);
if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength)
&& Utf8GamePath.FromString(tmp, out var path)
&& !_editor.CurrentSwaps.ContainsKey(path))
{
_editor.CurrentSwaps.Remove(gamePath);
if (path.Length > 0)
_editor.CurrentSwaps[path] = file;
}
&& !_editor.SwapEditor.Swaps.ContainsKey(path))
_editor.SwapEditor.Change(gamePath, path);
ImGui.TableNextColumn();
tmp = file.FullName;
ImGui.SetNextItemWidth(-1);
if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0)
_editor.CurrentSwaps[gamePath] = new FullPath(tmp);
_editor.SwapEditor.Change(gamePath, new FullPath(tmp));
}
ImGui.TableNextColumn();
@ -434,11 +442,11 @@ public partial class ModEditWindow : Window, IDisposable
&& newPath.Length > 0
&& _newSwapValue.Length > 0
&& _newSwapValue != _newSwapKey
&& !_editor.CurrentSwaps.ContainsKey(newPath);
&& !_editor.SwapEditor.Swaps.ContainsKey(newPath);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable,
true))
{
_editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue);
_editor.SwapEditor.Add(newPath, new FullPath(_newSwapValue));
_newSwapKey = string.Empty;
_newSwapValue = string.Empty;
}
@ -477,26 +485,21 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath(path);
}
public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog)
public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor)
: base(WindowBaseLabel)
{
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_fileDialog = fileDialog;
_swapWindow = new ItemSwapWindow(communicator);
_materialTab = new FileEditor<MtrlTab>("Materials", ".mtrl", _fileDialog,
() => _editor?.MtrlFiles ?? Array.Empty<Editor.FileRegistry>(),
DrawMaterialPanel,
() => _mod?.ModPath.FullName ?? string.Empty,
_materialTab = new FileEditor<MtrlTab>(gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlTab(this, new MtrlFile(bytes)));
_modelTab = new FileEditor<MdlFile>("Models", ".mdl", _fileDialog,
() => _editor?.MdlFiles ?? Array.Empty<Editor.FileRegistry>(),
DrawModelPanel,
() => _mod?.ModPath.FullName ?? string.Empty,
null);
_shaderPackageTab = new FileEditor<ShpkTab>("Shader Packages", ".shpk", _fileDialog,
() => _editor?.ShpkFiles ?? Array.Empty<Editor.FileRegistry>(),
DrawShaderPackagePanel,
() => _mod?.ModPath.FullName ?? string.Empty,
null);
_modelTab = new FileEditor<MdlFile>(gameData, config, _fileDialog, "Models", ".mdl",
() => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
_shaderPackageTab = new FileEditor<ShpkTab>(gameData, config, _fileDialog, "Shader Packages", ".shpk",
() => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
_center = new CombinedTexture(_left, _right);
}
@ -506,6 +509,5 @@ public partial class ModEditWindow : Window, IDisposable
_left.Dispose();
_right.Dispose();
_center.Dispose();
_swapWindow.Dispose();
}
}

View file

@ -1,267 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Reflection;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String.Classes;
namespace Penumbra.UI.Classes;
public partial class ModEditWindow
{
private class FileEditor<T> where T : class, IWritable
{
private readonly string _tabName;
private readonly string _fileType;
private readonly Func<IReadOnlyList<Mod.Editor.FileRegistry>> _getFiles;
private readonly Func<T, bool, bool> _drawEdit;
private readonly Func<string> _getInitialPath;
private readonly Func<byte[], T?> _parseFile;
private Mod.Editor.FileRegistry? _currentPath;
private T? _currentFile;
private Exception? _currentException;
private bool _changed;
private string _defaultPath = string.Empty;
private bool _inInput;
private T? _defaultFile;
private Exception? _defaultException;
private IReadOnlyList<Mod.Editor.FileRegistry> _list = null!;
private readonly FileDialogService _fileDialog;
public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func<IReadOnlyList<Mod.Editor.FileRegistry>> getFiles,
Func<T, bool, bool> drawEdit, Func<string> getInitialPath, Func<byte[], T?>? parseFile)
{
_tabName = tabName;
_fileType = fileType;
_getFiles = getFiles;
_drawEdit = drawEdit;
_getInitialPath = getInitialPath;
_fileDialog = fileDialog;
_parseFile = parseFile ?? DefaultParseFile;
}
public void Draw()
{
_list = _getFiles();
using var tab = ImRaii.TabItem(_tabName);
if (!tab)
return;
ImGui.NewLine();
DrawFileSelectCombo();
SaveButton();
ImGui.SameLine();
ResetButton();
ImGui.SameLine();
DefaultInput();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawFilePanel();
}
private void DefaultInput()
{
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale });
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight());
ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength);
_inInput = ImGui.IsItemActive();
if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0)
{
_fileDialog.Reset();
try
{
var file = DalamudServices.SGameData.GetFile(_defaultPath);
if (file != null)
{
_defaultException = null;
_defaultFile = _parseFile(file.Data);
}
else
{
_defaultFile = null;
_defaultException = new Exception("File does not exist.");
}
}
catch (Exception e)
{
_defaultFile = null;
_defaultException = e;
}
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.",
_defaultFile == null, true))
_fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType,
(success, name) =>
{
if (!success)
return;
try
{
File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid."));
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error);
}
}, _getInitialPath(), false);
_fileDialog.Draw();
}
public void Reset()
{
_currentException = null;
_currentPath = null;
_currentFile = null;
_changed = false;
}
private void DrawFileSelectCombo()
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...");
if (!combo)
return;
foreach (var file in _list)
{
if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath)))
UpdateCurrentFile(file);
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted("All Game Paths");
ImGui.Separator();
using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit);
foreach (var (option, gamePath) in file.SubModUsage)
{
ImGui.TableNextColumn();
UiHelpers.Text(gamePath.Path);
ImGui.TableNextColumn();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config));
ImGui.TextUnformatted(option.FullName);
}
}
if (file.SubModUsage.Count > 0)
{
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config));
ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString());
}
}
}
private static T? DefaultParseFile(byte[] bytes)
=> Activator.CreateInstance(typeof(T), bytes) as T;
private void UpdateCurrentFile(Mod.Editor.FileRegistry path)
{
if (ReferenceEquals(_currentPath, path))
return;
_changed = false;
_currentPath = path;
_currentException = null;
try
{
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
_currentFile = _parseFile(bytes);
}
catch (Exception e)
{
_currentFile = null;
_currentException = e;
}
}
private void SaveButton()
{
if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero,
$"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed))
{
File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
_changed = false;
}
}
private void ResetButton()
{
if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero,
$"Reset all changes made to the {_fileType} file.", !_changed))
{
var tmp = _currentPath;
_currentPath = null;
UpdateCurrentFile(tmp!);
}
}
private void DrawFilePanel()
{
using var child = ImRaii.Child("##filePanel", -Vector2.One, true);
if (!child)
return;
if (_currentPath != null)
{
if (_currentFile == null)
{
ImGui.TextUnformatted($"Could not parse selected {_fileType} file.");
if (_currentException != null)
{
using var tab = ImRaii.PushIndent();
ImGuiUtil.TextWrapped(_currentException.ToString());
}
}
else
{
using var id = ImRaii.PushId(0);
_changed |= _drawEdit(_currentFile, false);
}
}
if (!_inInput && _defaultPath.Length > 0)
{
if (_currentPath != null)
{
ImGui.NewLine();
ImGui.NewLine();
ImGui.TextUnformatted($"Preview of {_defaultPath}:");
ImGui.Separator();
}
if (_defaultFile == null)
{
ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n");
if (_defaultException != null)
{
using var tab = ImRaii.PushIndent();
ImGuiUtil.TextWrapped(_defaultException.ToString());
}
}
else
{
using var id = ImRaii.PushId(1);
_drawEdit(_defaultFile, true);
}
}
}
}
}

View file

@ -1,975 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.UI.Classes;
public partial class ModEditWindow
{
private const string ModelSetIdTooltip =
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
private const string PrimaryIdTooltip =
"Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
private const string ModelSetIdTooltipShort = "Model Set ID";
private const string EquipSlotTooltip = "Equip Slot";
private const string ModelRaceTooltip = "Model Race";
private const string GenderTooltip = "Gender";
private const string ObjectTypeTooltip = "Object Type";
private const string SecondaryIdTooltip = "Secondary ID";
private const string VariantIdTooltip = "Variant ID";
private const string EstTypeTooltip = "EST Type";
private const string RacialTribeTooltip = "Racial Tribe";
private const string ScalingTypeTooltip = "Scaling Type";
private void DrawMetaTab()
{
using var tab = ImRaii.TabItem( "Meta Manipulations" );
if( !tab )
{
return;
}
DrawOptionSelectHeader();
var setsEqual = !_editor!.Meta.Changes;
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine();
if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) )
{
_editor.ApplyManipulations();
}
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) )
{
_editor.RevertManipulations();
}
ImGui.SameLine();
AddFromClipboardButton();
ImGui.SameLine();
SetFromClipboardButton();
ImGui.SameLine();
CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() );
ImGui.SameLine();
if( ImGui.Button( "Write as TexTools Files" ) )
{
_mod!.WriteAllTexToolsMeta();
}
using var child = ImRaii.Child( "##meta", -Vector2.One, true );
if( !child )
{
return;
}
DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew );
DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew );
DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew );
DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew );
DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew );
DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew );
}
// The headers for the different meta changes all have basically the same structure for different types.
private void DrawEditHeader< T >( IReadOnlyCollection< T > items, string label, int numColumns, Action< T, Mod.Editor, Vector2 > draw,
Action< Mod.Editor, Vector2 > drawNew )
{
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
if( !ImGui.CollapsingHeader( $"{items.Count} {label}" ) )
{
return;
}
using( var table = ImRaii.Table( label, numColumns, flags ) )
{
if( table )
{
drawNew( _editor!, _iconSize );
foreach( var (item, index) in items.ToArray().WithIndex() )
{
using var id = ImRaii.PushId( index );
draw( item, _editor!, _iconSize );
}
}
}
ImGui.NewLine();
}
private static class EqpRow
{
private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize,
editor.Meta.Eqp.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var canAdd = editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId );
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry ) );
}
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
{
_new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), _new.Slot, setId );
}
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
ImGui.TableNextColumn();
if( Combos.EqpEquipSlot( "##eqpSlot", 100, _new.Slot, out var slot ) )
{
_new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId );
}
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] )
{
var value = defaultEntry.HasFlag( flag );
Checkmark( "##eqp", flag.ToLocalName(), value, value, out _ );
ImGui.SameLine();
}
ImGui.NewLine();
}
public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.SetId.ToString() );
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Slot.ToName() );
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
// Values
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
var idx = 0;
foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] )
{
using var id = ImRaii.PushId( idx++ );
var defaultValue = defaultEntry.HasFlag( flag );
var currentValue = meta.Entry.HasFlag( flag );
if( Checkmark( "##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value ) )
{
editor.Meta.Change( meta.Copy( value ? meta.Entry | flag : meta.Entry & ~flag ) );
}
ImGui.SameLine();
}
ImGui.NewLine();
}
}
private static class EqdpRow
{
private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize,
editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var raceCode = Names.CombinedRace( _new.Gender, _new.Race );
var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0;
var canAdd = validRaceCode && editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." :
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
var defaultEntry = validRaceCode
? ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId )
: 0;
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry ) );
}
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), setId );
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId );
}
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
ImGui.TableNextColumn();
if( Combos.Race( "##eqdpRace", _new.Race, out var race ) )
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId );
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId );
}
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
ImGui.TableNextColumn();
if( Combos.Gender( "##eqdpGender", _new.Gender, out var gender ) )
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId );
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId );
}
ImGuiUtil.HoverTooltip( GenderTooltip );
ImGui.TableNextColumn();
if( Combos.EqdpEquipSlot( "##eqdpSlot", _new.Slot, out var slot ) )
{
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId );
_new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId );
}
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
var (bit1, bit2) = defaultEntry.ToBits( _new.Slot );
Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ );
ImGui.SameLine();
Checkmark( "Model##eqdpCheck2", string.Empty, bit2, bit2, out _ );
}
public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.SetId.ToString() );
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Race.ToName() );
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Gender.ToName() );
ImGuiUtil.HoverTooltip( GenderTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Slot.ToName() );
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
// Values
var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId );
var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot );
var (bit1, bit2) = meta.Entry.ToBits( meta.Slot );
ImGui.TableNextColumn();
if( Checkmark( "Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) )
{
editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) ) );
}
ImGui.SameLine();
if( Checkmark( "Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) )
{
editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) ) );
}
}
}
private static class ImcRow
{
private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry());
private static float IdWidth
=> 80 * UiHelpers.Scale;
private static float SmallIdWidth
=> 45 * UiHelpers.Scale;
// Convert throwing to null-return if the file does not exist.
private static ImcEntry? GetDefault( ImcManipulation imc )
{
try
{
return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant, out _ );
}
catch
{
return null;
}
}
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize,
editor.Meta.Imc.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var defaultEntry = GetDefault( _new );
var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited.";
defaultEntry ??= new ImcEntry();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry.Value ) );
}
// Identifier
ImGui.TableNextColumn();
if( Combos.ImcType( "##imcType", _new.ObjectType, out var type ) )
{
var equipSlot = type switch
{
ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears,
_ => EquipSlot.Unknown,
};
_new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, _new.Variant, equipSlot, _new.Entry );
}
ImGuiUtil.HoverTooltip( ObjectTypeTooltip );
ImGui.TableNextColumn();
if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1 ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGuiUtil.HoverTooltip( PrimaryIdTooltip );
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
ImGui.TableNextColumn();
// Equipment and accessories are slightly different imcs than other types.
if( _new.ObjectType is ObjectType.Equipment )
{
if( Combos.EqpEquipSlot( "##imcSlot", 100, _new.EquipSlot, out var slot ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
}
else if( _new.ObjectType is ObjectType.Accessory )
{
if( Combos.AccessorySlot( "##imcSlot", _new.EquipSlot, out var slot ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
}
else
{
if( IdInput( "##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGuiUtil.HoverTooltip( SecondaryIdTooltip );
}
ImGui.TableNextColumn();
if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGui.TableNextColumn();
if( _new.ObjectType is ObjectType.DemiHuman )
{
if( Combos.EqpEquipSlot( "##imcSlot", 70, _new.EquipSlot, out var slot ) )
{
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
?? new ImcEntry() );
}
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
}
else
{
ImGui.Dummy( new Vector2( 70 * UiHelpers.Scale, 0 ) );
}
ImGuiUtil.HoverTooltip( VariantIdTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _,
1, byte.MaxValue, 0f );
ImGui.SameLine();
IntDragInput( "##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId,
defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f );
ImGui.TableNextColumn();
IntDragInput( "##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0,
byte.MaxValue, 0f );
ImGui.SameLine();
IntDragInput( "##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue,
0f );
ImGui.SameLine();
IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111,
0f );
ImGui.TableNextColumn();
for( var i = 0; i < 10; ++i )
{
using var id = ImRaii.PushId( i );
var flag = 1 << i;
Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( defaultEntry.Value.AttributeMask & flag ) != 0,
( defaultEntry.Value.AttributeMask & flag ) != 0, out _ );
ImGui.SameLine();
}
ImGui.NewLine();
}
public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.ObjectType.ToName() );
ImGuiUtil.HoverTooltip( ObjectTypeTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.PrimaryId.ToString() );
ImGuiUtil.HoverTooltip( "Primary ID" );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory )
{
ImGui.TextUnformatted( meta.EquipSlot.ToName() );
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
}
else
{
ImGui.TextUnformatted( meta.SecondaryId.ToString() );
ImGuiUtil.HoverTooltip( SecondaryIdTooltip );
}
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Variant.ToString() );
ImGuiUtil.HoverTooltip( VariantIdTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
if( meta.ObjectType is ObjectType.DemiHuman )
{
ImGui.TextUnformatted( meta.EquipSlot.ToName() );
}
// Values
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
ImGui.TableNextColumn();
var defaultEntry = GetDefault( meta ) ?? new ImcEntry();
if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId,
defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { MaterialId = ( byte )materialId } ) );
}
ImGui.SameLine();
if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth,
meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } ) );
}
ImGui.TableNextColumn();
if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId,
defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { DecalId = ( byte )decalId } ) );
}
ImGui.SameLine();
if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId,
out var vfxId, 0, byte.MaxValue, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { VfxId = ( byte )vfxId } ) );
}
ImGui.SameLine();
if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId,
defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { SoundId = ( byte )soundId } ) );
}
ImGui.TableNextColumn();
for( var i = 0; i < 10; ++i )
{
using var id = ImRaii.PushId( i );
var flag = 1 << i;
if( Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( meta.Entry.AttributeMask & flag ) != 0,
( defaultEntry.AttributeMask & flag ) != 0, out var val ) )
{
var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag;
editor.Meta.Change( meta.Copy( meta.Entry with { AttributeMask = ( ushort )attributes } ) );
}
ImGui.SameLine();
}
ImGui.NewLine();
}
}
private static class EstRow
{
private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0);
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize,
editor.Meta.Est.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var canAdd = editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId );
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry ) );
}
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
{
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), setId );
_new = new EstManipulation( _new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry );
}
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
ImGui.TableNextColumn();
if( Combos.Race( "##estRace", _new.Race, out var race ) )
{
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId );
_new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry );
}
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
ImGui.TableNextColumn();
if( Combos.Gender( "##estGender", _new.Gender, out var gender ) )
{
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId );
_new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry );
}
ImGuiUtil.HoverTooltip( GenderTooltip );
ImGui.TableNextColumn();
if( Combos.EstSlot( "##estSlot", _new.Slot, out var slot ) )
{
var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId );
_new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry );
}
ImGuiUtil.HoverTooltip( EstTypeTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f );
}
public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.SetId.ToString() );
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Race.ToName() );
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Gender.ToName() );
ImGuiUtil.HoverTooltip( GenderTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Slot.ToString() );
ImGuiUtil.HoverTooltip( EstTypeTooltip );
// Values
var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId );
ImGui.TableNextColumn();
if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
out var entry, 0, ushort.MaxValue, 0.05f ) )
{
editor.Meta.Change( meta.Copy( ( ushort )entry ) );
}
}
}
private static class GmpRow
{
private static GmpManipulation _new = new(GmpEntry.Default, 1);
private static float RotationWidth
=> 75 * UiHelpers.Scale;
private static float UnkWidth
=> 50 * UiHelpers.Scale;
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize,
editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var canAdd = editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId );
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry ) );
}
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
{
_new = new GmpManipulation( ExpandedGmpFile.GetDefault( setId ), setId );
}
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ );
ImGui.TableNextColumn();
Checkmark( "##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _ );
ImGui.TableNextColumn();
IntDragInput( "##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0,
360, 0f );
ImGui.SameLine();
IntDragInput( "##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0,
360, 0f );
ImGui.SameLine();
IntDragInput( "##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0,
360, 0f );
ImGui.TableNextColumn();
IntDragInput( "##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f );
ImGui.SameLine();
IntDragInput( "##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f );
}
public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.SetId.ToString() );
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
// Values
var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId );
ImGui.TableNextColumn();
if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { Enabled = enabled } ) );
}
ImGui.TableNextColumn();
if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { Animated = animated } ) );
}
ImGui.TableNextColumn();
if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth,
meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { RotationA = ( ushort )rotationA } ) );
}
ImGui.SameLine();
if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth,
meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { RotationB = ( ushort )rotationB } ) );
}
ImGui.SameLine();
if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth,
meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { RotationC = ( ushort )rotationC } ) );
}
ImGui.TableNextColumn();
if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA,
defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkA } ) );
}
ImGui.SameLine();
if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB,
defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) )
{
editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkB } ) );
}
}
}
private static class RspRow
{
private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f);
private static float FloatWidth
=> 150 * UiHelpers.Scale;
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize,
editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) );
ImGui.TableNextColumn();
var canAdd = editor.Meta.CanAdd( _new );
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute );
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
{
editor.Meta.Add( _new.Copy( defaultEntry ) );
}
// Identifier
ImGui.TableNextColumn();
if( Combos.SubRace( "##rspSubRace", _new.SubRace, out var subRace ) )
{
_new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) );
}
ImGuiUtil.HoverTooltip( RacialTribeTooltip );
ImGui.TableNextColumn();
if( Combos.RspAttribute( "##rspAttribute", _new.Attribute, out var attribute ) )
{
_new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) );
}
ImGuiUtil.HoverTooltip( ScalingTypeTooltip );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( FloatWidth );
ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f );
}
public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
DrawMetaButtons( meta, editor, iconSize );
// Identifier
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.SubRace.ToName() );
ImGuiUtil.HoverTooltip( RacialTribeTooltip );
ImGui.TableNextColumn();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
ImGui.TextUnformatted( meta.Attribute.ToFullString() );
ImGuiUtil.HoverTooltip( ScalingTypeTooltip );
ImGui.TableNextColumn();
// Values
var def = CmpFile.GetDefault( meta.SubRace, meta.Attribute );
var value = meta.Entry;
ImGui.SetNextItemWidth( FloatWidth );
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config),
def != value );
if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f )
{
editor.Meta.Change( meta.Copy( value ) );
}
ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" );
}
}
// A number input for ids with a optional max id of given width.
// Returns true if newId changed against currentId.
private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border )
{
int tmp = currentId;
ImGui.SetNextItemWidth( width );
using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border );
using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border );
if( ImGui.InputInt( label, ref tmp, 0 ) )
{
tmp = Math.Clamp( tmp, minId, maxId );
}
newId = ( ushort )tmp;
return newId != currentId;
}
// A checkmark that compares against a default value and shows a tooltip.
// Returns true if newValue is changed against currentValue.
private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue )
{
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue );
newValue = currentValue;
ImGui.Checkbox( label, ref newValue );
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
return newValue != currentValue;
}
// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
// Returns true if newValue changed against currentValue.
private static bool IntDragInput( string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue,
int minValue, int maxValue, float speed )
{
newValue = currentValue;
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config),
defaultValue != currentValue );
ImGui.SetNextItemWidth( width );
if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) )
{
newValue = Math.Clamp( newValue, minValue, maxValue );
}
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
return newValue != currentValue;
}
private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations )
{
if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) )
{
return;
}
var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion );
if( text.Length > 0 )
{
ImGui.SetClipboardText( text );
}
}
private void AddFromClipboardButton()
{
if( ImGui.Button( "Add from Clipboard" ) )
{
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
if( version == MetaManipulation.CurrentVersion && manips != null )
{
foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
_editor!.Meta.Set( manip );
}
}
}
ImGuiUtil.HoverTooltip(
"Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." );
}
private void SetFromClipboardButton()
{
if( ImGui.Button( "Set from Clipboard" ) )
{
var clipboard = ImGuiUtil.GetClipboardText();
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
if( version == MetaManipulation.CurrentVersion && manips != null )
{
_editor!.Meta.Clear();
foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
_editor!.Meta.Set( manip );
}
}
}
ImGuiUtil.HoverTooltip(
"Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." );
}
private static void DrawMetaButtons( MetaManipulation meta, Mod.Editor editor, Vector2 iconSize )
{
ImGui.TableNextColumn();
CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty< MetaManipulation >().Append( meta ) );
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true ) )
{
editor.Meta.Delete( meta );
}
}
}

View file

@ -20,7 +20,7 @@ using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.Util;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>
{
@ -31,13 +31,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
private readonly Mod.Manager _modManager;
private readonly ModCollection.Manager _collectionManager;
private readonly TutorialService _tutorial;
private readonly ModEditor _modEditor;
private TexToolsImporter? _import;
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager,
ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat)
ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat, ModEditor modEditor)
: base(fileSystem, DalamudServices.KeyState)
{
_communicator = communicator;
@ -47,6 +48,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
_tutorial = tutorial;
_fileDialog = fileDialog;
_chat = chat;
_modEditor = modEditor;
SubscribeRightClickFolder(EnableDescendants, 10);
SubscribeRightClickFolder(DisableDescendants, 10);
@ -228,7 +230,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return;
_import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)),
AddNewMod);
AddNewMod, _config, _modEditor);
ImGui.OpenPopup("Import Status");
}, 0, modPath, _config.AlwaysOpenDefaultImport);
}

View file

@ -1,6 +1,6 @@
using System;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
[Flags]
public enum ModFilter

View file

@ -1,9 +1,9 @@
using System;
using Dalamud.Plugin;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanel : IDisposable
{

View file

@ -8,7 +8,7 @@ using OtterGui.Widgets;
using Penumbra.Api;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelChangedItemsTab : ITab
{

View file

@ -9,7 +9,7 @@ using Penumbra.Mods;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelConflictsTab : ITab
{

View file

@ -7,7 +7,7 @@ using OtterGui.Widgets;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelDescriptionTab : ITab
{

View file

@ -12,10 +12,10 @@ using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow;
using Penumbra.Util;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab : ITab
{
@ -24,6 +24,7 @@ public class ModPanelEditTab : ITab
private readonly ModFileSystem _fileSystem;
private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow;
private readonly ModEditor _editor;
private readonly TagButtons _modTags = new();
@ -33,13 +34,14 @@ public class ModPanelEditTab : ITab
private Mod _mod = null!;
public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
ModEditWindow editWindow)
ModEditWindow editWindow, ModEditor editor)
{
_modManager = modManager;
_selector = selector;
_fileSystem = fileSystem;
_chat = chat;
_editWindow = editWindow;
_editor = editor;
}
public ReadOnlySpan<byte> Label
@ -126,10 +128,10 @@ public class ModPanelEditTab : ITab
{
if (ImGui.Button("Update Bibo Material", buttonSize))
{
var editor = new Mod.Editor(_mod, null);
editor.ReplaceAllMaterials("bibo", "b");
editor.ReplaceAllMaterials("bibopube", "c");
editor.SaveAllModels();
_editor.LoadMod(_mod);
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b");
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c");
_editor.MdlMaterialEditor.SaveAllModels();
_editWindow.UpdateModels();
}
@ -142,7 +144,7 @@ public class ModPanelEditTab : ITab
private void BackupButtons(Vector2 buttonSize)
{
var backup = new ModBackup(_mod);
var backup = new ModBackup(_modManager, _mod);
var tt = ModBackup.CreatingBackup
? "Already exporting a mod."
: backup.Exists

View file

@ -9,7 +9,7 @@ using OtterGui.Raii;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelHeader : IDisposable
{

View file

@ -13,7 +13,7 @@ using Penumbra.UI.Classes;
using Dalamud.Interface.Components;
using Dalamud.Interface;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelSettingsTab : ITab
{

View file

@ -6,9 +6,9 @@ using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.ModTab;
namespace Penumbra.UI.ModsTab;
public class ModPanelTabBar
{
@ -107,7 +107,7 @@ public class ModPanelTabBar
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
{
_modEditWindow.ChangeMod(mod);
_modEditWindow.ChangeOption(mod.Default);
_modEditWindow.ChangeOption((Mod.SubMod) mod.Default);
_modEditWindow.IsOpen = true;
}

View file

@ -12,8 +12,8 @@ using Penumbra.Api.Enums;
using Penumbra.Interop;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI.ModTab;
using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector;
using Penumbra.UI.ModsTab;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
namespace Penumbra.UI.Tabs;

View file

@ -14,7 +14,7 @@ using Penumbra.Interop.Services;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI.Classes;
using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
namespace Penumbra.UI.Tabs;

View file

@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra;