Why is this so much work?

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,288 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
private readonly SHA256 _hasher = SHA256.Create();
private readonly List< (FullPath[] Paths, long Size, byte[] Hash) > _duplicates = new();
public IReadOnlyList< (FullPath[] Paths, long Size, byte[] Hash) > Duplicates
=> _duplicates;
public long SavedSpace { get; private set; } = 0;
public bool DuplicatesFinished { get; private set; } = true;
public void DeleteDuplicates( bool useModManager = true )
{
if( !DuplicatesFinished || _duplicates.Count == 0 )
{
return;
}
foreach( var (set, _, _) in _duplicates )
{
if( set.Length < 2 )
{
continue;
}
var remaining = set[ 0 ];
foreach( var duplicate in set.Skip( 1 ) )
{
HandleDuplicate( duplicate, remaining, useModManager );
}
}
_duplicates.Clear();
DeleteEmptyDirectories( _mod.ModPath );
UpdateFiles();
}
private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager )
{
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
{
var changes = false;
var dict = subMod.Files.ToDictionary( kvp => kvp.Key,
kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) );
if( changes )
{
if( useModManager )
{
Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict );
}
else
{
var sub = ( SubMod )subMod;
sub.FileData = dict;
if( groupIdx == -1 )
{
_mod.SaveDefaultMod();
}
else
{
IModGroup.Save( _mod.Groups[ groupIdx ], _mod.ModPath, groupIdx );
}
}
}
}
ApplyToAllOptions( _mod, HandleSubMod );
try
{
File.Delete( duplicate.FullName );
}
catch( Exception e )
{
Penumbra.Log.Error( $"[DeleteDuplicates] Could not delete duplicate {duplicate.FullName} of {remaining.FullName}:\n{e}" );
}
}
private FullPath ChangeDuplicatePath( FullPath value, FullPath from, FullPath to, Utf8GamePath key, ref bool changes )
{
if( !value.Equals( from ) )
{
return value;
}
changes = true;
Penumbra.Log.Debug( $"[DeleteDuplicates] Changing {key} for {_mod.Name}\n : {from}\n -> {to}" );
return to;
}
public void StartDuplicateCheck()
{
if( DuplicatesFinished )
{
DuplicatesFinished = false;
UpdateFiles();
var files = _availableFiles.OrderByDescending( f => f.FileSize ).ToArray();
Task.Run( () => CheckDuplicates( files ) );
}
}
private void CheckDuplicates( IReadOnlyList< FileRegistry > files )
{
_duplicates.Clear();
SavedSpace = 0;
var list = new List< FullPath >();
var lastSize = -1L;
foreach( var file in files )
{
// Skip any UI Files because deduplication causes weird crashes for those.
if( file.SubModUsage.Any( f => f.Item2.Path.StartsWith( "ui/"u8 ) ) )
{
continue;
}
if( DuplicatesFinished )
{
return;
}
if( file.FileSize == lastSize )
{
list.Add( file.File );
continue;
}
if( list.Count >= 2 )
{
CheckMultiDuplicates( list, lastSize );
}
lastSize = file.FileSize;
list.Clear();
list.Add( file.File );
}
if( list.Count >= 2 )
{
CheckMultiDuplicates( list, lastSize );
}
_duplicates.Sort( ( a, b ) => a.Size != b.Size ? b.Size.CompareTo( a.Size ) : a.Paths[ 0 ].CompareTo( b.Paths[ 0 ] ) );
DuplicatesFinished = true;
}
private void CheckMultiDuplicates( IReadOnlyList< FullPath > list, long size )
{
var hashes = list.Select( f => ( f, ComputeHash( f ) ) ).ToList();
while( hashes.Count > 0 )
{
if( DuplicatesFinished )
{
return;
}
var set = new HashSet< FullPath > { hashes[ 0 ].Item1 };
var hash = hashes[ 0 ];
for( var j = 1; j < hashes.Count; ++j )
{
if( DuplicatesFinished )
{
return;
}
if( CompareHashes( hash.Item2, hashes[ j ].Item2 ) && CompareFilesDirectly( hashes[ 0 ].Item1, hashes[ j ].Item1 ) )
{
set.Add( hashes[ j ].Item1 );
}
}
hashes.RemoveAll( p => set.Contains( p.Item1 ) );
if( set.Count > 1 )
{
_duplicates.Add( ( set.OrderBy( f => f.FullName.Length ).ToArray(), size, hash.Item2 ) );
SavedSpace += ( set.Count - 1 ) * size;
}
}
}
private static unsafe bool CompareFilesDirectly( FullPath f1, FullPath f2 )
{
if( !f1.Exists || !f2.Exists )
{
return false;
}
using var s1 = File.OpenRead( f1.FullName );
using var s2 = File.OpenRead( f2.FullName );
var buffer1 = stackalloc byte[256];
var buffer2 = stackalloc byte[256];
var span1 = new Span< byte >( buffer1, 256 );
var span2 = new Span< byte >( buffer2, 256 );
while( true )
{
var bytes1 = s1.Read( span1 );
var bytes2 = s2.Read( span2 );
if( bytes1 != bytes2 )
{
return false;
}
if( !span1[ ..bytes1 ].SequenceEqual( span2[ ..bytes2 ] ) )
{
return false;
}
if( bytes1 < 256 )
{
return true;
}
}
}
public static bool CompareHashes( byte[] f1, byte[] f2 )
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
public byte[] ComputeHash( FullPath f )
{
using var stream = File.OpenRead( f.FullName );
return _hasher.ComputeHash( stream );
}
// Recursively delete all empty directories starting from the given directory.
// Deletes inner directories first, so that a tree of empty directories is actually deleted.
private static void DeleteEmptyDirectories( DirectoryInfo baseDir )
{
try
{
if( !baseDir.Exists )
{
return;
}
foreach( var dir in baseDir.EnumerateDirectories( "*", SearchOption.TopDirectoryOnly ) )
{
DeleteEmptyDirectories( dir );
}
baseDir.Refresh();
if( !baseDir.EnumerateFileSystemInfos().Any() )
{
Directory.Delete( baseDir.FullName, false );
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" );
}
}
// Deduplicate a mod simply by its directory without any confirmation or waiting time.
internal static void DeduplicateMod( DirectoryInfo modDirectory )
{
try
{
var mod = new Mod( modDirectory );
mod.Reload( true, out _ );
var editor = new Editor( mod, mod.Default );
editor.DuplicatesFinished = false;
editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() );
editor.DeleteDuplicates( false );
}
catch( Exception e )
{
Penumbra.Log.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" );
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,56 +0,0 @@
using System;
using System.IO;
using OtterGui;
namespace Penumbra.Mods;
public partial class Mod : IMod
{
public partial class Editor : IDisposable
{
private readonly Mod _mod;
public Editor( Mod mod, ISubMod? option )
{
_mod = mod;
_subMod = null!;
SetSubMod( option );
UpdateFiles();
ScanModels();
}
public void Cancel()
{
DuplicatesFinished = true;
}
public void Dispose()
=> Cancel();
// Does not delete the base directory itself even if it is completely empty at the end.
private static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{
foreach( var subDir in baseDir.GetDirectories() )
{
ClearEmptySubDirectories( subDir );
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
{
subDir.Delete();
}
}
}
// Apply a option action to all available option in a mod, including the default option.
private static void ApplyToAllOptions( Mod mod, Action< ISubMod, int, int > action )
{
action( mod.Default, -1, 0 );
foreach( var (group, groupIdx) in mod.Groups.WithIndex() )
{
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{
action( group[ optionIdx ], groupIdx, optionIdx );
}
}
}
}
}

View file

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

View file

@ -14,10 +14,10 @@ public class ModBackup
public readonly string Name; public readonly string Name;
public readonly bool Exists; public readonly bool Exists;
public ModBackup( Mod mod ) public ModBackup( Mod.Manager modManager, Mod mod )
{ {
_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 ); Exists = File.Exists( Name );
} }

View file

@ -0,0 +1,128 @@
using System;
using System.IO;
using OtterGui;
namespace Penumbra.Mods;
public class ModEditor : IDisposable
{
public readonly ModNormalizer ModNormalizer;
public readonly ModMetaEditor MetaEditor;
public readonly ModFileEditor FileEditor;
public readonly DuplicateManager Duplicates;
public readonly ModFileCollection Files;
public readonly ModSwapEditor SwapEditor;
public readonly MdlMaterialEditor MdlMaterialEditor;
public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; }
public int OptionIdx { get; private set; }
public IModGroup? Group { get; private set; }
public ISubMod? Option { get; private set; }
public ModEditor(ModNormalizer modNormalizer, ModMetaEditor metaEditor, ModFileCollection files,
ModFileEditor fileEditor, DuplicateManager duplicates, ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor)
{
ModNormalizer = modNormalizer;
MetaEditor = metaEditor;
Files = files;
FileEditor = fileEditor;
Duplicates = duplicates;
SwapEditor = swapEditor;
MdlMaterialEditor = mdlMaterialEditor;
}
public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0);
public void LoadMod(Mod mod, int groupIdx, int optionIdx)
{
Mod = mod;
LoadOption(groupIdx, optionIdx, true);
Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!);
MetaEditor.Load(Option!);
Duplicates.Clear();
}
public void LoadOption(int groupIdx, int optionIdx)
{
LoadOption(groupIdx, optionIdx, true);
SwapEditor.Revert(Option!);
Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Option!);
FileEditor.Clear();
Duplicates.Clear();
}
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
private void LoadOption(int groupIdx, int optionIdx, bool message)
{
if (Mod != null && Mod.Groups.Count > groupIdx)
{
if (groupIdx == -1 && optionIdx == 0)
{
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
}
if (groupIdx >= 0)
{
Group = Mod.Groups[groupIdx];
if (optionIdx >= 0 && optionIdx < Group.Count)
{
Option = Group[optionIdx];
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
}
}
}
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
OptionIdx = 0;
if (message)
global::Penumbra.Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}.");
}
public void Clear()
{
Duplicates.Clear();
FileEditor.Clear();
Files.Clear();
MetaEditor.Clear();
Mod = null;
LoadOption(0, 0, false);
}
public void Dispose()
=> Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<ISubMod, int, int> action)
{
action(mod.Default, -1, 0);
foreach (var (group, groupIdx) in mod.Groups.WithIndex())
{
for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx)
action(group[optionIdx], groupIdx, optionIdx);
}
}
// Does not delete the base directory itself even if it is completely empty at the end.
public static void ClearEmptySubDirectories(DirectoryInfo baseDir)
{
foreach (var subDir in baseDir.GetDirectories())
{
ClearEmptySubDirectories(subDir);
if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0)
subDir.Delete();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ public partial class Mod
// The default mod contains setting-independent sets of file replacements, file swaps and meta changes. // 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. // Every mod has an default mod, though it may be empty.
private void SaveDefaultMod() public void SaveDefaultMod()
{ {
var defaultFile = DefaultFile; var defaultFile = DefaultFile;
@ -100,7 +100,7 @@ public partial class Mod
// It can be loaded and reloaded from Json. // It can be loaded and reloaded from Json.
// Nothing is checked for existence or validity when loading. // 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. // 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"; public string Name { get; set; } = "Default";

View file

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

View file

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

View file

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

View file

@ -9,27 +9,29 @@ using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow public partial class ModEditWindow
{ {
private readonly HashSet<Mod.Editor.FileRegistry> _selectedFiles = new(256); private readonly HashSet<FileRegistry> _selectedFiles = new(256);
private LowerString _fileFilter = LowerString.Empty; private LowerString _fileFilter = LowerString.Empty;
private bool _showGamePaths = true; private bool _showGamePaths = true;
private string _gamePathEdit = string.Empty; private string _gamePathEdit = string.Empty;
private int _fileIdx = -1; private int _fileIdx = -1;
private int _pathIdx = -1; private int _pathIdx = -1;
private int _folderSkip = 0; private int _folderSkip;
private bool _overviewMode = false; private bool _overviewMode;
private LowerString _fileOverviewFilter1 = LowerString.Empty;
private LowerString _fileOverviewFilter2 = LowerString.Empty;
private LowerString _fileOverviewFilter3 = LowerString.Empty;
private bool CheckFilter(Mod.Editor.FileRegistry registry) private 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); => _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); => CheckFilter(p.Item1);
private void DrawFileTab() private void DrawFileTab()
@ -74,13 +76,13 @@ public partial class ModEditWindow
var idx = 0; var idx = 0;
var files = _editor!.AvailableFiles.SelectMany(f => var files = _editor.Files.Available.SelectMany(f =>
{ {
var file = f.RelPath.ToString(); var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0 return f.SubModUsage.Count == 0
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, : 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) void DrawLine((string, string, string, uint) data)
@ -119,7 +121,7 @@ public partial class ModEditWindow
if (!list) if (!list)
return; 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); using var id = ImRaii.PushId(i);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -133,17 +135,17 @@ public partial class ModEditWindow
for (var j = 0; j < registry.SubModUsage.Count; ++j) for (var j = 0; j < registry.SubModUsage.Count; ++j)
{ {
var (subMod, gamePath) = registry.SubModUsage[j]; var (subMod, gamePath) = registry.SubModUsage[j];
if (subMod != _editor.CurrentOption) if (subMod != _editor.Option)
continue; continue;
PrintGamePath(i, j, registry, subMod, gamePath); 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() (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 selected = _selectedFiles.Contains(registry);
var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod :
@ -192,7 +194,7 @@ public partial class ModEditWindow
ImGuiUtil.RightAlign(rightText); 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); using var id = ImRaii.PushId(j);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -211,7 +213,7 @@ public partial class ModEditWindow
if (ImGui.IsItemDeactivatedAfterEdit()) if (ImGui.IsItemDeactivatedAfterEdit())
{ {
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) if (Utf8GamePath.FromString(_gamePathEdit, out var path, false))
_editor!.SetGamePath(_fileIdx, _pathIdx, path); _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path);
_fileIdx = -1; _fileIdx = -1;
_pathIdx = -1; _pathIdx = -1;
@ -219,7 +221,7 @@ public partial class ModEditWindow
else if (_fileIdx == i else if (_fileIdx == i
&& _pathIdx == j && _pathIdx == j
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) && (!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.SameLine();
ImGui.SetCursorPosX(pos); 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 tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();
@ -243,7 +245,7 @@ public partial class ModEditWindow
if (ImGui.IsItemDeactivatedAfterEdit()) if (ImGui.IsItemDeactivatedAfterEdit())
{ {
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) 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; _fileIdx = -1;
_pathIdx = -1; _pathIdx = -1;
@ -251,7 +253,7 @@ public partial class ModEditWindow
else if (_fileIdx == i else if (_fileIdx == i
&& _pathIdx == -1 && _pathIdx == -1
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false)
|| !path.IsEmpty && !_editor!.CanAddGamePath(path))) || !path.IsEmpty && !_editor.FileEditor.CanAddGamePath(path)))
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(pos); ImGui.SetCursorPosX(pos);
@ -271,7 +273,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
spacing.Pop(); spacing.Pop();
if (ImGui.Button("Add Paths")) 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( 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."); "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(); ImGui.SameLine();
if (ImGui.Button("Remove Paths")) 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."); ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option.");
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Delete Selected Files")) 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( 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!!!"); "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(); 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."; 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)) 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) 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 label = changes ? "Revert Changes" : "Reload Files";
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
if (ImGui.Button(label, length)) 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."); 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(); ImGui.SameLine();
if (ImGui.Button("Select Visible")) if (ImGui.Button("Select Visible"))
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter)); _selectedFiles.UnionWith(_editor.Files.Available.Where(CheckFilter));
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Select Unused")) 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(); ImGui.SameLine();
if (ImGui.Button("Select Used Here")) 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(); ImGui.SameLine();
ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected"); ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected");
} }
private void DrawFileManagementOverview() private void DrawFileManagementOverview()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Import.Textures; using Penumbra.Import.Textures;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow 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, tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName,
_fileDialog); _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))); .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.", 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); files, _mod.ModPath.FullName.Length + 1);

View file

@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using Dalamud.Data;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
@ -12,31 +13,32 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Import.Textures; using Penumbra.Import.Textures;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.Util; using Penumbra.Util;
using static Penumbra.Mods.Mod; using static Penumbra.Mods.Mod;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow : Window, IDisposable public partial class ModEditWindow : Window, IDisposable
{ {
private const string WindowBaseLabel = "###SubModEdit"; private const string WindowBaseLabel = "###SubModEdit";
internal readonly ItemSwapWindow _swapWindow;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private Editor? _editor;
private Mod? _mod; private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero; private Vector2 _iconSize = Vector2.Zero;
private bool _allowReduplicate = false; private bool _allowReduplicate;
public void ChangeMod(Mod mod) public void ChangeMod(Mod mod)
{ {
if (mod == _mod) if (mod == _mod)
return; return;
_editor?.Dispose(); _editor.LoadMod(mod, -1, 0);
_editor = new Editor(mod, mod.Default); _mod = mod;
_mod = mod;
SizeConstraints = new WindowSizeConstraints SizeConstraints = new WindowSizeConstraints
{ {
@ -47,17 +49,20 @@ public partial class ModEditWindow : Window, IDisposable
_modelTab.Reset(); _modelTab.Reset();
_materialTab.Reset(); _materialTab.Reset();
_shaderPackageTab.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) public void ChangeOption(SubMod? subMod)
=> _editor?.SetSubMod(subMod); => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.GroupIdx ?? 0);
public void UpdateModels() public void UpdateModels()
=> _editor?.ScanModels(); {
if (_mod != null)
_editor.MdlMaterialEditor.ScanModels(_mod);
}
public override bool DrawConditions() public override bool DrawConditions()
=> _editor != null; => _mod != null;
public override void PreDraw() public override void PreDraw()
{ {
@ -67,7 +72,7 @@ public partial class ModEditWindow : Window, IDisposable
var redirections = 0; var redirections = 0;
var unused = 0; var unused = 0;
var size = _editor!.AvailableFiles.Sum(f => var size = _editor.Files.Available.Sum(f =>
{ {
if (f.SubModUsage.Count > 0) if (f.SubModUsage.Count > 0)
redirections += f.SubModUsage.Count; redirections += f.SubModUsage.Count;
@ -89,13 +94,13 @@ public partial class ModEditWindow : Window, IDisposable
sb.Append($" | {subMods} Options"); sb.Append($" | {subMods} Options");
if (size > 0) 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) if (unused > 0)
sb.Append($" | {unused} Unused Files"); sb.Append($" | {unused} Unused Files");
if (_editor.MissingFiles.Count > 0) if (_editor.Files.Missing.Count > 0)
sb.Append($" | {_editor.MissingFiles.Count} Missing Files"); sb.Append($" | {_editor.Files.Available.Count} Missing Files");
if (redirections > 0) if (redirections > 0)
sb.Append($" | {redirections} Redirections"); sb.Append($" | {redirections} Redirections");
@ -106,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable
if (swaps > 0) if (swaps > 0)
sb.Append($" | {swaps} Swaps"); 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); sb.Append(WindowBaseLabel);
WindowName = sb.ToString(); WindowName = sb.ToString();
} }
@ -136,7 +141,7 @@ public partial class ModEditWindow : Window, IDisposable
_materialTab.Draw(); _materialTab.Draw();
DrawTextureTab(); DrawTextureTab();
_shaderPackageTab.Draw(); _shaderPackageTab.Draw();
_swapWindow.DrawItemSwapPanel(); _itemSwapTab.DrawContent();
} }
// A row of three buttonSizes and a help marker that can be used for material suffix changing. // 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); DrawRaceCodeCombo(buttonSize);
ImGui.SameLine(); ImGui.SameLine();
@ -179,7 +184,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SetNextItemWidth(buttonSize.X); ImGui.SetNextItemWidth(buttonSize.X);
ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32); ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32);
ImGui.SameLine(); ImGui.SameLine();
var disabled = !Editor.ValidString(_materialSuffixTo); var disabled = !MdlMaterialEditor.ValidString(_materialSuffixTo);
var tt = _materialSuffixTo.Length == 0 var tt = _materialSuffixTo.Length == 0
? "Please enter a target suffix." ? "Please enter a target suffix."
: _materialSuffixFrom == _materialSuffixTo : _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 that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."
: $"Convert all skin material suffices for the given race code 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)) 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, if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize,
anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges)) anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges))
editor.SaveAllModels(); editor.MdlMaterialEditor.SaveAllModels();
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize, if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize,
anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges)) anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges))
editor.RestoreAllModels(); editor.MdlMaterialEditor.RestoreAllModels();
ImGui.SameLine(); ImGui.SameLine();
ImGuiComponents.HelpMarker( ImGuiComponents.HelpMarker(
@ -216,7 +221,7 @@ public partial class ModEditWindow : Window, IDisposable
private void DrawMissingFilesTab() private void DrawMissingFilesTab()
{ {
if (_editor!.MissingFiles.Count == 0) if (_editor.Files.Missing.Count == 0)
return; return;
using var tab = ImRaii.TabItem("Missing Files"); using var tab = ImRaii.TabItem("Missing Files");
@ -225,7 +230,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.NewLine(); ImGui.NewLine();
if (ImGui.Button("Remove Missing Files from Mod")) 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); using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true);
if (!child) if (!child)
@ -235,7 +240,7 @@ public partial class ModEditWindow : Window, IDisposable
if (!table) if (!table)
return; return;
foreach (var path in _editor.MissingFiles) foreach (var path in _editor.Files.Missing)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(path.FullName); ImGui.TextUnformatted(path.FullName);
@ -248,37 +253,44 @@ public partial class ModEditWindow : Window, IDisposable
if (!tab) if (!tab)
return; 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.", if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.",
!_editor.DuplicatesFinished)) !_editor.Duplicates.Finished))
_editor.StartDuplicateCheck(); _editor.Duplicates.StartDuplicateCheck(_editor.Files.Available);
const string desc = 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" "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" + "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."; + "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 : var tt = _allowReduplicate ? desc :
modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; 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)) if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier))
{ {
_mod!.Normalize(Penumbra.ModManager); _editor.ModNormalizer.Normalize(_mod!);
_editor.RevertFiles(); _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(); ImGui.SameLine();
if (ImGui.Button("Cancel")) if (ImGui.Button("Cancel"))
_editor.Cancel(); _editor.Duplicates.Clear();
return; return;
} }
if (_editor.Duplicates.Count == 0) if (_editor.Duplicates.Duplicates.Count == 0)
{ {
ImGui.NewLine(); ImGui.NewLine();
ImGui.TextUnformatted("No duplicates found."); ImGui.TextUnformatted("No duplicates found.");
@ -286,12 +298,12 @@ public partial class ModEditWindow : Window, IDisposable
} }
if (ImGui.Button("Delete and Redirect Duplicates")) 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.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); 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("size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN ").X);
ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed, ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed,
ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); 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(); ImGui.TableNextColumn();
using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], 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); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0);
var width = new Vector2(ImGui.GetWindowWidth() / 3, 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.", if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
_editor!.CurrentOption.IsDefault)) _editor!.Option!.IsDefault))
_editor.SetSubMod(_mod!.Default); _editor.LoadOption(-1, 0);
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) 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(); 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) if (!combo)
return; return;
foreach (var option in _mod!.AllSubMods) foreach (var option in _mod!.AllSubMods.Cast<SubMod>())
{ {
if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption)) if (ImGui.Selectable(option.FullName, option == _editor.Option))
_editor.SetSubMod(option); _editor.LoadOption(option.GroupIdx, option.OptionIdx);
} }
} }
@ -377,16 +389,16 @@ public partial class ModEditWindow : Window, IDisposable
DrawOptionSelectHeader(); 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."; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine(); ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.ApplySwaps(); _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx);
ImGui.SameLine(); ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) 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); using var child = ImRaii.Child("##swaps", -Vector2.One, true);
if (!child) if (!child)
@ -403,30 +415,26 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize); ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize);
ImGui.TableSetupColumn("value", 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++); using var id = ImRaii.PushId(idx++);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true))
_editor.CurrentSwaps.Remove(gamePath); _editor.SwapEditor.Remove(gamePath);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var tmp = gamePath.Path.ToString(); var tmp = gamePath.Path.ToString();
ImGui.SetNextItemWidth(-1); ImGui.SetNextItemWidth(-1);
if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength) if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength)
&& Utf8GamePath.FromString(tmp, out var path) && Utf8GamePath.FromString(tmp, out var path)
&& !_editor.CurrentSwaps.ContainsKey(path)) && !_editor.SwapEditor.Swaps.ContainsKey(path))
{ _editor.SwapEditor.Change(gamePath, path);
_editor.CurrentSwaps.Remove(gamePath);
if (path.Length > 0)
_editor.CurrentSwaps[path] = file;
}
ImGui.TableNextColumn(); ImGui.TableNextColumn();
tmp = file.FullName; tmp = file.FullName;
ImGui.SetNextItemWidth(-1); ImGui.SetNextItemWidth(-1);
if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) 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(); ImGui.TableNextColumn();
@ -434,13 +442,13 @@ public partial class ModEditWindow : Window, IDisposable
&& newPath.Length > 0 && newPath.Length > 0
&& _newSwapValue.Length > 0 && _newSwapValue.Length > 0
&& _newSwapValue != _newSwapKey && _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, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable,
true)) true))
{ {
_editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue); _editor.SwapEditor.Add(newPath, new FullPath(_newSwapValue));
_newSwapKey = string.Empty; _newSwapKey = string.Empty;
_newSwapValue = string.Empty; _newSwapValue = string.Empty;
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -477,26 +485,21 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath(path); return new FullPath(path);
} }
public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog) public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor)
: base(WindowBaseLabel) : base(WindowBaseLabel)
{ {
_fileDialog = fileDialog; _itemSwapTab = itemSwapTab;
_swapWindow = new ItemSwapWindow(communicator); _config = config;
_materialTab = new FileEditor<MtrlTab>("Materials", ".mtrl", _fileDialog, _editor = editor;
() => _editor?.MtrlFiles ?? Array.Empty<Editor.FileRegistry>(), _fileDialog = fileDialog;
DrawMaterialPanel, _materialTab = new FileEditor<MtrlTab>(gameData, config, _fileDialog, "Materials", ".mtrl",
() => _mod?.ModPath.FullName ?? string.Empty, () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlTab(this, new MtrlFile(bytes))); bytes => new MtrlTab(this, new MtrlFile(bytes)));
_modelTab = new FileEditor<MdlFile>("Models", ".mdl", _fileDialog, _modelTab = new FileEditor<MdlFile>(gameData, config, _fileDialog, "Models", ".mdl",
() => _editor?.MdlFiles ?? Array.Empty<Editor.FileRegistry>(), () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
DrawModelPanel, _shaderPackageTab = new FileEditor<ShpkTab>(gameData, config, _fileDialog, "Shader Packages", ".shpk",
() => _mod?.ModPath.FullName ?? string.Empty, () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
null);
_shaderPackageTab = new FileEditor<ShpkTab>("Shader Packages", ".shpk", _fileDialog,
() => _editor?.ShpkFiles ?? Array.Empty<Editor.FileRegistry>(),
DrawShaderPackagePanel,
() => _mod?.ModPath.FullName ?? string.Empty,
null);
_center = new CombinedTexture(_left, _right); _center = new CombinedTexture(_left, _right);
} }
@ -506,6 +509,5 @@ public partial class ModEditWindow : Window, IDisposable
_left.Dispose(); _left.Dispose();
_right.Dispose(); _right.Dispose();
_center.Dispose(); _center.Dispose();
_swapWindow.Dispose();
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,16 @@
using System; using System;
using Dalamud.Plugin; using Dalamud.Plugin;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.UI.Classes; using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.ModTab; namespace Penumbra.UI.ModsTab;
public class ModPanel : IDisposable public class ModPanel : IDisposable
{ {
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow; private readonly ModEditWindow _editWindow;
private readonly ModPanelHeader _header; private readonly ModPanelHeader _header;
private readonly ModPanelTabBar _tabs; private readonly ModPanelTabBar _tabs;
public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs)
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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