Skin Fixer (fixes modding of skin.shpk)

This commit is contained in:
Exter-N 2023-08-27 03:42:34 +02:00
parent 4f71065d67
commit ead88f9fa6
8 changed files with 302 additions and 30 deletions

View file

@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable
public event Action LoadingFinished; public event Action LoadingFinished;
public nint DefaultTransparentResource { get; private set; } public nint DefaultTransparentResource { get; private set; }
public nint DefaultDecalResource { get; private set; } public nint DefaultDecalResource { get; private set; }
public nint DefaultSkinShpkResource { get; private set; }
/// <summary> /// <summary>
/// The relevant indices depend on which meta manipulations we allow for. /// 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; anyMissing |= DefaultDecalResource == nint.Zero;
} }
if (DefaultSkinShpkResource == nint.Zero)
{
DefaultSkinShpkResource = (nint)Address->SkinShpkResource;
anyMissing |= DefaultSkinShpkResource == nint.Zero;
}
if (anyMissing) if (anyMissing)
return; return;
@ -149,6 +156,7 @@ public unsafe partial class CharacterUtility : IDisposable
Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource;
Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource;
Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource;
} }
public void Dispose() public void Dispose()

View file

@ -0,0 +1,161 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
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.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.ResourceLoading;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Services;
public 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 CollectionResolver _collectionResolver;
private readonly GameEventManager _gameEvents;
private readonly ResourceLoader _resources;
private readonly CharacterUtility _utility;
private readonly ConcurrentDictionary<nint /* CharacterBase* */, nint /* ResourceHandle* */> _skinShpks = new();
private readonly object _lock = new();
private bool _enabled = true;
private int _moddedSkinShpkCount = 0;
private ulong _slowPathCallDelta = 0;
public bool Enabled
{
get => _enabled;
set => _enabled = value;
}
public int ModdedSkinShpkCount
=> _moddedSkinShpkCount;
public SkinFixer(CollectionResolver collectionResolver, GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, DrawObjectState _)
{
SignatureHelper.Initialise(this);
_collectionResolver = collectionResolver;
_gameEvents = gameEvents;
_resources = resources;
_utility = utility;
_onRenderMaterialHook = Hook<OnRenderMaterialDelegate>.FromAddress(_humanVTable[62], OnRenderHumanMaterial);
_gameEvents.CharacterBaseCreated += OnCharacterBaseCreated; // The dependency on DrawObjectState shall ensure that this handler is registered after its one.
_gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor;
_onRenderMaterialHook.Enable();
}
~SkinFixer()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
_onRenderMaterialHook.Dispose();
_gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated;
_gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor;
foreach (var skinShpk in _skinShpks.Values)
if (skinShpk != nint.Zero)
((ResourceHandle*)skinShpk)->DecRef();
_skinShpks.Clear();
_moddedSkinShpkCount = 0;
}
public ulong GetAndResetSlowPathCallDelta()
=> Interlocked.Exchange(ref _slowPathCallDelta, 0);
private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject)
{
if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human)
return;
nint skinShpk;
try
{
var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
skinShpk = data.Valid ? (nint)_resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data) : nint.Zero;
}
catch (Exception e)
{
Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}");
skinShpk = nint.Zero;
}
if (skinShpk != nint.Zero && _skinShpks.TryAdd(drawObject, skinShpk) && skinShpk != _utility.DefaultSkinShpkResource)
Interlocked.Increment(ref _moddedSkinShpkCount);
}
private void OnCharacterBaseDestructor(nint characterBase)
{
if (_skinShpks.Remove(characterBase, out var skinShpk) && skinShpk != nint.Zero)
{
((ResourceHandle*)skinShpk)->DecRef();
if (skinShpk != _utility.DefaultSkinShpkResource)
Interlocked.Decrement(ref _moddedSkinShpkCount);
}
}
private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param)
{
if (!_enabled || // Can be toggled on the debug tab.
_moddedSkinShpkCount == 0 || // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all.
!_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk == nint.Zero)
return _onRenderMaterialHook!.Original(human, param);
var material = param->Model->Materials[param->MaterialIndex];
var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle;
if ((nint)shpkResource != skinShpk)
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;
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 IndexTransparentTex = 72;
public const int IndexDecalTex = 73; public const int IndexDecalTex = 73;
public const int IndexSkinShpk = 76;
public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >()
.Zip( Enum.GetValues< MetaIndex >() ) .Zip( Enum.GetValues< MetaIndex >() )
@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData
[FieldOffset( 8 + IndexDecalTex * 8 )] [FieldOffset( 8 + IndexDecalTex * 8 )]
public TextureResourceHandle* DecalTexResource; public TextureResourceHandle* DecalTexResource;
[FieldOffset( 8 + IndexSkinShpk * 8 )]
public ResourceHandle* SkinShpkResource;
// not included resources have no known use case. // not included resources have no known use case.
} }

View file

@ -8,8 +8,11 @@ public unsafe struct MtrlResource
[FieldOffset( 0x00 )] [FieldOffset( 0x00 )]
public ResourceHandle Handle; public ResourceHandle Handle;
[FieldOffset( 0xC8 )]
public ShaderPackageResourceHandle* ShpkResourceHandle;
[FieldOffset( 0xD0 )] [FieldOffset( 0xD0 )]
public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. public TextureEntry* TexSpace; // Contains the offsets for the tex files inside the string list.
[FieldOffset( 0xE0 )] [FieldOffset( 0xE0 )]
public byte* StringList; public byte* StringList;
@ -24,8 +27,21 @@ public unsafe struct MtrlResource
=> StringList + ShpkOffset; => StringList + ShpkOffset;
public byte* TexString( int idx ) public byte* TexString( int idx )
=> StringList + *( TexSpace + 4 + idx * 8 ); => StringList + TexSpace[idx].PathOffset;
public bool TexIsDX11( int idx ) public bool TexIsDX11( int idx )
=> *(TexSpace + 5 + idx * 8) >= 0x8000; => TexSpace[idx].Flags >= 0x8000;
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct TextureEntry
{
[FieldOffset( 0x00 )]
public TextureResourceHandle* ResourceHandle;
[FieldOffset( 0x08 )]
public ushort PathOffset;
[FieldOffset( 0x0A )]
public ushort Flags;
}
} }

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle
public IntPtr Unk; public IntPtr Unk;
[FieldOffset( 0x118 )] [FieldOffset( 0x118 )]
public IntPtr KernelTexture; public Texture* KernelTexture;
[FieldOffset( 0x20 )] [FieldOffset( 0x20 )]
public IntPtr NewKernelTexture; public IntPtr NewKernelTexture;
} }
[StructLayout(LayoutKind.Explicit)]
public unsafe struct ShaderPackageResourceHandle
{
[FieldOffset( 0x0 )]
public ResourceHandle Handle;
[FieldOffset( 0xB0 )]
public ShaderPackage* ShaderPackage;
}
[StructLayout( LayoutKind.Explicit )] [StructLayout( LayoutKind.Explicit )]
public unsafe struct ResourceHandle public unsafe struct ResourceHandle
{ {

View file

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

View file

@ -117,7 +117,8 @@ public static class ServiceManager
=> services.AddSingleton<ResourceLoader>() => services.AddSingleton<ResourceLoader>()
.AddSingleton<ResourceWatcher>() .AddSingleton<ResourceWatcher>()
.AddSingleton<ResourceTreeFactory>() .AddSingleton<ResourceTreeFactory>()
.AddSingleton<MetaFileManager>(); .AddSingleton<MetaFileManager>()
.AddSingleton<SkinFixer>();
private static IServiceCollection AddResolvers(this IServiceCollection services) private static IServiceCollection AddResolvers(this IServiceCollection services)
=> services.AddSingleton<AnimationHookService>() => services.AddSingleton<AnimationHookService>()

View file

@ -34,6 +34,9 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Penumbra.Interop.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using static Lumina.Data.Parsing.Layer.LayerCommon;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.Tabs;
@ -63,6 +66,7 @@ public class DebugTab : Window, ITab
private readonly ImportPopup _importPopup; private readonly ImportPopup _importPopup;
private readonly FrameworkManager _framework; private readonly FrameworkManager _framework;
private readonly TextureManager _textureManager; private readonly TextureManager _textureManager;
private readonly SkinFixer _skinFixer;
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService,
@ -70,7 +74,7 @@ public class DebugTab : Window, ITab
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager) TextureManager textureManager, SkinFixer skinFixer)
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false)
{ {
IsOpen = true; IsOpen = true;
@ -103,6 +107,7 @@ public class DebugTab : Window, ITab
_importPopup = importPopup; _importPopup = importPopup;
_framework = framework; _framework = framework;
_textureManager = textureManager; _textureManager = textureManager;
_skinFixer = skinFixer;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -144,6 +149,8 @@ public class DebugTab : Window, ITab
ImGui.NewLine(); ImGui.NewLine();
DrawPlayerModelInfo(); DrawPlayerModelInfo();
ImGui.NewLine(); ImGui.NewLine();
DrawGlobalVariableInfo();
ImGui.NewLine();
DrawDebugTabIpc(); DrawDebugTabIpc();
ImGui.NewLine(); ImGui.NewLine();
} }
@ -338,7 +345,7 @@ public class DebugTab : Window, ITab
if (!ImGui.CollapsingHeader("Actors")) if (!ImGui.CollapsingHeader("Actors"))
return; return;
using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
-Vector2.UnitX); -Vector2.UnitX);
if (!table) if (!table)
return; return;
@ -350,6 +357,7 @@ public class DebugTab : Window, ITab
ImGuiUtil.DrawTableColumn(name); ImGuiUtil.DrawTableColumn(name);
ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(string.Empty);
ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id));
ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(string.Empty);
} }
@ -363,6 +371,7 @@ public class DebugTab : Window, ITab
{ {
ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}");
ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); 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); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false);
ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier));
var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString();
@ -582,45 +591,76 @@ public class DebugTab : Window, ITab
if (!ImGui.CollapsingHeader("Character Utility")) if (!ImGui.CollapsingHeader("Character Utility"))
return; 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); -Vector2.UnitX);
if (!table) if (!table)
return; 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 = CharacterUtility.ReverseIndices[idx];
var intern = new CharacterUtility.InternalIndex(i);
var resource = _characterUtility.Address->Resource(idx); var resource = _characterUtility.Address->Resource(idx);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted($"[{idx}]");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"0x{(ulong)resource:X}"); ImGui.TextUnformatted($"0x{(ulong)resource:X}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (resource == null)
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableNextColumn();
continue;
}
UiHelpers.Text(resource); UiHelpers.Text(resource);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Selectable($"0x{resource->GetData().Data:X}"); var data = (nint)ResourceHandle.GetData(resource);
var length = ResourceHandle.GetLength(resource);
ImGui.Selectable($"0x{data:X}");
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
var (data, length) = resource->GetData();
if (data != nint.Zero && length > 0) if (data != nint.Zero && length > 0)
ImGui.SetClipboardText(string.Join("\n", 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."); ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard.");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{length}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted($"{resource->GetData().Length}"); if (intern.Value != -1)
ImGui.TableNextColumn(); {
ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}");
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
ImGui.SetClipboardText(string.Join("\n", ImGui.SetClipboardText(string.Join("\n",
new ReadOnlySpan<byte>((byte*)_characterUtility.DefaultResource(intern).Address, new ReadOnlySpan<byte>((byte*)_characterUtility.DefaultResource(intern).Address,
_characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); _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.TableNextColumn();
ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}");
}
else
ImGui.TableNextColumn();
} }
} }
@ -665,6 +705,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> /// <summary> Draw information about the models, materials and resources currently loaded by the local player. </summary>
private unsafe void DrawPlayerModelInfo() private unsafe void DrawPlayerModelInfo()
{ {
@ -673,10 +725,14 @@ public class DebugTab : Window, ITab
if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null)
return; return;
DrawCopyableAddress("PlayerCharacter", player.Address);
var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject();
if (model == null) if (model == null)
return; return;
DrawCopyableAddress("CharacterBase", model);
using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit))
{ {
if (t1) if (t1)
@ -730,6 +786,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> /// <summary> Draw resources with unusual reference count. </summary>
private unsafe void DrawResourceProblems() private unsafe void DrawResourceProblems()
{ {