diff --git a/Penumbra.GameData/UtilityFunctions.cs b/Penumbra.GameData/UtilityFunctions.cs index 71365c09..704d7a0c 100644 --- a/Penumbra.GameData/UtilityFunctions.cs +++ b/Penumbra.GameData/UtilityFunctions.cs @@ -10,4 +10,26 @@ public static class UtilityFunctions [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static T? FirstOrNull(this IEnumerable values, Func predicate) where T : struct => values.Cast().FirstOrDefault(v => predicate(v!.Value)); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T[] AddItem(this T[] array, T element, int count = 1) + { + var length = array.Length; + var newArray = new T[array.Length + count]; + Array.Copy(array, newArray, length); + for (var i = length; i < newArray.Length; ++i) + newArray[i] = element; + + return newArray; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T[] RemoveItems(this T[] array, int offset, int count = 1) + { + var newArray = new T[array.Length - count]; + Array.Copy(array, newArray, offset); + Array.Copy(array, offset + count, newArray, offset, newArray.Length - offset); + return newArray; + } } diff --git a/Penumbra.String b/Penumbra.String index 574fd9f8..84f9ec42 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 574fd9f8bb7d957457775a698f5e29a246fab8bd +Subproject commit 84f9ec42cc7039d0731f538e11b0c5be3f766f29 diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs new file mode 100644 index 00000000..1e3e78e3 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -0,0 +1,596 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using Lumina.Data.Parsing; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using Penumbra.GameData; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + + private FullPath FindAssociatedShpk( MtrlFile mtrl ) + { + if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) + { + return FullPath.Empty; + } + + return FindBestMatch( shpkPath ); + } + + private void LoadAssociatedShpk( MtrlFile mtrl ) + { + try + { + _mtrlTabState.LoadedShpkPath = FindAssociatedShpk( mtrl ); + var data = _mtrlTabState.LoadedShpkPath.IsRooted + ? File.ReadAllBytes( _mtrlTabState.LoadedShpkPath.FullName ) + : Dalamud.GameData.GetFile( _mtrlTabState.LoadedShpkPath.InternalName.ToString() )?.Data; + if( data?.Length > 0 ) + { + mtrl.AssociatedShpk = new ShpkFile( data ); + } + } + catch( Exception e ) + { + Penumbra.Log.Debug( $"Could not parse associated file {_mtrlTabState.LoadedShpkPath} to Shpk:\n{e}" ); + _mtrlTabState.LoadedShpkPath = FullPath.Empty; + mtrl.AssociatedShpk = null; + } + + UpdateTextureLabels( mtrl ); + } + + private void UpdateTextureLabels( MtrlFile file ) + { + var samplers = file.GetSamplersByTexture(); + _mtrlTabState.TextureLabels.Clear(); + _mtrlTabState.TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + for( var i = 0; i < file.Textures.Length; ++i ) + { + var (sampler, shpkSampler) = samplers[ i ]; + var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; + _mtrlTabState.TextureLabels.Add( name ); + _mtrlTabState.TextureLabelWidth = Math.Max( _mtrlTabState.TextureLabelWidth, ImGui.CalcTextSize( name ).X ); + } + } + + _mtrlTabState.TextureLabelWidth = _mtrlTabState.TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + } + + private bool DrawPackageNameInput( MtrlFile file, bool disabled ) + { + var ret = false; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + file.AssociatedShpk = null; + _mtrlTabState.LoadedShpkPath = FullPath.Empty; + } + + if( ImGui.IsItemDeactivatedAfterEdit() ) + { + LoadAssociatedShpk( file ); + } + + return ret; + } + + private static bool DrawShaderFlagsInput( MtrlFile file, bool disabled ) + { + var ret = false; + var shpkFlags = ( int )file.ShaderPackage.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Flags = ( uint )shpkFlags; + ret = true; + } + + return ret; + } + + private void DrawCustomAssociations( MtrlFile file, bool disabled ) + { + var text = file.AssociatedShpk == null + ? "Associated .shpk file: None" + : $"Associated .shpk file: {_mtrlTabState.LoadedShpkPath}"; + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Selectable( text ); + + if( disabled ) + { + return; + } + + if( ImGui.Button( "Associate custom ShPk file" ) ) + { + _materialFileDialog.OpenFileDialog( "Associate custom .shpk file...", ".shpk", ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); + _mtrlTabState.LoadedShpkPath = new FullPath( name ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not load .shpk file {name}:\n{e}" ); + ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + } + + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); + }, 1 ); + } + + var defaultFile = FindAssociatedShpk( file ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Associate default ShPk file", Vector2.Zero, defaultFile.FullName, defaultFile.Equals( _mtrlTabState.LoadedShpkPath ) ) ) + { + LoadAssociatedShpk( file ); + if( file.AssociatedShpk != null ) + { + ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", + "Penumbra Advanced Editing", NotificationType.Success ); + } + else + { + ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); + } + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + } + + private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) + { + var ret = false; + if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) + { + return ret; + } + + ret |= DrawPackageNameInput( file, disabled ); + ret |= DrawShaderFlagsInput( file, disabled ); + DrawCustomAssociations( file, disabled ); + + if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) + { + using var t = ImRaii.TreeNode( "Shader Keys" ); + if( t ) + { + var definedKeys = new HashSet< uint >(); + + foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) + { + definedKeys.Add( key.Category ); + using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); + if( t2 ) + { + if( !disabled ) + { + var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); + if( shpkKey.HasValue ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 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 ) ) + { + file.ShaderPackage.ShaderKeys[ idx ].Value = value; + ret = true; + } + } + } + } + + if( ImGui.Button( "Remove Key" ) ) + { + file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.RemoveItems( idx ); + ret = true; + } + } + } + } + + if( !disabled && file.AssociatedShpk != null ) + { + var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); + if( missingKeys.Length > 0 ) + { + var selectedKey = Array.Find( missingKeys, key => key.Id == _mtrlTabState.MaterialNewKeyId ); + if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) + { + selectedKey = missingKeys[ 0 ]; + _mtrlTabState.MaterialNewKeyId = selectedKey.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) + { + if( c ) + { + foreach( var key in missingKeys ) + { + if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _mtrlTabState.MaterialNewKeyId ) ) + { + selectedKey = key; + _mtrlTabState.MaterialNewKeyId = key.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Key" ) ) + { + file.ShaderPackage.ShaderKeys = file.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + { + Category = selectedKey.Id, + Value = selectedKey.DefaultValue, + } ); + ret = true; + } + } + } + } + } + + if( file.AssociatedShpk != null ) + { + var definedKeys = new Dictionary< uint, uint >(); + foreach( var key in file.ShaderPackage.ShaderKeys ) + { + definedKeys[ key.Category ] = key.Value; + } + + var materialKeys = Array.ConvertAll( file.AssociatedShpk.MaterialKeys, key => + { + if( definedKeys.TryGetValue( key.Id, out var value ) ) + { + return value; + } + else + { + return key.DefaultValue; + } + } ); + var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); + var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); + foreach( var node in file.AssociatedShpk.Nodes ) + { + if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[ key.Index ] ) ) + { + foreach( var pass in node.Passes ) + { + vertexShaders.Add( ( int )pass.VertexShader ); + pixelShaders.Add( ( int )pass.PixelShader ); + } + } + } + + ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); + ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( file.ShaderPackage.Constants.Length > 0 + || file.ShaderPackage.ShaderValues.Length > 0 + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) + { + var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); + + using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); + if( t ) + { + var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); + var aliasedValueCount = 0; + var definedConstants = new HashSet< uint >(); + var hasMalformedConstants = false; + + foreach( var constant in file.ShaderPackage.Constants ) + { + definedConstants.Add( constant.Id ); + var values = file.GetConstantValues( constant ); + if( file.GetConstantValues( constant ).Length > 0 ) + { + var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); + aliasedValueCount += values.Length - unique; + } + else + { + hasMalformedConstants = true; + } + } + + foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) + { + var values = file.GetConstantValues( constant ); + var paramValueOffset = -values.Length; + if( values.Length > 0 ) + { + var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); + var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; + if( ( paramByteOffset & 0x3 ) == 0 ) + { + paramValueOffset = paramByteOffset >> 2; + } + } + + var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); + + using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ": " + constantName : "" )} (ID: 0x{constant.Id:X8})" ); + if( t2 ) + { + if( values.Length > 0 ) + { + var valueOffset = constant.ByteOffset >> 2; + + for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) + { + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputFloat( + $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( valueOffset + valueIdx ) << 2:X4})", + ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + else + { + ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + + if( !disabled + && !hasMalformedConstants + && orphanValues.Count == 0 + && aliasedValueCount == 0 + && ImGui.Button( "Remove Constant" ) ) + { + file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + file.ShaderPackage.Constants = file.ShaderPackage.Constants.RemoveItems( idx ); + for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) + { + if( file.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) + { + file.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; + } + } + + ret = true; + } + } + } + + if( orphanValues.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); + if( t2 ) + { + foreach( var idx in orphanValues ) + { + ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); + if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", + ref file.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + } + } + } + else if( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) + { + var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant + => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); + if( missingConstants.Length > 0 ) + { + var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _mtrlTabState.MaterialNewConstantId ); + if( selectedConstant.ByteSize == 0 ) + { + selectedConstant = missingConstants[ 0 ]; + _mtrlTabState.MaterialNewConstantId = selectedConstant.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); + using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) + { + if( c ) + { + foreach( var constant in missingConstants ) + { + var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); + if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _mtrlTabState.MaterialNewConstantId ) ) + { + selectedConstant = constant; + _mtrlTabState.MaterialNewConstantId = constant.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Constant" ) ) + { + file.ShaderPackage.ShaderValues = file.ShaderPackage.ShaderValues.AddItem( 0.0f, selectedConstant.ByteSize >> 2 ); + file.ShaderPackage.Constants = file.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + { + Id = _mtrlTabState.MaterialNewConstantId, + ByteOffset = ( ushort )( file.ShaderPackage.ShaderValues.Length << 2 ), + ByteSize = selectedConstant.ByteSize, + } ); + ret = true; + } + } + } + } + } + + if( file.ShaderPackage.Samplers.Length > 0 + || file.Textures.Length > 0 + || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) + { + using var t = ImRaii.TreeNode( "Samplers" ); + if( t ) + { + var orphanTextures = new IndexSet( file.Textures.Length, true ); + var aliasedTextureCount = 0; + var definedSamplers = new HashSet< uint >(); + + foreach( var sampler in file.ShaderPackage.Samplers ) + { + if( !orphanTextures.Remove( sampler.TextureIndex ) ) + { + ++aliasedTextureCount; + } + + definedSamplers.Add( sampler.SamplerId ); + } + + foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) + { + var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); + using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ": " + shpkSampler.Value.Name : "" )} (ID: 0x{sampler.SamplerId:X8})" ); + if( t2 ) + { + ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[ sampler.TextureIndex ].Path )}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); + + // FIXME this probably doesn't belong here + static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + { + fixed( ushort* v2 = &v ) + { + return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, nint.Zero, nint.Zero, "%04X", flags ); + } + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( InputHexUInt16( "Texture Flags", ref file.Textures[ sampler.TextureIndex ].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + { + ret = true; + } + + var sampFlags = ( int )sampler.Flags; + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + { + file.ShaderPackage.Samplers[ idx ].Flags = ( uint )sampFlags; + ret = true; + } + + if( !disabled + && orphanTextures.Count == 0 + && aliasedTextureCount == 0 + && ImGui.Button( "Remove Sampler" ) ) + { + file.Textures = file.Textures.RemoveItems( sampler.TextureIndex ); + file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.RemoveItems( idx ); + for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) + { + if( file.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) + { + --file.ShaderPackage.Samplers[ i ].TextureIndex; + } + } + + ret = true; + } + } + } + + if( orphanTextures.Count > 0 ) + { + using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); + if( t2 ) + { + foreach( var idx in orphanTextures ) + { + ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[ idx ].Path )} - {file.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + } + } + } + else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) + { + var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); + if( missingSamplers.Length > 0 ) + { + var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _mtrlTabState.MaterialNewSamplerId ); + if( selectedSampler.Name == null ) + { + selectedSampler = missingSamplers[ 0 ]; + _mtrlTabState.MaterialNewSamplerId = selectedSampler.Id; + } + + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); + using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) + { + if( c ) + { + foreach( var sampler in missingSamplers ) + { + if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _mtrlTabState.MaterialNewSamplerId ) ) + { + selectedSampler = sampler; + _mtrlTabState.MaterialNewSamplerId = sampler.Id; + } + } + } + } + + ImGui.SameLine(); + if( ImGui.Button( "Add Sampler" ) ) + { + file.Textures = file.Textures.AddItem( new MtrlFile.Texture + { + Path = string.Empty, + Flags = 0, + } ); + file.ShaderPackage.Samplers = file.ShaderPackage.Samplers.AddItem( new Sampler + { + SamplerId = _mtrlTabState.MaterialNewSamplerId, + TextureIndex = ( byte )file.Textures.Length, + Flags = 0, + } ); + ret = true; + } + } + } + } + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index f88fa5cf..ef9e463c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,21 +1,15 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Internal.Notifications; using ImGuiNET; -using Lumina.Data.Parsing; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.String.Functions; -using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -23,41 +17,32 @@ public partial class ModEditWindow { private readonly FileEditor< MtrlFile > _materialTab; - private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + private struct MtrlTabState + { + public uint MaterialNewKeyId = 0; + public uint MaterialNewConstantId = 0; + public uint MaterialNewSamplerId = 0; + + public readonly List< string > TextureLabels = new(4); + public FullPath LoadedShpkPath = FullPath.Empty; + public float TextureLabelWidth = 0f; + + public MtrlTabState() + { } + } + + private MtrlTabState _mtrlTabState = new(); - private uint _materialNewKeyId = 0; - private uint _materialNewConstantId = 0; - private uint _materialNewSamplerId = 0; /// Load the material with an associated shader package if it can be found. See . private MtrlFile LoadMtrl( byte[] bytes ) { var mtrl = new MtrlFile( bytes ); - if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) - { - return mtrl; - } - - try - { - var shpkFilePath = FindBestMatch( shpkPath ); - var data = shpkFilePath.IsRooted - ? File.ReadAllBytes( shpkFilePath.FullName ) - : Dalamud.GameData.GetFile( shpkFilePath.FullName )?.Data; - if( data?.Length > 0 ) - { - mtrl.AssociatedShpk = new ShpkFile( data ); - } - } - catch( Exception e ) - { - Penumbra.Log.Debug( $"Could not parse associated file {shpkPath} to Shpk:\n{e}" ); - mtrl.AssociatedShpk = null; - } - + LoadAssociatedShpk( mtrl ); return mtrl; } + private bool DrawMaterialPanel( MtrlFile file, bool disabled ) { var ret = DrawMaterialTextureChange( file, disabled ); @@ -79,27 +64,26 @@ public partial class ModEditWindow return !disabled && ret; } - private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + private bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) { - var samplers = file.GetSamplersByTexture(); - var names = new List(); - var maxWidth = 0.0f; - for( var i = 0; i < file.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[i]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - names.Add( name ); - maxWidth = Math.Max( maxWidth, ImGui.CalcTextSize( name ).X ); - } - - using var id = ImRaii.PushId( "Textures" ); - var ret = false; + var ret = false; + using var table = ImRaii.Table( "##Textures", 2 ); + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); for( var i = 0; i < file.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); var tmp = file.Textures[ i ].Path; - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - maxWidth ); - if( ImGui.InputText( names[i], ref tmp, Utf8GamePath.MaxGamePathLength, + ImGui.TableNextColumn(); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( _mtrlTabState.TextureLabels[ i ] ); + } + + 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 != file.Textures[ i ].Path ) @@ -197,458 +181,6 @@ public partial class ModEditWindow return ret; } - private bool DrawMaterialShaderResources( MtrlFile file, bool disabled ) - { - var ret = false; - - if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) - { - return false; - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref file.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - var shpkFlags = ( int )file.ShaderPackage.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) - { - file.ShaderPackage.Flags = ( uint )shpkFlags; - ret = true; - } - ImRaii.TreeNode( $"Has associated ShPk file (for advanced editing): {( file.AssociatedShpk != null ? "Yes" : "No" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - if( !disabled ) - { - if( ImGui.Button( "Associate custom ShPk file" ) ) - { - _materialFileDialog.OpenFileDialog( $"Associate custom ShPk file...", ".shpk", ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - file.AssociatedShpk = new ShpkFile( File.ReadAllBytes( name ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not load ShPk file {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not load {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); - return; - } - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the supplied {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - } ); - } - ImGui.SameLine(); - if( ImGui.Button( "Associate default ShPk file" ) ) - { - var shpk = LoadAssociatedShpk( file.ShaderPackage.Name ); - if( null != shpk ) - { - file.AssociatedShpk = shpk; - ChatUtil.NotificationMessage( $"Advanced Shader Resources for this material will now be based on the default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Success ); - } - else - { - ChatUtil.NotificationMessage( $"Could not load default {file.ShaderPackage.Name}", "Penumbra Advanced Editing", NotificationType.Error ); - } - } - } - - if( file.ShaderPackage.ShaderKeys.Length > 0 || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.MaterialKeys.Length > 0 ) - { - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( t ) - { - var definedKeys = new HashSet< uint >(); - - foreach( var (key, idx) in file.ShaderPackage.ShaderKeys.WithIndex() ) - { - definedKeys.Add( key.Category ); - using var t2 = ImRaii.TreeNode( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}", disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( t2 ) - { - if( !disabled ) - { - var shpkKey = file.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 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 ) ) - { - file.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - } - } - } - } - if( ImGui.Button( "Remove Key" ) ) - { - ArrayRemove( ref file.ShaderPackage.ShaderKeys, idx ); - ret = true; - } - } - } - } - - if( !disabled && file.AssociatedShpk != null ) - { - var missingKeys = file.AssociatedShpk.MaterialKeys.Where( key => !definedKeys.Contains( key.Id ) ).ToArray(); - if( missingKeys.Length > 0 ) - { - var selectedKey = Array.Find( missingKeys, key => key.Id == _materialNewKeyId ); - if( Array.IndexOf( missingKeys, selectedKey ) < 0 ) - { - selectedKey = missingKeys[0]; - _materialNewKeyId = selectedKey.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{selectedKey.Id:X8}" ) ) - { - if( c ) - { - foreach( var key in missingKeys ) - { - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == _materialNewKeyId ) ) - { - selectedKey = key; - _materialNewKeyId = key.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) - { - ArrayAdd( ref file.ShaderPackage.ShaderKeys, new ShaderKey - { - Category = selectedKey.Id, - Value = selectedKey.DefaultValue, - } ); - ret = true; - } - } - } - } - } - - if( file.AssociatedShpk != null ) - { - var definedKeys = new Dictionary< uint, uint >(); - foreach( var key in file.ShaderPackage.ShaderKeys ) - { - definedKeys[key.Category] = key.Value; - } - var materialKeys = Array.ConvertAll(file.AssociatedShpk.MaterialKeys, key => - { - if( definedKeys.TryGetValue( key.Id, out var value ) ) - { - return value; - } - else - { - return key.DefaultValue; - } - } ); - var vertexShaders = new IndexSet( file.AssociatedShpk.VertexShaders.Length, false ); - var pixelShaders = new IndexSet( file.AssociatedShpk.PixelShaders.Length, false ); - foreach( var node in file.AssociatedShpk.Nodes ) - { - if( node.MaterialKeys.WithIndex().All( key => key.Value == materialKeys[key.Index] ) ) - { - foreach( var pass in node.Passes ) - { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); - } - } - } - ImRaii.TreeNode( $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( file.ShaderPackage.Constants.Length > 0 || file.ShaderPackage.ShaderValues.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Constants.Length > 0 ) - { - var materialParams = file.AssociatedShpk?.GetConstantById( ShpkFile.MaterialParamsConstantId ); - - using var t = ImRaii.TreeNode( materialParams?.Name ?? "Constants" ); - if( t ) - { - var orphanValues = new IndexSet( file.ShaderPackage.ShaderValues.Length, true ); - var aliasedValueCount = 0; - var definedConstants = new HashSet< uint >(); - var hasMalformedConstants = false; - - foreach( var constant in file.ShaderPackage.Constants ) - { - definedConstants.Add( constant.Id ); - var values = file.GetConstantValues( constant ); - if( file.GetConstantValues( constant ).Length > 0 ) - { - var unique = orphanValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - aliasedValueCount += values.Length - unique; - } - else - { - hasMalformedConstants = true; - } - } - - foreach( var (constant, idx) in file.ShaderPackage.Constants.WithIndex() ) - { - var values = file.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) - { - var shpkParam = file.AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam.HasValue ? shpkParam.Value.ByteOffset : -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - } - var (constantName, componentOnly) = MaterialParamRangeName( materialParams?.Name ?? "", paramValueOffset, values.Length ); - - using var t2 = ImRaii.TreeNode( $"#{idx}{( constantName != null ? ( ": " + constantName ) : "" )} (ID: 0x{constant.Id:X8})" ); - if( t2 ) - { - if( values.Length > 0 ) - { - var valueOffset = constant.ByteOffset >> 2; - - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) - { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( $"{MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"} (at 0x{( ( valueOffset + valueIdx ) << 2 ):X4})", - ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - else - { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - - if( !disabled && !hasMalformedConstants && orphanValues.Count == 0 && aliasedValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) - { - ArrayRemove( ref file.ShaderPackage.ShaderValues, constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - ArrayRemove( ref file.ShaderPackage.Constants, idx ); - for( var i = 0; i < file.ShaderPackage.Constants.Length; ++i ) - { - if( file.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset ) - { - file.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - } - ret = true; - } - } - } - - if( orphanValues.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({orphanValues.Count})" ); - if( t2 ) - { - foreach( var idx in orphanValues ) - { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{( idx << 2 ):X4})", - ref file.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - } - } - } - else if ( !disabled && !hasMalformedConstants && file.AssociatedShpk != null ) - { - var missingConstants = file.AssociatedShpk.MaterialParams.Where( constant => ( constant.ByteOffset & 0x3 ) == 0 && ( constant.ByteSize & 0x3 ) == 0 && !definedConstants.Contains( constant.Id ) ).ToArray(); - if( missingConstants.Length > 0 ) - { - var selectedConstant = Array.Find( missingConstants, constant => constant.Id == _materialNewConstantId ); - if( selectedConstant.ByteSize == 0 ) - { - selectedConstant = missingConstants[0]; - _materialNewConstantId = selectedConstant.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - var (selectedConstantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", selectedConstant.ByteOffset >> 2, selectedConstant.ByteSize >> 2 ); - using( var c = ImRaii.Combo( "##NewConstantId", $"{selectedConstantName} (ID: 0x{selectedConstant.Id:X8})" ) ) - { - if( c ) - { - foreach( var constant in missingConstants ) - { - var (constantName, _) = MaterialParamRangeName( materialParams?.Name ?? "", constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - if( ImGui.Selectable( $"{constantName} (ID: 0x{constant.Id:X8})", constant.Id == _materialNewConstantId ) ) - { - selectedConstant = constant; - _materialNewConstantId = constant.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) - { - var valueOffset = ArrayAdd( ref file.ShaderPackage.ShaderValues, 0.0f, selectedConstant.ByteSize >> 2 ); - ArrayAdd( ref file.ShaderPackage.Constants, new MtrlFile.Constant - { - Id = _materialNewConstantId, - ByteOffset = ( ushort )( valueOffset << 2 ), - ByteSize = selectedConstant.ByteSize, - } ); - ret = true; - } - } - } - } - } - - if( file.ShaderPackage.Samplers.Length > 0 || file.Textures.Length > 0 - || !disabled && file.AssociatedShpk != null && file.AssociatedShpk.Samplers.Any( sampler => sampler.Slot == 2 ) ) - { - using var t = ImRaii.TreeNode( "Samplers" ); - if( t ) - { - var orphanTextures = new IndexSet( file.Textures.Length, true ); - var aliasedTextureCount = 0; - var definedSamplers = new HashSet< uint >(); - - foreach( var sampler in file.ShaderPackage.Samplers ) - { - if( !orphanTextures.Remove( sampler.TextureIndex ) ) - { - ++aliasedTextureCount; - } - definedSamplers.Add( sampler.SamplerId ); - } - - foreach( var (sampler, idx) in file.ShaderPackage.Samplers.WithIndex() ) - { - var shpkSampler = file.AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - using var t2 = ImRaii.TreeNode( $"#{idx}{( shpkSampler.HasValue ? ( ": " + shpkSampler.Value.Name ) : "" )} (ID: 0x{sampler.SamplerId:X8})" ); - if( t2 ) - { - ImRaii.TreeNode( $"Texture: #{sampler.TextureIndex} - {Path.GetFileName( file.Textures[sampler.TextureIndex].Path )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) - { - fixed( ushort* v2 = &v ) - { - return ImGui.InputScalar( label, ImGuiDataType.U16, new nint( v2 ), nint.Zero, nint.Zero, "%04X", flags ); - } - } - - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref file.Textures[sampler.TextureIndex].Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { - ret = true; - } - var sampFlags = ( int )sampler.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Sampler Flags", ref sampFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) - { - file.ShaderPackage.Samplers[idx].Flags = ( uint )sampFlags; - ret = true; - } - - if( !disabled && orphanTextures.Count == 0 && aliasedTextureCount == 0 - && ImGui.Button( "Remove Sampler" ) ) - { - ArrayRemove( ref file.Textures, sampler.TextureIndex ); - ArrayRemove( ref file.ShaderPackage.Samplers, idx ); - for( var i = 0; i < file.ShaderPackage.Samplers.Length; ++i ) - { - if( file.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex ) - { - --file.ShaderPackage.Samplers[i].TextureIndex; - } - } - ret = true; - } - } - } - - if( orphanTextures.Count > 0 ) - { - using var t2 = ImRaii.TreeNode( $"Orphan Textures ({orphanTextures.Count})" ); - if( t2 ) - { - foreach( var idx in orphanTextures ) - { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( file.Textures[idx].Path )} - {file.Textures[idx].Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - } - } - } - else if( !disabled && file.AssociatedShpk != null && aliasedTextureCount == 0 && file.Textures.Length < 255 ) - { - var missingSamplers = file.AssociatedShpk.Samplers.Where( sampler => sampler.Slot == 2 && !definedSamplers.Contains( sampler.Id ) ).ToArray(); - if( missingSamplers.Length > 0 ) - { - var selectedSampler = Array.Find( missingSamplers, sampler => sampler.Id == _materialNewSamplerId ); - if( selectedSampler.Name == null ) - { - selectedSampler = missingSamplers[0]; - _materialNewSamplerId = selectedSampler.Id; - } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var c = ImRaii.Combo( "##NewSamplerId", $"{selectedSampler.Name} (ID: 0x{selectedSampler.Id:X8})" ) ) - { - if( c ) - { - foreach( var sampler in missingSamplers ) - { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == _materialNewSamplerId ) ) - { - selectedSampler = sampler; - _materialNewSamplerId = sampler.Id; - } - } - } - } - ImGui.SameLine(); - if( ImGui.Button( "Add Sampler" ) ) - { - var texIndex = ArrayAdd( ref file.Textures, new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - } ); - ArrayAdd( ref file.ShaderPackage.Samplers, new Sampler - { - SamplerId = _materialNewSamplerId, - TextureIndex = ( byte )texIndex, - Flags = 0, - } ); - ret = true; - } - } - } - } - } - - return ret; - } - private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled ) { var ret = false; @@ -1116,58 +648,35 @@ public partial class ModEditWindow } } - // FIXME this probably doesn't belong here - // Also used in ShaderPackages - private static int ArrayAdd( ref T[] array, T element, int count = 1 ) - { - var length = array.Length; - var newArray = new T[array.Length + count]; - Array.Copy( array, newArray, length ); - for( var i = 0; i < count; ++i ) - { - newArray[length + i] = element; - } - array = newArray; - return length; - } - - private static void ArrayRemove( ref T[] array, int offset, int count = 1 ) - { - var newArray = new T[array.Length - count]; - Array.Copy( array, newArray, offset ); - Array.Copy( array, offset + count, newArray, offset, newArray.Length - offset ); - array = newArray; - } - private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) { if( valueLength == 0 || valueOffset < 0 ) { - return (null, false); + return ( null, false ); } - var firstVector = valueOffset >> 2; - var lastVector = ( valueOffset + valueLength - 1 ) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; + var firstVector = valueOffset >> 2; + var lastVector = ( valueOffset + valueLength - 1 ) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; static string VectorSwizzle( int firstComponent, int numComponents ) - => ( numComponents == 4 ) ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); + => numComponents == 4 ? "" : string.Concat( ".", "xyzw".AsSpan( firstComponent, numComponents ) ); if( firstVector == lastVector ) { - return ($"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true); + return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent + 1 - firstComponent )}", true ); } var parts = new string[lastVector + 1 - firstVector]; - parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; - parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; + parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; + parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; for( var i = firstVector + 1; i < lastVector; ++i ) { - parts[i - firstVector] = $"[{i}]"; + parts[ i - firstVector ] = $"[{i}]"; } - return (string.Join( ", ", parts ), false); + return ( string.Join( ", ", parts ), false ); } private static string? MaterialParamName( bool componentOnly, int offset ) @@ -1176,7 +685,8 @@ public partial class ModEditWindow { return null; } - var component = "xyzw"[offset & 0x3]; + + var component = "xyzw"[ offset & 0x3 ]; return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; } diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 4f5e72ba..9809874b 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -11,6 +11,7 @@ using Lumina.Misc; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Util; @@ -19,14 +20,14 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileEditor _shaderPackageTab; + private readonly FileEditor< ShpkFile > _shaderPackageTab; private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); - private string _shaderPackageNewMaterialParamName = string.Empty; - private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + private string _shaderPackageNewMaterialParamName = string.Empty; + private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); private ushort _shaderPackageNewMaterialParamStart = 0; - private ushort _shaderPackageNewMaterialParamEnd = 0; + private ushort _shaderPackageNewMaterialParamEnd = 0; private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) { @@ -86,7 +87,7 @@ public partial class ModEditWindow _ => throw new NotImplementedException(), }; var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); - var blob = shader.Blob; + var blob = shader.Blob; _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) => { if( !success ) @@ -101,12 +102,16 @@ public partial class ModEditWindow catch( Exception e ) { Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Could not export {defaultName}{extension} to {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); return; } - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); + + ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{extension} exported successfully to {Path.GetFileName( name )}", + "Penumbra Advanced Editing", NotificationType.Success ); } ); } + if( !disabled ) { ImGui.SameLine(); @@ -121,7 +126,7 @@ public partial class ModEditWindow try { - shaders[idx].Blob = File.ReadAllBytes( name ); + shaders[ idx ].Blob = File.ReadAllBytes( name ); } catch( Exception e ) { @@ -129,18 +134,21 @@ public partial class ModEditWindow ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } + try { - shaders[idx].UpdateResources( file ); + shaders[ idx ].UpdateResources( file ); file.UpdateResources(); } catch( Exception e ) { file.SetInvalid(); Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Failed to update resources after importing {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", + NotificationType.Error ); return; } + file.SetChanged(); ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); } ); @@ -187,7 +195,7 @@ public partial class ModEditWindow return false; } - var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == ( materialParams.Value.Size << 4 ) ); + var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 ); if( !isSizeWellDefined ) { @@ -201,26 +209,27 @@ public partial class ModEditWindow } } - var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu) >> 2]; - var orphanParameters = new IndexSet( parameters.Length, true ); - var definedParameters = new HashSet< uint >(); + var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 2]; + var orphanParameters = new IndexSet( parameters.Length, true ); + var definedParameters = new HashSet< uint >(); var hasMalformedParameters = false; foreach( var param in file.MaterialParams ) { definedParameters.Add( param.Id ); - if( ( param.ByteOffset & 0x3 ) == 0 && ( param.ByteSize & 0x3 ) == 0 - && ( param.ByteOffset + param.ByteSize ) <= file.MaterialParamsSize ) + if( ( param.ByteOffset & 0x3 ) == 0 + && ( param.ByteSize & 0x3 ) == 0 + && param.ByteOffset + param.ByteSize <= file.MaterialParamsSize ) { var valueOffset = param.ByteOffset >> 2; - var valueCount = param.ByteSize >> 2; + var valueCount = param.ByteSize >> 2; orphanParameters.RemoveRange( valueOffset, valueCount ); - parameters[valueOffset] = (param.Id, true); + parameters[ valueOffset ] = ( param.Id, true ); for( var i = 1; i < valueCount; ++i ) { - parameters[valueOffset + i] = (param.Id, false); + parameters[ valueOffset + i ] = ( param.Id, false ); } } else @@ -232,7 +241,7 @@ public partial class ModEditWindow ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); using( var table = ImRaii.Table( "##MaterialParamLayout", 5, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) { if( table ) { @@ -247,25 +256,26 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableHeader( "w" ); - var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); - var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity - var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red - var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); + var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity + var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red + var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); for( var idx = 0; idx < parameters.Length; idx += 4 ) { - var usedComponents = ( materialParams?.Used?[idx >> 2] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); + var usedComponents = ( materialParams?.Used?[ idx >> 2 ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); ImGui.TableNextColumn(); ImGui.Text( $"[{idx >> 2}]" ); for( var col = 0; col < 4; ++col ) { - var cell = parameters[idx + col]; + var cell = parameters[ idx + col ]; ImGui.TableNextColumn(); - var start = cell.HasValue && cell.Value.Item2; - var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; - using var c = ImRaii.PushColor( ImGuiCol.Text, used ? ( start ? textColorStart : textColorCont ) : ( start ? textColorUnusedStart : textColorUnusedCont ) ); + var start = cell.HasValue && cell.Value.Item2; + var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; + using var c = ImRaii.PushColor( ImGuiCol.Text, used ? start ? textColorStart : textColorCont : start ? textColorUnusedStart : textColorUnusedCont ); ImGui.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" ); } + ImGui.TableNextRow(); } } @@ -282,9 +292,10 @@ public partial class ModEditWindow { ImRaii.TreeNode( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } - else if( ( param.ByteOffset + param.ByteSize ) > file.MaterialParamsSize ) + else if( param.ByteOffset + param.ByteSize > file.MaterialParamsSize ) { - ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})", + ImGuiTreeNodeFlags.Leaf ).Dispose(); } } } @@ -296,27 +307,30 @@ public partial class ModEditWindow { for( var i = 0; i < file.MaterialParams.Length; ++i ) { - var param = file.MaterialParams[i]; - using var t2 = ImRaii.TreeNode( $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); + var param = file.MaterialParams[ i ]; + using var t2 = ImRaii.TreeNode( + $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})" ); if( t2 ) { if( ImGui.Button( "Remove" ) ) { - ArrayRemove( ref file.MaterialParams, i ); - ret = true; + file.MaterialParams = file.MaterialParams.RemoveItems( i ); + ret = true; } } } + if( orphanParameters.Count > 0 ) { using var t2 = ImRaii.TreeNode( "New Parameter" ); if( t2 ) { var starts = orphanParameters.ToArray(); - if( !orphanParameters[_shaderPackageNewMaterialParamStart] ) + if( !orphanParameters[ _shaderPackageNewMaterialParamStart ] ) { - _shaderPackageNewMaterialParamStart = ( ushort )starts[0]; + _shaderPackageNewMaterialParamStart = ( ushort )starts[ 0 ]; } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) @@ -333,16 +347,19 @@ public partial class ModEditWindow } } } + var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; - var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => { + var ends = starts.SkipWhile( i => i < _shaderPackageNewMaterialParamStart ).TakeWhile( i => + { var ret = i <= lastEndCandidate + 1; lastEndCandidate = i; return ret; } ).ToArray(); - if( Array.IndexOf(ends, _shaderPackageNewMaterialParamEnd) < 0 ) + if( Array.IndexOf( ends, _shaderPackageNewMaterialParamEnd ) < 0 ) { - _shaderPackageNewMaterialParamEnd = ( ushort )ends[0]; + _shaderPackageNewMaterialParamEnd = ( ushort )ends[ 0 ]; } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) @@ -359,26 +376,29 @@ public partial class ModEditWindow } } } + ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) { _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); } + ImGui.SameLine(); ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); if( ImGui.Button( "Add" ) ) { if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) { - ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", NotificationType.Error ); + ChatUtil.NotificationMessage( $"Duplicate parameter ID 0x{_shaderPackageNewMaterialParamId:X8}", "Penumbra Advanced Editing", + NotificationType.Error ); } else { - ArrayAdd( ref file.MaterialParams, new ShpkFile.MaterialParam + file.MaterialParams = file.MaterialParams.AddItem( new ShpkFile.MaterialParam { - Id = _shaderPackageNewMaterialParamId, - ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), - ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), + Id = _shaderPackageNewMaterialParamId, + ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), + ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), } ); ret = true; } @@ -408,7 +428,10 @@ public partial class ModEditWindow foreach( var (buf, idx) in resources.WithIndex() ) { - using var t2 = ImRaii.TreeNode( $"#{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 ), ( !disabled || buf.Used != null ) ? 0 : ImGuiTreeNodeFlags.Leaf ); + using var t2 = ImRaii.TreeNode( + $"#{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 ), + !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf ); if( t2 ) { if( !disabled ) @@ -423,22 +446,22 @@ public partial class ModEditWindow } ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputUInt16( $"{char.ToUpper( slotLabel[0] )}{slotLabel[1..].ToLower()}", ref resources[idx].Slot, ImGuiInputTextFlags.None ) ) + if( InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resources[ idx ].Slot, ImGuiInputTextFlags.None ) ) { ret = true; } } + if( buf.Used != null ) { - var used = new List(); + var used = new List< string >(); if( withSize ) { - foreach( var (components, i) in ( buf.Used ?? Array.Empty() ).WithIndex() ) + foreach( var (components, i) in ( buf.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() ) { switch( components ) { - case 0: - break; + case 0: break; case DisassembledShader.VectorComponents.All: used.Add( $"[{i}]" ); break; @@ -447,10 +470,10 @@ public partial class ModEditWindow break; } } + switch( buf.UsedDynamically ?? 0 ) { - case 0: - break; + case 0: break; case DisassembledShader.VectorComponents.All: used.Add( "[*]" ); break; @@ -461,24 +484,28 @@ public partial class ModEditWindow } else { - var components = ( ( buf.Used != null && buf.Used.Length > 0 ) ? buf.Used[0] : 0 ) | ( buf.UsedDynamically ?? 0 ); + var components = ( buf.Used != null && buf.Used.Length > 0 ? buf.Used[ 0 ] : 0 ) | ( buf.UsedDynamically ?? 0 ); if( ( components & DisassembledShader.VectorComponents.X ) != 0 ) { used.Add( "Red" ); } + if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) { used.Add( "Green" ); } + if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) { used.Add( "Blue" ); } + if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) { used.Add( "Alpha" ); } } + if( used.Count > 0 ) { ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); @@ -552,24 +579,29 @@ public partial class ModEditWindow { foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() ) { - ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"System Key 0x{file.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() ) { - ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Scene Key 0x{file.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() ) { - ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode( $"Material Key 0x{file.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() ) { ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); } + ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).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 ).Dispose(); + ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf ) + .Dispose(); } } }