Merge branch 'feature/skin-shpk-fixer'

This commit is contained in:
Ottermandias 2023-08-30 20:53:07 +02:00
commit 5ba993cd6f
12 changed files with 357 additions and 57 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

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

@ -8,8 +8,11 @@ public unsafe struct MtrlResource
[FieldOffset( 0x00 )]
public ResourceHandle Handle;
[FieldOffset( 0xC8 )]
public ShaderPackageResourceHandle* ShpkResourceHandle;
[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 )]
public byte* StringList;
@ -24,8 +27,21 @@ public unsafe struct MtrlResource
=> StringList + ShpkOffset;
public byte* TexString( int idx )
=> StringList + *( TexSpace + 4 + idx * 8 );
=> StringList + TexSpace[idx].PathOffset;
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.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
@ -18,12 +20,22 @@ public unsafe struct TextureResourceHandle
public IntPtr Unk;
[FieldOffset( 0x118 )]
public IntPtr KernelTexture;
public Texture* KernelTexture;
[FieldOffset( 0x20 )]
public IntPtr NewKernelTexture;
}
[StructLayout(LayoutKind.Explicit)]
public unsafe struct ShaderPackageResourceHandle
{
[FieldOffset( 0x0 )]
public ResourceHandle Handle;
[FieldOffset( 0xB0 )]
public ShaderPackage* ShaderPackage;
}
[StructLayout( LayoutKind.Explicit )]
public unsafe struct ResourceHandle
{

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

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