diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 01078450..9d578190 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -85,26 +85,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public event CreatedCharacterBaseDelegate? CreatedCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value), - Communication.CreatedCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); - } - } + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; public bool Valid => _lumina != null; @@ -157,6 +138,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() @@ -167,6 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded -= OnResourceLoaded; _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); _lumina = null; _communicator = null!; _modManager = null!; @@ -1189,4 +1172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index cbb86fc2..48ba86a5 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,26 +1,30 @@ using System; using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Collections; namespace Penumbra.Communication; /// /// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. +/// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> { public enum Priority { /// - Api = 0, + Api = int.MinValue, + + /// + SkinFixer = 0, } public CreatedCharacterBase() : base(nameof(CreatedCharacterBase)) { } - public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) - => Invoke(this, gameObject, appliedCollectionName, drawObject); + public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) + => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index a32b9578..9bc4a2a5 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -6,217 +6,403 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; +using Dalamud.Interface; +using Penumbra.UI; +using System.Linq; namespace Penumbra.Import.Textures; public partial class CombinedTexture { private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; + private Vector4 _constantLeft = Vector4.Zero; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; - private bool _invertLeft = false; - private bool _invertRight = false; + private Vector4 _constantRight = Vector4.Zero; private int _offsetX = 0; - private int _offsetY = 0; + private int _offsetY = 0; + private CombineOp _combineOp = CombineOp.Over; + private ResizeOp _resizeOp = ResizeOp.None; + private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha; - private Vector4 DataLeft( int offset ) - => CappedVector( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft ); + private RgbaPixelData _leftPixels = RgbaPixelData.Empty; + private RgbaPixelData _rightPixels = RgbaPixelData.Empty; - private Vector4 DataRight( int offset ) - => CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); + private const float OneThird = 1.0f / 3.0f; + private const float RWeight = 0.2126f; + private const float GWeight = 0.7152f; + private const float BWeight = 0.0722f; - private Vector4 DataRight( int x, int y ) + // @formatter:off + private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = + new[] + { + ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero ), + ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + ("Make Opaque (Drop Alpha)", new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), + }; + // @formatter:on + + private Vector4 DataLeft(int offset) + => CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft); + + private Vector4 DataRight(int offset) + => CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); + + private Vector4 DataRight(int x, int y) { x += _offsetX; y += _offsetY; - if( x < 0 || x >= _right.TextureWrap!.Width || y < 0 || y >= _right.TextureWrap!.Height ) - { + if (x < 0 || x >= _rightPixels.Width || y < 0 || y >= _rightPixels.Height) return Vector4.Zero; - } - var offset = ( y * _right.TextureWrap!.Width + x ) * 4; - return CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight ); + var offset = (y * _rightPixels.Width + x) * 4; + return CappedVector(_rightPixels.PixelData, offset, _multiplierRight, _constantRight); } - private void AddPixelsMultiplied( int y, ParallelLoopState _ ) + private void AddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var right = DataRight( x, y ); - var alpha = right.W + left.W * ( 1 - right.W ); + var offset = (_leftPixels.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = right.W + left.W * (1 - right.W); var rgba = alpha == 0 ? new Rgba32() - : new Rgba32( ( ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha ) with { W = alpha } ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + : new Rgba32(((right * right.W + left * left.W * (1 - right.W)) / alpha) with { W = alpha }); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void MultiplyPixelsLeft( int y, ParallelLoopState _ ) + private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _left.TextureWrap!.Width; ++x ) + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = ( _left.TextureWrap!.Width * y + x ) * 4; - var left = DataLeft( offset ); - var rgba = new Rgba32( left ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_leftPixels.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var alpha = left.W + right.W * (1 - left.W); + var rgba = alpha == 0 + ? new Rgba32() + : new Rgba32(((left * left.W + right * right.W * (1 - left.W)) / alpha) with { W = alpha }); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } - private void MultiplyPixelsRight( int y, ParallelLoopState _ ) + private void ChannelMergePixelsMultiplied(int y, ParallelLoopState _) { - for( var x = 0; x < _right.TextureWrap!.Width; ++x ) + var channels = _copyChannels; + for (var x = 0; x < _leftPixels.Width; ++x) { - var offset = ( _right.TextureWrap!.Width * y + x ) * 4; - var left = DataRight( offset ); - var rgba = new Rgba32( left ); - _centerStorage.RgbaPixels[ offset ] = rgba.R; - _centerStorage.RgbaPixels[ offset + 1 ] = rgba.G; - _centerStorage.RgbaPixels[ offset + 2 ] = rgba.B; - _centerStorage.RgbaPixels[ offset + 3 ] = rgba.A; + var offset = (_leftPixels.Width * y + x) * 4; + var left = DataLeft(offset); + var right = DataRight(x, y); + var rgba = new Rgba32((channels & Channels.Red) != 0 ? right.X : left.X, + (channels & Channels.Green) != 0 ? right.Y : left.Y, + (channels & Channels.Blue) != 0 ? right.Z : left.Z, + (channels & Channels.Alpha) != 0 ? right.W : left.W); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; } } + private void MultiplyPixelsLeft(int y, ParallelLoopState _) + { + for (var x = 0; x < _leftPixels.Width; ++x) + { + var offset = (_leftPixels.Width * y + x) * 4; + var left = DataLeft(offset); + var rgba = new Rgba32(left); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; + } + } + + private void MultiplyPixelsRight(int y, ParallelLoopState _) + { + for (var x = 0; x < _rightPixels.Width; ++x) + { + var offset = (_rightPixels.Width * y + x) * 4; + var right = DataRight(offset); + var rgba = new Rgba32(right); + _centerStorage.RgbaPixels[offset] = rgba.R; + _centerStorage.RgbaPixels[offset + 1] = rgba.G; + _centerStorage.RgbaPixels[offset + 2] = rgba.B; + _centerStorage.RgbaPixels[offset + 3] = rgba.A; + } + } private (int Width, int Height) CombineImage() { - var (width, height) = _left.IsLoaded - ? ( _left.TextureWrap!.Width, _left.TextureWrap!.Height ) - : ( _right.TextureWrap!.Width, _right.TextureWrap!.Height ); - _centerStorage.RgbaPixels = new byte[width * height * 4]; - _centerStorage.Type = TextureType.Bitmap; - if( _left.IsLoaded ) + var combineOp = GetActualCombineOp(); + var resizeOp = GetActualResizeOp(_resizeOp, combineOp); + + var left = resizeOp != ResizeOp.RightOnly ? RgbaPixelData.FromTexture(_left) : RgbaPixelData.Empty; + var right = resizeOp != ResizeOp.LeftOnly ? RgbaPixelData.FromTexture(_right) : RgbaPixelData.Empty; + + var targetSize = resizeOp switch { - Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft ); + ResizeOp.RightOnly => right.Size, + ResizeOp.ToRight => right.Size, + _ => left.Size, + }; + + try + { + _centerStorage.RgbaPixels = RgbaPixelData.NewPixelData(targetSize); + _centerStorage.Type = TextureType.Bitmap; + + _leftPixels = resizeOp switch + { + ResizeOp.RightOnly => RgbaPixelData.Empty, + _ => left.Resize(targetSize), + }; + _rightPixels = resizeOp switch + { + ResizeOp.LeftOnly => RgbaPixelData.Empty, + ResizeOp.None => right, + _ => right.Resize(targetSize), + }; + + Parallel.For(0, targetSize.Height, combineOp switch + { + CombineOp.Over => AddPixelsMultiplied, + CombineOp.Under => ReverseAddPixelsMultiplied, + CombineOp.LeftMultiply => MultiplyPixelsLeft, + CombineOp.RightMultiply => MultiplyPixelsRight, + CombineOp.CopyChannels => ChannelMergePixelsMultiplied, + _ => throw new InvalidOperationException($"Cannot combine images with operation {combineOp}"), + }); } - else + finally { - Parallel.For( 0, height, MultiplyPixelsRight ); + _leftPixels = RgbaPixelData.Empty; + _rightPixels = RgbaPixelData.Empty; } - return ( width, height ); + return targetSize; } - private static Vector4 CappedVector( IReadOnlyList< byte > bytes, int offset, Matrix4x4 transform, bool invert ) + + private static Vector4 CappedVector(IReadOnlyList bytes, int offset, Matrix4x4 transform, Vector4 constant) { - if( bytes.Count == 0 ) - { + if (bytes.Count == 0) return Vector4.Zero; - } - var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); - var transformed = Vector4.Transform( rgba.ToVector4(), transform ); - if( invert ) - { - transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); - } + var rgba = new Rgba32(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]); + var transformed = Vector4.Transform(rgba.ToVector4(), transform) + constant; - transformed.X = Math.Clamp( transformed.X, 0, 1 ); - transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); - transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); - transformed.W = Math.Clamp( transformed.W, 0, 1 ); + transformed.X = Math.Clamp(transformed.X, 0, 1); + transformed.Y = Math.Clamp(transformed.Y, 0, 1); + transformed.Z = Math.Clamp(transformed.Z, 0, 1); + transformed.W = Math.Clamp(transformed.W, 0, 1); return transformed; } - private static bool DragFloat( string label, float width, ref float value ) + private static bool DragFloat(string label, float width, ref float value) { var tmp = value; ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( width ); - if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) - { + ImGui.SetNextItemWidth(width); + if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f)) value = tmp; - } return ImGui.IsItemDeactivatedAfterEdit(); } - public void DrawMatrixInputLeft( float width ) + public void DrawMatrixInputLeft(float width) { - var ret = DrawMatrixInput( ref _multiplierLeft, width ); - ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft ); - if( ret ) - { + var ret = DrawMatrixInput(ref _multiplierLeft, ref _constantLeft, width); + ret |= DrawMatrixTools(ref _multiplierLeft, ref _constantLeft); + if (ret) Update(); - } } - public void DrawMatrixInputRight( float width ) + public void DrawMatrixInputRight(float width) { - var ret = DrawMatrixInput( ref _multiplierRight, width ); - ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); - ImGui.DragInt( "##XOffset", ref _offsetX, 0.5f ); + var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); + ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); + + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); ImGui.SameLine(); - ImGui.SetNextItemWidth( 75 ); - ImGui.DragInt( "Offsets##YOffset", ref _offsetY, 0.5f ); + ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); + ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); ret |= ImGui.IsItemDeactivatedAfterEdit(); - if( ret ) + + ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); + using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) { - Update(); + if (c) + foreach (var op in Enum.GetValues()) + { + if ((int)op < 0) // Negative codes are for internal use only. + continue; + + if (ImGui.Selectable(CombineOpLabels[(int)op], op == _combineOp)) + { + _combineOp = op; + ret = true; + } + + ImGuiUtil.SelectableHelpMarker(CombineOpTooltips[(int)op]); + } } + + var resizeOp = GetActualResizeOp(_resizeOp, _combineOp); + using (var dis = ImRaii.Disabled((int)resizeOp < 0)) + { + ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, + Enum.GetValues().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]); + } + + using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) + { + ImGui.TextUnformatted("Copy"); + foreach (var channel in Enum.GetValues()) + { + ImGui.SameLine(); + var copy = (_copyChannels & channel) != 0; + if (ImGui.Checkbox(channel.ToString(), ref copy)) + { + _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; + ret = true; + } + } + } + + if (ret) + Update(); } - private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width ) + private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { + using var table = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit); + if (!table) return false; - } var changes = false; ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGuiUtil.Center( "R" ); + ImGuiUtil.Center("R"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "G" ); + ImGuiUtil.Center("G"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "B" ); + ImGuiUtil.Center("B"); ImGui.TableNextColumn(); - ImGuiUtil.Center( "A" ); + ImGuiUtil.Center("A"); var inputWidth = width / 6; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "R " ); - changes |= DragFloat( "##RR", inputWidth, ref multiplier.M11 ); - changes |= DragFloat( "##RG", inputWidth, ref multiplier.M12 ); - changes |= DragFloat( "##RB", inputWidth, ref multiplier.M13 ); - changes |= DragFloat( "##RA", inputWidth, ref multiplier.M14 ); + ImGui.Text("R "); + changes |= DragFloat("##RR", inputWidth, ref multiplier.M11); + changes |= DragFloat("##RG", inputWidth, ref multiplier.M12); + changes |= DragFloat("##RB", inputWidth, ref multiplier.M13); + changes |= DragFloat("##RA", inputWidth, ref multiplier.M14); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "G " ); - changes |= DragFloat( "##GR", inputWidth, ref multiplier.M21 ); - changes |= DragFloat( "##GG", inputWidth, ref multiplier.M22 ); - changes |= DragFloat( "##GB", inputWidth, ref multiplier.M23 ); - changes |= DragFloat( "##GA", inputWidth, ref multiplier.M24 ); + ImGui.Text("G "); + changes |= DragFloat("##GR", inputWidth, ref multiplier.M21); + changes |= DragFloat("##GG", inputWidth, ref multiplier.M22); + changes |= DragFloat("##GB", inputWidth, ref multiplier.M23); + changes |= DragFloat("##GA", inputWidth, ref multiplier.M24); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "B " ); - changes |= DragFloat( "##BR", inputWidth, ref multiplier.M31 ); - changes |= DragFloat( "##BG", inputWidth, ref multiplier.M32 ); - changes |= DragFloat( "##BB", inputWidth, ref multiplier.M33 ); - changes |= DragFloat( "##BA", inputWidth, ref multiplier.M34 ); + ImGui.Text("B "); + changes |= DragFloat("##BR", inputWidth, ref multiplier.M31); + changes |= DragFloat("##BG", inputWidth, ref multiplier.M32); + changes |= DragFloat("##BB", inputWidth, ref multiplier.M33); + changes |= DragFloat("##BA", inputWidth, ref multiplier.M34); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Text( "A " ); - changes |= DragFloat( "##AR", inputWidth, ref multiplier.M41 ); - changes |= DragFloat( "##AG", inputWidth, ref multiplier.M42 ); - changes |= DragFloat( "##AB", inputWidth, ref multiplier.M43 ); - changes |= DragFloat( "##AA", inputWidth, ref multiplier.M44 ); + ImGui.Text("A "); + changes |= DragFloat("##AR", inputWidth, ref multiplier.M41); + changes |= DragFloat("##AG", inputWidth, ref multiplier.M42); + changes |= DragFloat("##AB", inputWidth, ref multiplier.M43); + changes |= DragFloat("##AA", inputWidth, ref multiplier.M44); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("1 "); + changes |= DragFloat("##1R", inputWidth, ref constant.X); + changes |= DragFloat("##1G", inputWidth, ref constant.Y); + changes |= DragFloat("##1B", inputWidth, ref constant.Z); + changes |= DragFloat("##1A", inputWidth, ref constant.W); return changes; } -} \ No newline at end of file + + private static bool DrawMatrixTools(ref Matrix4x4 multiplier, ref Vector4 constant) + { + var changes = PresetCombo(ref multiplier, ref constant); + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted("Invert"); + ImGui.SameLine(); + + Channels channels = 0; + if (ImGui.Button("Colors")) + channels |= Channels.Red | Channels.Green | Channels.Blue; + ImGui.SameLine(); + if (ImGui.Button("R")) + channels |= Channels.Red; + + ImGui.SameLine(); + if (ImGui.Button("G")) + channels |= Channels.Green; + + ImGui.SameLine(); + if (ImGui.Button("B")) + channels |= Channels.Blue; + + ImGui.SameLine(); + if (ImGui.Button("A")) + channels |= Channels.Alpha; + + changes |= InvertChannels(channels, ref multiplier, ref constant); + return changes; + } + + private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant) + { + using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview); + if (!combo) + return false; + + var ret = false; + foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) + { + if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) + continue; + + multiplier = preMultiplier; + constant = preConstant; + ret = true; + } + + return ret; + } +} diff --git a/Penumbra/Import/Textures/CombinedTexture.Operations.cs b/Penumbra/Import/Textures/CombinedTexture.Operations.cs new file mode 100644 index 00000000..441cd3f0 --- /dev/null +++ b/Penumbra/Import/Textures/CombinedTexture.Operations.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + private enum CombineOp + { + LeftMultiply = -4, + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + Over = 0, + Under = 1, + RightMultiply = 2, + CopyChannels = 3, + } + + private enum ResizeOp + { + LeftOnly = -2, + RightOnly = -1, + None = 0, + ToLeft = 1, + ToRight = 2, + } + + [Flags] + private enum Channels : byte + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + + private static readonly IReadOnlyList CombineOpLabels = new[] + { + "Overlay over Input", + "Input over Overlay", + "Replace Input", + "Copy Channels", + }; + + private static readonly IReadOnlyList CombineOpTooltips = new[] + { + "Standard composition.\nApply the overlay over the input.", + "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", + "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", + "Replace some input channels with those from the overlay.\nUseful for Multi maps.", + }; + + private static readonly IReadOnlyList ResizeOpLabels = new string[] + { + "No Resizing", + "Adjust Overlay to Input", + "Adjust Input to Overlay", + }; + + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, + }; + } + + + private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) + { + if (channels.HasFlag(Channels.Red)) + InvertRed(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Green)) + InvertGreen(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Blue)) + InvertBlue(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Alpha)) + InvertAlpha(ref multiplier, ref constant); + return channels != 0; + } + + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } +} diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c26cb900..b7e2a90a 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Numerics; using System.Threading.Tasks; @@ -68,6 +69,34 @@ public partial class CombinedTexture : IDisposable _current.TextureWrap!.Height); } + public void SaveAs(TextureType? texType, TextureManager textures, string path, TextureSaveType type, bool mipMaps) + { + var finalTexType = texType + ?? Path.GetExtension(path).ToLowerInvariant() switch + { + ".tex" => TextureType.Tex, + ".dds" => TextureType.Dds, + ".png" => TextureType.Png, + _ => TextureType.Unknown, + }; + + switch (finalTexType) + { + case TextureType.Tex: + SaveAsTex(textures, path, type, mipMaps); + break; + case TextureType.Dds: + SaveAsDds(textures, path, type, mipMaps); + break; + case TextureType.Png: + SaveAsPng(textures, path); + break; + default: + throw new ArgumentException( + $"Cannot save texture as TextureType {finalTexType} with extension {Path.GetExtension(path).ToLowerInvariant()}"); + } + } + public void SaveAsTex(TextureManager textures, string path, TextureSaveType type, bool mipMaps) => SaveAs(textures, path, type, mipMaps, true); @@ -97,36 +126,21 @@ public partial class CombinedTexture : IDisposable public void Update() { Clean(); - if (_left.IsLoaded) + switch (GetActualCombineOp()) { - if (_right.IsLoaded) - { - _current = _centerStorage; - _mode = Mode.Custom; - } - else if (!_invertLeft && _multiplierLeft.IsIdentity) - { + case CombineOp.Invalid: break; + case CombineOp.LeftCopy: _mode = Mode.LeftCopy; _current = _left; - } - else - { - _current = _centerStorage; - _mode = Mode.Custom; - } - } - else if (_right.IsLoaded) - { - if (!_invertRight && _multiplierRight.IsIdentity) - { - _current = _right; + break; + case CombineOp.RightCopy: _mode = Mode.RightCopy; - } - else - { - _current = _centerStorage; + _current = _right; + break; + default: _mode = Mode.Custom; - } + _current = _centerStorage; + break; } } diff --git a/Penumbra/Import/Textures/RgbaPixelData.cs b/Penumbra/Import/Textures/RgbaPixelData.cs new file mode 100644 index 00000000..0314b104 --- /dev/null +++ b/Penumbra/Import/Textures/RgbaPixelData.cs @@ -0,0 +1,43 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace Penumbra.Import.Textures; + +public readonly record struct RgbaPixelData(int Width, int Height, byte[] PixelData) +{ + public static readonly RgbaPixelData Empty = new(0, 0, Array.Empty()); + + public (int Width, int Height) Size + => (Width, Height); + + public RgbaPixelData((int Width, int Height) size, byte[] pixelData) + : this(size.Width, size.Height, pixelData) + { + } + + public Image ToImage() + => Image.LoadPixelData(PixelData, Width, Height); + + public RgbaPixelData Resize((int Width, int Height) size) + { + if (Width == size.Width && Height == size.Height) + return this; + + var result = new RgbaPixelData(size, NewPixelData(size)); + using (var image = ToImage()) + { + image.Mutate(ctx => ctx.Resize(size.Width, size.Height, KnownResamplers.Lanczos3)); + image.CopyPixelDataTo(result.PixelData); + } + + return result; + } + + public static byte[] NewPixelData((int Width, int Height) size) + => new byte[size.Width * size.Height * 4]; + + public static RgbaPixelData FromTexture(Texture texture) + => new(texture.TextureWrap!.Width, texture.TextureWrap!.Height, texture.RgbaPixels); +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a4cbc967..1a257a96 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable _characterBaseCreateMetaChanges = DisposableContainer.Empty; if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, drawObject); + _lastCreatedCollection.ModCollection, drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs new file mode 100644 index 00000000..7ec0f218 --- /dev/null +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.SafeHandles; + +public unsafe class SafeResourceHandle : SafeHandle +{ + public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; + + public override bool IsInvalid => handle == 0; + + public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public static SafeResourceHandle CreateInvalid() + => new(null, incRef: false); + + protected override bool ReleaseHandle() + { + var handle = Interlocked.Exchange(ref this.handle, 0); + if (handle != 0) + ((ResourceHandle*)handle)->DecRef(); + + return true; + } +} diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index ef706f6d..00eab531 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable public event Action LoadingFinished; public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -102,6 +103,12 @@ public unsafe partial class CharacterUtility : IDisposable anyMissing |= DefaultDecalResource == nint.Zero; } + if (DefaultSkinShpkResource == nint.Zero) + { + DefaultSkinShpkResource = (nint)Address->SkinShpkResource; + anyMissing |= DefaultSkinShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable /// Return all relevant resources to the default resource. public void ResetAll() - { + { if (!Ready) - return; + return; foreach (var list in _lists) list.Dispose(); Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs new file mode 100644 index 00000000..be45708f --- /dev/null +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.Services; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class SkinFixer : IDisposable +{ + public static readonly Utf8GamePath SkinShpkPath = + Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _onRenderMaterialHook; + + private readonly GameEventManager _gameEvents; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + // CharacterBase to ShpkHandle + private readonly ConcurrentDictionary _skinShpks = new(); + + private readonly object _lock = new(); + + private int _moddedSkinShpkCount = 0; + private ulong _slowPathCallDelta = 0; + + public bool Enabled { get; internal set; } = true; + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) + { + SignatureHelper.Initialise(this); + _gameEvents = gameEvents; + _resources = resources; + _utility = utility; + _communicator = communicator; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); + _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _onRenderMaterialHook.Enable(); + } + + public void Dispose() + { + _onRenderMaterialHook.Dispose(); + _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); + _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; + foreach (var skinShpk in _skinShpks.Values) + skinShpk.Dispose(); + _skinShpks.Clear(); + _moddedSkinShpkCount = 0; + } + + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) + { + if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) + return; + + Task.Run(() => + { + var skinShpk = SafeResourceHandle.CreateInvalid(); + try + { + var data = collection.ToResolveData(gameObject); + if (data.Valid) + { + var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + } + + if (!skinShpk.IsInvalid) + { + if (_skinShpks.TryAdd(drawObject, skinShpk)) + { + if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + else + { + skinShpk.Dispose(); + } + } + }); + } + + private void OnCharacterBaseDestructor(nint characterBase) + { + if (!_skinShpks.Remove(characterBase, out var skinShpk)) + return; + + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) + return _onRenderMaterialHook!.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; + if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) + return _onRenderMaterialHook!.Original(human, param); + + Interlocked.Increment(ref _slowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_lock) + { + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; + return _onRenderMaterialHook!.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index b273091b..765ad25f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -10,6 +10,7 @@ public unsafe struct CharacterUtilityData { public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; + public const int IndexSkinShpk = 76; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() .Zip( Enum.GetValues< MetaIndex >() ) @@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData .Select( n => n.Second ).ToArray(); public const int TotalNumResources = 87; - - /// Obtain the index for the eqdp file corresponding to the given race code and accessory. + + /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) => +( int )raceCode switch { @@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData [FieldOffset( 8 + IndexDecalTex * 8 )] public TextureResourceHandle* DecalTexResource; + [FieldOffset( 8 + IndexSkinShpk * 8 )] + public ResourceHandle* SkinShpkResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b291e392..eeee8fd0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; -using OtterGui.Tasks; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -81,6 +81,7 @@ public class Penumbra : IDalamudPlugin { _services.GetRequiredService(); } + _services.GetRequiredService(); SetupInterface(); SetupApi(); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 728585ae..8bea52e3 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -117,7 +117,8 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddResolvers(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4d36ff8a..12f98ccb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Numerics; using System.Threading.Tasks; -using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -90,7 +89,7 @@ public partial class ModEditWindow if (ImGui.Selectable(newText, idx == _currentSaveAs)) _currentSaveAs = idx; - ImGuiUtil.HoverTooltip(newDesc); + ImGuiUtil.SelectableHelpMarker(newDesc); } } @@ -114,73 +113,58 @@ public partial class ModEditWindow SaveAsCombo(); ImGui.SameLine(); MipMapInput(); - if (ImGui.Button("Save as TEX", -Vector2.UnitX)) + + var canSaveInPlace = Path.IsPathRooted(_left.Path) && _left.Type is TextureType.Tex or TextureType.Dds or TextureType.Png; + + var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, + "This saves the texture in place. This is not revertible.", + !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { - var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => - { - if (a) - _center.SaveAsTex(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + AddReloadTask(_left.Path, false); } - if (ImGui.Button("Save as DDS", -Vector2.UnitX)) - { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => - { - if (a) - _center.SaveAsDds(_textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; - } + ImGui.SameLine(); + if (ImGui.Button("Save as TEX", buttonSize2)) + OpenSaveAsDialog(".tex"); + + if (ImGui.Button("Export as PNG", buttonSize2)) + OpenSaveAsDialog(".png"); + ImGui.SameLine(); + if (ImGui.Button("Export as DDS", buttonSize2)) + OpenSaveAsDialog(".dds"); ImGui.NewLine(); - if (ImGui.Button("Save as PNG", -Vector2.UnitX)) + var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy; + + var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); + if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3, + "This converts the texture to BC7 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { - var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); - _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => - { - if (a) - _center.SaveAsPng(_textures, b); - }, _mod!.ModPath.FullName, _forceTextureStartPath); - _forceTextureStartPath = false; + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - if (_left.Type is TextureType.Tex && _center.IsLeftCopy) + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize3, + "This converts the texture to BC3 format in place. This is not revertible.", + !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { - var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); - if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize, - "This converts the texture to BC7 format in place. This is not revertible.", - _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize, - "This converts the texture to BC3 format in place. This is not revertible.", - _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize, - "This converts the texture to RGBA format in place. This is not revertible.", - _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) - { - _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - AddReloadTask(_left.Path); - } + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } - else + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3, + "This converts the texture to RGBA format in place. This is not revertible.", + !canConvertInPlace + || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { - ImGui.NewLine(); + _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + AddReloadTask(_left.Path, false); } } @@ -212,17 +196,36 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } - private void AddReloadTask(string path) + private void OpenSaveAsDialog(string defaultExtension) + { + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, (a, b) => + { + if (a) + { + _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + if (b == _left.Path) + AddReloadTask(_left.Path, false); + else if (b == _right.Path) + AddReloadTask(_right.Path, true); + } + }, _mod!.ModPath.FullName, _forceTextureStartPath); + _forceTextureStartPath = false; + } + + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => { if (!t.IsCompletedSuccessfully) return; - if (_left.Path != path) + var tex = right ? _right : _left; + + if (tex.Path != path) return; - _dalamud.Framework.RunOnFrameworkThread(() => _left.Reload(_textures)); + _dalamud.Framework.RunOnFrameworkThread(() => tex.Reload(_textures)); }); } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index a48fd714..c24d64fa 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,6 +34,8 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; namespace Penumbra.UI.Tabs; @@ -63,6 +65,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; + private readonly SkinFixer _skinFixer; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -70,7 +73,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager) + TextureManager textureManager, SkinFixer skinFixer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -103,6 +106,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; + _skinFixer = skinFixer; } public ReadOnlySpan Label @@ -144,6 +148,8 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawPlayerModelInfo(); ImGui.NewLine(); + DrawGlobalVariableInfo(); + ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } @@ -338,7 +344,7 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -350,6 +356,7 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(name); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); } @@ -363,6 +370,7 @@ public class DebugTab : Window, ITab { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + ImGuiUtil.DrawTableColumn((obj.Address == nint.Zero) ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); @@ -582,45 +590,72 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + var enableSkinFixer = _skinFixer.Enabled; + if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) + _skinFixer.Enabled = enableSkinFixer; + + if (enableSkinFixer) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + } + + using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) { - var idx = CharacterUtility.RelevantIndices[i]; - var intern = new CharacterUtility.InternalIndex(i); + var intern = CharacterUtility.ReverseIndices[idx]; var resource = _characterUtility.Address->Resource(idx); ImGui.TableNextColumn(); + ImGui.TextUnformatted($"[{idx}]"); + ImGui.TableNextColumn(); ImGui.TextUnformatted($"0x{(ulong)resource:X}"); ImGui.TableNextColumn(); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } UiHelpers.Text(resource); ImGui.TableNextColumn(); - ImGui.Selectable($"0x{resource->GetData().Data:X}"); - if (ImGui.IsItemClicked()) + var data = (nint)ResourceHandle.GetData(resource); + var length = ResourceHandle.GetLength(resource); + if (ImGui.Selectable($"0x{data:X}")) { - var (data, length) = resource->GetData(); if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); } ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(length.ToString()); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{resource->GetData().Length}"); - ImGui.TableNextColumn(); - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + if (intern.Value != -1) + { + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + else + ImGui.TableNextColumn(); } } @@ -665,6 +700,18 @@ public class DebugTab : Window, ITab } } + private static void DrawCopyableAddress(string label, nint address) + { + using (var _ = PushFont(UiBuilder.MonoFont)) + if (ImGui.Selectable($"0x{address:X16} {label}")) + ImGui.SetClipboardText($"{address:X16}"); + + ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); + } + + private static unsafe void DrawCopyableAddress(string label, void* address) + => DrawCopyableAddress(label, (nint)address); + /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -673,10 +720,14 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; + DrawCopyableAddress("PlayerCharacter", player.Address); + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; + DrawCopyableAddress("CharacterBase", model); + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { if (t1) @@ -730,6 +781,19 @@ public class DebugTab : Window, ITab } } + /// Draw information about some game global variables. + private unsafe void DrawGlobalVariableInfo() + { + var header = ImGui.CollapsingHeader("Global Variables"); + ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); + if (!header) + return; + + DrawCopyableAddress("CharacterUtility", _characterUtility.Address); + DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); + DrawCopyableAddress("Device", Device.Instance()); + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() {