mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 12:44:19 +01:00
Why is this so much work?
This commit is contained in:
parent
651c7410ac
commit
b92a3161b5
53 changed files with 3054 additions and 2936 deletions
|
|
@ -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 )
|
||||||
|
|
|
||||||
258
Penumbra/Mods/Editor/DuplicateManager.cs
Normal file
258
Penumbra/Mods/Editor/DuplicateManager.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Penumbra/Mods/Editor/FileRegistry.cs
Normal file
57
Penumbra/Mods/Editor/FileRegistry.cs
Normal 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();
|
||||||
|
}
|
||||||
97
Penumbra/Mods/Editor/MdlMaterialEditor.cs
Normal file
97
Penumbra/Mods/Editor/MdlMaterialEditor.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
128
Penumbra/Mods/Editor/ModEditor.cs
Normal file
128
Penumbra/Mods/Editor/ModEditor.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
Penumbra/Mods/Editor/ModFileCollection.cs
Normal file
198
Penumbra/Mods/Editor/ModFileCollection.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
Penumbra/Mods/Editor/ModFileEditor.cs
Normal file
173
Penumbra/Mods/Editor/ModFileEditor.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
Penumbra/Mods/Editor/ModMetaEditor.cs
Normal file
154
Penumbra/Mods/Editor/ModMetaEditor.cs
Normal 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));
|
||||||
|
}
|
||||||
289
Penumbra/Mods/Editor/ModNormalizer.cs
Normal file
289
Penumbra/Mods/Editor/ModNormalizer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Penumbra/Mods/Editor/ModSwapEditor.cs
Normal file
51
Penumbra/Mods/Editor/ModSwapEditor.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
Penumbra/Mods/Editor/ModelMaterialInfo.cs
Normal file
82
Penumbra/Mods/Editor/ModelMaterialInfo.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>()
|
||||||
|
|
|
||||||
268
Penumbra/UI/AdvancedWindow/FileEditor.cs
Normal file
268
Penumbra/UI/AdvancedWindow/FileEditor.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -169,40 +195,37 @@ public class ItemSwapWindow : IDisposable
|
||||||
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 ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing );
|
_useCurrentCollection ? _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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,9 +425,7 @@ public class ItemSwapWindow : IDisposable
|
||||||
{
|
{
|
||||||
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);
|
||||||
|
|
@ -419,24 +440,21 @@ public class ItemSwapWindow : IDisposable
|
||||||
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;
|
_dirty = true;
|
||||||
_slotFrom = slot;
|
_slotFrom = slot;
|
||||||
if (slot == _slotTo)
|
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();
|
||||||
|
|
@ -448,32 +466,30 @@ public class ItemSwapWindow : IDisposable
|
||||||
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;
|
_dirty = true;
|
||||||
_slotTo = slot;
|
_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 })
|
||||||
|
return;
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg );
|
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
|
||||||
|
Colors.PressEnterWarningBg);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
|
||||||
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2))
|
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, selector.CurrentSelection.Item2))
|
||||||
.Select(i => i.Name.ToDalamudString().TextValue)));
|
.Select(i => i.Name.ToDalamudString().TextValue)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source)
|
private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source)
|
||||||
{
|
{
|
||||||
|
|
@ -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,8 +540,9 @@ 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();
|
ImGui.SameLine();
|
||||||
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
|
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
|
||||||
Colors.PressEnterWarningBg);
|
Colors.PressEnterWarningBg);
|
||||||
|
|
@ -533,7 +550,6 @@ public class ItemSwapWindow : IDisposable
|
||||||
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
|
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
|
||||||
.Select(i => i.Name.ToDalamudString().TextValue)));
|
.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);
|
_swapData.LoadMod(_mod, _modSettings);
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
@ -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 _fileOverviewFilter1 = LowerString.Empty;
|
||||||
private LowerString _fileOverviewFilter2 = LowerString.Empty;
|
private LowerString _fileOverviewFilter2 = LowerString.Empty;
|
||||||
private LowerString _fileOverviewFilter3 = LowerString.Empty;
|
private LowerString _fileOverviewFilter3 = LowerString.Empty;
|
||||||
|
|
||||||
private bool CheckFilter(Mod.Editor.FileRegistry registry)
|
private bool CheckFilter(FileRegistry registry)
|
||||||
=> _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase);
|
=> _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()
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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();
|
||||||
886
Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs
Normal file
886
Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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,30 +13,31 @@ 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 Editor? _editor;
|
private readonly ModEditor _editor;
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly ItemSwapTab _itemSwapTab;
|
||||||
|
|
||||||
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,11 +442,11 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
_itemSwapTab = itemSwapTab;
|
||||||
|
_config = config;
|
||||||
|
_editor = editor;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_swapWindow = new ItemSwapWindow(communicator);
|
_materialTab = new FileEditor<MtrlTab>(gameData, config, _fileDialog, "Materials", ".mtrl",
|
||||||
_materialTab = new FileEditor<MtrlTab>("Materials", ".mtrl", _fileDialog,
|
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||||
() => _editor?.MtrlFiles ?? Array.Empty<Editor.FileRegistry>(),
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Penumbra.UI.ModTab;
|
namespace Penumbra.UI.ModsTab;
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum ModFilter
|
public enum ModFilter
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue