mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Merge branch 'master' into feature/material-editor-2099
This commit is contained in:
commit
8695e89792
15 changed files with 923 additions and 255 deletions
|
|
@ -85,26 +85,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
}
|
||||
}
|
||||
|
||||
public event CreatedCharacterBaseDelegate? CreatedCharacterBase
|
||||
{
|
||||
add
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
CheckInitialized();
|
||||
_communicator.CreatedCharacterBase.Subscribe(new Action<nint, string, nint>(value),
|
||||
Communication.CreatedCharacterBase.Priority.Api);
|
||||
}
|
||||
remove
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
CheckInitialized();
|
||||
_communicator.CreatedCharacterBase.Unsubscribe(new Action<nint, string, nint>(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,30 @@
|
|||
using System;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Collections;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary> <list type="number">
|
||||
/// <item>Parameter is the game object for which a draw object is created. </item>
|
||||
/// <item>Parameter is the name of the applied collection. </item>
|
||||
/// <item>Parameter is the applied collection. </item>
|
||||
/// <item>Parameter is the created draw object. </item>
|
||||
/// </list> </summary>
|
||||
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, string, nint>, CreatedCharacterBase.Priority>
|
||||
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, ModCollection, nint>, CreatedCharacterBase.Priority>
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
|
||||
Api = 0,
|
||||
Api = int.MinValue,
|
||||
|
||||
/// <seealso cref="Interop.Services.SkinFixer.OnCharacterBaseCreated"/>
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<byte> 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<CombineOp>())
|
||||
{
|
||||
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<ResizeOp>().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<Channels>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
150
Penumbra/Import/Textures/CombinedTexture.Operations.cs
Normal file
150
Penumbra/Import/Textures/CombinedTexture.Operations.cs
Normal file
|
|
@ -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<string> CombineOpLabels = new[]
|
||||
{
|
||||
"Overlay over Input",
|
||||
"Input over Overlay",
|
||||
"Replace Input",
|
||||
"Copy Channels",
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<string> 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<string> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
Penumbra/Import/Textures/RgbaPixelData.cs
Normal file
43
Penumbra/Import/Textures/RgbaPixelData.cs
Normal file
|
|
@ -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<byte>());
|
||||
|
||||
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<Rgba32> ToImage()
|
||||
=> Image.LoadPixelData<Rgba32>(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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
34
Penumbra/Interop/SafeHandles/SafeResourceHandle.cs
Normal file
34
Penumbra/Interop/SafeHandles/SafeResourceHandle.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
|
||||
/// <summary> Return all relevant resources to the default resource. </summary>
|
||||
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()
|
||||
|
|
|
|||
170
Penumbra/Interop/Services/SkinFixer.cs
Normal file
170
Penumbra/Interop/Services/SkinFixer.cs
Normal file
|
|
@ -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<OnRenderMaterialDelegate> _onRenderMaterialHook;
|
||||
|
||||
private readonly GameEventManager _gameEvents;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ResourceLoader _resources;
|
||||
private readonly CharacterUtility _utility;
|
||||
|
||||
// CharacterBase to ShpkHandle
|
||||
private readonly ConcurrentDictionary<nint, SafeResourceHandle> _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<OnRenderMaterialDelegate>.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary>
|
||||
|
||||
/// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary>
|
||||
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.
|
||||
}
|
||||
|
|
@ -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<PathResolver>();
|
||||
}
|
||||
_services.GetRequiredService<SkinFixer>();
|
||||
|
||||
SetupInterface();
|
||||
SetupApi();
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ public static class ServiceManager
|
|||
=> services.AddSingleton<ResourceLoader>()
|
||||
.AddSingleton<ResourceWatcher>()
|
||||
.AddSingleton<ResourceTreeFactory>()
|
||||
.AddSingleton<MetaFileManager>();
|
||||
.AddSingleton<MetaFileManager>()
|
||||
.AddSingleton<SkinFixer>();
|
||||
|
||||
private static IServiceCollection AddResolvers(this IServiceCollection services)
|
||||
=> services.AddSingleton<AnimationHookService>()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<byte> 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>((byte*)data, length).ToArray().Select(b => b.ToString("X2"))));
|
||||
new ReadOnlySpan<byte>((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>((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>((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);
|
||||
|
||||
/// <summary> Draw information about the models, materials and resources currently loaded by the local player. </summary>
|
||||
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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary> Draw information about some game global variables. </summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary> Draw resources with unusual reference count. </summary>
|
||||
private unsafe void DrawResourceProblems()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue