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 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 bool Valid public bool Valid
=> _lumina != null; => _lumina != null;
@ -157,6 +138,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.ResourceLoaded += OnResourceLoaded;
_communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api);
_communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api);
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
} }
public unsafe void Dispose() public unsafe void Dispose()
@ -167,6 +149,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_resourceLoader.ResourceLoaded -= OnResourceLoaded; _resourceLoader.ResourceLoaded -= OnResourceLoaded;
_communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase);
_lumina = null; _lumina = null;
_communicator = null!; _communicator = null!;
_modManager = 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) 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); => 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 System;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.Collections;
namespace Penumbra.Communication; namespace Penumbra.Communication;
/// <summary> <list type="number"> /// <summary> <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item> /// <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> /// <item>Parameter is the created draw object. </item>
/// </list> </summary> /// </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 public enum Priority
{ {
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/> /// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
Api = 0, Api = int.MinValue,
/// <seealso cref="Interop.Services.SkinFixer.OnCharacterBaseCreated"/>
SkinFixer = 0,
} }
public CreatedCharacterBase() public CreatedCharacterBase()
: base(nameof(CreatedCharacterBase)) : base(nameof(CreatedCharacterBase))
{ } { }
public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject)
=> Invoke(this, gameObject, appliedCollectionName, drawObject); => Invoke(this, gameObject, appliedCollection, drawObject);
} }

View file

@ -142,7 +142,7 @@ public unsafe class MetaState : IDisposable
_characterBaseCreateMetaChanges = DisposableContainer.Empty; _characterBaseCreateMetaChanges = DisposableContainer.Empty;
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, drawObject); _lastCreatedCollection.ModCollection, drawObject);
_lastCreatedCollection = ResolveData.Invalid; _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 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;
@ -140,15 +147,16 @@ public unsafe partial class CharacterUtility : IDisposable
/// <summary> Return all relevant resources to the default resource. </summary> /// <summary> Return all relevant resources to the default resource. </summary>
public void ResetAll() public void ResetAll()
{ {
if (!Ready) if (!Ready)
return; return;
foreach (var list in _lists) foreach (var list in _lists)
list.Dispose(); list.Dispose();
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,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 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 >() )
@ -17,8 +18,8 @@ public unsafe struct CharacterUtilityData
.Select( n => n.Second ).ToArray(); .Select( n => n.Second ).ToArray();
public const int TotalNumResources = 87; public const int TotalNumResources = 87;
/// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary> /// <summary> Obtain the index for the eqdp file corresponding to the given race code and accessory. </summary>
public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory ) public static MetaIndex EqdpIdx( GenderRace raceCode, bool accessory )
=> +( int )raceCode switch => +( int )raceCode switch
{ {
@ -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

@ -22,8 +22,8 @@ using Penumbra.Collections.Manager;
using Penumbra.UI.Tabs; using Penumbra.UI.Tabs;
using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
using OtterGui.Tasks; using OtterGui.Tasks;
namespace Penumbra; namespace Penumbra;
public class Penumbra : IDalamudPlugin public class Penumbra : IDalamudPlugin
@ -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,8 @@ 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;
namespace Penumbra.UI.Tabs; namespace Penumbra.UI.Tabs;
@ -63,6 +65,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 +73,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 +106,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 +148,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 +344,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 +356,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 +370,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 +590,72 @@ 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.TableNextRow();
continue;
}
UiHelpers.Text(resource); UiHelpers.Text(resource);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Selectable($"0x{resource->GetData().Data:X}"); var data = (nint)ResourceHandle.GetData(resource);
if (ImGui.IsItemClicked()) var length = ResourceHandle.GetLength(resource);
if (ImGui.Selectable($"0x{data:X}"))
{ {
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.ToString());
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 +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> /// <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 +720,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 +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> /// <summary> Draw resources with unusual reference count. </summary>
private unsafe void DrawResourceProblems() private unsafe void DrawResourceProblems()
{ {