diff --git a/OtterGui b/OtterGui index c8394607..728dd8c3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c8394607addd29cb7f8ae3257f635a4486c40a63 +Subproject commit 728dd8c33f8b43f7a2725ac7c8886fe7cb3f04a9 diff --git a/Penumbra.GameData b/Penumbra.GameData index 97643cad..1c68fd5e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf +Subproject commit 1c68fd5efb23798d13154c1de0ad010db319abe2 diff --git a/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs new file mode 100644 index 00000000..18afa949 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveColorSetPreviewer.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading; +using Dalamud.Game; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.MaterialPreview; + +public sealed unsafe class LiveColorSetPreviewer : LiveMaterialPreviewerBase +{ + public const int TextureWidth = 4; + public const int TextureHeight = MtrlFile.ColorSet.RowArray.NumRows; + public const int TextureLength = TextureWidth * TextureHeight * 4; + + private readonly Framework _framework; + + private readonly Texture** _colorSetTexture; + private readonly Texture* _originalColorSetTexture; + + private Half[] _colorSet; + private bool _updatePending; + + public Half[] ColorSet + => _colorSet; + + public LiveColorSetPreviewer(IObjectTable objects, Framework framework, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + _framework = framework; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + throw new InvalidOperationException("Draw object doesn't have color set textures"); + + _colorSetTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + + _originalColorSetTexture = *_colorSetTexture; + if (_originalColorSetTexture == null) + throw new InvalidOperationException("Material doesn't have a color set"); + + Structs.TextureUtility.IncRef(_originalColorSetTexture); + + _colorSet = new Half[TextureLength]; + _updatePending = true; + + framework.Update += OnFrameworkUpdate; + } + + protected override void Clear(bool disposing, bool reset) + { + _framework.Update -= OnFrameworkUpdate; + + base.Clear(disposing, reset); + + if (reset) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); + } + else + { + Structs.TextureUtility.DecRef(_originalColorSetTexture); + } + } + + public void ScheduleUpdate() + { + _updatePending = true; + } + + private void OnFrameworkUpdate(Framework _) + { + if (!_updatePending) + return; + + _updatePending = false; + + if (!CheckValidity()) + return; + + var textureSize = stackalloc int[2]; + textureSize[0] = TextureWidth; + textureSize[1] = TextureHeight; + + var newTexture = Structs.TextureUtility.Create2D(Device.Instance(), textureSize, 1, 0x2460, 0x80000804, 7); + if (newTexture == null) + return; + + bool success; + lock (_colorSet) + { + fixed (Half* colorSet = _colorSet) + { + success = Structs.TextureUtility.InitializeContents(newTexture, colorSet); + } + } + + if (success) + { + var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); + } + else + { + Structs.TextureUtility.DecRef(newTexture); + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; + if (colorSetTextures == null) + return false; + + if (_colorSetTexture != colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs new file mode 100644 index 00000000..1b280b20 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -0,0 +1,149 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; + +namespace Penumbra.Interop.MaterialPreview; + +public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase +{ + private readonly ShaderPackage* _shaderPackage; + + private readonly uint _originalShPkFlags; + private readonly float[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; + + public LiveMaterialPreviewer(IObjectTable objects, MaterialInfo materialInfo) + : base(objects, materialInfo) + { + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + throw new InvalidOperationException("Material doesn't have a resource handle"); + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + throw new InvalidOperationException("Material doesn't have a ShPk resource handle"); + + _shaderPackage = shpkHandle->ShaderPackage; + if (_shaderPackage == null) + throw new InvalidOperationException("Material doesn't have a shader package"); + + var material = (Structs.Material*)Material; + + _originalShPkFlags = material->ShaderPackageFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter = materialParameter.ToArray(); + else + _originalMaterialParameter = Array.Empty(); + + _originalSamplerFlags = new uint[material->TextureCount]; + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + _originalSamplerFlags[i] = material->Textures[i].SamplerFlags; + } + + protected override void Clear(bool disposing, bool reset) + { + base.Clear(disposing, reset); + + if (reset) + { + var material = (Structs.Material*)Material; + + material->ShaderPackageFlags = _originalShPkFlags; + + if (material->MaterialParameter->TryGetBuffer(out var materialParameter)) + _originalMaterialParameter.AsSpan().CopyTo(materialParameter); + + for (var i = 0; i < _originalSamplerFlags.Length; ++i) + material->Textures[i].SamplerFlags = _originalSamplerFlags[i]; + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + if (!CheckValidity()) + return; + + ((Structs.Material*)Material)->ShaderPackageFlags = shPkFlags; + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + if (!CheckValidity()) + return; + + var constantBuffer = ((Structs.Material*)Material)->MaterialParameter; + if (constantBuffer == null) + return; + + if (!constantBuffer->TryGetBuffer(out var buffer)) + return; + + for (var i = 0; i < _shaderPackage->MaterialElementCount; ++i) + { + ref var parameter = ref _shaderPackage->MaterialElements[i]; + if (parameter.CRC == parameterCrc) + { + if ((parameter.Offset & 0x3) != 0 + || (parameter.Size & 0x3) != 0 + || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + return; + + value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + return; + } + } + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + if (!CheckValidity()) + return; + + var id = 0u; + var found = false; + + var samplers = (Structs.ShaderPackageUtility.Sampler*)_shaderPackage->Samplers; + for (var i = 0; i < _shaderPackage->SamplerCount; ++i) + { + if (samplers[i].Crc == samplerCrc) + { + id = samplers[i].Id; + found = true; + break; + } + } + + if (!found) + return; + + var material = (Structs.Material*)Material; + for (var i = 0; i < material->TextureCount; ++i) + { + if (material->Textures[i].Id == id) + { + material->Textures[i].SamplerFlags = (samplerFlags & 0xFFFFFDFF) | 0x000001C0; + break; + } + } + } + + protected override bool IsStillValid() + { + if (!base.IsStillValid()) + return false; + + var mtrlHandle = Material->MaterialResourceHandle; + if (mtrlHandle == null) + return false; + + var shpkHandle = ((Structs.MtrlResource*)mtrlHandle)->ShpkResourceHandle; + if (shpkHandle == null) + return false; + + if (_shaderPackage != shpkHandle->ShaderPackage) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs new file mode 100644 index 00000000..88369725 --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewerBase.cs @@ -0,0 +1,70 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.MaterialPreview; + +public abstract unsafe class LiveMaterialPreviewerBase : IDisposable +{ + private readonly IObjectTable _objects; + + public readonly MaterialInfo MaterialInfo; + public readonly CharacterBase* DrawObject; + protected readonly Material* Material; + + protected bool Valid; + + public LiveMaterialPreviewerBase(IObjectTable objects, MaterialInfo materialInfo) + { + _objects = objects; + + MaterialInfo = materialInfo; + var gameObject = MaterialInfo.GetCharacter(objects); + if (gameObject == nint.Zero) + throw new InvalidOperationException("Cannot retrieve game object."); + + DrawObject = (CharacterBase*)MaterialInfo.GetDrawObject(gameObject); + if (DrawObject == null) + throw new InvalidOperationException("Cannot retrieve draw object."); + + Material = MaterialInfo.GetDrawObjectMaterial(DrawObject); + if (Material == null) + throw new InvalidOperationException("Cannot retrieve material."); + + Valid = true; + } + + public void Dispose() + { + if (Valid) + Clear(true, IsStillValid()); + } + + public bool CheckValidity() + { + if (Valid && !IsStillValid()) + Clear(false, false); + return Valid; + } + + protected virtual void Clear(bool disposing, bool reset) + { + Valid = false; + } + + protected virtual bool IsStillValid() + { + var gameObject = MaterialInfo.GetCharacter(_objects); + if (gameObject == nint.Zero) + return false; + + if ((nint)DrawObject != MaterialInfo.GetDrawObject(gameObject)) + return false; + + if (Material != MaterialInfo.GetDrawObjectMaterial(DrawObject)) + return false; + + return true; + } +} diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs new file mode 100644 index 00000000..0146cf6f --- /dev/null +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Penumbra.Interop.ResourceTree; +using Penumbra.String; + +namespace Penumbra.Interop.MaterialPreview; + +public enum DrawObjectType +{ + PlayerCharacter, + PlayerMainhand, + PlayerOffhand, + PlayerVfx, + MinionCharacter, + MinionUnk1, + MinionUnk2, + MinionUnk3, +}; + +public readonly record struct MaterialInfo(DrawObjectType Type, int ModelSlot, int MaterialSlot) +{ + public nint GetCharacter(IObjectTable objects) + => GetCharacter(Type, objects); + + public static nint GetCharacter(DrawObjectType type, IObjectTable objects) + => type switch + { + DrawObjectType.PlayerCharacter => objects.GetObjectAddress(0), + DrawObjectType.PlayerMainhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerOffhand => objects.GetObjectAddress(0), + DrawObjectType.PlayerVfx => objects.GetObjectAddress(0), + DrawObjectType.MinionCharacter => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk1 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk2 => objects.GetObjectAddress(1), + DrawObjectType.MinionUnk3 => objects.GetObjectAddress(1), + _ => nint.Zero, + }; + + public nint GetDrawObject(nint address) + => GetDrawObject(Type, address); + + public static nint GetDrawObject(DrawObjectType type, IObjectTable objects) + => GetDrawObject(type, GetCharacter(type, objects)); + + public static unsafe nint GetDrawObject(DrawObjectType type, nint address) + { + var gameObject = (Character*)address; + if (gameObject == null) + return nint.Zero; + + return type switch + { + DrawObjectType.PlayerCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.PlayerMainhand => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.PlayerOffhand => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.PlayerVfx => *((nint*)&gameObject->DrawData.UnkF0 + 1), + DrawObjectType.MinionCharacter => (nint)gameObject->GameObject.GetDrawObject(), + DrawObjectType.MinionUnk1 => *((nint*)&gameObject->DrawData.MainHand + 1), + DrawObjectType.MinionUnk2 => *((nint*)&gameObject->DrawData.OffHand + 1), + DrawObjectType.MinionUnk3 => *((nint*)&gameObject->DrawData.UnkF0 + 1), + _ => nint.Zero, + }; + } + + public unsafe Material* GetDrawObjectMaterial(CharacterBase* drawObject) + { + if (drawObject == null) + return null; + + if (ModelSlot < 0 || ModelSlot >= drawObject->SlotCount) + return null; + + var model = drawObject->Models[ModelSlot]; + if (model == null) + return null; + + if (MaterialSlot < 0 || MaterialSlot >= model->MaterialCount) + return null; + + return model->Materials[MaterialSlot]; + } + + public static unsafe List FindMaterials(IObjectTable objects, string materialPath) + { + var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; + + var result = new List(Enum.GetValues().Length); + foreach (var type in Enum.GetValues()) + { + var drawObject = (CharacterBase*)GetDrawObject(type, objects); + if (drawObject == null) + continue; + + for (var i = 0; i < drawObject->SlotCount; ++i) + { + var model = drawObject->Models[i]; + if (model == null) + continue; + + for (var j = 0; j < model->MaterialCount; ++j) + { + var material = model->Materials[j]; + if (material == null) + continue; + + var mtrlHandle = material->MaterialResourceHandle; + var path = ResolveContext.GetResourceHandlePath((Structs.ResourceHandle*)mtrlHandle); + if (path == needle) + result.Add(new MaterialInfo(type, i, j)); + } + } + } + + return result; + } +} diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 90ee1a16..0cb854f3 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -65,26 +65,13 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide private ResourceNode CreateNodeFromGamePath(ResourceType type, nint sourceAddress, Utf8GamePath gamePath, bool @internal) => new(null, type, sourceAddress, gamePath, FilterFullPath(Collection.ResolvePath(gamePath) ?? new FullPath(gamePath)), @internal); - private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, - bool withName) + private unsafe ResourceNode? CreateNodeFromResourceHandle(ResourceType type, nint sourceAddress, ResourceHandle* handle, bool @internal, + bool withName) { - if (handle == null) - return null; - - var name = handle->FileName(); - if (name.IsEmpty) - return null; - - if (name[0] == (byte)'|') - { - var pos = name.IndexOf((byte)'|', 1); - if (pos < 0) - return null; - - name = name.Substring(pos + 1); - } - - var fullPath = new FullPath(Utf8GamePath.FromByteString(name, out var p) ? p : Utf8GamePath.Empty); + var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(handle), out var p) ? new FullPath(p) : FullPath.Empty; + if (fullPath.InternalName.IsEmpty) + return null; + var gamePaths = Collection.ReverseResolvePath(fullPath).ToList(); fullPath = FilterFullPath(fullPath); @@ -161,7 +148,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (mtrl == null) return null; - var resource = (MtrlResource*)mtrl->ResourceHandle; + var resource = mtrl->ResourceHandle; var node = CreateNodeFromResourceHandle(ResourceType.Mtrl, (nint) mtrl, &resource->Handle, false, WithNames); if (node == null) return null; @@ -182,7 +169,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (WithNames) { - var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null; + var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else @@ -286,4 +273,25 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide var i = index.GetOffset(array.Length); return i >= 0 && i < array.Length ? array[i] : null; } + + internal static unsafe ByteString GetResourceHandlePath(ResourceHandle* handle) + { + if (handle == null) + return ByteString.Empty; + + var name = handle->FileName(); + if (name.IsEmpty) + return ByteString.Empty; + + if (name[0] == (byte)'|') + { + var pos = name.IndexOf((byte)'|', 1); + if (pos < 0) + return ByteString.Empty; + + name = name.Substring(pos + 1); + } + + return name; + } } diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs new file mode 100644 index 00000000..3bbbeca9 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterBaseExt +{ + [FieldOffset( 0x0 )] + public CharacterBase CharacterBase; + + [FieldOffset( 0x258 )] + public Texture** ColorSetTextures; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ConstantBuffer.cs b/Penumbra/Interop/Structs/ConstantBuffer.cs new file mode 100644 index 00000000..52df6477 --- /dev/null +++ b/Penumbra/Interop/Structs/ConstantBuffer.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout(LayoutKind.Explicit, Size = 0x70)] +public unsafe struct ConstantBuffer +{ + [FieldOffset(0x20)] + public int Size; + + [FieldOffset(0x24)] + public int Flags; + + [FieldOffset(0x28)] + private void* _maybeSourcePointer; + + public bool TryGetBuffer(out Span buffer) + { + if ((Flags & 0x4003) == 0 && _maybeSourcePointer != null) + { + buffer = new Span(_maybeSourcePointer, Size >> 2); + return true; + } + else + { + buffer = null; + return false; + } + } +} diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 7af5cee4..33d83b06 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -9,6 +9,9 @@ public unsafe struct HumanExt [FieldOffset( 0x0 )] public Human Human; + [FieldOffset( 0x0 )] + public CharacterBaseExt CharacterBase; + [FieldOffset( 0x9E8 )] public ResourceHandle* Decal; diff --git a/Penumbra/Interop/Structs/Material.cs b/Penumbra/Interop/Structs/Material.cs index 7cee271e..7b66531c 100644 --- a/Penumbra/Interop/Structs/Material.cs +++ b/Penumbra/Interop/Structs/Material.cs @@ -3,17 +3,42 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; namespace Penumbra.Interop.Structs; -[StructLayout( LayoutKind.Explicit )] +[StructLayout( LayoutKind.Explicit, Size = 0x40 )] public unsafe struct Material { [FieldOffset( 0x10 )] - public ResourceHandle* ResourceHandle; + public MtrlResource* ResourceHandle; + + [FieldOffset( 0x18 )] + public uint ShaderPackageFlags; + + [FieldOffset( 0x20 )] + public uint* ShaderKeys; + + public int ShaderKeyCount + => (int)((uint*)Textures - ShaderKeys); [FieldOffset( 0x28 )] - public void* MaterialData; + public ConstantBuffer* MaterialParameter; [FieldOffset( 0x30 )] - public void** Textures; + public TextureEntry* Textures; - public Texture* Texture( int index ) => ( Texture* )Textures[3 * index + 1]; + [FieldOffset( 0x38 )] + public ushort TextureCount; + + public Texture* Texture( int index ) => Textures[index].ResourceHandle->KernelTexture; + + [StructLayout( LayoutKind.Explicit, Size = 0x18 )] + public struct TextureEntry + { + [FieldOffset( 0x00 )] + public uint Id; + + [FieldOffset( 0x08 )] + public TextureResourceHandle* ResourceHandle; + + [FieldOffset( 0x10 )] + public uint SamplerFlags; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ShaderPackageUtility.cs b/Penumbra/Interop/Structs/ShaderPackageUtility.cs new file mode 100644 index 00000000..5bf95f5b --- /dev/null +++ b/Penumbra/Interop/Structs/ShaderPackageUtility.cs @@ -0,0 +1,19 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +public static class ShaderPackageUtility +{ + [StructLayout(LayoutKind.Explicit, Size = 0xC)] + public unsafe struct Sampler + { + [FieldOffset(0x0)] + public uint Crc; + + [FieldOffset(0x4)] + public uint Id; + + [FieldOffset(0xA)] + public ushort Slot; + } +} diff --git a/Penumbra/Interop/Structs/TextureUtility.cs b/Penumbra/Interop/Structs/TextureUtility.cs new file mode 100644 index 00000000..ec9c4b71 --- /dev/null +++ b/Penumbra/Interop/Structs/TextureUtility.cs @@ -0,0 +1,36 @@ +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +public unsafe static class TextureUtility +{ + private static readonly Functions Funcs = new(); + + public static Texture* Create2D(Device* device, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + => ((delegate* unmanaged)Funcs.TextureCreate2D)(device, size, mipLevel, textureFormat, flags, unk); + + public static bool InitializeContents(Texture* texture, void* contents) + => ((delegate* unmanaged)Funcs.TextureInitializeContents)(texture, contents); + + public static void IncRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[2])(texture); + + public static void DecRef(Texture* texture) + => ((delegate* unmanaged)(*(void***)texture)[3])(texture); + + private sealed class Functions + { + [Signature("E8 ?? ?? ?? ?? 8B 0F 48 8D 54 24")] + public nint TextureCreate2D = nint.Zero; + + [Signature("E9 ?? ?? ?? ?? 8B 02 25")] + public nint TextureInitializeContents = nint.Zero; + + public Functions() + { + SignatureHelper.Initialise(this); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index acead332..d0e9504c 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -18,7 +18,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor where T : class, IWritable +public class FileEditor : IDisposable where T : class, IWritable { private readonly FileDialogService _fileDialog; private readonly IDataManager _gameData; @@ -26,7 +26,7 @@ public class FileEditor where T : class, IWritable public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) + Func parseFile) { _owner = owner; _gameData = gameData; @@ -60,11 +60,19 @@ public class FileEditor where T : class, IWritable DrawFilePanel(); } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; + public void Dispose() + { + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; + } + + private readonly string _tabName; + private readonly string _fileType; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private FileRegistry? _currentPath; private T? _currentFile; @@ -99,7 +107,9 @@ public class FileEditor where T : class, IWritable if (file != null) { _defaultException = null; - _defaultFile = _parseFile(file.Data); + (_defaultFile as IDisposable)?.Dispose(); + _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _defaultFile = _parseFile(file.Data, _defaultPath, false); } else { @@ -158,6 +168,7 @@ public class FileEditor where T : class, IWritable { _currentException = null; _currentPath = null; + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _changed = false; } @@ -181,10 +192,13 @@ public class FileEditor where T : class, IWritable try { var bytes = File.ReadAllBytes(_currentPath.File.FullName); - _currentFile = _parseFile(bytes); + (_currentFile as IDisposable)?.Dispose(); + _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. + _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { + (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _currentException = e; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index cd599f11..e1ba045d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,57 +13,77 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + + private bool DrawMaterialColorSetChange(MtrlTab tab, bool disabled) { - if( !file.ColorSets.Any( c => c.HasRows ) ) - { + if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.ColorSets.Any(c => c.HasRows)) return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Set", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + var hasAnyDye = tab.UseColorDyeSet; + + ColorSetCopyAllClipboardButton(tab.Mtrl, 0); + ImGui.SameLine(); + var ret = ColorSetPasteAllClipboardButton(tab, 0, disabled); + if (!disabled) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= ColorSetDyeableCheckbox(tab, ref hasAnyDye); } - ColorSetCopyAllClipboardButton( file, 0 ); - ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( file, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( file, disabled ); - - using var table = ImRaii.Table( "##ColorSets", 11, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); - if( !table ) + if (hasAnyDye) { - return false; + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(tab, disabled); } - ImGui.TableNextColumn(); - ImGui.TableHeader( string.Empty ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Row" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Diffuse" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Specular" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Emissive" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Gloss" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Tile" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Repeat" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); + using var table = ImRaii.Table("##ColorSets", hasAnyDye ? 11 : 9, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!table) + return false; - for( var j = 0; j < file.ColorSets.Length; ++j ) + ImGui.TableNextColumn(); + ImGui.TableHeader(string.Empty); + ImGui.TableNextColumn(); + ImGui.TableHeader("Row"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Diffuse"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Specular"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Emissive"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Gloss"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Tile"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Repeat"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Skew"); + if (hasAnyDye) { - using var _ = ImRaii.PushId( j ); - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye Preview"); + } + + for (var j = 0; j < tab.Mtrl.ColorSets.Length; ++j) + { + using var _ = ImRaii.PushId(j); + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) { - ret |= DrawColorSetRow( file, j, i, disabled ); + ret |= DrawColorSetRow(tab, j, i, disabled, hasAnyDye); ImGui.TableNextRow(); } } @@ -72,22 +92,20 @@ public partial class ModEditWindow } - private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static void ColorSetCopyAllClipboardButton(MtrlFile file, int colorSetIdx) { - if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) ) - { + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) return; - } try { - var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes(); - var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty; + var data1 = file.ColorSets[colorSetIdx].Rows.AsBytes(); + var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[colorSetIdx].Rows.AsBytes() : ReadOnlySpan.Empty; var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo( array ); - data2.TryCopyTo( array.AsSpan( data1.Length ) ); - var text = Convert.ToBase64String( array ); - ImGui.SetClipboardText( text ); + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); } catch { @@ -95,61 +113,64 @@ public partial class ModEditWindow } } - private bool DrawPreviewDye( MtrlFile file, bool disabled ) + private bool DrawPreviewDye(MtrlTab tab, bool disabled) { var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + var tt = dyeId == 0 + ? "Select a preview dye first." + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) { var ret = false; - for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + for (var j = 0; j < tab.Mtrl.ColorDyeSets.Length; ++j) { - for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) - { - ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId ); - } + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, j, i, dyeId); } + tab.UpdateColorSetPreview(); + return ret; } ImGui.SameLine(); var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - _stainService.StainCombo.Draw( label, dyeColor, string.Empty, true, gloss); + if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) + tab.UpdateColorSetPreview(); return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton(MtrlTab tab, int colorSetIdx, bool disabled) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) - { + if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) + || tab.Mtrl.ColorSets.Length <= colorSetIdx) return false; - } try { var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ) - { + var data = Convert.FromBase64String(text); + if (data.Length < Marshal.SizeOf()) return false; - } - ref var rows = ref file.ColorSets[ colorSetIdx ].Rows; - fixed( void* ptr = data, output = &rows ) + ref var rows = ref tab.Mtrl.ColorSets[colorSetIdx].Rows; + fixed (void* ptr = data, output = &rows) { - MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() ); - if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() - && file.ColorDyeSets.Length > colorSetIdx ) + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + && tab.Mtrl.ColorDyeSets.Length > colorSetIdx) { - ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows; - fixed( void* output2 = &dyeRows ) + ref var dyeRows = ref tab.Mtrl.ColorDyeSets[colorSetIdx].Rows; + fixed (void* output2 = &dyeRows) { - MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() ); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } + tab.UpdateColorSetPreview(); + return true; } catch @@ -158,279 +179,372 @@ public partial class ModEditWindow } } - private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + private static unsafe void ColorSetCopyClipboardButton(MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true ) ) - { - try - { - var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; - fixed( byte* ptr = data ) - { - MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); - MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); - } + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true)) + return; - var text = Convert.ToBase64String( data ); - ImGui.SetClipboardText( text ); - } - catch + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed (byte* ptr = data) { - // ignored + MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorSet.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorSet.Row.Size, &dye, 2); } + + var text = Convert.ToBase64String(data); + ImGui.SetClipboardText(text); + } + catch + { + // ignored } } - private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static bool ColorSetDyeableCheckbox(MtrlTab tab, ref bool dyeable) { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true ) ) + var ret = ImGui.Checkbox("Dyeable", ref dyeable); + + if (ret) { - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String( text ); - if( data.Length != MtrlFile.ColorSet.Row.Size + 2 - || file.ColorSets.Length <= colorSetIdx ) - { - return false; - } - - fixed( byte* ptr = data ) - { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; - if( colorSetIdx < file.ColorDyeSets.Length ) - { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); - } - } - - return true; - } - catch - { - // ignored - } + tab.UseColorDyeSet = dyeable; + if (dyeable) + tab.Mtrl.FindOrAddColorDyeSet(); + tab.UpdateColorSetPreview(); } - return false; + return ret; } - private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + private static unsafe bool ColorSetPasteFromClipboardButton(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled) { - static bool FixFloat( ref float val, float current ) + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true)) + return false; + + try { - val = ( float )( Half )val; + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + if (data.Length != MtrlFile.ColorSet.Row.Size + 2 + || tab.Mtrl.ColorSets.Length <= colorSetIdx) + return false; + + fixed (byte* ptr = data) + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorSet.Row*)ptr; + if (colorSetIdx < tab.Mtrl.ColorDyeSets.Length) + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] = *(MtrlFile.ColorDyeSet.Row*)(ptr + MtrlFile.ColorSet.Row.Size); + } + + tab.UpdateColorSetRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + } + + private static void ColorSetHighlightButton(MtrlTab tab, int rowIdx, bool disabled) + { + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Highlight this row on your character, if possible.", disabled || tab.ColorSetPreviewers.Count == 0, true); + + if (ImGui.IsItemHovered()) + tab.HighlightColorSetRow(rowIdx); + else if (tab.HighlightedColorSetRow == rowIdx) + tab.CancelColorSetHighlight(); + } + + private bool DrawColorSetRow(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye) + { + static bool FixFloat(ref float val, float current) + { + val = (float)(Half)val; return val != current; } - using var id = ImRaii.PushId( rowIdx ); - var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = file.ColorDyeSets.Length > colorSetIdx; - var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + using var id = ImRaii.PushId(rowIdx); + var row = tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx]; + var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); - ColorSetCopyClipboardButton( row, dye ); + ColorSetCopyClipboardButton(row, dye); ImGui.SameLine(); - var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + var ret = ColorSetPasteFromClipboardButton(tab, colorSetIdx, rowIdx, disabled); + ImGui.SameLine(); + ColorSetHighlightButton(tab, rowIdx, disabled); ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled( disabled ); - ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); - if( hasDye ) + using var dis = ImRaii.Disabled(disabled); + ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Diffuse = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Specular = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); ImGui.SameLine(); var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.SpecularStrength)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = tmpFloat; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if( hasDye ) + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Specular = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].SpecularStrength = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); - ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); - if( hasDye ) + ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => + { + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].Emissive = c; + tab.UpdateColorSetRowPreview(rowIdx); + }); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Emissive = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), HalfEpsilon, HalfMaxValue, "%.1f") + && FixFloat(ref tmpFloat, row.GlossStrength)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); - if( hasDye ) + ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); + if (hasDye) { ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => + { + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Gloss = b; + tab.UpdateColorSetRowPreview(rowIdx); + }, ImGuiHoveredFlags.AllowWhenDisabled); } ImGui.TableNextColumn(); int tmpInt = row.TileSet; - ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + ImGui.SetNextItemWidth(intSize); + if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") + && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + ImGui.SetNextItemWidth(floatSize); + if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) { - file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + tab.Mtrl.ColorSets[colorSetIdx].Rows[rowIdx].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.TableNextColumn(); - if( hasDye ) + if (hasDye) { - if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + ImGui.TableNextColumn(); + if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; + tab.Mtrl.ColorDyeSets[colorSetIdx].Rows[rowIdx].Template = _stainService.TemplateCombo.CurrentSelection; ret = true; + tab.UpdateColorSetRowPreview(rowIdx); } - ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); ImGui.TableNextColumn(); - ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); + ret |= DrawDyePreview(tab, colorSetIdx, rowIdx, disabled, dye, floatSize); } - else + else if (hasAnyDye) { ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } return ret; } - private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + private bool DrawDyePreview(MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; - if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { + if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) return false; - } - var values = entry[ ( int )stain ]; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + var values = entry[(int)stain]; + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), - "Apply the selected dye to this row.", disabled, true ); + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); - ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain ); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain); + if (ret) + tab.UpdateColorSetRowPreview(rowIdx); ImGui.SameLine(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); ImGui.SameLine(); using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + ImGui.SetNextItemWidth(floatSize); + ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); return ret; } - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) + private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") { - var ret = false; - var tmp = input; - if( ImGui.ColorEdit3( label, ref tmp, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) - && tmp != input ) + var ret = false; + var inputSqrt = PseudoSqrtRgb(input); + var tmp = inputSqrt; + if (ImGui.ColorEdit3(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) { - setter( tmp ); + setter(PseudoSquareRgb(tmp)); ret = true; } - if( letter.Length > 0 && ImGui.IsItemVisible() ) + if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImGui.CalcTextSize( letter ); - var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textSize = ImGui.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + ImGui.GetWindowDrawList().AddText(center, textColor, letter); } - ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); return ret; } -} \ No newline at end of file + + // Functions to deal with squared RGB values without making negatives useless. + + private static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : x * x; + + private static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + private static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + private static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + private static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + private static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs new file mode 100644 index 00000000..e5b16a47 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.GameData; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private interface IConstantEditor + { + bool Draw(Span values, bool disabled, float editorWidth); + } + + private sealed class FloatConstantEditor : IConstantEditor + { + public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); + + private readonly float? _minimum; + private readonly float? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, + string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = $"%.{Math.Min(precision, (byte)9)}f"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (values[valueIdx] - _bias) / _factor; + if (disabled) + { + ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } + else + { + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, + _maximum ?? 0.0f, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private float Clamp(float value) + => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); + } + + private sealed class IntConstantEditor : IConstantEditor + { + private readonly int? _minimum; + private readonly int? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = "%d"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); + if (disabled) + { + ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + } + else + { + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, + _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private int Clamp(int value) + => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); + } + + private sealed class ColorConstantEditor : IConstantEditor + { + private readonly bool _squaredRgb; + private readonly bool _clamped; + + public ColorConstantEditor(bool squaredRgb, bool clamped) + { + _squaredRgb = squaredRgb; + _clamped = clamped; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + switch (values.Length) + { + case 3: + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled) + return false; + + if (_squaredRgb) + value = PseudoSquareRgb(value); + if (_clamped) + value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); + value.CopyTo(values); + return true; + } + case 4: + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = PseudoSqrtRgb(value); + if (!ImGui.ColorEdit4("##0", ref value, + ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) + || disabled) + return false; + + if (_squaredRgb) + value = PseudoSquareRgb(value); + if (_clamped) + value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); + value.CopyTo(values); + return true; + } + default: return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); + } + } + } + + private sealed class EnumConstantEditor : IConstantEditor + { + private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; + + public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) + => _values = values; + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + using var id = ImRaii.PushId(valueIdx); + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var currentValue = values[valueIdx]; + var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label + ?? currentValue.ToString(CultureInfo.CurrentCulture); + ret = disabled + ? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly) + : DrawCombo(currentLabel, ref values[valueIdx]); + } + + return ret; + } + + private bool DrawCombo(string label, ref float currentValue) + { + using var c = ImRaii.Combo(string.Empty, label); + if (!c) + return false; + + var ret = false; + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + currentValue = value; + ret = true; + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + + return ret; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 753ad8e9..6bb8b8c8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,293 +1,783 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; -using Penumbra.Services; +using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; +using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private sealed class MtrlTab : IWritable + private sealed class MtrlTab : IWritable, IDisposable { + private const int ShpkPrefixLength = 16; + + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; - public uint NewKeyId; - public uint NewKeyDefault; - public uint NewConstantId; - public int NewConstantIdx; - public uint NewSamplerId; - public int NewSamplerIdx; + private string[]? _shpkNames; + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public JObject? AssociatedShpkDevkit; - public ShpkFile? AssociatedShpk; - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public float TextureLabelWidth; + public readonly string LoadedBaseDevkitPathName; + public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< string > ShaderKeyLabels = new(16); - public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); - public readonly List< int > MissingShaderKeyIndices = new(16); - public readonly List< uint > AvailableKeyValues = new(16); - public string VertexShaders = "Vertex Shaders: ???"; - public string PixelShaders = "Pixel Shaders: ???"; + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); + + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; + + // Textures & Samplers + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); + + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); - public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); - public readonly HashSet< uint > DefinedMaterialConstants = new(16); + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> + Constants)> Constants = new(16); - public string MaterialConstantLabel = "Constants###Constants"; - public IndexSet OrphanedMaterialValues = new(0, false); - public int AliasedMaterialValueCount; - public bool HasMalformedMaterialConstants; + // Live-Previewers + public readonly List MaterialPreviewers = new(4); + public readonly List ColorSetPreviewers = new(4); + public int HighlightedColorSetRow = -1; + public readonly Stopwatch HighlightTime = new(); - // Samplers - public readonly List< (string Label, string FileName) > Samplers = new(4); - public readonly List< (string Name, uint Id) > MissingSamplers = new(4); - public readonly HashSet< uint > DefinedSamplers = new(4); - public IndexSet OrphanedSamplers = new(0, false); - public int AliasedSamplerCount; - - public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath ) + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { - defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name ); - if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) ) - { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true)) return FullPath.Empty; - } - return _edit.FindBestMatch( defaultGamePath ); + return _edit.FindBestMatch(defaultGamePath); } - public void LoadShpk( FullPath path ) + public string[] GetShpkNames() { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + + public void LoadShpk(FullPath path) + { + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + try { LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : _edit._dalamud.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); + ? File.ReadAllBytes(LoadedShpkPath.FullName) + : _edit._dalamud.GameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); LoadedShpkPathName = path.ToPath(); } - catch( Exception e ) + catch (Exception e) { LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.Chat.NotificationMessage($"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", + NotificationType.Error); } + if (LoadedShpkPath.InternalName.IsEmpty) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } + + UpdateShaderKeys(); Update(); } - public void UpdateTextureLabels() + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) { - var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); - TextureLabels.Clear(); - TextureLabelWidth = 50f * UiHelpers.Scale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + try { - for( var i = 0; i < Mtrl.Textures.Length; ++i ) + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); + + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } + } + + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; + + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - TextureLabels.Add( name ); - TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); + var selector = BuildSelector(data["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; + } + + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + private void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + foreach (var key in AssociatedShpk.MaterialKeys) + { + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); + + return ($"0x{value:X8}", value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); + } + else + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); + } + + private void UpdateShaders() + { + VertexShaders.Clear(); + PixelShaders.Clear(); + if (AssociatedShpk == null) + { + ShadersKnown = false; + } + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) + { + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + foreach (var pass in node.Value.Passes) + { + VertexShaders.Add((int)pass.VertexShader); + PixelShaders.Add((int)pass.PixelShader); + } + else + ShadersKnown = false; + } + } + } + } + + var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); + var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); + + VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + } + + private void UpdateTextures() + { + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); + } + + foreach (var samplerId in SamplerIds) + { + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (shpkSampler is not { Slot: 2 }) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); + } + + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.FindOrAddColorSet(); + } + + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); + + TextureLabelWidth = 50f * UiHelpers.Scale; + + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } + + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); } } TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateShaderKeyLabels() + private void UpdateConstants() { - ShaderKeyLabels.Clear(); - DefinedShaderKeys.Clear(); - foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) + static List FindOrAddGroup(List<(string, List)> groups, string name) { - ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); - DefinedShaderKeys.Add( key.Category, key.Value ); - } - - MissingShaderKeyIndices.Clear(); - AvailableKeyValues.Clear(); - var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); - var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); - if( AssociatedShpk != null ) - { - MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); - - if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) ) + foreach (var (groupName, group) in groups) { - var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; - NewKeyId = key.Id; - NewKeyDefault = key.DefaultValue; + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; } - AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); - foreach( var node in AssociatedShpk.Nodes ) + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; + } + + Constants.Clear(); + if (AssociatedShpk == null) + { + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) { - if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + var values = Mtrl.GetConstantValues(constant); + for (var i = 0; i < values.Length; i += 4) { - foreach( var pass in node.Passes ) + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + FloatConstantEditor.Default)); + } + } + } + else + { + var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + if ((shpkConstant.ByteSize & 0x3) != 0) + continue; + + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) + foreach (var dkConstant in dkData) { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); + var offset = (int)dkConstant.Offset; + var length = values.Length - offset; + if (dkConstant.Length.HasValue) + length = Math.Min(length, (int)dkConstant.Length.Value); + if (length <= 0) + continue; + + var editor = dkConstant.CreateEditor(); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } + + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(true)) + { + if ((shpkConstant.ByteOffset & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset >> 2; + for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 4, end); + if (rangeEnd > rangeStart) + fcGroup.Add(( + $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + for (var i = start; i < end; i += 4) + { + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, + FloatConstantEditor.Default)); + } } } } } - VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; - PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; - } - - public void UpdateConstantLabels() - { - var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; - MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; - - DefinedMaterialConstants.Clear(); - MaterialConstants.Clear(); - HasMalformedMaterialConstants = false; - AliasedMaterialValueCount = 0; - OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true ); - foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() ) + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => { - DefinedMaterialConstants.Add( constant.Id ); - var values = Mtrl.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) - { - var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam?.ByteOffset ?? -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; - var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - AliasedMaterialValueCount += values.Length - unique; - } - else - { - HasMalformedMaterialConstants = true; - } - - var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); - var label = name == null - ? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}" - : $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}"; - - MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) ); - } - - MissingMaterialConstants.Clear(); - if( AssociatedShpk != null ) + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme + foreach (var (_, group) in Constants) { - var setIdx = false; - foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) ) - { - var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); - var label = name == null - ? $"(ID: 0x{param.Id:X8})" - : $"{name} (ID: 0x{param.Id:X8})"; - if( NewConstantId == param.Id ) - { - setIdx = true; - NewConstantIdx = MissingMaterialConstants.Count; - } - - MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); - } - - if( !setIdx && MissingMaterialConstants.Count > 0 ) - { - NewConstantIdx = 0; - NewConstantId = MissingMaterialConstants[ 0 ].Id; - } + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); } } - public void UpdateSamplers() + public unsafe void BindToMaterialInstances() { - Samplers.Clear(); - DefinedSamplers.Clear(); - OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true ); - foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() ) - { - DefinedSamplers.Add( sampler.SamplerId ); - if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) - { - ++AliasedSamplerCount; - } + UnbindFromMaterialInstances(); - var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - var label = shpk.HasValue - ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" - : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; - var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; - Samplers.Add( ( label, fileName ) ); + var instances = MaterialInfo.FindMaterials(_edit._dalamud.Objects, FilePath); + + var foundMaterials = new HashSet(); + foreach (var materialInfo in instances) + { + var drawObject = (CharacterBase*)MaterialInfo.GetDrawObject(materialInfo.Type, _edit._dalamud.Objects); + var material = materialInfo.GetDrawObjectMaterial(drawObject); + if (foundMaterials.Contains((nint)material)) + continue; + + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._dalamud.Objects, materialInfo)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } } - MissingSamplers.Clear(); - if( AssociatedShpk != null ) + UpdateMaterialPreview(); + + var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + + if (!colorSet.HasValue) + return; + + foreach (var materialInfo in instances) { - var setSampler = false; - foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) + try { - if( sampler.Id == NewSamplerId ) - { - setSampler = true; - NewSamplerIdx = MissingSamplers.Count; - } - - MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); + ColorSetPreviewers.Add(new LiveColorSetPreviewer(_edit._dalamud.Objects, _edit._dalamud.Framework, materialInfo)); } - - if( !setSampler && MissingSamplers.Count > 0 ) + catch (InvalidOperationException) { - NewSamplerIdx = 0; - NewSamplerId = MissingSamplers[ 0 ].Id; + // Carry on without that previewer. } } + + UpdateColorSetPreview(); + } + + private void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorSetPreviewers) + previewer.Dispose(); + ColorSetPreviewers.Clear(); + } + + private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0;) + { + var previewer = MaterialPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorSetPreviewers.Count; i-- > 0;) + { + var previewer = ColorSetPreviewers[i]; + if ((nint)previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorSetPreviewers.RemoveAt(i); + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + private void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValues(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + public void HighlightColorSetRow(int rowIdx) + { + var oldRowIdx = HighlightedColorSetRow; + + if (HighlightedColorSetRow != rowIdx) + { + HighlightedColorSetRow = rowIdx; + HighlightTime.Restart(); + } + + if (oldRowIdx >= 0) + UpdateColorSetRowPreview(oldRowIdx); + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public void CancelColorSetHighlight() + { + var rowIdx = HighlightedColorSetRow; + + HighlightedColorSetRow = -1; + HighlightTime.Reset(); + + if (rowIdx >= 0) + UpdateColorSetRowPreview(rowIdx); + } + + public void UpdateColorSetRowPreview(int rowIdx) + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var row = colorSet.Rows[rowIdx]; + if (maybeColorDyeSet.HasValue && UseColorDyeSet) + { + var stm = _edit._stainService.StmFile; + var dye = maybeColorDyeSet.Value.Rows[rowIdx]; + if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) + row.ApplyDyeTemplate(dye, dyes); + } + + if (HighlightedColorSetRow == rowIdx) + ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorSetPreviewers) + { + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan() + .Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); + previewer.ScheduleUpdate(); + } + } + + public void UpdateColorSetPreview() + { + if (ColorSetPreviewers.Count == 0) + return; + + var maybeColorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); + if (!maybeColorSet.HasValue) + return; + + var colorSet = maybeColorSet.Value; + var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); + + var rows = colorSet.Rows; + if (maybeColorDyeSet.HasValue && UseColorDyeSet) + { + var stm = _edit._stainService.StmFile; + var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; + var colorDyeSet = maybeColorDyeSet.Value; + for (var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i) + { + ref var row = ref rows[i]; + var dye = colorDyeSet.Rows[i]; + if (stm.TryGetValue(dye.Template, stainId, out var dyes)) + row.ApplyDyeTemplate(dye, dyes); + } + } + + if (HighlightedColorSetRow >= 0) + ApplyHighlight(ref rows[HighlightedColorSetRow], (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorSetPreviewers) + { + rows.AsHalves().CopyTo(previewer.ColorSet); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, float time) + { + var level = Math.Sin(time * 2.0 * Math.PI) * 0.25 + 0.5; + var levelSq = (float)(level * level); + + row.Diffuse = Vector3.Zero; + row.Specular = Vector3.Zero; + row.Emissive = new Vector3(levelSq); } public void Update() { - UpdateTextureLabels(); - UpdateShaderKeyLabels(); - UpdateConstantLabels(); - UpdateSamplers(); + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); } - public MtrlTab( ModEditWindow edit, MtrlFile file ) + public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) { - _edit = edit; - Mtrl = file; - LoadShpk( FindAssociatedShpk( out _, out _ ) ); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + LoadShpk(FindAssociatedShpk(out _, out _)); + if (writable) + { + _edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances; + BindToMaterialInstances(); + } + } + + public void Dispose() + { + UnbindFromMaterialInstances(); + if (Writable) + _edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances; } public bool Valid - => Mtrl.Valid; + => ShadersKnown && Mtrl.Valid; public byte[] Write() - => Mtrl.Write(); + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet); + + return output.Write(); + } + + private sealed record DevkitShaderKeyValue(string Label = "", string Description = ""); + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = new(); + } + + private sealed record DevkitSampler(string Label = "", string Description = "", string DefaultTexture = ""); + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + Integer = 1, + Color = 2, + Enum = 3, + } + + private sealed record DevkitConstantValue(string Label = "", string Description = "", float Value = 0); + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = Array.Empty(); + + public IConstantEditor? CreateEditor() + => Type switch + { + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, + Unit), + DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Factor, Bias, Unit), + DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), + DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, + value => (value.Label, value.Value, value.Description))), + _ => FloatConstantEditor.Default, + }; + + private static int? ToInteger(float? value) + => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; + } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 16ad708c..8fca8aa6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,16 +1,12 @@ using System; -using System.IO; -using System.Linq; +using System.Collections.Generic; using System.Numerics; using System.Text; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; -using Lumina.Data.Parsing; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; -using Penumbra.GameData.Files; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -19,37 +15,107 @@ public partial class ModEditWindow { private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput(MtrlTab tab, bool disabled) + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + private static readonly IReadOnlyList StandardShaderPackages = new[] { - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bg.shpk", + "bguvscroll.shpk", + "channeling.shpk", + "characterglass.shpk", + "character.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "grass.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private enum TextureAddressMode : uint + { + Wrap = 0, + Mirror = 1, + Clamp = 2, + Border = 3, + } + + private static readonly IReadOnlyList TextureAddressModeTooltips = new[] + { + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", + "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", + }; + + private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) + { + if (disabled) { - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); + return false; } - if (ImGui.IsItemDeactivatedAfterEdit()) - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in tab.GetShpkNames()) + { + if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) + { + tab.Mtrl.ShaderPackage.Name = value; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + } + } return ret; } - private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled) + private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) { - var ret = false; - var shpkFlags = (int)file.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - file.ShaderPackage.Flags = (uint)shpkFlags; - ret = true; - } + return false; - return ret; + tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + tab.SetShaderPackageFlags((uint)shpkFlags); + return true; } /// @@ -58,16 +124,22 @@ public partial class ModEditWindow /// private void DrawCustomAssociations(MtrlTab tab) { + const string tooltip = "Click to copy file path to clipboard."; var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; + var devkitText = tab.AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; + var baseDevkitText = tab.AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (ImGui.Selectable(text)) - ImGui.SetClipboardText(tab.LoadedShpkPathName); - - ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip); if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => @@ -93,94 +165,56 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - - private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) - { - var ret = false; - using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); - if (!t2 || disabled) - return ret; - - var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - if (shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); - if (c) - foreach (var value in shpkKey.Value.Values) - { - if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) - { - tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - if (ImGui.Button("Remove Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - - private static bool DrawNewShaderKey(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - var ret = false; - using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) - { - if (c) - foreach (var idx in tab.MissingShaderKeyIndices) - { - var key = tab.AssociatedShpk!.MaterialKeys[idx]; - - if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) - { - tab.NewKeyDefault = key.DefaultValue; - tab.NewKeyId = key.Id; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey - { - Category = tab.NewKeyId, - Value = tab.NewKeyDefault, - }); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) - return false; - - using var t = ImRaii.TreeNode("Shader Keys"); - if (!t) + if (tab.ShaderKeys.Count == 0) return false; var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) - ret |= DrawShaderKey(tab, disabled, ref idx); + foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = + values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + { + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + tab.Update(); + } - if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) - ret |= DrawNewShaderKey(tab); + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + { + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); + } + else + { + ImGui.TextUnformatted($"{label}: {currentLabel}"); + } + } return ret; } @@ -190,160 +224,65 @@ public partial class ModEditWindow if (tab.AssociatedShpk == null) return; - ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - } + ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) - { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name); - if (!t2) - return false; - - font.Dispose(); - - var constant = tab.Mtrl.ShaderPackage.Constants[idx]; - var ret = false; - var values = tab.Mtrl.GetConstantValues(constant); - if (values.Length > 0) + if (tab.ShaderComment.Length > 0) { - var valueOffset = constant.ByteOffset >> 2; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - } - } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(tab.ShaderComment); } - else - { - ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (!disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button("Remove Constant")) - { - tab.Mtrl.ShaderPackage.ShaderValues = - tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) - tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - - ret = true; - tab.UpdateConstantLabels(); - } - - return ret; - } - - private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) - { - using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); - if (!t2) - return false; - - var ret = false; - foreach (var idx in tab.OrphanedMaterialValues) - { - ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); - if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - } - } - - return ret; - } - - private static bool DrawNewMaterialParam(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - { - using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); - if (c) - foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) - { - if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) - { - tab.NewConstantIdx = idx; - tab.NewConstantId = constant.Id; - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Constant")) - { - var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant - { - Id = tab.NewConstantId, - ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), - ByteSize = byteSize, - }); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); - tab.UpdateConstantLabels(); - return true; - } - - return false; } private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) + if (tab.Constants.Count == 0) return false; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); - if (!t) + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) return false; - font.Dispose(); + using var _ = ImRaii.PushId("MaterialConstants"); + var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) - ret |= DrawMaterialConstantValues(tab, disabled, ref idx); + foreach (var (header, group) in tab.Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; - if (tab.OrphanedMaterialValues.Count > 0) - ret |= DrawMaterialOrphans(tab, disabled); - else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) - ret |= DrawNewMaterialParam(tab); + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = tab.Mtrl.GetConstantValues(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + if (editor.Draw(buffer[slice], disabled, 250.0f)) + { + ret = true; + tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } return ret; } - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) { - var (label, filename) = tab.Samplers[idx]; - using var tree = ImRaii.TreeNode(label); - if (!tree) - return false; - - ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); - var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; + var ret = false; + ref var texture = ref tab.Mtrl.Textures[textureIdx]; + ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; // FIXME this probably doesn't belong here static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) @@ -354,125 +293,134 @@ public partial class ModEditWindow } } - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) + { + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == current)) + { + samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); + ret = true; + } + + ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); + } + + return ret; + } + + var dx11 = texture.DX11; + if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) + | ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10)); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = (int)((sampler.Flags >> 20) & 0xF); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + + using var t = ImRaii.TreeNode("Advanced Settings"); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (InputHexUInt16("Texture Flags", ref texture.Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; - ret = true; - } - - if (!disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button("Remove Sampler")) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; - } - - ret = true; - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); + sampler.Flags = (uint)samplerFlags; + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); } return ret; } - private static bool DrawMaterialNewSampler(MtrlTab tab) - { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) - { - if (c) - foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) - { - if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) - { - tab.NewSamplerIdx = idx; - tab.NewSamplerId = sampler.Id; - } - } - } - - ImGui.SameLine(); - if (!ImGui.Button("Add Sampler")) - return false; - - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler - { - SamplerId = tab.NewSamplerId, - TextureIndex = (byte)tab.Mtrl.Textures.Length, - Flags = 0, - }); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - }); - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); - return true; - } - - private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) - { - if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) - return false; - - using var t = ImRaii.TreeNode("Samplers"); - if (!t) - return false; - - var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) - ret |= DrawMaterialSampler(tab, disabled, ref idx); - - if (tab.OrphanedSamplers.Count > 0) - { - using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); - if (t2) - foreach (var idx in tab.OrphanedSamplers) - { - ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", - ImGuiTreeNodeFlags.Leaf) - .Dispose(); - } - } - else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) - { - ret |= DrawMaterialNewSampler(tab); - } - - return ret; - } - - private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) + private bool DrawMaterialShader(MtrlTab tab, bool disabled) { var ret = false; - if (!ImGui.CollapsingHeader("Advanced Shader Resources")) - return ret; + if (ImGui.CollapsingHeader(tab.ShaderHeader)) + { + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); + } + + if (tab.AssociatedShpkDevkit == null) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + GC.KeepAlive(tab); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = + (textColor & 0xFF000000u) + | ((textColor & 0x00FEFEFE) >> 1) + | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted(tab.AssociatedShpk == null + ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." + : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); + } - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab.Mtrl, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - ret |= DrawMaterialConstants(tab, disabled); - ret |= DrawMaterialSamplers(tab, disabled); return ret; } @@ -495,25 +443,25 @@ public partial class ModEditWindow }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz ", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw ", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; + private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - if (valueLength == 0 || valueOffset < 0) return (null, false); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index d7e23ac3..102a6778 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -6,189 +6,221 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private readonly FileEditor< MtrlTab > _materialTab; + private readonly FileEditor _materialTab; - private bool DrawMaterialPanel( MtrlTab tab, bool disabled ) + private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - var ret = DrawMaterialTextureChange( tab, disabled ); + DrawMaterialLivePreviewRebind(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawMaterialShader(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( tab, disabled ); + ret |= DrawMaterialTextureChange(tab, disabled); + ret |= DrawMaterialColorSetChange(tab, disabled); + ret |= DrawMaterialConstants(tab, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - DrawOtherMaterialDetails( tab.Mtrl, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(tab.Mtrl, disabled); return !disabled && ret; } - private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) + private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) { - var ret = false; - using var table = ImRaii.Table( "##Textures", 2 ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) + if (disabled) + return; + + if (ImGui.Button("Reload live preview")) + tab.BindToMaterialInstances(); + + if (tab.MaterialPreviewers.Count != 0 || tab.ColorSetPreviewers.Count != 0) + return; + + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGui.TextUnformatted( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."); + } + + private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled) + { + if (tab.Textures.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table("##Textures", 3); + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) { - using var _ = ImRaii.PushId( i ); - var tmp = tab.Mtrl.Textures[ i ].Path; + using var _ = ImRaii.PushId(samplerI); + var tmp = tab.Mtrl.Textures[textureI].Path; + var unfolded = tab.UnfoldedTextures.Contains(samplerI); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ i ].Path ) + if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), + new Vector2(frameHeight), + "Settings for this texture and the associated sampler", false, true)) { - ret = true; - tab.Mtrl.Textures[ i ].Path = tmp; + unfolded = !unfolded; + if (unfolded) + tab.UnfoldedTextures.Add(samplerI); + else + tab.UnfoldedTextures.Remove(samplerI); } ImGui.TableNextColumn(); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( tab.TextureLabels[ i ] ); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != tab.Mtrl.Textures[textureI].Path) + { + ret = true; + tab.Mtrl.Textures[textureI].Path = tmp; + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + + if (unfolded) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI); + ImGui.TableNextColumn(); + } } return ret; } - private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled ) + private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled) { const uint transparencyBit = 0x10; const uint backfaceBit = 0x01; var ret = false; - using var dis = ImRaii.Disabled( disabled ); + using var dis = ImRaii.Disabled(disabled); - var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0; - if( ImGui.Checkbox( "Enable Transparency", ref tmp ) ) + var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = + tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } - ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); - tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; - if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) { - file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit; - ret = true; + tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; + ret = true; + tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); } return ret; } - private static void DrawOtherMaterialDetails( MtrlFile file, bool _ ) + private static void DrawOtherMaterialDetails(MtrlFile file, bool _) { - if( !ImGui.CollapsingHeader( "Further Content" ) ) - { + if (!ImGui.CollapsingHeader("Further Content")) return; + + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in file.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) ) - { - if( sets ) - { - foreach( var set in file.UvSets ) - { - ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - - if( file.AdditionalData.Length <= 0 ) - { + if (file.AdditionalData.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } + using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}"))); } private void DrawMaterialReassignmentTab() { - if( _editor.Files.Mdl.Count == 0 ) - { + if (_editor.Files.Mdl.Count == 0) return; - } - using var tab = ImRaii.TabItem( "Material Reassignment" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Material Reassignment"); + if (!tab) return; - } ImGui.NewLine(); - MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) return; - } var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex() ) + foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) { - using var id = ImRaii.PushId( idx ); + using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) info.Save(); - } ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true ) ) - { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true)) info.Restore(); - } ImGui.TableNextColumn(); - ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - var tmp = info.CurrentMaterials[ 0 ]; - if( ImGui.InputText( "##0", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, 0 ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + var tmp = info.CurrentMaterials[0]; + if (ImGui.InputText("##0", ref tmp, 64)) + info.SetMaterial(tmp, 0); - for( var i = 1; i < info.Count; ++i ) + for (var i = 1; i < info.Count; ++i) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); - tmp = info.CurrentMaterials[ i ]; - if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, i ); - } + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + tmp = info.CurrentMaterials[i]; + if (ImGui.InputText($"##{i}", ref tmp, 64)) + info.SetMaterial(tmp, i); } } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index b212e791..518566f5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -5,7 +5,6 @@ using Penumbra.GameData.Files; using Penumbra.String.Classes; using System.Globalization; using System.Linq; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 4e8a4f45..c0868b71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -14,33 +14,38 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String; -using Penumbra.UI.AdvancedWindow; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true ); + private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); - private readonly FileEditor< ShpkTab > _shaderPackageTab; + private readonly FileEditor _shaderPackageTab; - private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled ) + private static bool DrawShaderPackagePanel(ShpkTab file, bool disabled) { - DrawShaderPackageSummary( file ); + DrawShaderPackageSummary(file); var ret = false; - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageShaderArray(file, "Pixel Shader", file.Shpk.PixelShaders, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageMaterialParamLayout(file, disabled); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherShaderPackageDetails( file, disabled ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderPackageResources(file, disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageSelection(file); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherShaderPackageDetails(file); file.FileDialog.Draw(); @@ -49,17 +54,26 @@ public partial class ModEditWindow return !disabled && ret; } - private static void DrawShaderPackageSummary( ShpkTab tab ) - => ImGui.TextUnformatted( tab.Header ); - - private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) + private static void DrawShaderPackageSummary(ShpkTab tab) { - if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) ) + ImGui.TextUnformatted(tab.Header); + if (!tab.Shpk.Disassembled) { - return; - } + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - var defaultName = objectName[ 0 ] switch + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing."); + } + } + + private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) + { + if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) + return; + + var defaultName = objectName[0] switch { 'V' => $"vs{idx}", 'P' => $"ps{idx}", @@ -67,244 +81,225 @@ public partial class ModEditWindow }; var blob = shader.Blob; - tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenSavePicker($"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, + (success, name) => { - return; - } + if (!success) + return; - try - { - File.WriteAllBytes( name, blob ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + File.WriteAllBytes(name, blob); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - Penumbra.Chat.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", - "Penumbra Advanced Editing", NotificationType.Success ); - }, null, false ); + Penumbra.Chat.NotificationMessage( + $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName(name)}", + "Penumbra Advanced Editing", NotificationType.Success); + }, null, false); } - private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) + private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if( !ImGui.Button( "Replace Shader Program Blob" ) ) - { + if (!ImGui.Button("Replace Shader Program Blob")) return; - } - tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => - { - if( !success ) + tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", + (success, name) => { - return; - } + if (!success) + return; - try - { - shaders[ idx ].Blob = File.ReadAllBytes(name[0] ); - } - catch( Exception e ) - { - Penumbra.Chat.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } + try + { + shaders[idx].Blob = File.ReadAllBytes(name[0]); + } + catch (Exception e) + { + Penumbra.Chat.NotificationMessage($"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - try - { - shaders[ idx ].UpdateResources( tab.Shpk ); - tab.Shpk.UpdateResources(); - } - catch( Exception e ) - { - tab.Shpk.SetInvalid(); - Penumbra.Chat.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", - NotificationType.Error ); - return; - } + try + { + shaders[idx].UpdateResources(tab.Shpk); + tab.Shpk.UpdateResources(); + } + catch (Exception e) + { + tab.Shpk.SetInvalid(); + Penumbra.Chat.NotificationMessage($"Failed to update resources after importing {name}:\n{e.Message}", + "Penumbra Advanced Editing", + NotificationType.Error); + return; + } - tab.Shpk.SetChanged(); - }, 1, null, false ); + tab.Shpk.SetChanged(); + }, 1, null, false); } - private static unsafe void DrawRawDisassembly( Shader shader ) + private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" ); - if( !t2 ) - { + using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); + if (!t2) return; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 ); - ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size, - ImGuiInputTextFlags.ReadOnly, null, null ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20); + ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, + (uint)shader.Disassembly!.RawDisassembly.Length + 1, size, + ImGuiInputTextFlags.ReadOnly, null, null); } - private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled ) + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) ) - { + if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) return false; - } var ret = false; - for( var idx = 0; idx < shaders.Length; ++idx ) + for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[ idx ]; - using var t = ImRaii.TreeNode( $"{objectName} #{idx}" ); - if( !t ) - { + var shader = shaders[idx]; + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + if (!t) continue; - } - DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled ) + DrawShaderExportButton(tab, objectName, shader, idx); + if (!disabled && tab.Shpk.Disassembled) { ImGui.SameLine(); - DrawShaderImportButton( tab, objectName, shaders, idx ); + DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true ); - ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true ); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); - if( shader.AdditionalHeader.Length > 0 ) + if (shader.AdditionalHeader.Length > 0) { - using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" ); - if( t2 ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) ); - } + using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + if (t2) + ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}"))); } - DrawRawDisassembly( shader ); + if (tab.Shpk.Disassembled) + DrawRawDisassembly(shader); } return ret; } - private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled ) + private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled) { var ret = false; - if( !disabled ) + if (!disabled) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f ); - if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) - { + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGuiUtil.InputUInt16($"{char.ToUpper(slotLabel[0])}{slotLabel[1..].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None)) ret = true; - } } - if( resource.Used == null ) - { + if (resource.Used == null) return ret; - } - var usedString = UsedComponentString( withSize, resource ); - if( usedString.Length > 0 ) - { - ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + var usedString = UsedComponentString(withSize, resource); + if (usedString.Length > 0) + ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); else - { - ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return ret; } - private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled ) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled) { - if( resources.Length == 0 ) - { + if (resources.Length == 0) return false; - } - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { + using var t = ImRaii.TreeNode(arrayName); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < resources.Length; ++idx ) + for (var idx = 0; idx < resources.Length; ++idx) { - ref var buf = ref resources[ idx ]; + ref var buf = ref resources[idx]; var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" - + ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty ); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); + + (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); font.Dispose(); - if( t2 ) - { - ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled ); - } + if (t2) + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled); } return ret; } - private static bool DrawMaterialParamLayoutHeader( string label ) + private static bool DrawMaterialParamLayoutHeader(string label) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); var pos = ImGui.GetCursorScreenPos() - + new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y ); + + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), + ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader( label ); - ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" ); + var ret = ImGui.CollapsingHeader(label); + ImGui.GetWindowDrawList() + .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; } - private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams ) + private static bool DrawMaterialParamLayoutBufferSize(ShpkFile file, Resource? materialParams) { - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); - if( isSizeWellDefined ) - { + var isSizeWellDefined = (file.MaterialParamsSize & 0xF) == 0 + && (!materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4); + if (isSizeWellDefined) return true; - } - ImGui.TextUnformatted( materialParams.HasValue + ImGui.TextUnformatted(materialParams.HasValue ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" - : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" ); + : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16"); return false; } - private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + ImGui.TextUnformatted(tab.Shpk.Disassembled + ? "Parameter positions (continuations are grayed out, unused values are red):" + : "Parameter positions (continuations are grayed out):"); - using var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { + using var table = ImRaii.Table("##MaterialParamLayout", 5, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) return false; - } - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); - ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); + var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity + var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red + var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1); var ret = false; - for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i ) + for (var i = 0; i < tab.Matrix.GetLength(0); ++i) { ImGui.TableNextColumn(); - ImGui.TableHeader( $" [{i}]" ); - for( var j = 0; j < 4; ++j ) + ImGui.TableHeader($" [{i}]"); + for (var j = 0; j < 4; ++j) { - var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ]; + var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; var color = colorType switch { ShpkTab.ColorType.Unused => textColorUnusedStart, @@ -313,354 +308,307 @@ public partial class ModEditWindow ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, _ => textColorStart, }; - using var _ = ImRaii.PushId( i * 4 + j ); + using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) ) + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) ) + using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable( name ); - if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl ) + ImGui.Selectable(name); + if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx ); + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); ret = true; tab.Update(); } } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } - if( deletable ) - { - ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." ); - } + if (deletable) + ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); } } return ret; } - private static void DrawShaderPackageMisalignedParameters( ShpkTab tab ) + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + if (!t) return; - } - using var _ = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var name in tab.MalformedParameters ) - { - ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var name in tab.MalformedParameters) + ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - private static void DrawShaderPackageStartCombo( ShpkTab tab ) + private static void DrawShaderPackageStartCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); - if( c ) - { - foreach( var (start, idx) in tab.Orphans.WithIndex() ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + if (c) + foreach (var (start, idx) in tab.Orphans.WithIndex()) { - if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) ) - { - tab.UpdateOrphanStart( idx ); - } + if (ImGui.Selectable(start.Name, idx == tab.NewMaterialParamStart)) + tab.UpdateOrphanStart(idx); } - } } ImGui.SameLine(); - ImGui.TextUnformatted( "Start" ); + ImGui.TextUnformatted("Start"); } - private static void DrawShaderPackageEndCombo( ShpkTab tab ) + private static void DrawShaderPackageEndCombo(ShpkTab tab) { - using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); - if( c ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + if (c) { - var current = tab.Orphans[ tab.NewMaterialParamStart ].Index; - for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i ) + var current = tab.Orphans[tab.NewMaterialParamStart].Index; + for (var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i) { - var next = tab.Orphans[ i ]; - if( current++ != next.Index ) - { + var next = tab.Orphans[i]; + if (current++ != next.Index) break; - } - if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) ) - { + if (ImGui.Selectable(next.Name, i == tab.NewMaterialParamEnd)) tab.NewMaterialParamEnd = i; - } } } } ImGui.SameLine(); - ImGui.TextUnformatted( "End" ); + ImGui.TextUnformatted("End"); } - private static bool DrawShaderPackageNewParameter( ShpkTab tab ) + private static bool DrawShaderPackageNewParameter(ShpkTab tab) { - if( tab.Orphans.Count == 0 ) - { + if (tab.Orphans.Count == 0) return false; - } - DrawShaderPackageStartCombo( tab ); - DrawShaderPackageEndCombo( tab ); + DrawShaderPackageStartCombo(tab); + DrawShaderPackageEndCombo(tab); - ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); - if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) - { - tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); - } + ImGui.SetNextItemWidth(UiHelpers.Scale * 400); + if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) + tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); - var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) ? "The ID is already in use. Please choose a different name." : string.Empty; - if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip, - tooltip.Length > 0 ) ) - { + if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), + tooltip, + tooltip.Length > 0)) return false; - } - tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam + tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { Id = tab.NewMaterialParamId, - ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ), - ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ), - } ); + ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), + ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), + }); tab.Update(); return true; } - private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled ) + private static bool DrawShaderPackageMaterialParamLayout(ShpkTab tab, bool disabled) { var ret = false; - var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId ); - if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) ) - { + var materialParams = tab.Shpk.GetConstantById(MaterialParamsConstantId); + if (!DrawMaterialParamLayoutHeader(materialParams?.Name ?? "Material Parameter")) return false; - } - var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams ); + var sizeWellDefined = DrawMaterialParamLayoutBufferSize(tab.Shpk, materialParams); - ret |= DrawShaderPackageMaterialMatrix( tab, disabled ); + ret |= DrawShaderPackageMaterialMatrix(tab, disabled); - if( tab.MalformedParameters.Count > 0 ) - { - DrawShaderPackageMisalignedParameters( tab ); - } - else if( !disabled && sizeWellDefined ) - { - ret |= DrawShaderPackageNewParameter( tab ); - } + if (tab.MalformedParameters.Count > 0) + DrawShaderPackageMisalignedParameters(tab); + else if (!disabled && sizeWellDefined) + ret |= DrawShaderPackageNewParameter(tab); return ret; } - private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) + private static bool DrawShaderPackageResources(ShpkTab tab, bool disabled) { - if( keys.Count == 0 ) - { - return; - } + var ret = false; - using var t = ImRaii.TreeNode( arrayName ); - if( !t ) - { - return; - } + if (!ImGui.CollapsingHeader("Shader Resources")) + return false; - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (key, idx) in keys.WithIndex() ) + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled); + + return ret; + } + + private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) + { + if (keys.Count == 0) + return; + + using var t = ImRaii.TreeNode(arrayName); + if (!t) + return; + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" ); - if( t2 ) + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}"); + if (t2) { - ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); + ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - private static void DrawShaderPackageNodes( ShpkTab tab ) + private static void DrawShaderPackageNodes(ShpkTab tab) { - if( tab.Shpk.Nodes.Length <= 0 ) - { + if (tab.Shpk.Nodes.Length <= 0) return; - } - using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" ); - if( !t ) - { + using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + if (!t) return; - } - foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) + foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex()) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); - if( !t2 ) - { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + if (!t2) continue; - } - foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) - { - ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) + ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) + ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) + ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) - { - ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) + ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - foreach( var (pass, passIdx) in node.Passes.WithIndex() ) + ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ) - .Dispose(); + ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } - private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) + private static void DrawShaderPackageSelection(ShpkTab tab) { - var ret = false; + if (!ImGui.CollapsingHeader("Shader Selection")) + return; - if( !ImGui.CollapsingHeader( "Further Content" ) ) + DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys); + + DrawShaderPackageNodes(tab); + using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + if (t) { - return false; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var selector in tab.Shpk.NodeSelectors) + ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } - - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); - DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); - DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); - DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); - - DrawShaderPackageNodes( tab ); - if( tab.Shpk.Items.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); - if( t ) - { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } - } - } - - if( tab.Shpk.AdditionalData.Length > 0 ) - { - using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" ); - if( t ) - { - ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); - } - } - - return ret; } - private static string UsedComponentString( bool withSize, in Resource resource ) + private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - var sb = new StringBuilder( 256 ); - if( withSize ) + if (!ImGui.CollapsingHeader("Further Content")) + return; + + ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + + if (tab.Shpk.AdditionalData.Length > 0) { - foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) + using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + if (t) + ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}"))); + } + } + + private static string UsedComponentString(bool withSize, in Resource resource) + { + var sb = new StringBuilder(256); + if (withSize) + { + foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) { - switch( components ) + switch (components) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( $"[{i}], " ); + sb.Append($"[{i}], "); break; default: - sb.Append( $"[{i}]." ); - foreach( var c in components.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append($"[{i}]."); + foreach (var c in components.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } - switch( resource.UsedDynamically ?? 0 ) + switch (resource.UsedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: - sb.Append( "[*], " ); + sb.Append("[*], "); break; default: - sb.Append( "[*]." ); - foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) ) - { - sb.Append( char.ToLower( c ) ); - } + sb.Append("[*]."); + foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + sb.Append(char.ToLower(c)); - sb.Append( ", " ); + sb.Append(", "); break; } } else { - var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 ); - if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) - { - sb.Append( "Red, " ); - } + var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + if ((components & DisassembledShader.VectorComponents.X) != 0) + sb.Append("Red, "); - if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) - { - sb.Append( "Green, " ); - } + if ((components & DisassembledShader.VectorComponents.Y) != 0) + sb.Append("Green, "); - if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) - { - sb.Append( "Blue, " ); - } + if ((components & DisassembledShader.VectorComponents.Z) != 0) + sb.Append("Blue, "); - if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) - { - sb.Append( "Alpha, " ); - } + if ((components & DisassembledShader.VectorComponents.W) != 0) + sb.Append("Alpha, "); } - return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 ); + return sb.Length == 0 ? string.Empty : sb.ToString(0, sb.Length - 2); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 1720ec8c..2df52130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -27,8 +27,16 @@ public partial class ModEditWindow public ShpkTab(FileDialogService fileDialog, byte[] bytes) { FileDialog = fileDialog; - Shpk = new ShpkFile(bytes, true); - Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; + try + { + Shpk = new ShpkFile(bytes, true); + } + catch (NotImplementedException) + { + Shpk = new ShpkFile(bytes, false); + } + + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 59cf8b80..e90c148e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -17,10 +19,12 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.Import.Textures; using Penumbra.Interop.ResourceTree; +using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; @@ -42,6 +46,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly ModMergeTab _modMergeTab; private readonly CommunicatorService _communicator; private readonly IDragDropManager _dragDropManager; + private readonly GameEventManager _gameEvents; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -137,6 +142,9 @@ public partial class ModEditWindow : Window, IDisposable { _left.Dispose(); _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); } public override void Draw() @@ -520,10 +528,33 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } + private HashSet FindPathsStartingWith(ByteString prefix) + { + var ret = new HashSet(); + + foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + { + if (path.Path.StartsWith(prefix)) + ret.Add(path); + } + + if (_mod != null) + foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + { + foreach (var path in option.Files.Keys) + { + if (path.Path.StartsWith(prefix)) + ret.Add(path); + } + } + + return ret; + } + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, - CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager) + CommunicatorService communicator, TextureManager textures, IDragDropManager dragDropManager, GameEventManager gameEvents) : base(WindowBaseLabel) { _performance = performance; @@ -539,14 +570,15 @@ public partial class ModEditWindow : Window, IDisposable _dragDropManager = dragDropManager; _textures = textures; _fileDialog = fileDialog; + _gameEvents = gameEvents; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlTab(this, new MtrlFile(bytes))); + (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _fileDialog, "Models", ".mdl", - () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MdlFile(bytes)); + () => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new MdlFile(bytes)); _shaderPackageTab = new FileEditor(this, gameData, config, _fileDialog, "Shaders", ".shpk", () => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new ShpkTab(_fileDialog, bytes)); + (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor); _quickImportViewer = new ResourceTreeViewer(_config, resourceTreeFactory, 2, OnQuickImportRefresh, DrawQuickImportActions); @@ -557,6 +589,9 @@ public partial class ModEditWindow : Window, IDisposable { _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); _editor?.Dispose(); + _materialTab.Dispose(); + _modelTab.Dispose(); + _shaderPackageTab.Dispose(); _left.Dispose(); _right.Dispose(); _center.Dispose(); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 0378d620..ad0f2e40 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); - ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); - } - - if (hovered) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted(option.Description); - } - } + ImGuiUtil.SelectableHelpMarker(option.Description); id.Pop(); }