From b4ac5f8288cecaef908c098a267f75d20ed8af97 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Sat, 17 Jun 2023 18:37:28 -0500 Subject: [PATCH] Add Asset Compiler tab Add Loose Texture Compiler Core Submodule changes Correct text Add custom path function. Refactor name to asset compiler Right align buttons. Store project files outside of the folder of the currently selected mod. Add Simple Mode Fix issue with simple mode not clearing itself. Update constructors Add modifier keys to filepicker clearing buttons. Submodule changes Minor cleanup Adjust event names Fix project persistence. Add a cap to how many texture sets can exist. Only save project if asset compiler tab is actually used. --- .gitmodules | 3 + LooseTextureCompilerCore | 1 + Penumbra.sln | 6 + Penumbra/Import/Textures/CombinedTexture.cs | 576 +++-- Penumbra/Mods/Manager/ModManager.cs | 668 ++--- Penumbra/Penumbra.csproj | 3 +- Penumbra/Services/ServiceManager.cs | 3 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 34 + .../ModsTab/ModPanelLooseAssetCompilerTab.cs | 2284 +++++++++++++++++ Penumbra/UI/ModsTab/ModPanelTabBar.cs | 13 +- Penumbra/packages.lock.json | 48 +- 11 files changed, 3025 insertions(+), 614 deletions(-) create mode 160000 LooseTextureCompilerCore create mode 100644 Penumbra/UI/ModsTab/ModPanelLooseAssetCompilerTab.cs diff --git a/.gitmodules b/.gitmodules index 940493668..ba94fd274 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ path = Penumbra.String url = git@github.com:Ottermandias/Penumbra.String.git branch = main +[submodule "LooseTextureCompilerCore"] + path = LooseTextureCompilerCore + url = https://github.com/Sebane1/LooseTextureCompilerCore.git diff --git a/LooseTextureCompilerCore b/LooseTextureCompilerCore new file mode 160000 index 000000000..65cad5bce --- /dev/null +++ b/LooseTextureCompilerCore @@ -0,0 +1 @@ +Subproject commit 65cad5bce9be604363c3997fb33327f73b84f3b1 diff --git a/Penumbra.sln b/Penumbra.sln index bccc56d83..eb06a9713 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LooseTextureCompilerCore", "LooseTextureCompilerCore\LooseTextureCompilerCore.csproj", "{39769DC8-E6B7-4C5C-8758-7FB36F6A7FB4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU + {39769DC8-E6B7-4C5C-8758-7FB36F6A7FB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39769DC8-E6B7-4C5C-8758-7FB36F6A7FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39769DC8-E6B7-4C5C-8758-7FB36F6A7FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39769DC8-E6B7-4C5C-8758-7FB36F6A7FB4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index bf0170484..453f57b64 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,259 +1,325 @@ -using System; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using Lumina.Data.Files; -using OtterTex; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using DalamudUtil = Dalamud.Utility.Util; -using Image = SixLabors.ImageSharp.Image; - -namespace Penumbra.Import.Textures; - -public partial class CombinedTexture : IDisposable -{ - public enum TextureSaveType - { - AsIs, - Bitmap, - BC3, - BC7, - } - - private enum Mode - { - Empty, - LeftCopy, - RightCopy, - Custom, - } - - private readonly Texture _left; - private readonly Texture _right; - - private Texture? _current; - private Mode _mode = Mode.Empty; - - private readonly Texture _centerStorage = new(); - - public bool IsLoaded - => _mode != Mode.Empty; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using FFXIVLooseTextureCompiler.ImageProcessing; +using Lumina.Data.Files; +using OtterTex; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using DalamudUtil = Dalamud.Utility.Util; +using Image = SixLabors.ImageSharp.Image; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture : IDisposable +{ + public enum TextureSaveType + { + AsIs, + Bitmap, + BC3, + BC7, + } + + private enum Mode + { + Empty, + LeftCopy, + RightCopy, + Custom, + } + + private readonly Texture _left; + private readonly Texture _right; + + private Texture? _current; + private Mode _mode = Mode.Empty; + + private readonly Texture _centerStorage = new(); + + public bool IsLoaded + => _mode != Mode.Empty; public bool IsLeftCopy => _mode == Mode.LeftCopy; - - public Exception? SaveException { get; private set; } = null; - - public void Draw( UiBuilder builder, Vector2 size ) - { - if( _mode == Mode.Custom && !_centerStorage.IsLoaded ) - { - var (width, height) = CombineImage(); - _centerStorage.TextureWrap = - builder.LoadImageRaw( _centerStorage.RGBAPixels, width, height, 4 ); - } - - _current?.Draw( size ); - } - - - public void SaveAsPng( string path ) - { - if( !IsLoaded || _current == null ) - { - return; - } - - try - { - var image = Image.LoadPixelData< Rgba32 >( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height ); - image.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } - } - - private void SaveAs( string path, TextureSaveType type, bool mipMaps, bool writeTex ) - { - if( _current == null || _mode == Mode.Empty ) - { - return; - } - - try - { - if( _current.BaseImage is not ScratchImage s ) - { - s = ScratchImage.FromRGBA( _current.RGBAPixels, _current.TextureWrap!.Width, - _current.TextureWrap!.Height, out var i ).ThrowIfError( i ); - } - - var tex = type switch - { - TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed( s, mipMaps ) : s, - TextureSaveType.Bitmap => CreateUncompressed( s, mipMaps ), - TextureSaveType.BC3 => CreateCompressed( s, mipMaps, false ), - TextureSaveType.BC7 => CreateCompressed( s, mipMaps, true ), - _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), - }; - - if( !writeTex ) - { - tex.SaveDDS( path ); - } - else - { - SaveTex( path, tex ); - } - - SaveException = null; - } - catch( Exception e ) - { - SaveException = e; - } - } - - private static void SaveTex( string path, ScratchImage input ) - { - var header = input.ToTexHeader(); - if( header.Format == TexFile.TextureFormat.Unknown ) - { - throw new Exception( $"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats." ); - } - - using var stream = File.Open( path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); - using var w = new BinaryWriter( stream ); - header.Write( w ); - w.Write( input.Pixels ); - } - - private static ScratchImage AddMipMaps( ScratchImage input, bool mipMaps ) - { - if( !mipMaps ) - { - return input; - } - - var numMips = Math.Min( 13, 1 + BitOperations.Log2( ( uint )Math.Max( input.Meta.Width, input.Meta.Height ) ) ); - var ec = input.GenerateMipMaps( out var ret, numMips, ( DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0 ) | FilterFlags.SeparateAlpha ); - if (ec != ErrorCode.Ok) - { - throw new Exception( $"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}" ); - } - - return ret; - } - - private static ScratchImage CreateUncompressed( ScratchImage input, bool mipMaps ) - { - if( input.Meta.Format == DXGIFormat.B8G8R8A8UNorm ) - { - return AddMipMaps( input, mipMaps ); - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - else - { - input = input.Convert( DXGIFormat.B8G8R8A8UNorm ); - } - - return AddMipMaps( input, mipMaps ); - } - - private static ScratchImage CreateCompressed( ScratchImage input, bool mipMaps, bool bc7 ) - { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; - if( input.Meta.Format == format ) - { - return input; - } - - if( input.Meta.Format.IsCompressed() ) - { - input = input.Decompress( DXGIFormat.B8G8R8A8UNorm ); - } - - input = AddMipMaps( input, mipMaps ); - - return input.Compress( format, CompressFlags.BC7Quick | CompressFlags.Parallel ); - } - - public void SaveAsTex( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, true ); - - public void SaveAsDds( string path, TextureSaveType type, bool mipMaps ) - => SaveAs( path, type, mipMaps, false ); - - - public CombinedTexture( Texture left, Texture right ) - { - _left = left; - _right = right; - _left.Loaded += OnLoaded; - _right.Loaded += OnLoaded; - OnLoaded( false ); - } - - public void Dispose() - { - Clean(); - _left.Loaded -= OnLoaded; - _right.Loaded -= OnLoaded; - } - - private void OnLoaded( bool _ ) - => Update(); - - public void Update() - { - Clean(); - if( _left.IsLoaded ) - { - if( _right.IsLoaded ) - { - _current = _centerStorage; - _mode = Mode.Custom; - } - else if( !_invertLeft && _multiplierLeft.IsIdentity ) - { - _mode = Mode.LeftCopy; - _current = _left; - } - else - { - _current = _centerStorage; - _mode = Mode.Custom; - } - } - else if( _right.IsLoaded ) - { - if( !_invertRight && _multiplierRight.IsIdentity ) - { - _current = _right; - _mode = Mode.RightCopy; - } - else - { - _current = _centerStorage; - _mode = Mode.Custom; - } - } - } - - private void Clean() - { - _centerStorage.Dispose(); - _current = null; - _mode = Mode.Empty; - } + + public Exception? SaveException { get; private set; } = null; + + public void Draw(UiBuilder builder, Vector2 size) + { + if (_mode == Mode.Custom && !_centerStorage.IsLoaded) + { + var (width, height) = CombineImage(); + _centerStorage.TextureWrap = + builder.LoadImageRaw(_centerStorage.RGBAPixels, width, height, 4); + } + + _current?.Draw(size); + } + + + public void SaveAsPng(string path) + { + if (!IsLoaded || _current == null) + { + return; + } + + try + { + var image = Image.LoadPixelData(_current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); + image.Save(path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }); + SaveException = null; + } + catch (Exception e) + { + SaveException = e; + } + } + + public void ImageToEyeMaps(string path, string textureCompilerDLC) + { + if (!IsLoaded || _current == null) + { + return; + } + + try + { + var image = Image.LoadPixelData(_current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); + image.Save(path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }); + ImageManipulation.ConvertToEyeMaps(path, textureCompilerDLC); + SaveException = null; + } + catch (Exception e) + { + SaveException = e; + } + } + internal void EyeMultiToGrayscale(string path) + { + if (!IsLoaded || _current == null) + { + return; + } + + try + { + var image = Image.LoadPixelData(_current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); + image.Save(path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }); + Bitmap multi = TexLoader.ResolveBitmap(path); + ImageManipulation.ExtractRed(multi).Save(ImageManipulation.AddSuffix(path, "_grayscale")); + SaveException = null; + } + catch (Exception e) + { + SaveException = e; + } + } + + public void AtramentumLuminisDiffuseToGlowMap(string path) + { + if (!IsLoaded || _current == null) + { + return; + } + + try + { + var image = Image.LoadPixelData(_current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height); + image.Save(path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }); + Bitmap diffuse = TexLoader.ResolveBitmap(path); + AtramentumLuminisGlow.ExtractGlowMapFromDiffuse(diffuse).Save(path, ImageFormat.Png); + SaveException = null; + } + catch (Exception e) + { + SaveException = e; + } + } + private void SaveAs(string path, TextureSaveType type, bool mipMaps, bool writeTex) + { + if (_current == null || _mode == Mode.Empty) + { + return; + } + + try + { + if (_current.BaseImage is not ScratchImage s) + { + s = ScratchImage.FromRGBA(_current.RGBAPixels, _current.TextureWrap!.Width, + _current.TextureWrap!.Height, out var i).ThrowIfError(i); + } + + var tex = type switch + { + TextureSaveType.AsIs => _current.Type is Texture.FileType.Bitmap or Texture.FileType.Png ? CreateUncompressed(s, mipMaps) : s, + TextureSaveType.Bitmap => CreateUncompressed(s, mipMaps), + TextureSaveType.BC3 => CreateCompressed(s, mipMaps, false), + TextureSaveType.BC7 => CreateCompressed(s, mipMaps, true), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + + if (!writeTex) + { + tex.SaveDDS(path); + } + else + { + SaveTex(path, tex); + } + + SaveException = null; + } + catch (Exception e) + { + SaveException = e; + } + } + + private static void SaveTex(string path, ScratchImage input) + { + var header = input.ToTexHeader(); + if (header.Format == TexFile.TextureFormat.Unknown) + { + throw new Exception($"Could not save tex file with format {input.Meta.Format}, not convertible to a valid .tex formats."); + } + + using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew); + using var w = new BinaryWriter(stream); + header.Write(w); + w.Write(input.Pixels); + } + + private static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) + { + if (!mipMaps) + { + return input; + } + + var numMips = Math.Min(13, 1 + BitOperations.Log2((uint)Math.Max(input.Meta.Width, input.Meta.Height))); + var ec = input.GenerateMipMaps(out var ret, numMips, (DalamudUtil.IsLinux() ? FilterFlags.ForceNonWIC : 0) | FilterFlags.SeparateAlpha); + if (ec != ErrorCode.Ok) + { + throw new Exception($"Could not create the requested {numMips} mip maps, maybe retry with the top-right checkbox unchecked:\n{ec}"); + } + + return ret; + } + + private static ScratchImage CreateUncompressed(ScratchImage input, bool mipMaps) + { + if (input.Meta.Format == DXGIFormat.B8G8R8A8UNorm) + { + return AddMipMaps(input, mipMaps); + } + + if (input.Meta.Format.IsCompressed()) + { + input = input.Decompress(DXGIFormat.B8G8R8A8UNorm); + } + else + { + input = input.Convert(DXGIFormat.B8G8R8A8UNorm); + } + + return AddMipMaps(input, mipMaps); + } + + private static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7) + { + var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; + if (input.Meta.Format == format) + { + return input; + } + + if (input.Meta.Format.IsCompressed()) + { + input = input.Decompress(DXGIFormat.B8G8R8A8UNorm); + } + + input = AddMipMaps(input, mipMaps); + + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); + } + + public void SaveAsTex(string path, TextureSaveType type, bool mipMaps) + => SaveAs(path, type, mipMaps, true); + + public void SaveAsDds(string path, TextureSaveType type, bool mipMaps) + => SaveAs(path, type, mipMaps, false); + + + public CombinedTexture(Texture left, Texture right) + { + _left = left; + _right = right; + _left.Loaded += OnLoaded; + _right.Loaded += OnLoaded; + OnLoaded(false); + } + + public void Dispose() + { + Clean(); + _left.Loaded -= OnLoaded; + _right.Loaded -= OnLoaded; + } + + private void OnLoaded(bool _) + => Update(); + + public void Update() + { + Clean(); + if (_left.IsLoaded) + { + if (_right.IsLoaded) + { + _current = _centerStorage; + _mode = Mode.Custom; + } + else if (!_invertLeft && _multiplierLeft.IsIdentity) + { + _mode = Mode.LeftCopy; + _current = _left; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + else if (_right.IsLoaded) + { + if (!_invertRight && _multiplierRight.IsIdentity) + { + _current = _right; + _mode = Mode.RightCopy; + } + else + { + _current = _centerStorage; + _mode = Mode.Custom; + } + } + } + + private void Clean() + { + _centerStorage.Dispose(); + _current = null; + _mode = Mode.Empty; + } } \ No newline at end of file diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 38eff57b1..5ab3cb4a0 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,337 +1,337 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Penumbra.Communication; -using Penumbra.Mods.Editor; -using Penumbra.Services; - -namespace Penumbra.Mods.Manager; - -/// Describes the state of a potential move-target for a mod. -public enum NewDirectoryState -{ - NonExisting, - ExistsEmpty, - ExistsNonEmpty, - ExistsAsFile, - ContainsInvalidSymbols, - Identical, - Empty, -} - -/// Describes the state of a changed mod event. -public enum ModPathChangeType -{ - Added, - Deleted, - Moved, - Reloaded, - StartingReload, +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Penumbra.Communication; +using Penumbra.Mods.Editor; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +/// Describes the state of a potential move-target for a mod. +public enum NewDirectoryState +{ + NonExisting, + ExistsEmpty, + ExistsNonEmpty, + ExistsAsFile, + ContainsInvalidSymbols, + Identical, + Empty, +} + +/// Describes the state of a changed mod event. +public enum ModPathChangeType +{ + Added, + Deleted, + Moved, + Reloaded, + StartingReload, } - -public sealed class ModManager : ModStorage, IDisposable -{ - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - - public readonly ModCreator Creator; - public readonly ModDataEditor DataEditor; - public readonly ModOptionEditor OptionEditor; - - public DirectoryInfo BasePath { get; private set; } = null!; - public bool Valid { get; private set; } - - public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, - ModCreator creator) - { - _config = config; - _communicator = communicator; - DataEditor = dataEditor; - OptionEditor = optionEditor; - Creator = creator; + +public sealed class ModManager : ModStorage, IDisposable +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + + public readonly ModCreator Creator; + public readonly ModDataEditor DataEditor; + public readonly ModOptionEditor OptionEditor; + + public DirectoryInfo BasePath { get; private set; } = null!; + public bool Valid { get; private set; } + + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, + ModCreator creator) + { + _config = config; + _communicator = communicator; + DataEditor = dataEditor; + OptionEditor = optionEditor; + Creator = creator; SetBaseDirectory(config.ModDirectory, true); - _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); - DiscoverMods(); - } - - /// Change the mod base directory and discover available mods. - public void DiscoverMods(string newDir) - { - SetBaseDirectory(newDir, false); - DiscoverMods(); - } - - /// - /// Discover mods without changing the root directory. - /// - public void DiscoverMods() - { - _communicator.ModDiscoveryStarted.Invoke(); - ClearNewMods(); - Mods.Clear(); - BasePath.Refresh(); - - if (Valid && BasePath.Exists) - ScanMods(); - - _communicator.ModDiscoveryFinished.Invoke(); - Penumbra.Log.Information($"Rediscovered {Mods.Count} mods."); - - if (ModBackup.MigrateModBackups) - ModBackup.MigrateZipToPmp(this); - } - - /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) - { - if (this.Any(m => m.ModPath.Name == modFolder.Name)) - return; - - Creator.SplitMultiGroups(modFolder); - var mod = Creator.LoadMod(modFolder, true); - if (mod == null) - return; - - mod.Index = Count; - Mods.Add(mod); - _communicator.ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); - Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); - } - - /// - /// Delete a mod. The event is invoked before the mod is removed from the list. - /// Deletes from filesystem as well as from internal data. - /// Updates indices of later mods. - /// - public void DeleteMod(Mod mod) - { - if (Directory.Exists(mod.ModPath.FullName)) - try - { - Directory.Delete(mod.ModPath.FullName, true); - Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); - } - - _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - foreach (var remainingMod in Mods.Skip(mod.Index + 1)) - --remainingMod.Index; - Mods.RemoveAt(mod.Index); - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); - } - - /// - /// Reload a mod without changing its base directory. - /// If the base directory does not exist anymore, the mod will be deleted. - /// - public void ReloadMod(Mod mod) - { - var oldName = mod.Name; - - _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!Creator.ReloadMod(mod, true, out var metaChange)) - { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(mod); - return; - } - - _communicator.ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - - /// - /// Rename/Move a mod directory. - /// Updates all collection settings and sort order settings. - /// - public void MoveModDirectory(Mod mod, string newName) - { - var oldName = mod.Name; - var oldDirectory = mod.ModPath; - - switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) - { - case NewDirectoryState.NonExisting: - // Nothing to do - break; - case NewDirectoryState.ExistsEmpty: - try - { - Directory.Delete(dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); - return; - } - - break; - // Should be caught beforehand. - case NewDirectoryState.ExistsNonEmpty: - case NewDirectoryState.ExistsAsFile: - case NewDirectoryState.ContainsInvalidSymbols: - // Nothing to do at all. - case NewDirectoryState.Identical: - default: - return; - } - - try - { - Directory.Move(oldDirectory.FullName, dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); - return; - } - - DataEditor.MoveDataFile(oldDirectory, dir); - - dir.Refresh(); - mod.ModPath = dir; - if (!Creator.ReloadMod(mod, false, out var metaChange)) - { - Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); - return; - } - - _communicator.ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// Return the state of the new potential name of a directory. - public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) - { - directory = null; - if (newName.Length == 0) - return NewDirectoryState.Empty; - - if (oldName == newName) - return NewDirectoryState.Identical; - - var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); - if (fixedNewName != newName) - return NewDirectoryState.ContainsInvalidSymbols; - - directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); - if (File.Exists(directory.FullName)) - return NewDirectoryState.ExistsAsFile; - - if (!Directory.Exists(directory.FullName)) - return NewDirectoryState.NonExisting; - - if (directory.EnumerateFileSystemInfos().Any()) - return NewDirectoryState.ExistsNonEmpty; - - return NewDirectoryState.ExistsEmpty; - } - - - /// Add new mods to NewMods and remove deleted mods from NewMods. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); + DiscoverMods(); + } + + /// Change the mod base directory and discover available mods. + public void DiscoverMods(string newDir) + { + SetBaseDirectory(newDir, false); + DiscoverMods(); + } + + /// + /// Discover mods without changing the root directory. + /// + public void DiscoverMods() + { + _communicator.ModDiscoveryStarted.Invoke(); + ClearNewMods(); + Mods.Clear(); + BasePath.Refresh(); + + if (Valid && BasePath.Exists) + ScanMods(); + + _communicator.ModDiscoveryFinished.Invoke(); + Penumbra.Log.Information($"Rediscovered {Mods.Count} mods."); + + if (ModBackup.MigrateModBackups) + ModBackup.MigrateZipToPmp(this); + } + + /// Load a new mod and add it to the manager if successful. + public void AddMod(DirectoryInfo modFolder) + { + if (this.Any(m => m.ModPath.Name == modFolder.Name)) + return; + + Creator.SplitMultiGroups(modFolder); + var mod = Creator.LoadMod(modFolder, true); + if (mod == null) + return; + + mod.Index = Count; + Mods.Add(mod); + _communicator.ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); + Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); + } + + /// + /// Delete a mod. The event is invoked before the mod is removed from the list. + /// Deletes from filesystem as well as from internal data. + /// Updates indices of later mods. + /// + public void DeleteMod(Mod mod) + { + if (Directory.Exists(mod.ModPath.FullName)) + try + { + Directory.Delete(mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); + } + + _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); + foreach (var remainingMod in Mods.Skip(mod.Index + 1)) + --remainingMod.Index; + Mods.RemoveAt(mod.Index); + + Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + } + + /// + /// Reload a mod without changing its base directory. + /// If the base directory does not exist anymore, the mod will be deleted. + /// + public void ReloadMod(Mod mod) + { + var oldName = mod.Name; + + _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); + if (!Creator.ReloadMod(mod, true, out var metaChange)) + { + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); + + DeleteMod(mod); + return; + } + + _communicator.ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + + /// + /// Rename/Move a mod directory. + /// Updates all collection settings and sort order settings. + /// + public void MoveModDirectory(Mod mod, string newName) + { + var oldName = mod.Name; + var oldDirectory = mod.ModPath; + + switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) + { + case NewDirectoryState.NonExisting: + // Nothing to do + break; + case NewDirectoryState.ExistsEmpty: + try + { + Directory.Delete(dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); + return; + } + + break; + // Should be caught beforehand. + case NewDirectoryState.ExistsNonEmpty: + case NewDirectoryState.ExistsAsFile: + case NewDirectoryState.ContainsInvalidSymbols: + // Nothing to do at all. + case NewDirectoryState.Identical: + default: + return; + } + + try + { + Directory.Move(oldDirectory.FullName, dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); + return; + } + + DataEditor.MoveDataFile(oldDirectory, dir); + + dir.Refresh(); + mod.ModPath = dir; + if (!Creator.ReloadMod(mod, false, out var metaChange)) + { + Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); + return; + } + + _communicator.ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + /// Return the state of the new potential name of a directory. + public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + { + directory = null; + if (newName.Length == 0) + return NewDirectoryState.Empty; + + if (oldName == newName) + return NewDirectoryState.Identical; + + var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); + if (fixedNewName != newName) + return NewDirectoryState.ContainsInvalidSymbols; + + directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); + if (File.Exists(directory.FullName)) + return NewDirectoryState.ExistsAsFile; + + if (!Directory.Exists(directory.FullName)) + return NewDirectoryState.NonExisting; + + if (directory.EnumerateFileSystemInfos().Any()) + return NewDirectoryState.ExistsNonEmpty; + + return NewDirectoryState.ExistsEmpty; + } + + + /// Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { case ModPathChangeType.Added: - SetNew(mod); - break; - case ModPathChangeType.Deleted: - SetKnown(mod); - break; - case ModPathChangeType.Moved: - if (oldDirectory != null && newDirectory != null) - DataEditor.MoveDataFile(oldDirectory, newDirectory); - - break; - } - } - - public void Dispose() - => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - - /// - /// Set the mod base directory. - /// If its not the first time, check if it is the same directory as before. - /// Also checks if the directory is available and tries to create it if it is not. - /// - private void SetBaseDirectory(string newPath, bool firstTime) - { - if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) - return; - - if (newPath.Length == 0) - { - Valid = false; - BasePath = new DirectoryInfo("."); - if (_config.ModDirectory != BasePath.FullName) - TriggerModDirectoryChange(string.Empty, false); - } - else - { - var newDir = new DirectoryInfo(newPath); - if (!newDir.Exists) - try - { - Directory.CreateDirectory(newDir.FullName); - newDir.Refresh(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); - } - - BasePath = newDir; - Valid = Directory.Exists(newDir.FullName); - if (!firstTime && _config.ModDirectory != BasePath.FullName) - TriggerModDirectoryChange(BasePath.FullName, Valid); - } - } - - private void TriggerModDirectoryChange(string newPath, bool valid) - { - _config.ModDirectory = newPath; - _config.Save(); - Penumbra.Log.Information($"Set new mod base directory from {_config.ModDirectory} to {newPath}."); - _communicator.ModDirectoryChanged.Invoke(newPath, valid); - } - - - /// - /// Iterate through available mods with multiple threads and queue their loads, - /// then add the mods from the queue. - /// - private void ScanMods() - { - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, - }; - var queue = new ConcurrentQueue(); - Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => - { - var mod = Creator.LoadMod(dir, false); - if (mod != null) - queue.Enqueue(mod); - }); - - foreach (var mod in queue) - { - mod.Index = Count; - Mods.Add(mod); - } - } -} + SetNew(mod); + break; + case ModPathChangeType.Deleted: + SetKnown(mod); + break; + case ModPathChangeType.Moved: + if (oldDirectory != null && newDirectory != null) + DataEditor.MoveDataFile(oldDirectory, newDirectory); + + break; + } + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + + /// + /// Set the mod base directory. + /// If its not the first time, check if it is the same directory as before. + /// Also checks if the directory is available and tries to create it if it is not. + /// + private void SetBaseDirectory(string newPath, bool firstTime) + { + if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + return; + + if (newPath.Length == 0) + { + Valid = false; + BasePath = new DirectoryInfo("."); + if (_config.ModDirectory != BasePath.FullName) + TriggerModDirectoryChange(string.Empty, false); + } + else + { + var newDir = new DirectoryInfo(newPath); + if (!newDir.Exists) + try + { + Directory.CreateDirectory(newDir.FullName); + newDir.Refresh(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); + } + + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + if (!firstTime && _config.ModDirectory != BasePath.FullName) + TriggerModDirectoryChange(BasePath.FullName, Valid); + } + } + + private void TriggerModDirectoryChange(string newPath, bool valid) + { + _config.ModDirectory = newPath; + _config.Save(); + Penumbra.Log.Information($"Set new mod base directory from {_config.ModDirectory} to {newPath}."); + _communicator.ModDirectoryChanged.Invoke(newPath, valid); + } + + + /// + /// Iterate through available mods with multiple threads and queue their loads, + /// then add the mods from the queue. + /// + private void ScanMods() + { + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Creator.LoadMod(dir, false); + if (mod != null) + queue.Enqueue(mod); + }); + + foreach (var mod in queue) + { + mod.Index = Count; + Mods.Add(mod); + } + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index cddc58128..44b70a054 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -69,12 +69,13 @@ - + + diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 1ab1f3130..1d973d2cb 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -149,7 +149,8 @@ private static IServiceCollection AddInterface(this IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index f96ab4da6..b3f82e7f3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -133,6 +133,40 @@ private void DrawOutputChild(Vector2 size, Vector2 imageSize) }, _mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } + string eyeMapResources = _config.ModDirectory + @"\LooseTextureCompilerDLC\"; + if (Directory.Exists(eyeMapResources)) + { + if (ImGui.Button("Image To Eye Maps", -Vector2.UnitX)) + { + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => + { + if (a) + _center.ImageToEyeMaps(b, eyeMapResources); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } + if (ImGui.Button("Eye Multi To Grayscale", -Vector2.UnitX)) + { + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => + { + if (a) + _center.EyeMultiToGrayscale(b); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } + } + if (ImGui.Button("Seperate Glow Information From Diffuse", -Vector2.UnitX)) + { + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => + { + if (a) + _center.AtramentumLuminisDiffuseToGlowMap(b); + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } if (_left.Type is Texture.FileType.Tex && _center.IsLeftCopy) { diff --git a/Penumbra/UI/ModsTab/ModPanelLooseAssetCompilerTab.cs b/Penumbra/UI/ModsTab/ModPanelLooseAssetCompilerTab.cs new file mode 100644 index 000000000..62c38e6b2 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelLooseAssetCompilerTab.cs @@ -0,0 +1,2284 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Numerics; +using System.Threading.Tasks; +using FFXIVLooseTextureCompiler; +using FFXIVLooseTextureCompiler.Export; +using FFXIVLooseTextureCompiler.PathOrganization; +using FFXIVLooseTextureCompiler.Racial; +using ImGuiNET; +using Newtonsoft.Json; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Interop.Services; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using TypingConnector; + +namespace Penumbra.UI.ModsTab; + +public class ModPanelLooseAssetCompilerTab : ITab +{ + public ReadOnlySpan Label + => "Asset Compiler"u8; + + #region Variables + private string _xNormalPath; + private Configuration _config; + private FileDialogService _fileDialog; + private TextureProcessor _textureProcessor; + private Dictionary _watchers = new Dictionary(); + private bool _lockDuplicateGeneration; + + private List _textureSets = new List(); + + private Dictionary _groupOptionTypes = new Dictionary(); + private List _textureSetNames = new List(); + private Color _originalDiffuseBoxColour; + private Color _originalNormalBoxColour; + private Color _originalMultiBoxColour; + private ModManager _manager; + private RedrawService _redrawService; + private ModFileSystemSelector _selector; + + private string[] _choiceTypes; + private string[] _bodyNames; + private string[] _bodyNamesSimplified; + private string[] _genders; + private string[] _races; + private string[] _subRaces; + private string[] _faceTypes; + private string[] _faceParts; + private string[] _faceScales; + private string[] _tails; + private string[] _simpleModeNormalChoices; + private string[] _faceExtraValues; + + + private LTCFilePicker _diffuse; + private LTCFilePicker _normal; + private LTCFilePicker _multi; + private LTCFilePicker _glow; + private LTCFilePicker _mask; + + private LTCFilePicker _skin; + private LTCFilePicker _face; + private LTCFilePicker _eyes; + + private LTCComboBox _baseBodyList; + private LTCComboBox _baseBodyListSimplified; + private LTCComboBox _genderList; + private LTCComboBox _raceList; + private LTCComboBox _subRaceList; + private LTCComboBox _faceTypeList; + private LTCComboBox _facePartList; + private LTCComboBox _faceExtraList; + private LTCComboBox _auraFaceScalesDropdown; + private LTCComboBox _tailList; + private LTCComboBox _choiceTypeList; + private LTCComboBox _simpleModeNormalComboBox; + + private LTCCheckBox _bakeNormals; + private LTCCheckBox _generateMulti; + private LTCCheckBox _asymCheckbox; + private LTCCheckBox _uniqueAuRa; + + private LTCCustomPathConfigurator _customPathConfigurator; + private LTCFindAndReplace _ltcFindAndReplace; + private LTCTemplateConfigurator _ltcTemplateConfigurator; + + private TextureSet skinTextureSet; + private TextureSet faceTextureSet; + private TextureSet eyesTextureSet; + + private int _currentTextureSet = -1; + private int _lastTextureSet = -1; + + private string _currentEditLabel; + private int _lastRaceIndex; + private bool _isSimpleMode; + private Mod? _currentMod; + private bool _editingInternalValues; + private bool _bulkReplacingValues; + private string _exportStatus = ""; + private bool _configuringTemplate; + private bool _showOmniExportPrompt; + private bool _addingCustomValues; + private const int _textureSetLimit = 10510; + + #endregion + public ModPanelLooseAssetCompilerTab(RedrawService redrawService, ModManager manager, + ModFileSystemSelector selector, FileDialogService fileDialog, Configuration config) + { + // This will be used for underlay textures. + // The user will need to download a mod pack with the following path until there is a better way to aquire underlay assets. + string underlayTexturePath = manager.BasePath.FullName + @"\LooseTextureCompilerDLC\"; + + // This should reference the xNormal install no matter where its been installed. + // If this path is not found xNormal reliant functions will be disabled until xNormal is installed. + _xNormalPath = @"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\xNormal\3.19.3\xNormal (x64).lnk"; + + #region Initialization + _config = config; + _fileDialog = fileDialog; + _textureProcessor = new TextureProcessor(underlayTexturePath); + _textureProcessor.OnStartedProcessing += TextureProcessor_OnStartedProcessing; + _textureProcessor.OnLaunchedXnormal += TextureProcessor_OnLaunchedXnormal; + + _manager = manager; + _redrawService = redrawService; + _selector = selector; + _choiceTypes = new string[] { "Detailed", "Simple", "Dropdown", "Group Is Checkbox" }; + _bodyNames = new string[] { "Vanilla and Gen2", "BIBO+", "EVE", "Gen3 and T&F3", "SCALES+", "TBSE and HRBODY", "TAIL", "Otopop" }; + _bodyNamesSimplified = new string[] { "BIBO+ Based", "Gen3 Based", "TBSE and HRBODY", "Otopop" }; + _genders = new string[] { "Masculine", "Feminine" }; + _races = new string[] { "Midlander", "Highlander", "Elezen", "Miqo'te", "Roegadyn", "Lalafell", "Raen", "Xaela", "Hrothgar", "Viera" }; + _subRaces = new string[] { "Midlander", "Highlander", "Wildwood", "Duskwight", "Seeker", "Keeper", "Sea Wolf", "Hellsguard", "Plainsfolk", "Dunesfolk", "Raen", "Xaela", "Helions", "The Lost", "Rava", "Veena" }; + _faceTypes = new string[] { "Face 1", "Face 2", "Face 3", "Face 4", "Face 5", "Face 6", "Face 7", "Face 8", "Face 9" }; + _faceParts = new string[] { "Face", "Eyebrows", "Eyes", "Ears", "Face Paint", "Hair", "Face B", "Etc B" }; + _faceScales = new string[] { "Vanilla Scales", "Scaleless Vanilla", "Scaleless Varied" }; + _tails = new string[8]; + _simpleModeNormalChoices = new string[] { "No Bumps On Skin", "Bumps On Skin", "Inverted Bumps On Skin" }; + _faceExtraValues = new string[999]; + + for (int i = 0; i < _tails.Length; i++) + { + _tails[i] = (i + 1) + ""; + } + for (int i = 0; i < _faceExtraValues.Length; i++) + { + _faceExtraValues[i] = (i + 1) + ""; + } + + _baseBodyList = new LTCComboBox("bodyTypeList", _bodyNames, 0, 150); + _baseBodyList.OnSelectedIndexChanged += BaseBodyList_OnSelectedIndexChanged; + _baseBodyList.SelectedIndex = 0; + + _baseBodyListSimplified = new LTCComboBox("bodyTypeListSimplified", _bodyNamesSimplified, 0, 150); + _baseBodyListSimplified.OnSelectedIndexChanged += BaseBodyListSimplified_OnSelectedIndexChanged; + _baseBodyListSimplified.SelectedIndex = 0; + + _genderList = new LTCComboBox("genderList", _genders, 0, 100); + + _raceList = new LTCComboBox("raceList", _races, 0, 100); + _raceList.OnSelectedIndexChanged += RaceList_OnSelectedIndexChanged; + + _subRaceList = new LTCComboBox("subRaceList", _subRaces, 0, 100); + _subRaceList.OnSelectedIndexChanged += SubRaceList_OnSelectedIndexChanged; + _subRaceList.SelectedIndex = 0; + + _faceTypeList = new LTCComboBox("faceTypeList", _faceTypes, 0, 70); + _faceTypeList.OnSelectedIndexChanged += FaceTypeList_OnSelectedIndexChanged; + + _facePartList = new LTCComboBox("facePartList", _faceParts, 0, 90); + _facePartList.OnSelectedIndexChanged += FacePartList_OnSelectedIndexChanged; + + _auraFaceScalesDropdown = new LTCComboBox("faceScaleList", _faceScales, 0, 135); + _auraFaceScalesDropdown.Enabled = false; + + _faceExtraList = new LTCComboBox("faceExtraList", _faceExtraValues, 0, 70); + _faceExtraList.Enabled = false; + + _tailList = new LTCComboBox("tailList", _tails, 0, 62); + _tailList.Enabled = false; + + _choiceTypeList = new LTCComboBox("choiceTypeList", _choiceTypes, 0, 135); + + _simpleModeNormalComboBox = new LTCComboBox("simpleModeNormalChoices", _simpleModeNormalChoices, 0, 230); + _simpleModeNormalComboBox.OnSelectedIndexChanged += SimpleModeNormalComboBox_OnSelectedIndexChanged; + + _currentEditLabel = "Please highlight a texture set to start importing"; + + _diffuse = new LTCFilePicker("Diffuse", _config, fileDialog); + _normal = new LTCFilePicker("Normal", _config, fileDialog); + _multi = new LTCFilePicker("Multi", _config, fileDialog); + _glow = new LTCFilePicker("Glow", _config, fileDialog); + _mask = new LTCFilePicker("Mask", _config, fileDialog); + + _skin = new LTCFilePicker("Skin", _config, fileDialog); + _face = new LTCFilePicker("Face", _config, fileDialog); + _eyes = new LTCFilePicker("Eyes", _config, fileDialog); + + _customPathConfigurator = new LTCCustomPathConfigurator(_choiceTypes); + _customPathConfigurator.OnWantsToClose += CustomPathConfiguration_OnWantsToClose; + + _ltcFindAndReplace = new LTCFindAndReplace(_textureSets, config, fileDialog); + _ltcFindAndReplace.OnWantsToClose += LtcFindAndReplace_OnWantsToClose; + + _ltcTemplateConfigurator = new LTCTemplateConfigurator(this, _textureSets); + _ltcTemplateConfigurator.OnWantsToClose += LtcTemplateConfigurator_OnWantsToClose; + + _diffuse.OnFileSelected += TextureSelection_OnFileSelected; + _normal.OnFileSelected += TextureSelection_OnFileSelected; + _multi.OnFileSelected += TextureSelection_OnFileSelected; + _glow.OnFileSelected += TextureSelection_OnFileSelected; + _mask.OnFileSelected += TextureSelection_OnFileSelected; + + _skin.OnFileSelected += Skin_OnFileSelected; + _face.OnFileSelected += Face_OnFileSelected; + _eyes.OnFileSelected += Eyes_OnFileSelected; + + _diffuse.Enabled = false; + _normal.Enabled = false; + _multi.Enabled = false; + _mask.Enabled = false; + _glow.Enabled = false; + + _bakeNormals = new LTCCheckBox("Bake Normals", 90); + _bakeNormals.OnCheckedChanged += BakeNormals_OnCheckedChanged; + + _generateMulti = new LTCCheckBox("Bake Multi", 70); + _asymCheckbox = new LTCCheckBox("Asym", 50); + _uniqueAuRa = new LTCCheckBox("Unique Au Ra", 90); + _uniqueAuRa.Enabled = false; + + _originalDiffuseBoxColour = _diffuse.BackColor = Color.Pink; + _originalNormalBoxColour = _normal.BackColor = Color.AliceBlue; + _originalMultiBoxColour = _multi.BackColor = Color.Orange; + #endregion + } + #region UI + public void DrawContent() + { + if (!_lockDuplicateGeneration) + { + CheckForNewMod(); + DrawCoreWindow(); + } + else + { + ProgressDisplay(); + } + } + + private void DrawCoreWindow() + { + if (!_editingInternalValues && !_bulkReplacingValues + && !_configuringTemplate && !_addingCustomValues) + { + WarningsAndDisclaimers(); + if (!_isSimpleMode) + { + AdvancedMode(); + } + else + { + SimpleMode(); + } + } + else + { + ConfiguratorWindows(); + } + } + + private void SimpleMode() + { + _baseBodyListSimplified.Draw(); + ImGui.SameLine(); + _subRaceList.Draw(); + ImGui.SameLine(); + _faceTypeList.Draw(); + + _skin.Draw(); + _face.Draw(); + _eyes.Draw(); + + ImGui.SetNextItemWidth(80); + ImGui.LabelText("##skinBumpsLabel", "Skin Bumps"); + ImGui.SameLine(); + _simpleModeNormalComboBox.Draw(); + if (ImGui.Button("Preview (For quick edits)")) + { + Task.Run(() => Export(false)); + } + + ImGui.SameLine(); + + if (ImGui.Button("Finalize (To finish mod")) + { + Task.Run(() => Export(true)); + } + } + + private void AdvancedMode() + { + BodySelection(); + FaceSelection(); + TextureSetManagement(); + ExportSettings(); + } + + private void MenuBar() + { + TemplateButtons(); + } + + private void WarningsAndDisclaimers() + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - 180); + ImGui.LabelText("##disclaimer", "This tab will overwrite any previously autogenerated assets."); + ImGui.SameLine(); + if (ImGui.Button(_isSimpleMode ? "Enable Advanced Mode" : "Enable Simple Mode", new Vector2(170, 20))) + { + if (!_isSimpleMode) + { + if (_textureSets.Count == 3 || _textureSets.Count == 0) + { + SimpleModeSwitch(); + } + else + { + Penumbra.Chat.NotificationMessage("This project is too complex to switch to simple mode", "Cannot Switch Modes", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else + { + _isSimpleMode = false; + } + } + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - 410); + if (!Directory.Exists(_textureProcessor.BasePath)) + { + ImGui.LabelText("##dlcInstall", "Loose Texture Compiler DLC is not installed. Underlay support will not function."); + ImGui.SameLine(); + if (ImGui.Button("Download DLC")) + { + looseTextureDLCDownload(); + } + } + if (!File.Exists(_xNormalPath) || IsRunningUnderWine()) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - 540); + ImGui.LabelText("##xNormalInstall", IsRunningUnderWine() ? + "Universal mode is not supported on Wine. Universal options will be unavailable" : + "xNormal is not installed. Universal options will be unavailable"); + ImGui.SameLine(); + if (!IsRunningUnderWine()) + { + if (ImGui.Button("Download xNormal")) + { + xNormalDownload(); + } + } + } + } + + private void xNormalDownload() + { + try + { + Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = "https://xnormal.net/", + UseShellExecute = true, + Verb = "OPEN" + }); + } + catch + { + + } + } + + private void looseTextureDLCDownload() + { + try + { + Process.Start(new System.Diagnostics.ProcessStartInfo() + { + FileName = "https://drive.google.com/uc?id=1qUExzMshqVovo2cP5jkSpkkcC4pLpfHs", + UseShellExecute = true, + Verb = "OPEN" + }); + } + catch + { + + } + } + + private void ConfiguratorWindows() + { + if (_editingInternalValues || _addingCustomValues) + { + _customPathConfigurator.Draw(); + } + else if (_bulkReplacingValues) + { + _ltcFindAndReplace?.Draw(); + } + else if (_configuringTemplate) + { + _ltcTemplateConfigurator?.Draw(); + } + } + + private void ProgressDisplay() + { + ImGui.LabelText("##progressLabel", "This tab is unavailable until the previous operation completes."); + ImGui.LabelText("##progressStatus", _exportStatus + " " + _currentMod.Name); + ImGui.ProgressBar((float)_textureProcessor.ExportCompletion / (float)_textureProcessor.ExportMax, + new System.Numerics.Vector2(ImGui.GetContentRegionMax().X, 20)); + } + + private void CheckForNewMod() + { + if (!_lockDuplicateGeneration) + { + if (_selector.Selected != _currentMod) + { + string projectPath = ""; + if (_currentMod != null && _textureSets.Count > 0) + { + if (Directory.Exists(_currentMod.ModPath.FullName)) + { + projectPath = Path.Combine(_manager.BasePath.FullName, + (_currentMod.Name.Text.Replace("/", null).Replace(@"\", null)) + ".ffxivtp"); + SaveProject(projectPath); + } + } + if (_selector.Selected != null) + { + projectPath = Path.Combine(_manager.BasePath.FullName, + _selector.Selected.Name.Text.Replace("/", null).Replace(@"\", null) + ".ffxivtp"); + NewProject(); + ExitConfigurators(); + if (File.Exists(projectPath)) + { + OpenProject(projectPath); + } + _currentMod = _selector.Selected; + } + } + } + } + + private void ExitConfigurators() + { + _editingInternalValues = false; + _bulkReplacingValues = false; + _configuringTemplate = false; + _addingCustomValues = false; + } + + private void ExportSettings() + { + ImGui.SetNextItemWidth(80); + ImGui.LabelText("##choiceTypeLabel", "Choice Type"); + ImGui.SameLine(); + _choiceTypeList.Draw(); + ImGui.SameLine(); + + _bakeNormals.Draw(); + ImGui.SameLine(); + + _generateMulti.Draw(); + ImGui.SameLine(); + if (ImGui.Button("Preview")) + { + Task.Run(() => Export(false)); + } + ImGui.SameLine(); + if (ImGui.Button("Finalize")) + { + Task.Run(() => Export(true)); + } + } + + private void TextureSetManagement() + { + _textureSetNames.Clear(); + foreach (TextureSet textureSet in _textureSets) + { + _textureSetNames.Add(textureSet.ToString()); + } + ImGui.SetNextItemWidth(ImGui.GetWindowContentRegionMax().X - 105); + ImGui.LabelText("##textureSetsLabel", "Texture Sets"); + ImGui.SameLine(); + + if (ImGui.Button("Custom Path")) + { + OnCustomPath(); + } + ImGui.SetNextItemWidth(ImGui.GetWindowContentRegionMax().X - 10); + ImGui.ListBox("##textureSets", ref _currentTextureSet, _textureSetNames.ToArray(), _textureSetNames.Count, 6); + + if (_currentTextureSet != _lastTextureSet) + { + SelectedIndexChanged(); + } + + _lastTextureSet = _currentTextureSet; + + TextureSetContextMenu(); + TextureSetManagementButtons(); + ImGui.SameLine(); + MenuBar(); + TextureSetManagementFileEntries(); + } + + private void TextureSetManagementFileEntries() + { + ImGui.LabelText("##currentEditLabelText" + _currentEditLabel, _currentEditLabel); + + _diffuse.Draw(); + _normal.Draw(); + _multi.Draw(); + _glow.Draw(); + _mask.Draw(); + } + + private void TextureSetManagementButtons() + { + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.BeginDisabled(); + } + if (ImGui.Button("Delete")) + { + OnRemoveSelection(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Clear List")) + { + OnClearList(); + } + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.EndDisabled(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Move Up")) + { + OnMoveUp(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Move Down")) + { + OnMoveDown(); + } + + } + + private void OnCustomPath() + { + if (_textureSets.Count < _textureSetLimit) + { + + _customPathConfigurator.TextureSet = new TextureSet(); + _addingCustomValues = true; + } + else + { + Penumbra.Chat.NotificationMessage("You have hit the cap of " + _textureSetLimit + " texture sets", "Too Many Texture Sets", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + + private void TextureSetContextMenu() + { + if (_currentTextureSet > -1) + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right) && ImGui.IsItemHovered()) + { + ImGui.OpenPopup("##textureSetContextMenu"); + } + if (ImGui.BeginPopup("##textureSetContextMenu")) + { + if (ImGui.MenuItem("Edit Internal Values##context")) + { + OnEditInternalValues(); + } + if (File.Exists(_xNormalPath)) + { + if (ImGui.MenuItem(!_textureSets[_currentTextureSet].OmniExportMode + ? "Enable Universal Mode##context" : "Disable Universal Mode##context")) + { + ToggleUniversalModeOnCurrentTextureSet(); + } + } + + if (ImGui.MenuItem("Bulk Replace Values##context")) + { + OnBulkReplace(); + } + + if (ImGui.MenuItem("Move Up##context")) + { + OnMoveUp(); + } + + if (ImGui.MenuItem("Move Down##context")) + { + OnMoveDown(); + } + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.BeginDisabled(); + } + if (ImGui.MenuItem("Delete##context")) + { + OnRemoveSelection(); + } + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.EndDisabled(); + } + ImGui.EndPopup(); + } + } + if (_showOmniExportPrompt) + { + ImGui.OpenPopup("UniversalModeNotice"); + _showOmniExportPrompt = false; + } + UniversalModeDialog(); + } + + private void TemplateButtons() + { + ImGui.Dummy(new System.Numerics.Vector2(ImGui.GetContentRegionMax().X - 510, 5)); + ImGui.SameLine(); + if (ImGui.Button("Templates")) + { + ImGui.OpenPopup("##templateContextMenu"); + } + if (ImGui.BeginPopup("##templateContextMenu")) + { + if (ImGui.MenuItem("Import Template##context")) + { + _fileDialog.OpenFilePicker("Import Template", + "Template Project {.ffxivtp}", (s, f) => + { + if (!s) + { + return; + } + OpenTemplate(f[0]); + }, 1, "", false); + } + if (Directory.Exists(_textureProcessor.BasePath)) + { + foreach (string item in Directory.EnumerateFiles(_textureProcessor.BasePath + @"\res\templates")) + { + if (item.EndsWith(".ffxivtp")) + { + if (ImGui.MenuItem(Path.GetFileNameWithoutExtension(item) + "##context")) + { + OpenTemplate(item); + } + } + } + } + ImGui.EndPopup(); + } + ImGui.SameLine(); + if (ImGui.Button("Export Template")) + { + _fileDialog.OpenSavePicker("Save Template", "Template Project{.ffxivtp}", + "New template.ffxivtp", "{.ffxivtp}", (s, f) => + { + if (!s) + { + return; + } + SaveProject(f.Replace(".ffxivtp", "") + ".ffxivtp"); + }, "", false); + } + } + + private void OnEditInternalValues() + { + _editingInternalValues = true; + _customPathConfigurator.TextureSet = _textureSets[_currentTextureSet]; + _customPathConfigurator.GroupingType.SelectedIndex = ( + _groupOptionTypes.ContainsKey(_customPathConfigurator.TextureSet.GroupName) ? + _groupOptionTypes[_customPathConfigurator.TextureSet.GroupName] : 0); + } + + private void OnBulkReplace() + { + if (_currentTextureSet > -1) + { + TextureSet sourceTextureSet = _textureSets[_currentTextureSet]; + Tokenizer tokenizer = new Tokenizer(sourceTextureSet.TextureSetName); + _ltcFindAndReplace.TextureSetSearchString = tokenizer.GetToken(); + _ltcFindAndReplace.GroupSearchString = sourceTextureSet.GroupName + != sourceTextureSet.TextureSetName ? sourceTextureSet.GroupName : ""; + _ltcFindAndReplace.Diffuse.CurrentPath = _diffuse.CurrentPath; + _ltcFindAndReplace.Normal.CurrentPath = _normal.CurrentPath; + _ltcFindAndReplace.Multi.CurrentPath = _multi.CurrentPath; + _ltcFindAndReplace.Mask.CurrentPath = _mask.CurrentPath; + _ltcFindAndReplace.Glow.CurrentPath = _glow.CurrentPath; + _ltcFindAndReplace.IsForEyes = sourceTextureSet.InternalMultiPath.ToLower().Contains("catchlight"); + _bulkReplacingValues = true; + } + } + + private void OnMoveDown() + { + if (_currentTextureSet + 1 < _textureSets.Count && _currentTextureSet != -1) + { + TextureSet object1 = _textureSets[_currentTextureSet + 1]; + TextureSet object2 = _textureSets[_currentTextureSet]; + + _textureSets[_currentTextureSet] = object1; + _textureSets[_currentTextureSet + 1] = object2; + _currentTextureSet += 1; + } + } + + private void OnMoveUp() + { + if (_currentTextureSet > 0) + { + TextureSet object1 = _textureSets[_currentTextureSet - 1]; + TextureSet object2 = _textureSets[_currentTextureSet]; + + _textureSets[_currentTextureSet] = object1; + _textureSets[_currentTextureSet - 1] = object2; + _currentTextureSet -= 1; + } + } + + private void OnClearList() + { + _textureSets.Clear(); + _diffuse.CurrentPath = ""; + _normal.CurrentPath = ""; + _multi.CurrentPath = ""; + _mask.CurrentPath = ""; + _glow.CurrentPath = ""; + + _diffuse.Enabled = false; + _normal.Enabled = false; + _multi.Enabled = false; + _mask.Enabled = false; + _glow.Enabled = false; + + _currentEditLabel = "Please highlight a texture set to start importing"; + + _currentTextureSet = -1; + } + + private void OnRemoveSelection() + { + if (_currentTextureSet > -1) + { + _textureSets.RemoveAt(_currentTextureSet); + _diffuse.CurrentPath = ""; + _normal.CurrentPath = ""; + _multi.CurrentPath = ""; + _glow.CurrentPath = ""; + _currentEditLabel = "Please highlight a texture set to start importing"; + + _currentTextureSet = -1; + } + } + + private void FaceSelection() + { + #region Face Selection + if (_subRaceList.Enabled) + { + _subRaceList.Draw(); + ImGui.SameLine(); + } + + if (_faceTypeList.Enabled) + { + _faceTypeList.Draw(); + ImGui.SameLine(); + } + + _facePartList.Draw(); + ImGui.SameLine(); + + _faceExtraList.Draw(); + ImGui.SameLine(); + + _auraFaceScalesDropdown.Draw(); + ImGui.SameLine(); + + _asymCheckbox.Draw(); + ImGui.SameLine(); + ImGui.Dummy(new Vector2(ImGui.GetWindowContentRegionMax().X - + _subRaceList.Width - _faceTypeList.Width - + _facePartList.Width - _faceExtraList.Width - + _auraFaceScalesDropdown.Width - _asymCheckbox.Width - 100, 1)); + ImGui.SameLine(); + if (ImGui.Button("Add Face")) + { + AddFace(); + } + #endregion + } + + private void BodySelection() + { + #region Body Selection + _baseBodyList.Draw(); + ImGui.SameLine(); + + _genderList.Draw(); + ImGui.SameLine(); + + _raceList.Draw(); + ImGui.SameLine(); + + _tailList.Draw(); + ImGui.SameLine(); + + _uniqueAuRa.Draw(); + ImGui.SameLine(); + ImGui.Dummy(new Vector2(ImGui.GetWindowContentRegionMax().X - + _baseBodyList.Width - _genderList.Width - + _raceList.Width - _tailList.Width - + _uniqueAuRa.Width - 110, 1)); + ImGui.SameLine(); + if (ImGui.Button("Add Body")) + { + AddBody(); + } + #endregion + } + + private void SelectedIndexChanged() + { + if (_currentTextureSet == -1) + { + _currentEditLabel = "Please highlight a texture set to start importing"; + SetControlsEnabled(false); + } + else + { + TextureSet textureSet = _textureSets[_currentTextureSet]; + _currentEditLabel = "Editing: " + textureSet.TextureSetName; + SetControlsEnabled(true, textureSet); + SetControlsPaths(textureSet); + SetControlsColors(textureSet); + } + } + + private void SetControlsEnabled(bool enabled, TextureSet textureSet = null) + { + if (textureSet == null) + { + enabled = false; + } + _diffuse.Enabled = enabled && !string.IsNullOrEmpty(textureSet.InternalDiffusePath); + _normal.Enabled = enabled && !string.IsNullOrEmpty(textureSet.InternalNormalPath); + _multi.Enabled = enabled && !string.IsNullOrEmpty(textureSet.InternalMultiPath); + _mask.Enabled = enabled && _bakeNormals.Checked; + _glow.Enabled = enabled && !textureSet.TextureSetName.ToLower().Contains("face paint") + && !textureSet.TextureSetName.ToLower().Contains("hair") && _diffuse.Enabled; + } + + private void SetControlsPaths(TextureSet textureSet) + { + _diffuse.CurrentPath = textureSet.Diffuse; + _normal.CurrentPath = textureSet.Normal; + _multi.CurrentPath = textureSet.Multi; + _mask.CurrentPath = textureSet.NormalMask; + _glow.CurrentPath = textureSet.Glow; + } + + private void SetControlsColors(TextureSet textureSet) + { + if (textureSet.InternalMultiPath != null && textureSet.InternalMultiPath.ToLower().Contains("catchlight")) + { + _diffuse.LabelName = "Normal"; + _normal.LabelName = "Multi"; + _multi.LabelName = "Catchlight"; + _diffuse.BackColor = _originalNormalBoxColour; + _normal.BackColor = Color.Lavender; + _multi.BackColor = Color.LightGray; + } + else + { + _diffuse.LabelName = "Diffuse"; + _normal.LabelName = "Normal"; + _multi.LabelName = "Multi"; + _diffuse.BackColor = _originalDiffuseBoxColour; + _normal.BackColor = _originalNormalBoxColour; + _multi.BackColor = _originalMultiBoxColour; + } + } + #endregion + #region Event Callbacks + private void SimpleModeNormalComboBox_OnSelectedIndexChanged(object? sender, EventArgs e) + { + switch (_simpleModeNormalComboBox.SelectedIndex) + { + case 0: + _bakeNormals.Checked = false; + skinTextureSet.InvertNormalGeneration = false; + faceTextureSet.InvertNormalGeneration = false; + break; + case 1: + _bakeNormals.Checked = true; + skinTextureSet.InvertNormalGeneration = false; + faceTextureSet.InvertNormalGeneration = false; + break; + case 2: + _bakeNormals.Checked = true; + skinTextureSet.InvertNormalGeneration = true; + faceTextureSet.InvertNormalGeneration = true; + break; + } + SaveState(); + } + + private void Eyes_OnFileSelected(object? sender, EventArgs e) + { + eyesTextureSet.Normal = _eyes.FilePath; + AddWatcher(eyesTextureSet.Diffuse); + SaveState(); + } + + private void Face_OnFileSelected(object? sender, EventArgs e) + { + faceTextureSet.Diffuse = _face.FilePath; + AddWatcher(faceTextureSet.Diffuse); + SaveState(); + } + + private void Skin_OnFileSelected(object? sender, EventArgs e) + { + skinTextureSet.Diffuse = _skin.FilePath; + AddWatcher(skinTextureSet.Diffuse); + SaveState(); + } + + private void FaceTypeList_OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (_isSimpleMode) + { + _facePartList.SelectedIndex = 0; + AddFacePaths(faceTextureSet); + _facePartList.SelectedIndex = 2; + AddEyePaths(eyesTextureSet); + SaveState(); + } + } + + private void BaseBodyListSimplified_OnSelectedIndexChanged(object? sender, EventArgs e) + { + switch (_baseBodyListSimplified.SelectedIndex) + { + case 0: + _baseBodyList.SelectedIndex = 1; + break; + case 1: + _baseBodyList.SelectedIndex = 3; + break; + case 2: + _baseBodyList.SelectedIndex = 5; + break; + case 3: + _baseBodyList.SelectedIndex = 7; + break; + } + AddBodyPaths(skinTextureSet); + SaveState(); + } + private void LtcTemplateConfigurator_OnWantsToClose(object? sender, EventArgs e) + { + _configuringTemplate = false; + } + + private void TextureProcessor_OnLaunchedXnormal(object? sender, EventArgs e) + { + _exportStatus = "Waiting For XNormal To Generate Assets For"; + } + + private void TextureProcessor_OnStartedProcessing(object? sender, EventArgs e) + { + _exportStatus = "Compiling Assets For"; + } + + private void LtcFindAndReplace_OnWantsToClose(object? sender, EventArgs e) + { + if (_ltcFindAndReplace.WasValidated) + { + foreach (TextureSet textureSet in _textureSets) + { + AddWatcher(textureSet.Diffuse); + AddWatcher(textureSet.Normal); + AddWatcher(textureSet.Multi); + AddWatcher(textureSet.NormalMask); + AddWatcher(textureSet.Glow); + } + _currentTextureSet = -1; + _ltcFindAndReplace.WasValidated = false; + } + _bulkReplacingValues = false; + } + + private void CustomPathConfiguration_OnWantsToClose(object? sender, EventArgs e) + { + if (_customPathConfigurator.WasValidated) + { + _groupOptionTypes[_customPathConfigurator.TextureSet.GroupName] = _customPathConfigurator.GroupingType.SelectedIndex; + _customPathConfigurator.WasValidated = false; + } + if (_editingInternalValues) + { + _editingInternalValues = false; + } + if (_addingCustomValues) + { + _textureSets.Add(_customPathConfigurator.TextureSet); + _addingCustomValues = false; + SaveState(); + } + } + + private void TextureSelection_OnFileSelected(object? sender, EventArgs e) + { + SetPaths(); + SaveState(); + } + + private void BakeNormals_OnCheckedChanged(object? sender, EventArgs e) + { + _mask.Enabled = _bakeNormals.Checked && _currentTextureSet > -1; + } + + private void FacePartList_OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (_facePartList.SelectedIndex == 4) + { + _auraFaceScalesDropdown.Enabled = _asymCheckbox.Enabled = _faceTypeList.Enabled = _subRaceList.Enabled = false; + _faceExtraList.Enabled = true; + } + else if (_facePartList.SelectedIndex == 5) + { + _auraFaceScalesDropdown.Enabled = false; + _asymCheckbox.Enabled = _faceTypeList.Enabled; + _faceExtraList.Enabled = true; + } + else + { + _asymCheckbox.Enabled = _faceTypeList.Enabled = _subRaceList.Enabled = true; + if (_subRaceList.SelectedIndex == 10 || _subRaceList.SelectedIndex == 11) + { + _auraFaceScalesDropdown.Enabled = true; + } + _faceExtraList.Enabled = false; + } + } + + private void SubRaceList_OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (_subRaceList.SelectedIndex == 10 || _subRaceList.SelectedIndex == 11) + { + _auraFaceScalesDropdown.Enabled = true; + } + else + { + _auraFaceScalesDropdown.Enabled = false; + } + _raceList.SelectedIndex = RaceInfo.SubRaceToMainRace(_subRaceList.SelectedIndex); + } + + private void RaceList_OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (_baseBodyList.SelectedIndex == 6) + { + if (_raceList.SelectedIndex != 3 && _raceList.SelectedIndex != 6 && _raceList.SelectedIndex != 7) + { + _raceList.SelectedIndex = 3; + Penumbra.Chat.NotificationMessage("Tail is only compatible with Miqo'te Xaela, and Raen", "Bad Combination", Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else if (_baseBodyList.SelectedIndex == 4) + { + if (_raceList.SelectedIndex != 6 && _raceList.SelectedIndex != 7) + { + _raceList.SelectedIndex = 6; + Penumbra.Chat.NotificationMessage("Scales+ is only compatible with Xaela, and Raen", "Bad Combination", Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else if (_baseBodyList.SelectedIndex > 0 && _baseBodyList.SelectedIndex < 7) + { + if (_raceList.SelectedIndex == 5) + { + _raceList.SelectedIndex = _lastRaceIndex; + Penumbra.Chat.NotificationMessage("Lalafells are not compatible with the selected body", "Bad Combination", Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else if (_baseBodyList.SelectedIndex > 6) + { + if (_raceList.SelectedIndex != 5) + { + _raceList.SelectedIndex = 5; + Penumbra.Chat.NotificationMessage("Only Lalafells are compatible with the selected body", "Bad Combination", Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + _lastRaceIndex = _raceList.SelectedIndex; + } + + private void BaseBodyList_OnSelectedIndexChanged(object? sender, EventArgs e) + { + switch (_baseBodyList.SelectedIndex) + { + case 0: + _genderList.Enabled = true; + _tailList.Enabled = false; + _uniqueAuRa.Enabled = false; + break; + case 1: + case 2: + case 3: + _genderList.SelectedIndex = 1; + _genderList.Enabled = false; + _tailList.Enabled = false; + if (_raceList.SelectedIndex == 5) + { + _raceList.SelectedIndex = 0; + } + _uniqueAuRa.Enabled = false; + break; + case 4: + _raceList.SelectedIndex = 6; + _genderList.SelectedIndex = 1; + _genderList.Enabled = false; + _tailList.Enabled = false; + _uniqueAuRa.Enabled = false; + break; + case 5: + _genderList.SelectedIndex = 0; + _genderList.Enabled = false; + _tailList.Enabled = false; + if (_raceList.SelectedIndex == 5) + { + _raceList.SelectedIndex = 0; + } + _uniqueAuRa.Enabled = true; + break; + case 6: + _raceList.SelectedIndex = 6; + _genderList.Enabled = true; + _tailList.Enabled = true; + _uniqueAuRa.Enabled = false; + break; + case 7: + _genderList.Enabled = false; + _raceList.SelectedIndex = 5; + _tailList.Enabled = false; + break; + } + } + #endregion + #region File Persistence + private void NewProject() + { + _lockDuplicateGeneration = true; + _textureSets.Clear(); + _diffuse.CurrentPath = ""; + _normal.CurrentPath = ""; + _multi.CurrentPath = ""; + _glow.CurrentPath = ""; + + _diffuse.Enabled = false; + _normal.Enabled = false; + _multi.Enabled = false; + _mask.Enabled = false; + _glow.Enabled = false; + + _currentTextureSet = -1; + + foreach (FileSystemWatcher watcher in _watchers.Values) + { + watcher.Dispose(); + } + _watchers.Clear(); + _currentEditLabel = "Please highlight a texture set to start importing"; + _lockDuplicateGeneration = false; + _isSimpleMode = false; + } + private void OpenProject(string path) + { + using (StreamReader file = File.OpenText(path)) + { + JsonSerializer serializer = new JsonSerializer(); + ProjectFile projectFile = (ProjectFile)serializer.Deserialize(file, typeof(ProjectFile)); + _textureSets.Clear(); + _choiceTypeList.SelectedIndex = projectFile.ExportType; + _bakeNormals.Checked = projectFile.BakeMissingNormals; + _generateMulti.Checked = projectFile.GenerateMulti; + if (projectFile.GroupOptionTypes != null) + { + _groupOptionTypes = projectFile.GroupOptionTypes; + } + _textureSets.AddRange(projectFile.TextureSets?.ToArray()); + if (projectFile.SimpleMode) + { + SimpleModeSwitch(); + } + else + { + _isSimpleMode = false; + } + _baseBodyList.SelectedIndex = projectFile.SimpleBodyType; + _faceTypeList.SelectedIndex = projectFile.SimpleFaceType; + _subRaceList.SelectedIndex = projectFile.SimpleSubRaceType; + _simpleModeNormalComboBox.SelectedIndex = projectFile.SimpleNormalGeneration; + foreach (TextureSet textureSet in projectFile.TextureSets) + { + AddWatcher(textureSet.Diffuse); + AddWatcher(textureSet.Normal); + AddWatcher(textureSet.Multi); + AddWatcher(textureSet.NormalMask); + AddWatcher(textureSet.Glow); + BackupTexturePaths.AddBackupPaths(_genderList.SelectedIndex, _raceList.SelectedIndex, textureSet); + } + } + } + + private void OpenTemplate(string path) + { + _configuringTemplate = true; + _ltcTemplateConfigurator.OpenTemplate(path, _genderList.SelectedIndex, _raceList.SelectedIndex); + SaveState(); + } + private void SaveProject(string path) + { + using (StreamWriter writer = new StreamWriter(path)) + { + JsonSerializer serializer = new JsonSerializer(); + ProjectFile projectFile = new ProjectFile(); + projectFile.Name = _currentMod.Name; + projectFile.Author = _currentMod.Author; + projectFile.Version = _currentMod.Description; + projectFile.Description = _currentMod.Description; + projectFile.Website = _currentMod.Website; + projectFile.GroupOptionTypes = _groupOptionTypes; + projectFile.TextureSets = new List(); + projectFile.ExportType = _choiceTypeList.SelectedIndex; + projectFile.BakeMissingNormals = _bakeNormals.Checked; + projectFile.GenerateMulti = _generateMulti.Checked; + projectFile.SimpleMode = _isSimpleMode; + + projectFile.SimpleBodyType = _baseBodyList.SelectedIndex; + projectFile.SimpleFaceType = _faceTypeList.SelectedIndex; + projectFile.SimpleSubRaceType = _subRaceList.SelectedIndex; + projectFile.SimpleNormalGeneration = _simpleModeNormalComboBox.SelectedIndex; + foreach (TextureSet textureSet in _textureSets) + { + projectFile.TextureSets.Add(textureSet); + } + serializer.Serialize(writer, projectFile); + } + //Penumbra.Chat.NotificationMessage("Save successfull", "Changes Saved"); + } + private void SaveState() + { + string projectPath = Path.Combine(_manager.BasePath.FullName, _currentMod.Name + ".ffxivtp"); + SaveProject(projectPath); + } + #endregion + #region Texture Set Management + public void SimpleModeSwitch() + { + _isSimpleMode = true; + _baseBodyList.SelectedIndex = 1; + _simpleModeNormalComboBox.SelectedIndex = 0; + if (_textureSets.Count == 0) + { + skinTextureSet = new TextureSet(); + faceTextureSet = new TextureSet(); + eyesTextureSet = new TextureSet(); + _textureSets.Add(skinTextureSet); + _textureSets.Add(faceTextureSet); + _textureSets.Add(eyesTextureSet); + } + else if (_textureSets.Count == 3) + { + skinTextureSet = _textureSets[0]; + faceTextureSet = _textureSets[1]; + eyesTextureSet = _textureSets[2]; + } + else + { + return; + } + + // Enable universal export if XNormal is installed and not running under wine + if (!IsRunningUnderWine() && File.Exists(_xNormalPath)) + { + skinTextureSet.OmniExportMode = true; + } + + skinTextureSet.MaterialSetName = "Skin"; + faceTextureSet.MaterialSetName = "Face"; + eyesTextureSet.MaterialSetName = "Eyes"; + + skinTextureSet.MaterialGroupName = "Character Customization"; + faceTextureSet.MaterialGroupName = "Character Customization"; + eyesTextureSet.MaterialGroupName = "Character Customization"; + + _skin.FilePath = skinTextureSet.Diffuse; + _face.FilePath = faceTextureSet.Diffuse; + _eyes.FilePath = eyesTextureSet.Normal; + + AddBodyPaths(skinTextureSet); + _facePartList.SelectedIndex = 0; + AddFacePaths(faceTextureSet); + _facePartList.SelectedIndex = 2; + AddEyePaths(eyesTextureSet); + _choiceTypeList.SelectedIndex = 1; + } + private void ToggleUniversalModeOnCurrentTextureSet() + { + TextureSet textureSet = _textureSets[_currentTextureSet]; + if (textureSet != null) + { + if (!textureSet.OmniExportMode) + { + UniversalTextureSetCreator.ConfigureOmniConfiguration(textureSet); + _showOmniExportPrompt = true; + } + else + { + textureSet.OmniExportMode = false; + textureSet.ChildSets.Clear(); + _showOmniExportPrompt = false; + } + } + } + + public void UniversalModeDialog() + => ImGuiUtil.HelpPopup("UniversalModeNotice", new Vector2(500 * UiHelpers.Scale, 10 * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.NewLine(); + ImGui.TextWrapped( + "Enabling universal compatibility mode allows your currently selected body or face textures to be compatible with other body/face configurations on a best effort basis." + + "\r\n\r\nWarning: this slows down the generation process, so you will need to click the finalize button to update changes on bodies that arent this one."); + }); + private void SetPaths() + { + if (_currentTextureSet != -1) + { + TextureSet textureSet = _textureSets[_currentTextureSet]; + DisposeWatcher(textureSet.Diffuse, _diffuse); + DisposeWatcher(textureSet.Normal, _normal); + DisposeWatcher(textureSet.Multi, _multi); + DisposeWatcher(textureSet.NormalMask, _mask); + DisposeWatcher(textureSet.Glow, _glow); + + if (!string.IsNullOrWhiteSpace(textureSet.Glow)) + { + _generateMulti.Checked = true; + } + + textureSet.Diffuse = _diffuse.CurrentPath; + textureSet.Normal = _normal.CurrentPath; + textureSet.Multi = _multi.CurrentPath; + textureSet.NormalMask = _mask.CurrentPath; + textureSet.Glow = _glow.CurrentPath; + + AddWatcher(textureSet.Diffuse); + AddWatcher(textureSet.Normal); + AddWatcher(textureSet.Multi); + AddWatcher(textureSet.NormalMask); + AddWatcher(textureSet.Glow); + } + } + private void AddFace() + { + if (_textureSets.Count < _textureSetLimit) + { + TextureSet textureSet = new TextureSet(); + textureSet.TextureSetName = _facePartList.Text + (_facePartList.SelectedIndex == 4 ? " " + + (_faceExtraList.SelectedIndex + 1) : "") + ", " + (_facePartList.SelectedIndex != 4 ? _genderList.Text : "Unisex") + + ", " + (_facePartList.SelectedIndex != 4 ? _subRaceList.Text : "Multi Race") + ", " + + (_facePartList.SelectedIndex != 4 ? _faceTypeList.Text : "Multi Face"); + switch (_facePartList.SelectedIndex) + { + default: + AddFacePaths(textureSet); + break; + case 2: + AddEyePaths(textureSet); + break; + case 4: + AddDecalPath(textureSet); + break; + case 5: + AddHairPaths(textureSet); + break; + } + textureSet.IgnoreMultiGeneration = true; + textureSet.BackupTexturePaths = null; + _textureSets.Add(textureSet); + _currentTextureSet = _textureSets.Count - 1; + SaveState(); + } + else + { + Penumbra.Chat.NotificationMessage("You have hit the cap of " + _textureSetLimit + " texture sets", "Too Many Texture Sets", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + + private void AddBody() + { + if (_textureSets.Count < _textureSetLimit) + { + TextureSet textureSet = new TextureSet(); + textureSet.TextureSetName = _baseBodyList.Text + (_baseBodyList.Text.ToLower().Contains("tail") ? " " + + (_tailList.SelectedIndex + 1) : "") + ", " + (_raceList.SelectedIndex == 5 ? "Unisex" : _genderList.Text) + + ", " + _raceList.Text; + AddBodyPaths(textureSet); + _textureSets.Add(textureSet); + _currentTextureSet = _textureSets.Count - 1; + SaveState(); + } + else + { + Penumbra.Chat.NotificationMessage("You have hit the cap of " + _textureSetLimit + " texture sets", "Too Many Texture Sets", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + + private void AddBodyPaths(TextureSet textureSet) + { + if (_raceList.SelectedIndex != 3 || _baseBodyList.SelectedIndex != 6) + { + textureSet.InternalDiffusePath = RacePaths.GetBodyTexturePath(0, _genderList.SelectedIndex, + _baseBodyList.SelectedIndex, _raceList.SelectedIndex, _tailList.SelectedIndex, _uniqueAuRa.Checked); + } + textureSet.InternalNormalPath = RacePaths.GetBodyTexturePath(1, _genderList.SelectedIndex, + _baseBodyList.SelectedIndex, _raceList.SelectedIndex, _tailList.SelectedIndex, _uniqueAuRa.Checked); + + textureSet.InternalMultiPath = RacePaths.GetBodyTexturePath(2, _genderList.SelectedIndex, + _baseBodyList.SelectedIndex, _raceList.SelectedIndex, _tailList.SelectedIndex, _uniqueAuRa.Checked); + BackupTexturePaths.AddBackupPaths(_genderList.SelectedIndex, _raceList.SelectedIndex, textureSet); + } + + private void AddDecalPath(TextureSet textureSet) + { + textureSet.InternalDiffusePath = RacePaths.GetFaceTexturePath(_faceExtraList.SelectedIndex); + } + + private void AddHairPaths(TextureSet textureSet) + { + textureSet.TextureSetName = _faceParts[_facePartList.SelectedIndex] + " " + (_faceExtraList.SelectedIndex + 1) + + ", " + _genderList.Text + ", " + _races[_raceList.SelectedIndex]; + + textureSet.InternalNormalPath = RacePaths.GetHairTexturePath(1, _faceExtraList.SelectedIndex, + _genderList.SelectedIndex, _raceList.SelectedIndex, _subRaceList.SelectedIndex); + + textureSet.InternalMultiPath = RacePaths.GetHairTexturePath(2, _faceExtraList.SelectedIndex, + _genderList.SelectedIndex, _raceList.SelectedIndex, _subRaceList.SelectedIndex); + } + + private void AddEyePaths(TextureSet textureSet) + { + textureSet.InternalDiffusePath = RacePaths.GetFaceTexturePath(1, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + 2, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + + textureSet.InternalNormalPath = RacePaths.GetFaceTexturePath(2, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + 2, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + + textureSet.InternalMultiPath = RacePaths.GetFaceTexturePath(3, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + 2, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + } + + private void AddFacePaths(TextureSet textureSet) + { + if (_facePartList.SelectedIndex != 1) + { + textureSet.InternalDiffusePath = RacePaths.GetFaceTexturePath(0, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + _facePartList.SelectedIndex, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + } + + textureSet.InternalNormalPath = RacePaths.GetFaceTexturePath(1, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + _facePartList.SelectedIndex, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + + textureSet.InternalMultiPath = RacePaths.GetFaceTexturePath(2, _genderList.SelectedIndex, _subRaceList.SelectedIndex, + _facePartList.SelectedIndex, _faceTypeList.SelectedIndex, _auraFaceScalesDropdown.SelectedIndex, _asymCheckbox.Checked); + + if (_facePartList.SelectedIndex == 0) + { + if (_subRaceList.SelectedIndex == 10 || _subRaceList.SelectedIndex == 11) + { + if (_auraFaceScalesDropdown.SelectedIndex > 0) + { + if (_faceTypeList.SelectedIndex < 4) + { + if (_asymCheckbox.Checked) + { + textureSet.NormalCorrection = Path.Combine(_textureProcessor.BasePath, + @"res\textures\s" + (_genderList.SelectedIndex == 0 ? "m" : "f") + _faceTypeList.SelectedIndex + "a.png"); + } + else + { + textureSet.NormalCorrection = Path.Combine(_textureProcessor.BasePath, + @"res\textures\s" + (_genderList.SelectedIndex == 0 ? "m" : "f") + _faceTypeList.SelectedIndex + ".png"); + } + } + } + } + } + } + + private bool IsRunningUnderWine() + { + return Dalamud.Utility.Util.IsLinux(); + } + + + /// + /// Allows file changes to be autmatically pulled in game while the mod tab is open. Function is ignored while running on Wine. + /// + /// + private void AddWatcher(string path) + { + // Check to see if we are running under wine before attempting to watch for files changes. + if (!IsRunningUnderWine()) + { + string directory = Path.GetDirectoryName(path); + if (Directory.Exists(directory) && !string.IsNullOrWhiteSpace(path)) + { + FileSystemWatcher fileSystemWatcher = _watchers.ContainsKey(path) ? _watchers[path] : new FileSystemWatcher(); + fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite; + fileSystemWatcher.Changed += delegate (object sender, FileSystemEventArgs e) + { + if (e.Name.Contains(Path.GetFileName(path))) + { + Task.Run(() => Export(false)); + } + return; + }; + fileSystemWatcher.Path = directory; + fileSystemWatcher.EnableRaisingEvents = !string.IsNullOrEmpty(path); + _watchers[path] = fileSystemWatcher; + } + } + } + + /// + /// Untracks files from being automatically pulled in for changes. Function is ignored while running on Wine. + /// + private void DisposeWatcher(string path, LTCFilePicker filePicker) + { + // Check to see if we are running under wine. + if (!IsRunningUnderWine()) + { + if (!string.IsNullOrWhiteSpace(path)) + { + if (_watchers.ContainsKey(path)) + { + if (path != filePicker.CurrentPath) + { + _watchers[path].Dispose(); + _watchers.Remove(path); + } + } + } + } + } + #endregion + #region Export + public async Task Export(bool finalize) + { + if (!_lockDuplicateGeneration) + { + _exportStatus = "Initializing"; + _lockDuplicateGeneration = true; + List textureSets = new List(); + foreach (TextureSet item in _textureSets) + { + if (item.OmniExportMode) + { + UniversalTextureSetCreator.ConfigureOmniConfiguration(item); + } + textureSets.Add(item); + } + _textureProcessor.CleanGeneratedAssets(_selector.Selected.ModPath.FullName); + await _textureProcessor.Export( + textureSets, + _groupOptionTypes, + _selector.Selected.ModPath.FullName, + _choiceTypeList.SelectedIndex, + _bakeNormals.Checked, + _generateMulti.Checked, + File.Exists(_xNormalPath) && finalize, + _xNormalPath); + + _manager.ReloadMod(_currentMod); + _redrawService.RedrawObject(0, Api.Enums.RedrawType.Redraw); + _lockDuplicateGeneration = false; + } + return true; + } + #endregion + #region Classes + internal class LTCCheckBox + { + string _label = ""; + bool _isChecked = false; + bool _lastValue = false; + bool _enabled = true; + int _width = 100; + public LTCCheckBox(string _label, int width = 100) + { + this._label = _label; + this._width = width; + } + + public bool Enabled { get => _enabled; set => _enabled = value; } + public bool Checked { get => _isChecked; set => _isChecked = value; } + public int Width { get => (_enabled ? (45 + _width) : 0); set => _width = value; } + + public void Draw() + { + if (_enabled) + { + ImGui.Checkbox("##" + _label, ref _isChecked); + ImGui.SameLine(); + ImGui.SetNextItemWidth(_width); + ImGui.LabelText("##labelText" + _label, _label); + } + if (_isChecked != _lastValue) + { + if (OnCheckedChanged != null) + { + OnCheckedChanged.Invoke(this, EventArgs.Empty); + } + } + _lastValue = _isChecked; + } + + public event EventHandler OnCheckedChanged; + } + internal class LTCComboBox + { + string _label; + int _width; + int index = -1; + int _lastIndex; + bool _enabled = true; + string[] _contents; + public event EventHandler OnSelectedIndexChanged; + public string Text { get { return index > -1 ? _contents[index] : ""; } } + public LTCComboBox(string _label, string[] contents, int index, int width = 100) + { + this._label = _label; + this._width = width; + this.index = index; + this._contents = contents; + } + + public string[] Contents { get => _contents; set => _contents = value; } + public int SelectedIndex { get => index; set => index = value; } + public int Width { get => (_enabled ? _width : 0); set => _width = value; } + public string Label { get => _label; set => _label = value; } + public bool Enabled { get => _enabled; set => _enabled = value; } + + public void Draw() + { + if (_enabled) + { + ImGui.SetNextItemWidth(_width); + ImGui.Combo("##" + _label, ref index, _contents, _contents.Length); + } + if (index != _lastIndex) + { + if (OnSelectedIndexChanged != null) + { + OnSelectedIndexChanged.Invoke(this, EventArgs.Empty); + } + } + _lastIndex = index; + } + } + internal class LTCFilePicker + { + string _label = ""; + private Configuration _config; + string _filePath = ""; + private string _currentPath; + bool _enabled = true; + private string _lastText; + private FileDialogService _fileDialog; + public LTCFilePicker(string label, Configuration config, FileDialogService fileDialog) + { + _label = label; + _config = config; + _fileDialog = fileDialog; + OnTextChanged += LTCFilePicker_OnTextChanged; + } + + private void LTCFilePicker_OnTextChanged(object? sender, EventArgs e) + { + if (_filePath.ToLower().Contains("basetexbaked")) + { + _currentPath = ""; + _filePath = ""; + // Todo: this error needs to be communicated somehow. + Penumbra.Chat.NotificationMessage("Please remove the prefix 'baseTexBaked' from the file name! " + + "\r\n\r\nAlternatively, please use the source image that was used to generate this texture.", + "Invalid File", Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + else if (_filePath.Contains(".")) + { + if (CheckExtentions(_filePath)) + { + if (!_filePath.Contains("_generated") && !_filePath.Contains("-generated")) + { + _currentPath = _filePath; + if (OnFileSelected != null) + { + OnFileSelected.Invoke(this, EventArgs.Empty); + } + } + else + { + _currentPath = ""; + _filePath = ""; + Penumbra.Chat.NotificationMessage("Do not use autogenerated files here.", "Invalid File", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else + { + Penumbra.Chat.NotificationMessage("This is not a file this tool supports.", "Invalid File", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + else if (string.IsNullOrEmpty(_filePath)) + { + _currentPath = ""; + _filePath = ""; + if (OnFileSelected != null) + { + OnFileSelected.Invoke(this, EventArgs.Empty); + } + } + } + + public string CurrentPath + { + get => _currentPath; set + { + string newValue = value != null ? value : ""; + _currentPath = newValue; + _filePath = newValue; + } + } + + public Color BackColor { get; internal set; } + public string LabelName { get => _label; set => _label = value; } + public bool Enabled { get => _enabled; set => _enabled = value; } + public string FilePath { get => _filePath; set => _filePath = value; } + public static bool FileDialogInUse { get; private set; } + + public event EventHandler OnFileSelected; + public event EventHandler OnTextChanged; + public void Draw() + { + if (_enabled) + { + ImGui.SetNextItemWidth(80); + ImGui.LabelText("##labelText" + _label, _label); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetWindowContentRegionMax().X - 180); + ImGui.InputText("##" + _label, ref _filePath, 256); + ImGui.SameLine(); + + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.BeginDisabled(); + } + if (ImGui.Button("X" + "##" + _label)) + { + _filePath = ""; + } + if (!_config.DeleteModModifier.IsActive()) + { + ImGui.EndDisabled(); + } + ImGui.SameLine(); + if (_filePath != _currentPath) + { + OnTextChanged?.Invoke(this, EventArgs.Empty); + } + + if (ImGui.Button("Select" + "##" + LabelName)) + { + if (_fileDialog != null) + { + _fileDialog.OpenFilePicker("Import Texture", + "Textures{.png,.dds,.bmp,.tex}", (s, f) => + { + if (!s) + { + return; + } + string value = f[0]; + _filePath = value != null ? value : ""; + OnFileSelected?.Invoke(this, EventArgs.Empty); + }, 1, "", false); + } + } + _lastText = _filePath; + } + } + public static bool CheckExtentions(string file) + { + string[] extentions = new string[] { ".png", ".dds", ".bmp", ".tex" }; + foreach (string extention in extentions) + { + if (file.EndsWith(extention)) + { + return true; + } + } + return false; + } + + } + internal class LTCCustomPathConfigurator + { + TextureSet textureSet = new TextureSet(); + string _group = ""; + string _textureSetName = ""; + string _internalDiffusePath = ""; + string _internalNormalPath = ""; + string _internalMultiPath = ""; + LTCCheckBox _ignoreNormals; + LTCCheckBox _ignoreMulti; + LTCCheckBox _invertNormals; + string _normalCorrection = ""; + private LTCComboBox _groupChoiceType = null; + private string _diffuseLabel; + private string _normalLabel; + private string _multiLabel; + private string _errorText = ""; + + private bool _wasValidated; + public event EventHandler OnWantsToClose; + public LTCCustomPathConfigurator(string[] newGroupChoices) + { + List groupChoices = new List(); + groupChoices.Add("Use Global Setting"); + groupChoices.AddRange(newGroupChoices); + + _groupChoiceType = new LTCComboBox("Group Choice Type", groupChoices.ToArray(), 0, 150); + _ignoreNormals = new LTCCheckBox("Ignore Normals", 120); + _ignoreMulti = new LTCCheckBox("Ignore Multi", 120); + _invertNormals = new LTCCheckBox("Invert Normals", 120); + } + + public LTCComboBox GroupingType + { + get { return _groupChoiceType; } + } + public TextureSet TextureSet + { + get => textureSet; + set + { + textureSet = value; + if (textureSet != null) + { + _group = textureSet.GroupName; + _textureSetName = textureSet.TextureSetName; + + _internalDiffusePath = textureSet.InternalDiffusePath != null + ? textureSet.InternalDiffusePath : ""; + + _internalNormalPath = textureSet.InternalNormalPath != null + ? textureSet.InternalNormalPath : ""; + + _internalMultiPath = textureSet.InternalMultiPath != null + ? textureSet.InternalMultiPath : ""; + + _ignoreNormals.Checked = textureSet.IgnoreNormalGeneration; + _ignoreMulti.Checked = textureSet.IgnoreMultiGeneration; + _invertNormals.Checked = textureSet.InvertNormalGeneration; + + _normalCorrection = textureSet.NormalCorrection != null ? textureSet.NormalCorrection : ""; + } + _wasValidated = false; + } + } + + public bool WasValidated { get => _wasValidated; set => _wasValidated = value; } + + public void Draw() + { + int labelWidth = 120; + RefreshLabels(); + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##groupLabelText" + _group, "Group"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##group", ref _group, 256); + + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##groupChoiceType" + _group, "Group Choice Type"); + ImGui.SameLine(); + _groupChoiceType.Draw(); + + ImGui.Dummy(new System.Numerics.Vector2(0, 20)); + + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##textureSetNameLabel" + _textureSetName, "Texture Set Name"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##textureSet", ref _textureSetName, 256); + + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##diffuseLabel" + _diffuseLabel, _diffuseLabel); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##internalDiffuse", ref _internalDiffusePath, 256); + + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##normalLabel" + _normalLabel, _normalLabel); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##internalNormal", ref _internalNormalPath, 256); + + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##multiLabel" + _multiLabel, _multiLabel); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##internalMulti", ref _internalMultiPath, 256); + + if (!textureSet.InternalMultiPath.ToLower().Contains("catchlight")) + { + ImGui.SetNextItemWidth(labelWidth); + ImGui.LabelText("##normalCorrection", "Normal Correction"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X - labelWidth); + ImGui.InputText("##normalCorrection", ref _normalCorrection, 256); + _invertNormals.Draw(); + _ignoreNormals.Draw(); + _ignoreMulti.Draw(); + } + if (ImGui.Button("Confirm Changes")) + { + AcceptChanges(); + } + ImGui.SameLine(); + if (ImGui.Button("Cancel")) + { + Cancel(); + } + + ImGui.LabelText("##errorText", _errorText); + } + + private void Cancel() + { + _errorText = ""; + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + + public void AcceptChanges() + { + if (!string.IsNullOrWhiteSpace(_textureSetName)) + { + textureSet.GroupName = _group; + textureSet.TextureSetName = _textureSetName; + int validationCount = 0; + if (IsValidGamePathFormat(_internalDiffusePath)) + { + textureSet.InternalDiffusePath = _internalDiffusePath; + validationCount++; + } + else + { + _errorText = "Internal diffuse path is invalid. Make sure an in game path format is being used and that it points to a .tex file!"; + } + if (IsValidGamePathFormat(_internalNormalPath)) + { + textureSet.InternalNormalPath = _internalNormalPath; + validationCount++; + } + else + { + _errorText = "Internal normal path is invalid. Make sure an in game path format is being used and that it points to a .tex file!"; + } + if (IsValidGamePathFormat(_internalMultiPath)) + { + textureSet.InternalMultiPath = _internalMultiPath; + validationCount++; + } + else + { + _errorText = "Internal multi path is invalid. Make sure an in game path format is being used and that it points to a .tex file!"; + } + if (File.Exists(_normalCorrection) || string.IsNullOrEmpty(_normalCorrection)) + { + textureSet.NormalCorrection = _normalCorrection; + validationCount++; + } + else + { + _errorText = "Normal correction path is invalid."; + } + if (validationCount == 4) + { + textureSet.IgnoreNormalGeneration = _ignoreNormals.Checked; + textureSet.IgnoreMultiGeneration = _ignoreMulti.Checked; + textureSet.InvertNormalGeneration = _invertNormals.Checked; + _wasValidated = true; + _errorText = ""; + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + } + else + { + _errorText = "Please enter a name for your texture set!"; + } + } + public void RefreshLabels() + { + if (textureSet.InternalMultiPath != null + && textureSet.InternalMultiPath.ToLower().Contains("catchlight")) + { + _diffuseLabel = "Internal Normal"; + _normalLabel = "Internal Multi"; + _multiLabel = "Internal Catchlight"; + } + else + { + _diffuseLabel = "Internal Diffuse"; + _normalLabel = "Internal Normal"; + _multiLabel = "Internal Multi"; + } + } + public bool IsValidGamePathFormat(string input) + { + if ((input.Contains(@"\") || !input.Contains(".tex")) && !string.IsNullOrWhiteSpace(input)) + { + return false; + } + else + { + return true; + } + } + } + internal class LTCFindAndReplace + { + List _textureSets = new List(); + string _textureSetSearchString = ""; + string _groupSeatchString = ""; + LTCFilePicker _diffuse; + LTCFilePicker _normal; + LTCFilePicker _multi; + LTCFilePicker _glow; + LTCFilePicker _mask; + + private bool _wasValidated; + private string _errorText = ""; + + public event EventHandler OnWantsToClose; + + public LTCFindAndReplace(List textureSets, Configuration config, FileDialogService fileDialog) + { + _diffuse = new LTCFilePicker("Diffuse", config, fileDialog); + _normal = new LTCFilePicker("Normal", config, fileDialog); + _multi = new LTCFilePicker("Multi", config, fileDialog); + _glow = new LTCFilePicker("Glow", config, fileDialog); + _mask = new LTCFilePicker("Mask", config, fileDialog); + _textureSets = textureSets; + } + + public bool IsForEyes { get; internal set; } + public List TextureSets + { + get => _textureSets; + set + { + _textureSets = value; + } + } + + public bool WasValidated { get => _wasValidated; set => _wasValidated = value; } + public string TextureSetSearchString { get => _textureSetSearchString; set => _textureSetSearchString = value; } + public string GroupSearchString { get => _groupSeatchString; set => _groupSeatchString = value; } + public LTCFilePicker Diffuse { get => _diffuse; set => _diffuse = value; } + public LTCFilePicker Normal { get => _normal; set => _normal = value; } + public LTCFilePicker Multi { get => _multi; set => _multi = value; } + public LTCFilePicker Glow { get => _glow; set => _glow = value; } + public LTCFilePicker Mask { get => _mask; set => _mask = value; } + + public void Draw() + { + RefreshLabels(); + + ImGui.LabelText("##label1", "Find Texture Sets Containing This Name"); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X); + ImGui.InputText("##textureSetSearchString", ref _textureSetSearchString, 256); + ImGui.LabelText("##label2", "And In This Group"); + ImGui.SetNextItemWidth(ImGui.GetContentRegionMax().X); + ImGui.InputText("##groupSearchString", ref _groupSeatchString, 256); + ImGui.LabelText("##label3", "Then Replace Files With These"); + + _diffuse.Draw(); + _normal.Draw(); + _multi.Draw(); + _glow.Draw(); + _mask.Draw(); + + if (ImGui.Button("Confirm Replacement")) + { + AcceptChanges(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel")) + { + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + + ImGui.LabelText("##errorText", _errorText); + } + public void AcceptChanges() + { + if (!string.IsNullOrEmpty(_textureSetSearchString)) + { + foreach (TextureSet textureSet in _textureSets) + { + if (textureSet.TextureSetName.ToLower().Contains(_textureSetSearchString.ToLower()) + && textureSet.GroupName.ToLower().Contains(_groupSeatchString.ToLower())) + { + if (!string.IsNullOrEmpty(_diffuse.FilePath)) + { + textureSet.Diffuse = _diffuse.FilePath; + } + if (!string.IsNullOrEmpty(_normal.FilePath)) + { + textureSet.Normal = _normal.FilePath; + } + if (!string.IsNullOrEmpty(_multi.FilePath)) + { + textureSet.Multi = _multi.FilePath; + } + if (!string.IsNullOrEmpty(_mask.FilePath)) + { + textureSet.NormalMask = _mask.FilePath; + } + if (!string.IsNullOrEmpty(_glow.FilePath)) + { + textureSet.Glow = _glow.FilePath; + } + } + } + _wasValidated = true; + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + else + { + _errorText = "Search name value cannot be empty!"; + } + } + public void RefreshLabels() + { + if (IsForEyes) + { + _diffuse.LabelName = "Normal"; + _normal.LabelName = "Multi"; + _multi.LabelName = "Catchlight"; + } + else + { + _diffuse.LabelName = "Diffuse"; + _normal.LabelName = "Normal"; + _multi.LabelName = "Multi"; + } + } + } + internal class LTCTemplateConfigurator + { + ModPanelLooseAssetCompilerTab _compilerTab; + private bool _wasValidated; + string _groupName = "Default"; + private List _textureSets; + string _currentPath = ""; + private int _currentGenderIndex; + private int _currentRaceIndex; + + public LTCTemplateConfigurator(ModPanelLooseAssetCompilerTab compilerTab, List textureSets) + { + _compilerTab = compilerTab; + _textureSets = textureSets; + } + + public string GroupName { get => _groupName; set => _groupName = value; } + public bool WasValidated { get => _wasValidated; set => _wasValidated = value; } + + public event EventHandler OnWantsToClose; + public void OpenTemplate(string path, int genderIndex, int raceIndex) + { + _groupName = "Default"; + _currentPath = path; + _currentGenderIndex = genderIndex; + _currentRaceIndex = raceIndex; + } + public void Draw() + { + ImGui.LabelText("##groupNameLabel", "Enter Group Name For Template"); + ImGui.InputText("##groupName", ref _groupName, 256); + if (ImGui.Button("Confirm")) + { + using (StreamReader file = File.OpenText(_currentPath)) + { + JsonSerializer serializer = new JsonSerializer(); + ProjectFile projectFile = (ProjectFile)serializer.Deserialize(file, typeof(ProjectFile)); + if (_textureSets.Count + projectFile.TextureSets.Count < 10510) + { + foreach (TextureSet textureSet in projectFile.TextureSets) + { + if (!_groupName.Contains("Default")) + { + textureSet.GroupName = _groupName; + } + _compilerTab.AddWatcher(textureSet.Diffuse); + _compilerTab.AddWatcher(textureSet.Normal); + _compilerTab.AddWatcher(textureSet.Multi); + _compilerTab.AddWatcher(textureSet.NormalMask); + _compilerTab.AddWatcher(textureSet.Glow); + BackupTexturePaths.AddBackupPaths(_currentGenderIndex, _currentRaceIndex, textureSet); + } + _textureSets.AddRange(projectFile.TextureSets?.ToArray()); + } + else + { + Penumbra.Chat.NotificationMessage("Importing this template will go above the limit of 10510 texture sets", "Failed to import", + Dalamud.Interface.Internal.Notifications.NotificationType.Error); + } + } + _wasValidated = true; + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + ImGui.SameLine(); + if (ImGui.Button("Cancel")) + { + OnWantsToClose?.Invoke(this, EventArgs.Empty); + } + } + } + #endregion +} \ No newline at end of file diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 503e471f3..acdc2ebc2 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -21,13 +21,15 @@ private enum ModPanelTabType Conflicts, Collections, Edit, + AssetCompiler }; public readonly ModPanelSettingsTab Settings; public readonly ModPanelDescriptionTab Description; public readonly ModPanelCollectionsTab Collections; - public readonly ModPanelConflictsTab Conflicts; + public readonly ModPanelConflictsTab Conflicts; public readonly ModPanelChangedItemsTab ChangedItems; + public readonly ModPanelLooseAssetCompilerTab AssetCompiler; public readonly ModPanelEditTab Edit; private readonly ModEditWindow _modEditWindow; private readonly ModManager _modManager; @@ -38,7 +40,7 @@ private enum ModPanelTabType private Mod? _lastMod = null; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, - ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, + ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelLooseAssetCompilerTab assetCompiler, ModPanelEditTab edit, ModManager modManager, TutorialService tutorial, ModPanelCollectionsTab collections) { _modEditWindow = modEditWindow; @@ -50,6 +52,7 @@ public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, _modManager = modManager; _tutorial = tutorial; Collections = collections; + AssetCompiler = assetCompiler; Tabs = new ITab[] { @@ -59,6 +62,7 @@ public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ChangedItems, Collections, Edit, + AssetCompiler, }; } @@ -89,6 +93,7 @@ private ReadOnlySpan ToLabel(ModPanelTabType type) ModPanelTabType.Conflicts => Conflicts.Label, ModPanelTabType.Collections => Collections.Label, ModPanelTabType.Edit => Edit.Label, + ModPanelTabType.AssetCompiler => AssetCompiler.Label, _ => ReadOnlySpan.Empty, }; @@ -104,8 +109,8 @@ private ModPanelTabType ToType(ReadOnlySpan label) return ModPanelTabType.Conflicts; if (label == Collections.Label) return ModPanelTabType.Collections; - if (label == Edit.Label) - return ModPanelTabType.Edit; + if (label == AssetCompiler.Label) + return ModPanelTabType.AssetCompiler; return 0; } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 26a673671..65f74ab9d 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -28,35 +28,36 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[2.1.2, )", - "resolved": "2.1.2", - "contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "5.0.0", - "System.Text.Encoding.CodePages": "5.0.0" - } + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg==" + }, + "Lumina": { + "type": "Transitive", + "resolved": "3.10.2", + "contentHash": "2bm0s8yh9JsBrK93Pxaq66RkaQdQI5RGVz2c4IFJaRBH3OhueP50O9k0FA4+Z3m7Z+uHMM3Tz6b8qysK9LWvVw==" }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" }, - "Microsoft.NETCore.Platforms": { + "Microsoft.Win32.SystemEvents": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" }, - "System.Runtime.CompilerServices.Unsafe": { + "Newtonsoft.Json": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "System.Text.Encoding.CodePages": { + "System.Drawing.Common": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "resolved": "7.0.0", + "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" + "Microsoft.Win32.SystemEvents": "7.0.0" } }, "System.ValueTuple": { @@ -72,6 +73,15 @@ "System.ValueTuple": "4.5.0" } }, + "loosetexturecompilercore": { + "type": "Project", + "dependencies": { + "Lumina": "[3.10.2, )", + "Newtonsoft.Json": "[13.0.3, )", + "SixLabors.ImageSharp": "[3.0.1, )", + "System.Drawing.Common": "[7.0.0, )" + } + }, "ottergui": { "type": "Project" }, @@ -81,8 +91,8 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { - "Penumbra.Api": "[1.0.7, )", - "Penumbra.String": "[1.0.3, )" + "Penumbra.Api": "[1.0.8, )", + "Penumbra.String": "[1.0.4, )" } }, "penumbra.string": {