diff --git a/OtterGui b/OtterGui index 863d08bd..1e172ee9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 863d08bd83381bb7fe4a8d5c514f0ba55379336f +Subproject commit 1e172ee9a0f5946d67b848a36b2be97f6541453f diff --git a/Penumbra.GameData b/Penumbra.GameData index 97643cad..07c001c5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 97643cad67b6981c3ee510d1ca12c4321e6a80bf +Subproject commit 07c001c5b2b35b2dba2b8428389d3ed375728616 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 6dc005ee..d14bd68b 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -190,7 +190,7 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide if (WithNames) { - var name = samplers != null && i < samplers.Count ? samplers[i].Item2?.Name : null; + var name = samplers != null && i < samplers.Length ? samplers[i].ShpkSampler?.Name : null; node.Children.Add(texNode.WithName(name ?? $"Texture #{i}")); } else diff --git a/Penumbra/Interop/Structs/CharacterBaseExt.cs b/Penumbra/Interop/Structs/CharacterBaseExt.cs new file mode 100644 index 00000000..3bbbeca9 --- /dev/null +++ b/Penumbra/Interop/Structs/CharacterBaseExt.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct CharacterBaseExt +{ + [FieldOffset( 0x0 )] + public CharacterBase CharacterBase; + + [FieldOffset( 0x258 )] + public Texture** ColorSetTextures; +} \ No newline at end of file diff --git a/Penumbra/Interop/Structs/HumanExt.cs b/Penumbra/Interop/Structs/HumanExt.cs index 7af5cee4..33d83b06 100644 --- a/Penumbra/Interop/Structs/HumanExt.cs +++ b/Penumbra/Interop/Structs/HumanExt.cs @@ -9,6 +9,9 @@ public unsafe struct HumanExt [FieldOffset( 0x0 )] public Human Human; + [FieldOffset( 0x0 )] + public CharacterBaseExt CharacterBase; + [FieldOffset( 0x9E8 )] public ResourceHandle* Decal; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs index 1d6c480a..798939ca 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorSet.cs @@ -13,22 +13,44 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + private bool DrawMaterialColorSetChange( MtrlTab tab, bool disabled ) { - if( !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) + if( !tab.SamplerIds.Contains( ShpkFile.TableSamplerId ) || !tab.Mtrl.ColorSets.Any( c => c.HasRows ) ) { return false; } + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Color Set", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + return false; + } + + var hasAnyDye = tab.UseColorDyeSet; + ColorSetCopyAllClipboardButton( tab.Mtrl, 0 ); ImGui.SameLine(); - var ret = ColorSetPasteAllClipboardButton( tab, 0 ); - ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); - ImGui.SameLine(); - ret |= DrawPreviewDye( tab, disabled ); + var ret = ColorSetPasteAllClipboardButton( tab, 0, disabled ); + if( !disabled ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= ColorSetDyeableCheckbox( tab, ref hasAnyDye ); + } + if( hasAnyDye ) + { + ImGui.SameLine(); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); + ImGui.SameLine(); + ret |= DrawPreviewDye( tab, disabled ); + } - using var table = ImRaii.Table( "##ColorSets", 11, + using var table = ImRaii.Table( "##ColorSets", hasAnyDye ? 11 : 9, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); if( !table ) { @@ -53,17 +75,20 @@ public partial class ModEditWindow ImGui.TableHeader( "Repeat" ); ImGui.TableNextColumn(); ImGui.TableHeader( "Skew" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Dye Preview" ); + if( hasAnyDye ) + { + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Dye Preview"); + } for( var j = 0; j < tab.Mtrl.ColorSets.Length; ++j ) { using var _ = ImRaii.PushId( j ); for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) { - ret |= DrawColorSetRow( tab, j, i, disabled ); + ret |= DrawColorSetRow( tab, j, i, disabled, hasAnyDye ); ImGui.TableNextRow(); } } @@ -122,9 +147,9 @@ public partial class ModEditWindow return false; } - private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx ) + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlTab tab, int colorSetIdx, bool disabled ) { - if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) + if( !ImGuiUtil.DrawDisabledButton( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ), string.Empty, disabled ) || tab.Mtrl.ColorSets.Length <= colorSetIdx ) { return false; } @@ -187,6 +212,21 @@ public partial class ModEditWindow } } + private static bool ColorSetDyeableCheckbox( MtrlTab tab, ref bool dyeable ) + { + var ret = ImGui.Checkbox( "Dyeable", ref dyeable ); + + if( ret ) + { + tab.UseColorDyeSet = dyeable; + if( dyeable ) + tab.Mtrl.FindOrAddColorDyeSet(); + tab.UpdateColorSetPreview(); + } + + return ret; + } + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) { if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -235,7 +275,7 @@ public partial class ModEditWindow tab.CancelColorSetHighlight(); } - private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled ) + private bool DrawColorSetRow( MtrlTab tab, int colorSetIdx, int rowIdx, bool disabled, bool hasAnyDye ) { static bool FixFloat( ref float val, float current ) { @@ -245,7 +285,7 @@ public partial class ModEditWindow using var id = ImRaii.PushId( rowIdx ); var row = tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; - var hasDye = tab.Mtrl.ColorDyeSets.Length > colorSetIdx; + var hasDye = hasAnyDye && tab.Mtrl.ColorDyeSets.Length > colorSetIdx; var dye = hasDye ? tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); var floatSize = 70 * UiHelpers.Scale; var intSize = 45 * UiHelpers.Scale; @@ -274,7 +314,7 @@ public partial class ModEditWindow ImGui.SameLine(); var tmpFloat = row.SpecularStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.SpecularStrength ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; ret = true; @@ -305,9 +345,9 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, Math.Max( 0.1f, tmpFloat * 0.025f ), HalfEpsilon, HalfMaxValue, "%.1f" ) && FixFloat( ref tmpFloat, row.GlossStrength ) ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = Math.Max(tmpFloat, HalfEpsilon); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -323,9 +363,9 @@ public partial class ModEditWindow ImGui.TableNextColumn(); int tmpInt = row.TileSet; ImGui.SetNextItemWidth( intSize ); - if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + if( ImGui.DragInt( "##TileSet", ref tmpInt, 0.25f, 0, 63 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) { - tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )Math.Clamp(tmpInt, 0, 63); ret = true; tab.UpdateColorSetRowPreview(rowIdx); } @@ -335,7 +375,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialRepeat.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; ret = true; @@ -346,7 +386,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialRepeat.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; ret = true; @@ -358,7 +398,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.MaterialSkew.X; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; ret = true; @@ -370,7 +410,7 @@ public partial class ModEditWindow ImGui.SameLine(); tmpFloat = row.MaterialSkew.Y; ImGui.SetNextItemWidth( floatSize ); - if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f" ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) ) { tab.Mtrl.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; ret = true; @@ -379,10 +419,10 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); - ImGui.TableNextColumn(); if( hasDye ) { - if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.TableNextColumn(); + if (_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { tab.Mtrl.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection; @@ -395,9 +435,10 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ret |= DrawDyePreview( tab, colorSetIdx, rowIdx, disabled, dye, floatSize ); } - else + else if ( hasAnyDye ) { ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } @@ -431,10 +472,10 @@ public partial class ModEditWindow ImGui.SameLine(); using var dis = ImRaii.Disabled(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G" ); ImGui.SameLine(); ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S" ); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs new file mode 100644 index 00000000..5616425c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using Penumbra.GameData; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private interface IConstantEditor + { + bool Draw(Span values, bool disabled, float editorWidth); + } + + private sealed class FloatConstantEditor : IConstantEditor + { + public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); + + private readonly float? _minimum; + private readonly float? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = $"%.{Math.Min(precision, (byte)9)}f"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (values[valueIdx] - _bias) / _factor; + if (disabled) + ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, _maximum ?? 0.0f, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private float Clamp(float value) + => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); + } + + private sealed class IntConstantEditor : IConstantEditor + { + private readonly int? _minimum; + private readonly int? _maximum; + private readonly float _speed; + private readonly float _relativeSpeed; + private readonly float _factor; + private readonly float _bias; + private readonly string _format; + + public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) + { + _minimum = minimum; + _maximum = maximum; + _speed = speed; + _relativeSpeed = relativeSpeed; + _factor = factor; + _bias = bias; + _format = "%d"; + if (unit.Length > 0) + _format = $"{_format} {unit.Replace("%", "%%")}"; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); + if (disabled) + ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); + else + { + if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, _format)) + { + values[valueIdx] = Clamp(value) * _factor + _bias; + ret = true; + } + } + } + + return ret; + } + + private int Clamp(int value) + => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); + } + + private sealed class ColorConstantEditor : IConstantEditor + { + private readonly bool _squaredRgb; + private readonly bool _clamped; + + public ColorConstantEditor(bool squaredRgb, bool clamped) + { + _squaredRgb = squaredRgb; + _clamped = clamped; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + if (values.Length == 3) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector3(values); + if (_squaredRgb) + value = Vector3.SquareRoot(value); + if (ImGui.ColorEdit3("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= value; + if (_clamped) + value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); + value.CopyTo(values); + return true; + } + + return false; + } + else if (values.Length == 4) + { + ImGui.SetNextItemWidth(editorWidth); + var value = new Vector4(values); + if (_squaredRgb) + value = new Vector4(MathF.Sqrt(value.X), MathF.Sqrt(value.Y), MathF.Sqrt(value.Z), value.W); + if (ImGui.ColorEdit4("##0", ref value) && !disabled) + { + if (_squaredRgb) + value *= new Vector4(value.X, value.Y, value.Z, 1.0f); + if (_clamped) + value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); + value.CopyTo(values); + return true; + } + + return false; + } + else + return FloatConstantEditor.Default.Draw(values, disabled, editorWidth); + } + } + + private sealed class EnumConstantEditor : IConstantEditor + { + private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; + + public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) + { + _values = values; + } + + public bool Draw(Span values, bool disabled, float editorWidth) + { + var fieldWidth = (editorWidth - (values.Length - 1) * ImGui.GetStyle().ItemSpacing.X) / values.Length; + + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + if (valueIdx > 0) + ImGui.SameLine(); + + ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); + + var currentValue = values[valueIdx]; + var (currentLabel, _, currentDescription) = _values.FirstOrNull(v => v.Value == currentValue) ?? (currentValue.ToString(), currentValue, string.Empty); + if (disabled) + ImGui.InputText($"##{valueIdx}", ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly); + else + { + using var c = ImRaii.Combo($"##{valueIdx}", currentLabel); + { + if (c) + foreach (var (valueLabel, value, valueDescription) in _values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + values[valueIdx] = value; + ret = true; + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + } + } + + return ret; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs index 376e656f..7d400a71 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.LivePreview.cs @@ -398,7 +398,7 @@ public partial class ModEditWindow if (mtrlHandle == null) throw new InvalidOperationException("Material doesn't have a resource handle"); - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color set textures"); @@ -424,7 +424,8 @@ public partial class ModEditWindow if (reset) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)_originalColorSetTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(_originalColorSetTexture); @@ -460,7 +461,8 @@ public partial class ModEditWindow if (success) { var oldTexture = (Texture*)Interlocked.Exchange(ref *(nint*)_colorSetTexture, (nint)newTexture); - Structs.TextureUtility.DecRef(oldTexture); + if (oldTexture != null) + Structs.TextureUtility.DecRef(oldTexture); } else Structs.TextureUtility.DecRef(newTexture); @@ -471,7 +473,7 @@ public partial class ModEditWindow if (!base.IsStillValid()) return false; - var colorSetTextures = *(Texture***)((nint)DrawObject + 0x258); + var colorSetTextures = ((Structs.CharacterBaseExt*)DrawObject)->ColorSetTextures; if (colorSetTextures == null) return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9cff681a..17f34b64 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ using Dalamud.Interface.Internal.Notifications; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -16,6 +17,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; using static Penumbra.GameData.Files.ShpkFile; @@ -26,49 +28,47 @@ public partial class ModEditWindow { private sealed class MtrlTab : IWritable, IDisposable { + private const int ShpkPrefixLength = 16; + + private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; public readonly string FilePath; public readonly bool Writable; - public uint NewKeyId; - public uint NewKeyDefault; - public uint NewConstantId; - public int NewConstantIdx; - public uint NewSamplerId; - public int NewSamplerIdx; + private string[]? _shpkNames; + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public JObject? AssociatedShpkDevkit; - public ShpkFile? AssociatedShpk; - public readonly List< string > TextureLabels = new(4); - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public float TextureLabelWidth; + public readonly string LoadedBaseDevkitPathName = string.Empty; + public readonly JObject? AssociatedBaseDevkit; // Shader Key State - public readonly List< string > ShaderKeyLabels = new(16); - public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16); - public readonly List< int > MissingShaderKeyIndices = new(16); - public readonly List< uint > AvailableKeyValues = new(16); - public string VertexShaders = "Vertex Shaders: ???"; - public string PixelShaders = "Pixel Shaders: ???"; + public readonly List< (string Label, int Index, string Description, bool MonoFont, IReadOnlyList< (string Label, uint Value, string Description) > Values) > ShaderKeys = new(16); + + public readonly HashSet< int > VertexShaders = new(16); + public readonly HashSet< int > PixelShaders = new(16); + public bool ShadersKnown = false; + public string VertexShadersString = "Vertex Shaders: ???"; + public string PixelShadersString = "Pixel Shaders: ???"; + + // Textures & Samplers + public readonly List< (string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont) > Textures = new(4); + + public readonly HashSet< int > UnfoldedTextures = new(4); + public readonly HashSet< uint > SamplerIds = new(16); + public float TextureLabelWidth; + public bool UseColorDyeSet; // Material Constants - public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16); - public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16); - public readonly HashSet< uint > DefinedMaterialConstants = new(16); - - public string MaterialConstantLabel = "Constants###Constants"; - public IndexSet OrphanedMaterialValues = new(0, false); - public int AliasedMaterialValueCount; - public bool HasMalformedMaterialConstants; - - // Samplers - public readonly List< (string Label, string FileName, uint Id) > Samplers = new(4); - public readonly List< (string Name, uint Id) > MissingSamplers = new(4); - public readonly HashSet< uint > DefinedSamplers = new(4); - public IndexSet OrphanedSamplers = new(0, false); - public int AliasedSamplerCount; + public readonly List< (string Header, List< (string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor) > Constants) > Constants = new(16); // Live-Previewers public readonly List MaterialPreviewers = new(4); @@ -87,8 +87,24 @@ public partial class ModEditWindow return _edit.FindBestMatch( defaultGamePath ); } + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + public void LoadShpk( FullPath path ) { + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + try { LoadedShpkPath = path; @@ -106,180 +122,314 @@ public partial class ModEditWindow Penumbra.Chat.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } + if( LoadedShpkPath.InternalName.IsEmpty ) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + AssociatedShpkDevkit = TryLoadShpkDevkit( Path.GetFileNameWithoutExtension( Mtrl.ShaderPackage.Name ), out LoadedShpkDevkitPathName ); + + UpdateShaderKeys(); Update(); } - public void UpdateTextureLabels() + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) { - var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); - TextureLabels.Clear(); - TextureLabelWidth = 50f * UiHelpers.Scale; - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + try { - for( var i = 0; i < Mtrl.Textures.Length; ++i ) - { - var (sampler, shpkSampler) = samplers[ i ]; - var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}"; - TextureLabels.Add( name ); - TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X ); - } - } + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } } - public void UpdateShaderKeyLabels() + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class { - ShaderKeyLabels.Clear(); - DefinedShaderKeys.Clear(); - foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() ) - { - ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" ); - DefinedShaderKeys.Add( key.Category, key.Value ); - } + return TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + } - MissingShaderKeyIndices.Clear(); - AvailableKeyValues.Clear(); - var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false ); - var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false ); - if( AssociatedShpk != null ) - { - MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() ); + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; - if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) ) + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) { - var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ]; - NewKeyId = key.Id; - NewKeyDefault = key.DefaultValue; + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; } - AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) ); - foreach( var node in AssociatedShpk.Nodes ) + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + public void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + { + foreach (var key in AssociatedShpk.MaterialKeys) { - if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) ) + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => { - foreach( var pass in node.Passes ) + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); + else + return ($"0x{value:X8}", value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + return x.Label.CompareTo(y.Label); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); + } + } + else + { + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); + } + } + + public void UpdateShaders() + { + VertexShaders.Clear(); + PixelShaders.Clear(); + if (AssociatedShpk == null) + ShadersKnown = false; + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) { - vertexShaders.Add( ( int )pass.VertexShader ); - pixelShaders.Add( ( int )pass.PixelShader ); + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + { + foreach (var pass in node.Value.Passes) + { + VertexShaders.Add((int)pass.VertexShader); + PixelShaders.Add((int)pass.PixelShader); + } + } + else + ShadersKnown = false; } } } } - VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}"; - PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}"; + var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); + var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); + + VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; + PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } - public void UpdateConstantLabels() + public void UpdateTextures() { - var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty; - MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants"; - - DefinedMaterialConstants.Clear(); - MaterialConstants.Clear(); - HasMalformedMaterialConstants = false; - AliasedMaterialValueCount = 0; - OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true ); - foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() ) + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) { - DefinedMaterialConstants.Add( constant.Id ); - var values = Mtrl.GetConstantValues( constant ); - var paramValueOffset = -values.Length; - if( values.Length > 0 ) + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) { - var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id ); - var paramByteOffset = shpkParam?.ByteOffset ?? -1; - if( ( paramByteOffset & 0x3 ) == 0 ) - { - paramValueOffset = paramByteOffset >> 2; - } - - var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length ); - AliasedMaterialValueCount += values.Length - unique; + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.ColorSets.Any(c => c.HasRows)) + SamplerIds.Add(TableSamplerId); } - else + foreach (var samplerId in SamplerIds) { - HasMalformedMaterialConstants = true; + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (!shpkSampler.HasValue || shpkSampler.Value.Slot != 2) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); } + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.FindOrAddColorSet(); + } + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length ); - var label = name == null - ? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}" - : $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}"; + TextureLabelWidth = 50f * UiHelpers.Scale; - MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) ); + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + + foreach (var (label, _, _, description, monoFont) in Textures) + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); } - MissingMaterialConstants.Clear(); - if( AssociatedShpk != null ) - { - var setIdx = false; - foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) ) - { - var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 ); - var label = name == null - ? $"(ID: 0x{param.Id:X8})" - : $"{name} (ID: 0x{param.Id:X8})"; - if( NewConstantId == param.Id ) - { - setIdx = true; - NewConstantIdx = MissingMaterialConstants.Count; - } - - MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) ); - } - - if( !setIdx && MissingMaterialConstants.Count > 0 ) - { - NewConstantIdx = 0; - NewConstantId = MissingMaterialConstants[ 0 ].Id; - } - } + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } - public void UpdateSamplers() + public void UpdateConstants() { - Samplers.Clear(); - DefinedSamplers.Clear(); - OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true ); - foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() ) + static List FindOrAddGroup(List<(string, List)> groups, string name) { - DefinedSamplers.Add( sampler.SamplerId ); - if( !OrphanedSamplers.Remove( sampler.TextureIndex ) ) - { - ++AliasedSamplerCount; - } + foreach (var (groupName, group) in groups) + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; - var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId ); - var label = shpk.HasValue - ? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}" - : $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"; - var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}"; - Samplers.Add( ( label, fileName, sampler.SamplerId ) ); + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; } - MissingSamplers.Clear(); - if( AssociatedShpk != null ) + Constants.Clear(); + if (AssociatedShpk == null) { - var setSampler = false; - foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) ) + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) { - if( sampler.Id == NewSamplerId ) + var values = Mtrl.GetConstantValues(constant); + for (var i = 0; i < values.Length; i += 4) + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + if ((shpkConstant.ByteSize & 0x3) != 0) + continue; + + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); + var values = Mtrl.GetConstantValues(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) { - setSampler = true; - NewSamplerIdx = MissingSamplers.Count; + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.Offset; + var length = values.Length - offset; + if (dkConstant.Length.HasValue) + length = Math.Min(length, (int)dkConstant.Length.Value); + if (length <= 0) + continue; + var editor = dkConstant.CreateEditor(); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } } - MissingSamplers.Add( ( sampler.Name, sampler.Id ) ); - } - - if( !setSampler && MissingSamplers.Count > 0 ) - { - NewSamplerIdx = 0; - NewSamplerId = MissingSamplers[ 0 ].Id; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(true)) + { + if ((shpkConstant.ByteOffset & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset >> 2; + for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 4, end); + if (rangeEnd > rangeStart) + fcGroup.Add(($"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); + } + } + else + { + for (var i = start; i < end; i += 4) + fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, FloatConstantEditor.Default)); + } + } } } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme + foreach (var (_, group) in Constants) + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); } public unsafe void BindToMaterialInstances() @@ -329,6 +479,7 @@ public partial class ModEditWindow // Carry on without that previewer. } } + UpdateMaterialPreview(); var colorSet = Mtrl.ColorSets.FirstOrNull(colorSet => colorSet.HasRows); @@ -378,6 +529,19 @@ public partial class ModEditWindow previewer.SetSamplerFlags(samplerCrc, samplerFlags); } + public void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValues(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + public void HighlightColorSetRow(int rowIdx) { var oldRowIdx = HighlightedColorSetRow; @@ -402,7 +566,7 @@ public partial class ModEditWindow UpdateColorSetRowPreview(rowIdx); } - public unsafe void UpdateColorSetRowPreview(int rowIdx) + public void UpdateColorSetRowPreview(int rowIdx) { if (ColorSetPreviewers.Count == 0) return; @@ -415,12 +579,12 @@ public partial class ModEditWindow var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var row = colorSet.Rows[rowIdx]; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { var stm = _edit._stainService.StmFile; var dye = maybeColorDyeSet.Value.Rows[rowIdx]; if (stm.TryGetValue(dye.Template, (StainId)_edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - ApplyDye(ref row, dye, dyes); + row.ApplyDyeTemplate(dye, dyes); } if (HighlightedColorSetRow == rowIdx) @@ -428,13 +592,12 @@ public partial class ModEditWindow foreach (var previewer in ColorSetPreviewers) { - fixed (Half* pDest = previewer.ColorSet) - Buffer.MemoryCopy(&row, pDest + LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4 * sizeof(Half), sizeof(MtrlFile.ColorSet.Row)); + row.AsHalves().CopyTo(previewer.ColorSet.AsSpan().Slice(LiveColorSetPreviewer.TextureWidth * 4 * rowIdx, LiveColorSetPreviewer.TextureWidth * 4)); previewer.ScheduleUpdate(); } } - public unsafe void UpdateColorSetPreview() + public void UpdateColorSetPreview() { if (ColorSetPreviewers.Count == 0) return; @@ -447,7 +610,7 @@ public partial class ModEditWindow var maybeColorDyeSet = Mtrl.ColorDyeSets.FirstOrNull(colorDyeSet => colorDyeSet.Index == colorSet.Index); var rows = colorSet.Rows; - if (maybeColorDyeSet.HasValue) + if (maybeColorDyeSet.HasValue && UseColorDyeSet) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; @@ -457,7 +620,7 @@ public partial class ModEditWindow ref var row = ref rows[i]; var dye = colorDyeSet.Rows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - ApplyDye(ref row, dye, dyes); + row.ApplyDyeTemplate(dye, dyes); } } @@ -466,26 +629,11 @@ public partial class ModEditWindow foreach (var previewer in ColorSetPreviewers) { - fixed (Half* pDest = previewer.ColorSet) - Buffer.MemoryCopy(&rows, pDest, LiveColorSetPreviewer.TextureLength * sizeof(Half), sizeof(MtrlFile.ColorSet.RowArray)); + rows.AsHalves().CopyTo(previewer.ColorSet); previewer.ScheduleUpdate(); } } - private static void ApplyDye(ref MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye, StmFile.DyePack dyes) - { - if (dye.Diffuse) - row.Diffuse = dyes.Diffuse; - if (dye.Specular) - row.Specular = dyes.Specular; - if (dye.SpecularStrength) - row.SpecularStrength = dyes.SpecularPower; - if (dye.Emissive) - row.Emissive = dyes.Emissive; - if (dye.Gloss) - row.GlossStrength = dyes.Gloss; - } - private static void ApplyHighlight(ref MtrlFile.ColorSet.Row row, int time) { var level = Math.Sin(time * Math.PI / 16) * 0.5 + 0.5; @@ -498,18 +646,19 @@ public partial class ModEditWindow public void Update() { - UpdateTextureLabels(); - UpdateShaderKeyLabels(); - UpdateConstantLabels(); - UpdateSamplers(); + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); } public MtrlTab( ModEditWindow edit, MtrlFile file, string filePath, bool writable ) { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + UseColorDyeSet = file.ColorDyeSets.Length > 0; + AssociatedBaseDevkit = TryLoadShpkDevkit( "_base", out LoadedBaseDevkitPathName ); LoadShpk( FindAssociatedShpk( out _, out _ ) ); if (writable) BindToMaterialInstances(); @@ -532,9 +681,96 @@ public partial class ModEditWindow } public bool Valid - => Mtrl.Valid; + => ShadersKnown && Mtrl.Valid; public byte[] Write() - => Mtrl.Write(); + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds, UseColorDyeSet); + + return output.Write(); + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = new(); + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + Integer = 1, + Color = 2, + Enum = 3, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public float Value = 0.0f; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = Array.Empty(); + + public IConstantEditor? CreateEditor() + { + switch (Type) + { + case DevkitConstantType.Hidden: + return null; + case DevkitConstantType.Float: + return new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, Unit); + case DevkitConstantType.Integer: + return new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Factor, Bias, Unit); + case DevkitConstantType.Color: + return new ColorConstantEditor(SquaredRgb, Clamped); + case DevkitConstantType.Enum: + return new EnumConstantEditor(Array.ConvertAll(Values, value => (value.Label, value.Value, value.Description))); + default: + return FloatConstantEditor.Default; + } + } + + private int? ToInteger(float? value) + => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; + } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 2d1859bd..d3bc826a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +8,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Raii; using Penumbra.GameData; @@ -19,20 +21,92 @@ public partial class ModEditWindow { private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput(MtrlTab tab, bool disabled) + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + private static readonly IReadOnlyList StandardShaderPackages = new string[] { - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bg.shpk", + "bguvscroll.shpk", + "channeling.shpk", + "characterglass.shpk", + "character.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "grass.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private enum TextureAddressMode : uint + { + Wrap = 0, + Mirror = 1, + Clamp = 2, + Border = 3, + } + + private static readonly IReadOnlyList TextureAddressModeTooltips = new string[] + { + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", + "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", + }; + + private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) + { + if (disabled) { - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; + ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); + return false; } - if (ImGui.IsItemDeactivatedAfterEdit()) - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in tab.GetShpkNames()) + { + if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) + { + tab.Mtrl.ShaderPackage.Name = value; + ret = true; + tab.AssociatedShpk = null; + tab.LoadedShpkPath = FullPath.Empty; + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); + } + } return ret; } @@ -41,8 +115,8 @@ public partial class ModEditWindow { var ret = false; var shpkFlags = (int)tab.Mtrl.ShaderPackage.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + if (ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; @@ -62,6 +136,12 @@ public partial class ModEditWindow var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; + var devkitText = tab.AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; + var baseDevkitText = tab.AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -70,6 +150,16 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Selectable(devkitText)) + ImGui.SetClipboardText(tab.LoadedShpkDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + + if (ImGui.Selectable(baseDevkitText)) + ImGui.SetClipboardText(tab.LoadedBaseDevkitPathName); + + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); + if (ImGui.Button("Associate Custom .shpk File")) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { @@ -94,94 +184,50 @@ public partial class ModEditWindow ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - - private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) - { - var ret = false; - using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); - if (!t2 || disabled) - return ret; - - var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - if (shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); - if (c) - foreach (var value in shpkKey.Value.Values) - { - if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) - { - tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - if (ImGui.Button("Remove Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - - private static bool DrawNewShaderKey(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - var ret = false; - using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) - { - if (c) - foreach (var idx in tab.MissingShaderKeyIndices) - { - var key = tab.AssociatedShpk!.MaterialKeys[idx]; - - if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) - { - tab.NewKeyDefault = key.DefaultValue; - tab.NewKeyId = key.Id; - ret = true; - tab.UpdateShaderKeyLabels(); - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Key")) - { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey - { - Category = tab.NewKeyId, - Value = tab.NewKeyDefault, - }); - ret = true; - tab.UpdateShaderKeyLabels(); - } - - return ret; - } - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) - return false; - - using var t = ImRaii.TreeNode("Shader Keys"); - if (!t) + if (tab.ShaderKeys.Count == 0) return false; var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) - ret |= DrawShaderKey(tab, disabled, ref idx); + foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + { + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + tab.Update(); + } - if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) - ret |= DrawNewShaderKey(tab); + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + ((description.Length > 0 && currentDescription.Length > 0) ? "\n\n" : string.Empty) + currentDescription); + else + ImGui.TextUnformatted($"{label}: {currentLabel}"); + } return ret; } @@ -191,162 +237,64 @@ public partial class ModEditWindow if (tab.AssociatedShpk == null) return; - ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); - } + ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) - { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name); - if (!t2) - return false; - - font.Dispose(); - - var constant = tab.Mtrl.ShaderPackage.Constants[idx]; - var ret = false; - var values = tab.Mtrl.GetConstantValues(constant); - if (values.Length > 0) + if (tab.ShaderComment.Length > 0) { - var valueOffset = constant.ByteOffset >> 2; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, valueIdx, values.Slice(valueIdx, 1)); - } - } + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(tab.ShaderComment); } - else - { - ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (!disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button("Remove Constant")) - { - tab.Mtrl.ShaderPackage.ShaderValues = - tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) - tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; - } - - ret = true; - tab.UpdateConstantLabels(); - tab.SetMaterialParameter(constant.Id, 0, new float[constant.ByteSize >> 2]); - } - - return ret; - } - - private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) - { - using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); - if (!t2) - return false; - - var ret = false; - foreach (var idx in tab.OrphanedMaterialValues) - { - ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); - if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - { - ret = true; - tab.UpdateConstantLabels(); - } - } - - return ret; - } - - private static bool DrawNewMaterialParam(MtrlTab tab) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - { - using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); - if (c) - foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) - { - if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) - { - tab.NewConstantIdx = idx; - tab.NewConstantId = constant.Id; - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Add Constant")) - { - var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant - { - Id = tab.NewConstantId, - ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), - ByteSize = byteSize, - }); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); - tab.UpdateConstantLabels(); - return true; - } - - return false; } private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if (tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) + if (tab.Constants.Count == 0) return false; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); - if (!t) + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) return false; - font.Dispose(); + using var _ = ImRaii.PushId("MaterialConstants"); + var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) - ret |= DrawMaterialConstantValues(tab, disabled, ref idx); + foreach (var (header, group) in tab.Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; - if (tab.OrphanedMaterialValues.Count > 0) - ret |= DrawMaterialOrphans(tab, disabled); - else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) - ret |= DrawNewMaterialParam(tab); + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = tab.Mtrl.GetConstantValues(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + if (editor.Draw(buffer[slice], disabled, 250.0f)) + { + ret = true; + tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } return ret; } - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) { - var (label, filename, samplerCrc) = tab.Samplers[idx]; - using var tree = ImRaii.TreeNode(label); - if (!tree) - return false; - - ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); - var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; + var ret = false; + ref var texture = ref tab.Mtrl.Textures[textureIdx]; + ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; // FIXME this probably doesn't belong here static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) @@ -357,128 +305,123 @@ public partial class ModEditWindow } } - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); - if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) + { + var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); + using var c = ImRaii.Combo(label, current.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == current)) + { + samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); + ret = true; + } + + ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); + } + return ret; + } + + var dx11 = texture.DX11; + if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) + { + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) | (uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = (int)((sampler.Flags >> 20) & 0xF); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) + { + sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + + using var t = ImRaii.TreeNode("Advanced Settings"); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (InputHexUInt16("Texture Flags", ref texture.Flags, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(samplerCrc, (uint)samplerFlags); - } - - if (!disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button("Remove Sampler")) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); - for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) - { - if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) - --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; - } - - ret = true; - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); + sampler.Flags = (uint)samplerFlags; + ret = true; + tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); } return ret; } - private static bool DrawMaterialNewSampler(MtrlTab tab) - { - var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; - ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); - using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) - { - if (c) - foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) - { - if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) - { - tab.NewSamplerIdx = idx; - tab.NewSamplerId = sampler.Id; - } - } - } - - ImGui.SameLine(); - if (!ImGui.Button("Add Sampler")) - return false; - - var newSamplerId = tab.NewSamplerId; - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler - { - SamplerId = newSamplerId, - TextureIndex = (byte)tab.Mtrl.Textures.Length, - Flags = 0, - }); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture - { - Path = string.Empty, - Flags = 0, - }); - tab.UpdateSamplers(); - tab.UpdateTextureLabels(); - tab.SetSamplerFlags(newSamplerId, 0); - return true; - } - - private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) - { - if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) - return false; - - using var t = ImRaii.TreeNode("Samplers"); - if (!t) - return false; - - var ret = false; - for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) - ret |= DrawMaterialSampler(tab, disabled, ref idx); - - if (tab.OrphanedSamplers.Count > 0) - { - using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); - if (t2) - foreach (var idx in tab.OrphanedSamplers) - { - ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", - ImGuiTreeNodeFlags.Leaf) - .Dispose(); - } - } - else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) - { - ret |= DrawMaterialNewSampler(tab); - } - - return ret; - } - - private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) + private bool DrawMaterialShader(MtrlTab tab, bool disabled) { var ret = false; - if (!ImGui.CollapsingHeader("Advanced Shader Resources")) - return ret; + if (ImGui.CollapsingHeader(tab.ShaderHeader)) + { + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); + } + + if (tab.AssociatedShpkDevkit == null) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + GC.KeepAlive(tab); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted(tab.AssociatedShpk == null + ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." + : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); + } - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - ret |= DrawMaterialConstants(tab, disabled); - ret |= DrawMaterialSamplers(tab, disabled); return ret; } @@ -500,26 +443,25 @@ public partial class ModEditWindow _ => null, }; } + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz ", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw ", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - if (valueLength == 0 || valueOffset < 0) return (null, false); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index e4de66a8..b89bab01 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -18,16 +18,14 @@ public partial class ModEditWindow DrawMaterialLivePreviewRebind( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - var ret = DrawMaterialTextureChange( tab, disabled ); + var ret = DrawBackFaceAndTransparency( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawBackFaceAndTransparency( tab, disabled ); + ret |= DrawMaterialShader( tab, disabled ); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ret |= DrawMaterialTextureChange( tab, disabled ); ret |= DrawMaterialColorSetChange( tab, disabled ); - - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawMaterialShaderResources( tab, disabled ); + ret |= DrawMaterialConstants( tab, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); DrawOtherMaterialDetails( tab.Mtrl, disabled ); @@ -40,35 +38,87 @@ public partial class ModEditWindow if (disabled) return; - if (ImGui.Button("Reload live-preview")) + if (ImGui.Button("Reload live preview")) tab.BindToMaterialInstances(); + + if (tab.MaterialPreviewers.Count == 0 && tab.ColorSetPreviewers.Count == 0) + { + ImGui.SameLine(); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red + + using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); + + ImGui.TextUnformatted("The current material has not been found on your character. Please check the Import from Screen tab for more information."); + } } private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled ) { - var ret = false; - using var table = ImRaii.Table( "##Textures", 2 ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); - for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) + if( tab.Textures.Count == 0 ) { - using var _ = ImRaii.PushId( i ); - var tmp = tab.Mtrl.Textures[ i ].Path; + return false; + } + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + if( !ImGui.CollapsingHeader( "Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen ) ) + { + return false; + } + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table( "##Textures", 3 ); + + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight ); + ImGui.TableSetupColumn( "Path" , ImGuiTableColumnFlags.WidthStretch ); + ImGui.TableSetupColumn( "Name" , ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); + for( var i = 0; i < tab.Textures.Count; ++i ) + { + var (label, textureI, samplerI, description, monoFont) = tab.Textures[i]; + + using var _ = ImRaii.PushId( samplerI ); + var tmp = tab.Mtrl.Textures[ textureI ].Path; + var unfolded = tab.UnfoldedTextures.Contains( samplerI ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( ( unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight ).ToIconString(), new Vector2( frameHeight ), + "Settings for this texture and the associated sampler", false, true ) ) + { + unfolded = !unfolded; + if( unfolded ) + tab.UnfoldedTextures.Add( samplerI ); + else + tab.UnfoldedTextures.Remove( samplerI ); + } ImGui.TableNextColumn(); ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[ i ].Path ) + && tmp != tab.Mtrl.Textures[ textureI ].Path ) { - ret = true; - tab.Mtrl.Textures[ i ].Path = tmp; + ret = true; + tab.Mtrl.Textures[ textureI ].Path = tmp; } ImGui.TableNextColumn(); - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( tab.TextureLabels[ i ] ); + using( var font = ImRaii.PushFont( UiBuilder.MonoFont, monoFont ) ) + { + ImGui.AlignTextToFramePadding(); + if( description.Length > 0 ) + ImGuiUtil.LabeledHelpMarker( label, description ); + else + ImGui.TextUnformatted( label ); + } + + if( unfolded ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler( tab, disabled, textureI, samplerI ); + ImGui.TableNextColumn(); + } } return ret; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 4e8a4f45..1b159efc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -14,7 +14,6 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.String; -using Penumbra.UI.AdvancedWindow; using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.UI.AdvancedWindow; @@ -40,7 +39,13 @@ public partial class ModEditWindow ret |= DrawShaderPackageMaterialParamLayout( file, disabled ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ret |= DrawOtherShaderPackageDetails( file, disabled ); + ret |= DrawShaderPackageResources( file, disabled ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawShaderPackageSelection( file ); + + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + DrawOtherShaderPackageDetails( file ); file.FileDialog.Draw(); @@ -50,7 +55,18 @@ public partial class ModEditWindow } private static void DrawShaderPackageSummary( ShpkTab tab ) - => ImGui.TextUnformatted( tab.Header ); + { + ImGui.TextUnformatted( tab.Header ); + if( !tab.Shpk.Disassembled ) + { + var textColor = ImGui.GetColorU32( ImGuiCol.Text ); + var textColorWarning = ( textColor & 0xFF000000u ) | ( ( textColor & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red + + using var c = ImRaii.PushColor( ImGuiCol.Text, textColorWarning ); + + ImGui.TextUnformatted( "Your system doesn't support disassembling shaders. Some functionality will be missing." ); + } + } private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx ) { @@ -163,7 +179,7 @@ public partial class ModEditWindow } DrawShaderExportButton( tab, objectName, shader, idx ); - if( !disabled ) + if( !disabled && tab.Shpk.Disassembled ) { ImGui.SameLine(); DrawShaderImportButton( tab, objectName, shaders, idx ); @@ -182,7 +198,8 @@ public partial class ModEditWindow } } - DrawRawDisassembly( shader ); + if( tab.Shpk.Disassembled ) + DrawRawDisassembly( shader ); } return ret; @@ -276,7 +293,9 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled ) { - ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" ); + ImGui.TextUnformatted( tab.Shpk.Disassembled + ? "Parameter positions (continuations are grayed out, unused values are red):" + : "Parameter positions (continuations are grayed out):" ); using var table = ImRaii.Table( "##MaterialParamLayout", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); @@ -471,6 +490,22 @@ public partial class ModEditWindow return ret; } + private static bool DrawShaderPackageResources( ShpkTab tab, bool disabled ) + { + var ret = false; + + if( !ImGui.CollapsingHeader( "Shader Resources" ) ) + { + return false; + } + + ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); + ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); + ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); + + return ret; + } + private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys ) { if( keys.Count == 0 ) @@ -513,7 +548,7 @@ public partial class ModEditWindow foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() ) { using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" ); + using var t2 = ImRaii.TreeNode( $"#{idx:D4}: Selector: 0x{node.Selector:X8}" ); if( !t2 ) { continue; @@ -549,39 +584,38 @@ public partial class ModEditWindow } } - private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled ) + private static void DrawShaderPackageSelection( ShpkTab tab ) { - var ret = false; - - if( !ImGui.CollapsingHeader( "Further Content" ) ) + if( !ImGui.CollapsingHeader( "Shader Selection" ) ) { - return false; + return; } - ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - - ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled ); - ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled ); - ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled ); - DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys ); DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys ); DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys ); DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys ); DrawShaderPackageNodes( tab ); - if( tab.Shpk.Items.Length > 0 ) + using var t = ImRaii.TreeNode( $"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors" ); + if( t ) { - using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" ); - if( t ) + using var font = ImRaii.PushFont( UiBuilder.MonoFont ); + foreach( var selector in tab.Shpk.NodeSelectors ) { - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - foreach( var (item, idx) in tab.Shpk.Items.WithIndex() ) - { - ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); - } + ImRaii.TreeNode( $"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); } } + } + + private static void DrawOtherShaderPackageDetails( ShpkTab tab ) + { + if( !ImGui.CollapsingHeader( "Further Content" ) ) + { + return; + } + + ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose(); if( tab.Shpk.AdditionalData.Length > 0 ) { @@ -591,8 +625,6 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) ); } } - - return ret; } private static string UsedComponentString( bool withSize, in Resource resource ) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 1720ec8c..2df52130 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -27,8 +27,16 @@ public partial class ModEditWindow public ShpkTab(FileDialogService fileDialog, byte[] bytes) { FileDialog = fileDialog; - Shpk = new ShpkFile(bytes, true); - Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; + try + { + Shpk = new ShpkFile(bytes, true); + } + catch (NotImplementedException) + { + Shpk = new ShpkFile(bytes, false); + } + + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a37363e3..f1c78bf7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -21,6 +23,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; using Penumbra.Util; @@ -523,6 +526,23 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } + private HashSet FindPathsStartingWith(ByteString prefix) + { + var ret = new HashSet(); + + foreach (var path in _activeCollections.Current.ResolvedFiles.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + if (_mod != null) + foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + foreach (var path in option.Files.Keys) + if (path.Path.StartsWith(prefix)) + ret.Add(path); + + return ret; + } + public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, DalamudServices dalamud, ModMergeTab modMergeTab, diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 0378d620..ad0f2e40 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -195,21 +195,7 @@ public class ModPanelSettingsTab : ITab _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); if (option.Description.Length > 0) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); - ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); - } - - if (hovered) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted(option.Description); - } - } + ImGuiUtil.SelectableHelpMarker(option.Description); id.Pop(); }