diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index cdcdcd1d..89672cf1 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -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 ) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs new file mode 100644 index 00000000..44197193 --- /dev/null +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -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 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 files) + { + _duplicates.Clear(); + SavedSpace = 0; + var list = new List(); + 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 list, long size) + { + var hashes = list.Select(f => (f, ComputeHash(f))).ToList(); + while (hashes.Count > 0) + { + if (Finished) + return; + + var set = new HashSet { 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(buffer1, 256); + var span2 = new Span(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 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}"); + } + } +} diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs new file mode 100644 index 00000000..2ce22ec1 --- /dev/null +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -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 +{ + 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(); +} diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs new file mode 100644 index 00000000..dc32869f --- /dev/null +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -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 _modelFiles = new(); + + public IReadOnlyList 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(); + } + + /// + /// 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); + } + } + } + + /// Non-ASCII encoding is not supported. + public static bool ValidString(string to) + => to.Length != 0 + && to.Length < 16 + && Encoding.UTF8.GetByteCount(to) == to.Length; + + /// Find all model files in the mod that contain skin materials. + 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}"); + } + } + } +} diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs deleted file mode 100644 index d60356ac..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ /dev/null @@ -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}" ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs deleted file mode 100644 index c2221ecc..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ /dev/null @@ -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 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs deleted file mode 100644 index ea149216..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ /dev/null @@ -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(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs deleted file mode 100644 index 45c6707c..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ /dev/null @@ -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(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs b/Penumbra/Mods/Editor/Mod.Editor.Meta.cs deleted file mode 100644 index 0eec38ea..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.Meta.cs +++ /dev/null @@ -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 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs deleted file mode 100644 index 3120bcf3..00000000 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ /dev/null @@ -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 ); - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs deleted file mode 100644 index 0b487a8f..00000000 --- a/Penumbra/Mods/Editor/Mod.Normalization.cs +++ /dev/null @@ -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 ] ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index d554ee0e..fe533489 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -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 ); } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs new file mode 100644 index 00000000..c08b7bff --- /dev/null +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -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(); + } + + /// Load the correct option by indices for the currently loaded mod if possible, unload if not. + 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(); + + /// Apply a option action to all available option in a mod, including the default option. + public static void ApplyToAllOptions(Mod mod, Action 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(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs new file mode 100644 index 00000000..72eb742b --- /dev/null +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -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 _available = new(); + private readonly List _mtrl = new(); + private readonly List _mdl = new(); + private readonly List _tex = new(); + private readonly List _shpk = new(); + + private readonly SortedSet _missing = new(); + private readonly HashSet _usedPaths = new(); + + public IReadOnlySet Missing + => Ready ? _missing : new HashSet(); + + public IReadOnlySet UsedPaths + => Ready ? _usedPaths : new HashSet(); + + public IReadOnlyList Available + => Ready ? _available : Array.Empty(); + + public IReadOnlyList Mtrl + => Ready ? _mtrl : Array.Empty(); + + public IReadOnlyList Mdl + => Ready ? _mdl : Array.Empty(); + + public IReadOnlyList Tex + => Ready ? _tex : Array.Empty(); + + public IReadOnlyList Shpk + => Ready ? _shpk : Array.Empty(); + + 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)); + } + } + } + } +} diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs new file mode 100644 index 00000000..031c7485 --- /dev/null +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -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(); + 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; + } + + /// Remove all path redirections where the pointed-to file does not exist. + 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(); + } + + /// Return whether the given path is already used in the current option. + public bool CanAddGamePath(Utf8GamePath path) + => !_files.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(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; + } + + /// + /// 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(ISubMod option, IEnumerable 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; + } + + /// Remove all paths in the current option from the given files. + public void RemovePathsFromSelected(ISubMod option, IEnumerable files) + { + foreach (var file in files) + { + foreach (var (_, path) in file.SubModUsage.Where(p => p.Item1 == option)) + { + _files.RemoveUsedPath(option, file, path); + Changes = true; + } + } + } + + /// Delete all given files from your filesystem + public void DeleteFiles(Mod mod, ISubMod option, IEnumerable 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; + } +} diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs new file mode 100644 index 00000000..a211398b --- /dev/null +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -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 _imc = new(); + private readonly HashSet _eqp = new(); + private readonly HashSet _eqdp = new(); + private readonly HashSet _gmp = new(); + private readonly HashSet _est = new(); + private readonly HashSet _rsp = new(); + + public ModMetaEditor(Mod.Manager modManager) + => _modManager = modManager; + + public bool Changes { get; private set; } = false; + + public IReadOnlySet Imc + => _imc; + + public IReadOnlySet Eqp + => _eqp; + + public IReadOnlySet Eqdp + => _eqdp; + + public IReadOnlySet Gmp + => _gmp; + + public IReadOnlySet Est + => _est; + + public IReadOnlySet 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 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 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)); +} diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs new file mode 100644 index 00000000..9fc02d77 --- /dev/null +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -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>> _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>()); + + if (_redirections[0].Count == 0) + _redirections[0].Add(new Dictionary(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()); + + var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); + foreach (var option in group.OfType()) + { + 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()) + { + _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); + } + + ++Step; + } +} diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs new file mode 100644 index 00000000..0237d08f --- /dev/null +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -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 _swaps = new(); + + public IReadOnlyDictionary 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; + } +} diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs new file mode 100644 index 00000000..dc01ae7d --- /dev/null +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -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; + +/// 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 _materialIndices; + public bool Changed { get; private set; } + + public IReadOnlyList CurrentMaterials + => _currentMaterials; + + private IEnumerable 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 indices) + { + Path = path; + File = file; + _materialIndices = indices; + _currentMaterials = DefaultMaterials.ToArray(); + } +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 0c9c69cc..4b93bd24 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -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; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 83927a1e..bd7fb0fd 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -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 ); } } diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 3f5db087..8423d023 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -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(); diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index a03377ec..5ba44286 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -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() { diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 981b336b..c089cfda 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -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"; diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 9e196424..3d52c6be 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -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() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); + + // Add Mod Editor + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add API services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs new file mode 100644 index 00000000..47d53832 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -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 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> getFiles, Func drawEdit, Func getInitialPath, + Func? 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> _getFiles; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _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 _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); + } + } + } +} diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs similarity index 67% rename from Penumbra/UI/Classes/ItemSwapWindow.cs rename to Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index ce3b2dd3..f6def7d4 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -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 + { + // @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; + } + + /// Update the currently selected mod or its settings. + 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 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 _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 _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 EquipSlot _slotFrom = EquipSlot.Head; - private EquipSlot _slotTo = EquipSlot.Ears; + 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 bool _useFileSwaps = true; - private bool _useCurrentCollection = false; - private bool _useLeftRing = true; - private bool _useRightRing = true; + private string _newModName = string.Empty; + private string _newGroupName = "Swaps"; + private string _newOptionName = string.Empty; + private IModGroup? _selectedGroup; + private bool _subModValid; + private bool _useFileSwaps = true; + 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 (combo) + foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head)) { - if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom ) - { - _dirty = true; - _slotFrom = slot; - if( slot == _slotTo ) - { - _slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s ); - } - } + 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); } - } } 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 (combo) + foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom)) { - if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo ) - { - _dirty = true; - _slotTo = slot; - } + 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 } ) - { - 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 ) ) ); - } - } + + _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))); } - 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,15 +540,15 @@ public class ItemSwapWindow : IDisposable _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if (_affectedItems is { Length: > 1 }) - { - 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, targetSelector.CurrentSelection.Item2)) - .Select(i => i.Name.ToDalamudString().TextValue))); - } + 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, 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) - { - _swapData.LoadMod(_mod, _modSettings); - _dirty = true; - } + if (modIdx != _mod?.Index) + return; + + _swapData.LoadMod(_mod, _modSettings); + _dirty = true; } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs similarity index 80% rename from Penumbra/UI/Classes/ModEditWindow.Files.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 469b10a8..3d0df39d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -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 _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 LowerString _fileOverviewFilter1 = LowerString.Empty; - private LowerString _fileOverviewFilter2 = LowerString.Empty; - private LowerString _fileOverviewFilter3 = LowerString.Empty; + private readonly HashSet _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; + private bool _overviewMode; - private bool CheckFilter(Mod.Editor.FileRegistry registry) + private LowerString _fileOverviewFilter1 = LowerString.Empty; + private LowerString _fileOverviewFilter2 = LowerString.Empty; + private LowerString _fileOverviewFilter3 = LowerString.Empty; + + 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() diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index d259e300..5a338b9c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 7c121ca6..a0028a6e 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 92e4db6d..16ad708c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs similarity index 96% rename from Penumbra/UI/Classes/ModEditWindow.Materials.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 7a170803..306293af 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -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(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs new file mode 100644 index 00000000..31dce033 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -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(IReadOnlyCollection items, string label, int numColumns, Action draw, + Action 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 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(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(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().Append(meta)); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) + editor.MetaEditor.Delete(meta); + } +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs similarity index 98% rename from Penumbra/UI/Classes/ModEditWindow.Models.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 73f2a7dc..b212e791 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index bce1c61f..b6af9dd9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs similarity index 99% rename from Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 0bcac7a3..1720ec8c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -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 { diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs similarity index 98% rename from Penumbra/UI/Classes/ModEditWindow.Textures.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 05a878e9..2ddd3ded 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -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); diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs similarity index 76% rename from Penumbra/UI/Classes/ModEditWindow.cs rename to Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d6ad92db..fb9690c2 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -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,31 +13,32 @@ 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 const string WindowBaseLabel = "###SubModEdit"; + + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; - private Editor? _editor; 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); - _mod = mod; + _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()) { - 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,13 +442,13 @@ 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); - _newSwapKey = string.Empty; - _newSwapValue = string.Empty; + _editor.SwapEditor.Add(newPath, new FullPath(_newSwapValue)); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; } ImGui.TableNextColumn(); @@ -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) { - _fileDialog = fileDialog; - _swapWindow = new ItemSwapWindow(communicator); - _materialTab = new FileEditor("Materials", ".mtrl", _fileDialog, - () => _editor?.MtrlFiles ?? Array.Empty(), - DrawMaterialPanel, - () => _mod?.ModPath.FullName ?? string.Empty, + _itemSwapTab = itemSwapTab; + _config = config; + _editor = editor; + _fileDialog = fileDialog; + _materialTab = new FileEditor(gameData, config, _fileDialog, "Materials", ".mtrl", + () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor("Models", ".mdl", _fileDialog, - () => _editor?.MdlFiles ?? Array.Empty(), - DrawModelPanel, - () => _mod?.ModPath.FullName ?? string.Empty, - null); - _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", _fileDialog, - () => _editor?.ShpkFiles ?? Array.Empty(), - DrawShaderPackagePanel, - () => _mod?.ModPath.FullName ?? string.Empty, - null); + _modelTab = new FileEditor(gameData, config, _fileDialog, "Models", ".mdl", + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); + _shaderPackageTab = new FileEditor(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(); } } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs deleted file mode 100644 index 1d722927..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ /dev/null @@ -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 where T : class, IWritable - { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func> _getFiles; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _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 _list = null!; - - private readonly FileDialogService _fileDialog; - - public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func> getFiles, - Func drawEdit, Func getInitialPath, Func? 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); - } - } - } - } -} diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs deleted file mode 100644 index e1f348a5..00000000 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ /dev/null @@ -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 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c6384fd0..493c21e8 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -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 { @@ -31,13 +31,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector new FileInfo(file)), - AddNewMod); + AddNewMod, _config, _modEditor); ImGui.OpenPopup("Import Status"); }, 0, modPath, _config.AlwaysOpenDefaultImport); } diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs index 4c221b21..03fdc177 100644 --- a/Penumbra/UI/ModsTab/ModFilter.cs +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -1,6 +1,6 @@ using System; -namespace Penumbra.UI.ModTab; +namespace Penumbra.UI.ModsTab; [Flags] public enum ModFilter diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index e6999412..d85f77ff 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,16 +1,16 @@ 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 { - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) { diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 49f5d8cf..b38dd725 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -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 { diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index bcdb0f16..adce69e3 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -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 { diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 3cc770a2..d6d04872 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -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 { diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 89bdff92..8dbc94cb 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -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; + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + _editor = editor; } public ReadOnlySpan 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 diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 6c0a7efa..bb24a337 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -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 { diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1f528904..0127e140 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -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 { diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index a946a916..39f795d0 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -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; } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 6490dc2a..fc15bc77 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -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; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 60f781c5..0e1e4ce5 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -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; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 08143a57..378aebf0 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow; namespace Penumbra;