Merge branch 'master' into feature/material-editor-2099

This commit is contained in:
Ottermandias 2023-08-31 00:49:49 +02:00
commit 8695e89792
15 changed files with 923 additions and 255 deletions

View file

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

View file

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

View file

@ -6,42 +6,71 @@ 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 CombineOp _combineOp = CombineOp.Over;
private ResizeOp _resizeOp = ResizeOp.None;
private Channels _copyChannels = Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha;
private RgbaPixelData _leftPixels = RgbaPixelData.Empty;
private RgbaPixelData _rightPixels = RgbaPixelData.Empty;
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;
// @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( _left.RgbaPixels, offset, _multiplierLeft, _invertLeft );
=> CappedVector(_leftPixels.PixelData, offset, _multiplierLeft, _constantLeft);
private Vector4 DataRight(int offset)
=> CappedVector( _right.RgbaPixels, offset, _multiplierRight, _invertRight );
=> 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 _)
{
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 offset = (_leftPixels.Width * y + x) * 4;
var left = DataLeft(offset);
var right = DataRight(x, y);
var alpha = right.W + left.W * (1 - right.W);
@ -55,11 +84,48 @@ public partial class CombinedTexture
}
}
private void ReverseAddPixelsMultiplied(int y, ParallelLoopState _)
{
for (var x = 0; x < _leftPixels.Width; ++x)
{
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 ChannelMergePixelsMultiplied(int y, ParallelLoopState _)
{
var channels = _copyChannels;
for (var x = 0; x < _leftPixels.Width; ++x)
{
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 < _left.TextureWrap!.Width; ++x )
for (var x = 0; x < _leftPixels.Width; ++x)
{
var offset = ( _left.TextureWrap!.Width * y + x ) * 4;
var offset = (_leftPixels.Width * y + x) * 4;
var left = DataLeft(offset);
var rgba = new Rgba32(left);
_centerStorage.RgbaPixels[offset] = rgba.R;
@ -71,11 +137,11 @@ public partial class CombinedTexture
private void MultiplyPixelsRight(int y, ParallelLoopState _)
{
for( var x = 0; x < _right.TextureWrap!.Width; ++x )
for (var x = 0; x < _rightPixels.Width; ++x)
{
var offset = ( _right.TextureWrap!.Width * y + x ) * 4;
var left = DataRight( offset );
var rgba = new Rgba32( left );
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;
@ -83,38 +149,64 @@ public partial class CombinedTexture
}
}
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];
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
{
ResizeOp.RightOnly => right.Size,
ResizeOp.ToRight => right.Size,
_ => left.Size,
};
try
{
_centerStorage.RgbaPixels = RgbaPixelData.NewPixelData(targetSize);
_centerStorage.Type = TextureType.Bitmap;
if( _left.IsLoaded )
_leftPixels = resizeOp switch
{
Parallel.For( 0, height, _right.IsLoaded ? AddPixelsMultiplied : MultiplyPixelsLeft );
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)
{
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 transformed = Vector4.Transform(rgba.ToVector4(), transform) + constant;
transformed.X = Math.Clamp(transformed.X, 0, 1);
transformed.Y = Math.Clamp(transformed.Y, 0, 1);
@ -129,48 +221,82 @@ public partial class CombinedTexture
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(width);
if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f))
{
value = tmp;
}
return ImGui.IsItemDeactivatedAfterEdit();
}
public void DrawMatrixInputLeft(float width)
{
var ret = DrawMatrixInput( ref _multiplierLeft, width );
ret |= ImGui.Checkbox( "Invert Colors##Left", ref _invertLeft );
var ret = DrawMatrixInput(ref _multiplierLeft, ref _constantLeft, width);
ret |= DrawMatrixTools(ref _multiplierLeft, ref _constantLeft);
if (ret)
{
Update();
}
}
public void DrawMatrixInputRight(float width)
{
var ret = DrawMatrixInput( ref _multiplierRight, width );
ret |= ImGui.Checkbox( "Invert Colors##Right", ref _invertRight );
ImGui.SameLine();
ImGui.SetNextItemWidth( 75 );
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.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]);
}
}
private static bool DrawMatrixInput( ref Matrix4x4 multiplier, float width )
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, ref Vector4 constant, float width)
{
using var table = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return false;
}
var changes = false;
@ -217,6 +343,66 @@ public partial class CombinedTexture
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;
}
}

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

View file

@ -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)
{
if (_right.IsLoaded)
{
_current = _centerStorage;
_mode = Mode.Custom;
}
else if (!_invertLeft && _multiplierLeft.IsIdentity)
switch (GetActualCombineOp())
{
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;
}
}

View 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);
}

View file

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

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

View file

@ -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;
@ -149,6 +156,7 @@ public unsafe partial class CharacterUtility : IDisposable
Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource;
Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource;
Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource;
}
public void Dispose()

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

View file

@ -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 >() )
@ -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.
}

View file

@ -81,6 +81,7 @@ public class Penumbra : IDalamudPlugin
{
_services.GetRequiredService<PathResolver>();
}
_services.GetRequiredService<SkinFixer>();
SetupInterface();
SetupApi();

View file

@ -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>()

View file

@ -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 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;
}
var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy;
if (_left.Type is TextureType.Tex && _center.IsLeftCopy)
{
var buttonSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0);
if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize,
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.",
_left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB))
!canConvertInPlace || _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);
AddReloadTask(_left.Path, false);
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize,
if (ImGuiUtil.DrawDisabledButton("Convert to BC3", buttonSize3,
"This converts the texture to BC3 format in place. This is not revertible.",
_left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB))
!canConvertInPlace || _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);
AddReloadTask(_left.Path, false);
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize,
if (ImGuiUtil.DrawDisabledButton("Convert to RGBA", buttonSize3,
"This converts the texture to RGBA format in place. This is not revertible.",
_left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB))
!canConvertInPlace
|| _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);
}
}
else
{
ImGui.NewLine();
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));
});
}

View file

@ -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,35 +590,59 @@ 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();
if (intern.Value != -1)
{
ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}");
if (ImGui.IsItemClicked())
ImGui.SetClipboardText(string.Join("\n",
@ -622,6 +654,9 @@ public class DebugTab : Window, ITab
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}");
}
else
ImGui.TableNextColumn();
}
}
private void DrawDebugTabMetaLists()
@ -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()
{