diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 01078450..9d578190 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -85,26 +85,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } - public event CreatedCharacterBaseDelegate? CreatedCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Subscribe(new Action(value), - Communication.CreatedCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); - } - } + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; public bool Valid => _lumina != null; @@ -157,6 +138,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded += OnResourceLoaded; _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() @@ -167,6 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _resourceLoader.ResourceLoaded -= OnResourceLoaded; _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); _lumina = null; _communicator = null!; _modManager = null!; @@ -1189,4 +1172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); } diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index cbb86fc2..48ba86a5 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,26 +1,30 @@ using System; using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Collections; namespace Penumbra.Communication; /// /// Parameter is the game object for which a draw object is created. -/// Parameter is the name of the applied collection. +/// Parameter is the applied collection. /// Parameter is the created draw object. /// -public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> +public sealed class CreatedCharacterBase : EventWrapper, CreatedCharacterBase.Priority> { public enum Priority { /// - Api = 0, + Api = int.MinValue, + + /// + SkinFixer = 0, } public CreatedCharacterBase() : base(nameof(CreatedCharacterBase)) { } - public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) - => Invoke(this, gameObject, appliedCollectionName, drawObject); + public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject) + => Invoke(this, gameObject, appliedCollection, drawObject); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a4cbc967..1a257a96 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable _characterBaseCreateMetaChanges = DisposableContainer.Empty; if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, drawObject); + _lastCreatedCollection.ModCollection, drawObject); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs new file mode 100644 index 00000000..7ec0f218 --- /dev/null +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.SafeHandles; + +public unsafe class SafeResourceHandle : SafeHandle +{ + public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; + + public override bool IsInvalid => handle == 0; + + public SafeResourceHandle(ResourceHandle* handle, bool incRef, bool ownsHandle = true) : base(0, ownsHandle) + { + if (incRef && !ownsHandle) + throw new ArgumentException("Non-owning SafeResourceHandle with IncRef is unsupported"); + if (incRef && handle != null) + handle->IncRef(); + SetHandle((nint)handle); + } + + public static SafeResourceHandle CreateInvalid() + => new(null, incRef: false); + + protected override bool ReleaseHandle() + { + var handle = Interlocked.Exchange(ref this.handle, 0); + if (handle != 0) + ((ResourceHandle*)handle)->DecRef(); + + return true; + } +} diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index ef706f6d..00eab531 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -33,6 +33,7 @@ public unsafe partial class CharacterUtility : IDisposable public event Action LoadingFinished; public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -102,6 +103,12 @@ public unsafe partial class CharacterUtility : IDisposable anyMissing |= DefaultDecalResource == nint.Zero; } + if (DefaultSkinShpkResource == nint.Zero) + { + DefaultSkinShpkResource = (nint)Address->SkinShpkResource; + anyMissing |= DefaultSkinShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable /// Return all relevant resources to the default resource. public void ResetAll() - { + { if (!Ready) - return; + return; foreach (var list in _lists) list.Dispose(); Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/SkinFixer.cs b/Penumbra/Interop/Services/SkinFixer.cs new file mode 100644 index 00000000..be45708f --- /dev/null +++ b/Penumbra/Interop/Services/SkinFixer.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.Services; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class SkinFixer : IDisposable +{ + public static readonly Utf8GamePath SkinShpkPath = + Utf8GamePath.FromSpan("shader/sm5/shpk/skin.shpk"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + private delegate nint OnRenderMaterialDelegate(nint drawObject, OnRenderMaterialParams* param); + + [StructLayout(LayoutKind.Explicit)] + private struct OnRenderMaterialParams + { + [FieldOffset(0x0)] + public Model* Model; + + [FieldOffset(0x8)] + public uint MaterialIndex; + } + + private readonly Hook _onRenderMaterialHook; + + private readonly GameEventManager _gameEvents; + private readonly CommunicatorService _communicator; + private readonly ResourceLoader _resources; + private readonly CharacterUtility _utility; + + // CharacterBase to ShpkHandle + private readonly ConcurrentDictionary _skinShpks = new(); + + private readonly object _lock = new(); + + private int _moddedSkinShpkCount = 0; + private ulong _slowPathCallDelta = 0; + + public bool Enabled { get; internal set; } = true; + + public int ModdedSkinShpkCount + => _moddedSkinShpkCount; + + public SkinFixer(GameEventManager gameEvents, ResourceLoader resources, CharacterUtility utility, CommunicatorService communicator) + { + SignatureHelper.Initialise(this); + _gameEvents = gameEvents; + _resources = resources; + _utility = utility; + _communicator = communicator; + _onRenderMaterialHook = Hook.FromAddress(_humanVTable[62], OnRenderHumanMaterial); + _communicator.CreatedCharacterBase.Subscribe(OnCharacterBaseCreated, CreatedCharacterBase.Priority.SkinFixer); + _gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor; + _onRenderMaterialHook.Enable(); + } + + public void Dispose() + { + _onRenderMaterialHook.Dispose(); + _communicator.CreatedCharacterBase.Unsubscribe(OnCharacterBaseCreated); + _gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor; + foreach (var skinShpk in _skinShpks.Values) + skinShpk.Dispose(); + _skinShpks.Clear(); + _moddedSkinShpkCount = 0; + } + + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + + private void OnCharacterBaseCreated(nint gameObject, ModCollection collection, nint drawObject) + { + if (((CharacterBase*)drawObject)->GetModelType() != CharacterBase.ModelType.Human) + return; + + Task.Run(() => + { + var skinShpk = SafeResourceHandle.CreateInvalid(); + try + { + var data = collection.ToResolveData(gameObject); + if (data.Valid) + { + var loadedShpk = _resources.LoadResolvedResource(ResourceCategory.Shader, ResourceType.Shpk, SkinShpkPath.Path, data); + skinShpk = new SafeResourceHandle((ResourceHandle*)loadedShpk, false); + } + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while resolving skin.shpk for human {drawObject:X}: {e}"); + } + + if (!skinShpk.IsInvalid) + { + if (_skinShpks.TryAdd(drawObject, skinShpk)) + { + if ((nint)skinShpk.ResourceHandle != _utility.DefaultSkinShpkResource) + Interlocked.Increment(ref _moddedSkinShpkCount); + } + else + { + skinShpk.Dispose(); + } + } + }); + } + + private void OnCharacterBaseDestructor(nint characterBase) + { + if (!_skinShpks.Remove(characterBase, out var skinShpk)) + return; + + var handle = skinShpk.ResourceHandle; + skinShpk.Dispose(); + if ((nint)handle != _utility.DefaultSkinShpkResource) + Interlocked.Decrement(ref _moddedSkinShpkCount); + } + + private nint OnRenderHumanMaterial(nint human, OnRenderMaterialParams* param) + { + // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. + if (!Enabled || _moddedSkinShpkCount == 0 || !_skinShpks.TryGetValue(human, out var skinShpk) || skinShpk.IsInvalid) + return _onRenderMaterialHook!.Original(human, param); + + var material = param->Model->Materials[param->MaterialIndex]; + var shpkResource = ((Structs.MtrlResource*)material->MaterialResourceHandle)->ShpkResourceHandle; + if ((nint)shpkResource != (nint)skinShpk.ResourceHandle) + return _onRenderMaterialHook!.Original(human, param); + + Interlocked.Increment(ref _slowPathCallDelta); + + // Performance considerations: + // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; + // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Swapping path is taken up to hundreds of times a frame. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + lock (_lock) + { + try + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)skinShpk.ResourceHandle; + return _onRenderMaterialHook!.Original(human, param); + } + finally + { + _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + } + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index b273091b..765ad25f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -10,6 +10,7 @@ public unsafe struct CharacterUtilityData { public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; + public const int IndexSkinShpk = 76; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames< MetaIndex >() .Zip( Enum.GetValues< MetaIndex >() ) @@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData .Select( n => n.Second ).ToArray(); public const int TotalNumResources = 87; - - /// Obtain the index for the eqdp file corresponding to the given race code and accessory. + + /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) => +( int )raceCode switch { @@ -95,5 +96,8 @@ public unsafe struct CharacterUtilityData [FieldOffset( 8 + IndexDecalTex * 8 )] public TextureResourceHandle* DecalTexResource; + [FieldOffset( 8 + IndexSkinShpk * 8 )] + public ResourceHandle* SkinShpkResource; + // not included resources have no known use case. } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/MtrlResource.cs b/Penumbra/Interop/Structs/MtrlResource.cs index 28756877..424adfe4 100644 --- a/Penumbra/Interop/Structs/MtrlResource.cs +++ b/Penumbra/Interop/Structs/MtrlResource.cs @@ -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; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 4de81903..5db0f8e1 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -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 { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b291e392..eeee8fd0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -22,8 +22,8 @@ using Penumbra.Collections.Manager; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; -using OtterGui.Tasks; - +using OtterGui.Tasks; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -81,6 +81,7 @@ public class Penumbra : IDalamudPlugin { _services.GetRequiredService(); } + _services.GetRequiredService(); SetupInterface(); SetupApi(); diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 728585ae..8bea52e3 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -117,7 +117,8 @@ public static class ServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddResolvers(this IServiceCollection services) => services.AddSingleton() diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index a48fd714..c24d64fa 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,6 +34,8 @@ using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBa using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Interop.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; namespace Penumbra.UI.Tabs; @@ -63,6 +65,7 @@ public class DebugTab : Window, ITab private readonly ImportPopup _importPopup; private readonly FrameworkManager _framework; private readonly TextureManager _textureManager; + private readonly SkinFixer _skinFixer; public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, @@ -70,7 +73,7 @@ public class DebugTab : Window, ITab ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager) + TextureManager textureManager, SkinFixer skinFixer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse, false) { IsOpen = true; @@ -103,6 +106,7 @@ public class DebugTab : Window, ITab _importPopup = importPopup; _framework = framework; _textureManager = textureManager; + _skinFixer = skinFixer; } public ReadOnlySpan Label @@ -144,6 +148,8 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawPlayerModelInfo(); ImGui.NewLine(); + DrawGlobalVariableInfo(); + ImGui.NewLine(); DrawDebugTabIpc(); ImGui.NewLine(); } @@ -338,7 +344,7 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -350,6 +356,7 @@ public class DebugTab : Window, ITab ImGuiUtil.DrawTableColumn(name); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); } @@ -363,6 +370,7 @@ public class DebugTab : Window, ITab { ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + ImGuiUtil.DrawTableColumn((obj.Address == nint.Zero) ? string.Empty : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); @@ -582,45 +590,72 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader("Character Utility")) return; - using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + var enableSkinFixer = _skinFixer.Enabled; + if (ImGui.Checkbox("Enable Skin Fixer", ref enableSkinFixer)) + _skinFixer.Enabled = enableSkinFixer; + + if (enableSkinFixer) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"\u0394 Slow-Path Calls: {_skinFixer.GetAndResetSlowPathCallDelta()}"); + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ImGui.TextUnformatted($"Draw Objects with Modded skin.shpk: {_skinFixer.ModdedSkinShpkCount}"); + } + + using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) { - var idx = CharacterUtility.RelevantIndices[i]; - var intern = new CharacterUtility.InternalIndex(i); + var intern = CharacterUtility.ReverseIndices[idx]; var resource = _characterUtility.Address->Resource(idx); ImGui.TableNextColumn(); + ImGui.TextUnformatted($"[{idx}]"); + ImGui.TableNextColumn(); ImGui.TextUnformatted($"0x{(ulong)resource:X}"); ImGui.TableNextColumn(); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } UiHelpers.Text(resource); ImGui.TableNextColumn(); - ImGui.Selectable($"0x{resource->GetData().Data:X}"); - if (ImGui.IsItemClicked()) + var data = (nint)ResourceHandle.GetData(resource); + var length = ResourceHandle.GetLength(resource); + if (ImGui.Selectable($"0x{data:X}")) { - var (data, length) = resource->GetData(); if (data != nint.Zero && length > 0) ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); } ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(length.ToString()); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{resource->GetData().Length}"); - ImGui.TableNextColumn(); - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + if (intern.Value != -1) + { + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + else + ImGui.TableNextColumn(); } } @@ -665,6 +700,18 @@ public class DebugTab : Window, ITab } } + private static void DrawCopyableAddress(string label, nint address) + { + using (var _ = PushFont(UiBuilder.MonoFont)) + if (ImGui.Selectable($"0x{address:X16} {label}")) + ImGui.SetClipboardText($"{address:X16}"); + + ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); + } + + private static unsafe void DrawCopyableAddress(string label, void* address) + => DrawCopyableAddress(label, (nint)address); + /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -673,10 +720,14 @@ public class DebugTab : Window, ITab if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; + DrawCopyableAddress("PlayerCharacter", player.Address); + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; + DrawCopyableAddress("CharacterBase", model); + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { if (t1) @@ -730,6 +781,19 @@ public class DebugTab : Window, ITab } } + /// Draw information about some game global variables. + private unsafe void DrawGlobalVariableInfo() + { + var header = ImGui.CollapsingHeader("Global Variables"); + ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); + if (!header) + return; + + DrawCopyableAddress("CharacterUtility", _characterUtility.Address); + DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); + DrawCopyableAddress("Device", Device.Instance()); + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() {