Some material shpk refactoring.

This commit is contained in:
Ottermandias 2023-02-23 17:47:59 +01:00
parent 7e56858bc6
commit ebbc3fed86
5 changed files with 754 additions and 594 deletions

View file

@ -10,4 +10,26 @@ public static class UtilityFunctions
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T? FirstOrNull<T>(this IEnumerable<T> values, Func<T, bool> predicate) where T : struct public static T? FirstOrNull<T>(this IEnumerable<T> values, Func<T, bool> predicate) where T : struct
=> values.Cast<T?>().FirstOrDefault(v => predicate(v!.Value)); => values.Cast<T?>().FirstOrDefault(v => predicate(v!.Value));
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T[] AddItem<T>(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<T>(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;
}
} }

@ -1 +1 @@
Subproject commit 574fd9f8bb7d957457775a698f5e29a246fab8bd Subproject commit 84f9ec42cc7039d0731f538e11b0c5be3f766f29

View file

@ -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;
}
}

View file

@ -1,21 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using ImGuiNET; using ImGuiNET;
using Lumina.Data.Parsing;
using OtterGui; using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.String.Functions; using Penumbra.String.Functions;
using Penumbra.Util;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -23,41 +17,32 @@ public partial class ModEditWindow
{ {
private readonly FileEditor< MtrlFile > _materialTab; 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;
/// <summary> Load the material with an associated shader package if it can be found. See <seealso cref="FindBestMatch"/>. </summary> /// <summary> Load the material with an associated shader package if it can be found. See <seealso cref="FindBestMatch"/>. </summary>
private MtrlFile LoadMtrl( byte[] bytes ) private MtrlFile LoadMtrl( byte[] bytes )
{ {
var mtrl = new MtrlFile( bytes ); var mtrl = new MtrlFile( bytes );
if( !Utf8GamePath.FromString( $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}", out var shpkPath, true ) ) LoadAssociatedShpk( mtrl );
{
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;
}
return mtrl; return mtrl;
} }
private bool DrawMaterialPanel( MtrlFile file, bool disabled ) private bool DrawMaterialPanel( MtrlFile file, bool disabled )
{ {
var ret = DrawMaterialTextureChange( file, disabled ); var ret = DrawMaterialTextureChange( file, disabled );
@ -79,27 +64,26 @@ public partial class ModEditWindow
return !disabled && ret; return !disabled && ret;
} }
private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) private bool DrawMaterialTextureChange( MtrlFile file, bool disabled )
{ {
var samplers = file.GetSamplersByTexture(); var ret = false;
var names = new List<string>(); using var table = ImRaii.Table( "##Textures", 2 );
var maxWidth = 0.0f; ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, _mtrlTabState.TextureLabelWidth * ImGuiHelpers.GlobalScale );
for( var i = 0; i < file.Textures.Length; ++i ) ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch );
{
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;
for( var i = 0; i < file.Textures.Length; ++i ) for( var i = 0; i < file.Textures.Length; ++i )
{ {
using var _ = ImRaii.PushId( i ); using var _ = ImRaii.PushId( i );
var tmp = file.Textures[ i ].Path; var tmp = file.Textures[ i ].Path;
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - maxWidth ); ImGui.TableNextColumn();
if( ImGui.InputText( names[i], ref tmp, Utf8GamePath.MaxGamePathLength, 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 ) disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
&& tmp.Length > 0 && tmp.Length > 0
&& tmp != file.Textures[ i ].Path ) && tmp != file.Textures[ i ].Path )
@ -197,458 +181,6 @@ public partial class ModEditWindow
return ret; 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 ) private bool DrawOtherMaterialDetails( MtrlFile file, bool disabled )
{ {
var ret = false; 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<T>( 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<T>( 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 ) private static (string?, bool) MaterialParamRangeName( string prefix, int valueOffset, int valueLength )
{ {
if( valueLength == 0 || valueOffset < 0 ) if( valueLength == 0 || valueOffset < 0 )
{ {
return (null, false); return ( null, false );
} }
var firstVector = valueOffset >> 2; var firstVector = valueOffset >> 2;
var lastVector = ( valueOffset + valueLength - 1 ) >> 2; var lastVector = ( valueOffset + valueLength - 1 ) >> 2;
var firstComponent = valueOffset & 0x3; var firstComponent = valueOffset & 0x3;
var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3;
static string VectorSwizzle( int firstComponent, int numComponents ) 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 ) 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]; var parts = new string[lastVector + 1 - firstVector];
parts[0] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}"; parts[ 0 ] = $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 4 - firstComponent )}";
parts[^1] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}"; parts[ ^1 ] = $"[{lastVector}]{VectorSwizzle( 0, lastComponent + 1 )}";
for( var i = firstVector + 1; i < lastVector; ++i ) 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 ) private static string? MaterialParamName( bool componentOnly, int offset )
@ -1176,7 +685,8 @@ public partial class ModEditWindow
{ {
return null; return null;
} }
var component = "xyzw"[offset & 0x3];
var component = "xyzw"[ offset & 0x3 ];
return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}"; return componentOnly ? new string( component, 1 ) : $"[{offset >> 2}].{component}";
} }

View file

@ -11,6 +11,7 @@ using Lumina.Misc;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Util; using Penumbra.Util;
@ -19,14 +20,14 @@ namespace Penumbra.UI.Classes;
public partial class ModEditWindow public partial class ModEditWindow
{ {
private readonly FileEditor<ShpkFile> _shaderPackageTab; private readonly FileEditor< ShpkFile > _shaderPackageTab;
private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager(); private readonly FileDialogManager _shaderPackageFileDialog = ConfigWindow.SetupFileManager();
private string _shaderPackageNewMaterialParamName = string.Empty; private string _shaderPackageNewMaterialParamName = string.Empty;
private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); private uint _shaderPackageNewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu );
private ushort _shaderPackageNewMaterialParamStart = 0; private ushort _shaderPackageNewMaterialParamStart = 0;
private ushort _shaderPackageNewMaterialParamEnd = 0; private ushort _shaderPackageNewMaterialParamEnd = 0;
private bool DrawShaderPackagePanel( ShpkFile file, bool disabled ) private bool DrawShaderPackagePanel( ShpkFile file, bool disabled )
{ {
@ -86,7 +87,7 @@ public partial class ModEditWindow
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
var defaultName = new string( objectName.Where( char.IsUpper ).ToArray() ).ToLower() + idx.ToString(); 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 ) => _shaderPackageFileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", extension, defaultName, extension, ( success, name ) =>
{ {
if( !success ) if( !success )
@ -101,12 +102,16 @@ public partial class ModEditWindow
catch( Exception e ) catch( Exception e )
{ {
Penumbra.Log.Error( $"Could not export {defaultName}{extension} to {name}:\n{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; 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 ) if( !disabled )
{ {
ImGui.SameLine(); ImGui.SameLine();
@ -121,7 +126,7 @@ public partial class ModEditWindow
try try
{ {
shaders[idx].Blob = File.ReadAllBytes( name ); shaders[ idx ].Blob = File.ReadAllBytes( name );
} }
catch( Exception e ) 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 ); ChatUtil.NotificationMessage( $"Could not import {Path.GetFileName( name )}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error );
return; return;
} }
try try
{ {
shaders[idx].UpdateResources( file ); shaders[ idx ].UpdateResources( file );
file.UpdateResources(); file.UpdateResources();
} }
catch( Exception e ) catch( Exception e )
{ {
file.SetInvalid(); file.SetInvalid();
Penumbra.Log.Error( $"Failed to update resources after importing Shader Blob {name}:\n{e}" ); 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; return;
} }
file.SetChanged(); file.SetChanged();
ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success ); ChatUtil.NotificationMessage( $"Shader Blob {Path.GetFileName( name )} imported successfully", "Penumbra Advanced Editing", NotificationType.Success );
} ); } );
@ -187,7 +195,7 @@ public partial class ModEditWindow
return false; 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 ) if( !isSizeWellDefined )
{ {
@ -201,26 +209,27 @@ public partial class ModEditWindow
} }
} }
var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu) >> 2]; var parameters = new (uint, bool)?[( ( file.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 2];
var orphanParameters = new IndexSet( parameters.Length, true ); var orphanParameters = new IndexSet( parameters.Length, true );
var definedParameters = new HashSet< uint >(); var definedParameters = new HashSet< uint >();
var hasMalformedParameters = false; var hasMalformedParameters = false;
foreach( var param in file.MaterialParams ) foreach( var param in file.MaterialParams )
{ {
definedParameters.Add( param.Id ); definedParameters.Add( param.Id );
if( ( param.ByteOffset & 0x3 ) == 0 && ( param.ByteSize & 0x3 ) == 0 if( ( param.ByteOffset & 0x3 ) == 0
&& ( param.ByteOffset + param.ByteSize ) <= file.MaterialParamsSize ) && ( param.ByteSize & 0x3 ) == 0
&& param.ByteOffset + param.ByteSize <= file.MaterialParamsSize )
{ {
var valueOffset = param.ByteOffset >> 2; var valueOffset = param.ByteOffset >> 2;
var valueCount = param.ByteSize >> 2; var valueCount = param.ByteSize >> 2;
orphanParameters.RemoveRange( valueOffset, valueCount ); orphanParameters.RemoveRange( valueOffset, valueCount );
parameters[valueOffset] = (param.Id, true); parameters[ valueOffset ] = ( param.Id, true );
for( var i = 1; i < valueCount; ++i ) for( var i = 1; i < valueCount; ++i )
{ {
parameters[valueOffset + i] = (param.Id, false); parameters[ valueOffset + i ] = ( param.Id, false );
} }
} }
else else
@ -232,7 +241,7 @@ public partial class ModEditWindow
ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" ); ImGui.Text( "Parameter positions (continuations are grayed out, unused values are red):" );
using( var table = ImRaii.Table( "##MaterialParamLayout", 5, using( var table = ImRaii.Table( "##MaterialParamLayout", 5,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) ) ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ) )
{ {
if( table ) if( table )
{ {
@ -247,25 +256,26 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableHeader( "w" ); ImGui.TableHeader( "w" );
var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); var textColorStart = ImGui.GetColorU32( ImGuiCol.Text );
var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity var textColorCont = ( textColorStart & 0xFFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity
var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0xFEFEFE ) >> 1 ) | 0x80u; // Half red
var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 ); var textColorUnusedCont = ( textColorUnusedStart & 0xFFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 );
for( var idx = 0; idx < parameters.Length; idx += 4 ) 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.TableNextColumn();
ImGui.Text( $"[{idx >> 2}]" ); ImGui.Text( $"[{idx >> 2}]" );
for( var col = 0; col < 4; ++col ) for( var col = 0; col < 4; ++col )
{ {
var cell = parameters[idx + col]; var cell = parameters[ idx + col ];
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var start = cell.HasValue && cell.Value.Item2; var start = cell.HasValue && cell.Value.Item2;
var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0; var used = ( ( byte )usedComponents & ( 1 << col ) ) != 0;
using var c = ImRaii.PushColor( ImGuiCol.Text, used ? ( start ? textColorStart : textColorCont ) : ( start ? textColorUnusedStart : textColorUnusedCont ) ); 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.Text( cell.HasValue ? $"0x{cell.Value.Item1:X8}" : "(none)" );
} }
ImGui.TableNextRow(); 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(); 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 ) for( var i = 0; i < file.MaterialParams.Length; ++i )
{ {
var param = file.MaterialParams[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})" ); 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( t2 )
{ {
if( ImGui.Button( "Remove" ) ) if( ImGui.Button( "Remove" ) )
{ {
ArrayRemove( ref file.MaterialParams, i ); file.MaterialParams = file.MaterialParams.RemoveItems( i );
ret = true; ret = true;
} }
} }
} }
if( orphanParameters.Count > 0 ) if( orphanParameters.Count > 0 )
{ {
using var t2 = ImRaii.TreeNode( "New Parameter" ); using var t2 = ImRaii.TreeNode( "New Parameter" );
if( t2 ) if( t2 )
{ {
var starts = orphanParameters.ToArray(); var starts = orphanParameters.ToArray();
if( !orphanParameters[_shaderPackageNewMaterialParamStart] ) if( !orphanParameters[ _shaderPackageNewMaterialParamStart ] )
{ {
_shaderPackageNewMaterialParamStart = ( ushort )starts[0]; _shaderPackageNewMaterialParamStart = ( ushort )starts[ 0 ];
} }
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f );
var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!; var startName = MaterialParamName( false, _shaderPackageNewMaterialParamStart )!;
using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) ) using( var c = ImRaii.Combo( "Start", $"{materialParams?.Name ?? ""}{startName}" ) )
@ -333,16 +347,19 @@ public partial class ModEditWindow
} }
} }
} }
var lastEndCandidate = ( int )_shaderPackageNewMaterialParamStart; 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; var ret = i <= lastEndCandidate + 1;
lastEndCandidate = i; lastEndCandidate = i;
return ret; return ret;
} ).ToArray(); } ).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 ); ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f );
var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!; var endName = MaterialParamName( false, _shaderPackageNewMaterialParamEnd )!;
using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) ) using( var c = ImRaii.Combo( "End", $"{materialParams?.Name ?? ""}{endName}" ) )
@ -359,26 +376,29 @@ public partial class ModEditWindow
} }
} }
} }
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f ); ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 225.0f );
if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) ) if( ImGui.InputText( $"Name", ref _shaderPackageNewMaterialParamName, 63 ) )
{ {
_shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu ); _shaderPackageNewMaterialParamId = Crc32.Get( _shaderPackageNewMaterialParamName, 0xFFFFFFFFu );
} }
ImGui.SameLine(); ImGui.SameLine();
ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" ); ImGui.Text( $"(ID: 0x{_shaderPackageNewMaterialParamId:X8})" );
if( ImGui.Button( "Add" ) ) if( ImGui.Button( "Add" ) )
{ {
if( definedParameters.Contains( _shaderPackageNewMaterialParamId ) ) 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 else
{ {
ArrayAdd( ref file.MaterialParams, new ShpkFile.MaterialParam file.MaterialParams = file.MaterialParams.AddItem( new ShpkFile.MaterialParam
{ {
Id = _shaderPackageNewMaterialParamId, Id = _shaderPackageNewMaterialParamId,
ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ), ByteOffset = ( ushort )( _shaderPackageNewMaterialParamStart << 2 ),
ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ), ByteSize = ( ushort )( ( _shaderPackageNewMaterialParamEnd + 1 - _shaderPackageNewMaterialParamStart ) << 2 ),
} ); } );
ret = true; ret = true;
} }
@ -408,7 +428,10 @@ public partial class ModEditWindow
foreach( var (buf, idx) in resources.WithIndex() ) 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( t2 )
{ {
if( !disabled ) if( !disabled )
@ -423,22 +446,22 @@ public partial class ModEditWindow
} }
ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); 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; ret = true;
} }
} }
if( buf.Used != null ) if( buf.Used != null )
{ {
var used = new List<string>(); var used = new List< string >();
if( withSize ) if( withSize )
{ {
foreach( var (components, i) in ( buf.Used ?? Array.Empty<DisassembledShader.VectorComponents>() ).WithIndex() ) foreach( var (components, i) in ( buf.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() )
{ {
switch( components ) switch( components )
{ {
case 0: case 0: break;
break;
case DisassembledShader.VectorComponents.All: case DisassembledShader.VectorComponents.All:
used.Add( $"[{i}]" ); used.Add( $"[{i}]" );
break; break;
@ -447,10 +470,10 @@ public partial class ModEditWindow
break; break;
} }
} }
switch( buf.UsedDynamically ?? 0 ) switch( buf.UsedDynamically ?? 0 )
{ {
case 0: case 0: break;
break;
case DisassembledShader.VectorComponents.All: case DisassembledShader.VectorComponents.All:
used.Add( "[*]" ); used.Add( "[*]" );
break; break;
@ -461,24 +484,28 @@ public partial class ModEditWindow
} }
else 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 ) if( ( components & DisassembledShader.VectorComponents.X ) != 0 )
{ {
used.Add( "Red" ); used.Add( "Red" );
} }
if( ( components & DisassembledShader.VectorComponents.Y ) != 0 ) if( ( components & DisassembledShader.VectorComponents.Y ) != 0 )
{ {
used.Add( "Green" ); used.Add( "Green" );
} }
if( ( components & DisassembledShader.VectorComponents.Z ) != 0 ) if( ( components & DisassembledShader.VectorComponents.Z ) != 0 )
{ {
used.Add( "Blue" ); used.Add( "Blue" );
} }
if( ( components & DisassembledShader.VectorComponents.W ) != 0 ) if( ( components & DisassembledShader.VectorComponents.W ) != 0 )
{ {
used.Add( "Alpha" ); used.Add( "Alpha" );
} }
} }
if( used.Count > 0 ) if( used.Count > 0 )
{ {
ImRaii.TreeNode( $"Used: {string.Join( ", ", used )}", ImGuiTreeNodeFlags.Leaf ).Dispose(); 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() ) 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() ) 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() ) 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() ) foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() )
{ {
ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf ).Dispose(); 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(); ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf ).Dispose();
foreach( var (pass, passIdx) in node.Passes.WithIndex() ) 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();
} }
} }
} }