diff --git a/OtterGui b/OtterGui index 089ed82a..86b49242 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2 +Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c diff --git a/Penumbra.GameData b/Penumbra.GameData index 002260d9..0ca50105 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 002260d9815e571f1496c50374f5b712818e9880 +Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6 diff --git a/Penumbra/Communication/ModelAttributeComputed.cs b/Penumbra/Communication/ModelAttributeComputed.cs new file mode 100644 index 00000000..389f56b6 --- /dev/null +++ b/Penumbra/Communication/ModelAttributeComputed.cs @@ -0,0 +1,24 @@ +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a model recomputes its attribute masks. +/// +/// Parameter is the game object that recomputed its attributes. +/// Parameter is the draw object on which the recomputation was called. +/// Parameter is the collection associated with the game object. +/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. +/// +public sealed class ModelAttributeComputed() + : EventWrapper(nameof(ModelAttributeComputed)) +{ + public enum Priority + { + /// + ShapeManager = 0, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index bd6ccfb1..8c50dad7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -67,6 +67,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; + public bool EnableCustomShapes { get; set; } = true; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; @@ -84,9 +85,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs new file mode 100644 index 00000000..861962ee --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs @@ -0,0 +1,110 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public sealed unsafe class AttributeHooks : IRequiredService, IDisposable +{ + private delegate void SetupAttributes(Human* human, byte* data); + private delegate void AttributeUpdate(Human* human); + + private readonly Configuration _config; + private readonly ModelAttributeComputed _event; + private readonly CollectionResolver _resolver; + + private readonly AttributeHook[] _hooks; + private readonly Task> _updateHook; + private ModCollection _identifiedCollection = ModCollection.Empty; + private Actor _identifiedActor = Actor.Null; + private bool _inUpdate; + + public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks) + { + _config = config; + _event = communication.ModelAttributeComputed; + _resolver = resolver; + _hooks = + [ + new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body), + new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands), + new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs), + new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet), + ]; + _updateHook = hooks.CreateHook("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour, + _config.EnableCustomShapes); + } + + private class AttributeHook + { + private readonly AttributeHooks _parent; + public readonly string Name; + public readonly Task> Hook; + public readonly uint ModelIndex; + public readonly HumanSlot Slot; + + public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot) + { + _parent = parent; + Name = $"Setup{slot}Attributes"; + Slot = slot; + ModelIndex = slot.ToIndex(); + Hook = hooks.CreateHook(Name, signature, Detour, enabled); + } + + private void Detour(Human* human, byte* data) + { + Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X})."); + Hook.Result.Original(human, data); + if (_parent is { _inUpdate: true, _identifiedActor.Valid: true }) + _parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot); + } + } + + private void UpdateAttributesDetour(Human* human) + { + var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); + _identifiedActor = resolveData.AssociatedGameObject; + _identifiedCollection = resolveData.ModCollection; + _inUpdate = true; + Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X})."); + _event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown); + _updateHook.Result.Original(human); + _inUpdate = false; + } + + public void SetState(bool enabled) + { + if (_config.EnableCustomShapes == enabled) + return; + + _config.EnableCustomShapes = enabled; + _config.Save(); + if (enabled) + { + foreach (var hook in _hooks) + hook.Hook.Result.Enable(); + _updateHook.Result.Enable(); + } + else + { + foreach (var hook in _hooks) + hook.Hook.Result.Disable(); + _updateHook.Result.Disable(); + } + } + + public void Dispose() + { + foreach (var hook in _hooks) + hook.Hook.Result.Dispose(); + _updateHook.Result.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 54066782..8a45ec2c 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -246,28 +246,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } - [StructLayout(LayoutKind.Explicit)] - private struct ChangedEquipData - { - [FieldOffset(0)] - public PrimaryId Model; - - [FieldOffset(2)] - public Variant Variant; - - [FieldOffset(8)] - public PrimaryId BonusModel; - - [FieldOffset(10)] - public Variant BonusVariant; - - [FieldOffset(20)] - public ushort VfxId; - - [FieldOffset(22)] - public GenderRace GenderRace; - } - private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { switch (slotIndex) diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs new file mode 100644 index 00000000..4356086a --- /dev/null +++ b/Penumbra/Meta/ShapeManager.cs @@ -0,0 +1,127 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Services; + +namespace Penumbra.Meta; + +public class ShapeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 4; + private readonly CommunicatorService _communicator; + + private static ReadOnlySpan UsedModels + => [1, 2, 3, 4]; + + public ShapeManager(CommunicatorService communicator) + { + _communicator = communicator; + _communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager); + } + + private readonly Dictionary[] _temporaryIndices = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly uint[] _temporaryMasks = new uint[NumSlots]; + private readonly uint[] _temporaryValues = new uint[NumSlots]; + + private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot) + { + int index; + switch (slot) + { + case HumanSlot.Unknown: + ResetCache(model); + return; + case HumanSlot.Body: index = 0; break; + case HumanSlot.Hands: index = 1; break; + case HumanSlot.Legs: index = 2; break; + case HumanSlot.Feet: index = 3; break; + default: return; + } + + if (_temporaryMasks[index] is 0) + return; + + var modelIndex = UsedModels[index]; + var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask; + var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index]; + Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); + model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask; + } + + public void Dispose() + { + _communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed); + } + + private unsafe void ResetCache(Model human) + { + for (var i = 0; i < NumSlots; ++i) + { + _temporaryMasks[i] = 0; + _temporaryValues[i] = 0; + _temporaryIndices[i].Clear(); + + var modelIndex = UsedModels[i]; + var model = human.AsHuman->Models[modelIndex]; + if (model is null || model->ModelResourceHandle is null) + continue; + + ref var shapes = ref model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex))) + { + if (ShapeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryIndices[i].TryAdd(shapeString, index); + _temporaryMasks[i] |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + UpdateMasks(); + } + + private static bool CheckShapes(ReadOnlySpan shape, byte index) + => index switch + { + 1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8), + 2 => shape.StartsWith("shp_wr_"u8), + 3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8), + 4 => shape.StartsWith("shp_an"u8), + _ => false, + }; + + private void UpdateMasks() + { + foreach (var (shape, topIndex) in _temporaryIndices[0]) + { + if (_temporaryIndices[1].TryGetValue(shape, out var handIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[1] |= 1u << handIndex; + } + + if (_temporaryIndices[2].TryGetValue(shape, out var legIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[2] |= 1u << legIndex; + } + } + + foreach (var (shape, bottomIndex) in _temporaryIndices[2]) + { + if (_temporaryIndices[3].TryGetValue(shape, out var footIndex)) + { + _temporaryValues[2] |= 1u << bottomIndex; + _temporaryValues[3] |= 1u << footIndex; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs new file mode 100644 index 00000000..987ed474 --- /dev/null +++ b/Penumbra/Meta/ShapeString.cs @@ -0,0 +1,89 @@ +using Lumina.Misc; +using Newtonsoft.Json; +using Penumbra.GameData.Files.PhybStructs; + +namespace Penumbra.Meta; + +[JsonConverter(typeof(Converter))] +public struct ShapeString : IEquatable +{ + public const int MaxLength = 30; + + public static readonly ShapeString Empty = new(); + + private FixedString32 _buffer; + + public int Count + => _buffer[31]; + + public int Length + => _buffer[31]; + + public override string ToString() + => Encoding.UTF8.GetString(_buffer[..Length]); + + public bool Equals(ShapeString other) + => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); + + public override bool Equals(object? obj) + => obj is ShapeString other && Equals(other); + + public override int GetHashCode() + => (int)Crc32.Get(_buffer[..Length]); + + public static bool operator ==(ShapeString left, ShapeString right) + => left.Equals(right); + + public static bool operator !=(ShapeString left, ShapeString right) + => !left.Equals(right); + + public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + { + var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); + return TryRead(span, out ret); + } + + public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + { + if (utf8.Length is 0 or > MaxLength) + { + ret = Empty; + return false; + } + + ret = Empty; + utf8.CopyTo(ret._buffer); + ret._buffer[utf8.Length] = 0; + ret._buffer[31] = (byte)utf8.Length; + return true; + } + + public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + { + ret = Empty; + if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) + return false; + + ret._buffer[written] = 0; + ret._buffer[31] = (byte)written; + return true; + } + + private sealed class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + if (!TryRead(value, out existingValue)) + throw new JsonReaderException($"Could not parse {value} into ShapeString."); + + return existingValue; + } + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 7f4c1b23..70636bbf 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,6 +34,7 @@ public class Penumbra : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; + public static DynamisIpc Dynamis { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -59,8 +60,9 @@ public class Penumbra : IDalamudPlugin _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); // Invoke the IPC Penumbra.Launching method before any hooks or other services are created. _services.GetService(); - Messager = _services.GetService(); - _validityChecker = _services.GetService(); + Messager = _services.GetService(); + Dynamis = _services.GetService(); + _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); var startup = _services.GetService() @@ -228,6 +230,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n"); sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); + sb.Append($"> **`Custom Shapes Enabled: `** {_config.EnableCustomShapes}\n"); sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 5d745419..e008752f 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,6 +81,9 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); + /// + public readonly ModelAttributeComputed ModelAttributeComputed = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -105,5 +108,6 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); + ModelAttributeComputed.Dispose(); } } diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index eb9f05d9..3b25c1a9 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,11 +1,11 @@ using ImGuiNET; -using OtterGui.Extensions; -using OtterGui.Text; +using OtterGui.Extensions; +using OtterGui.Text; using Penumbra.GameData.Files; -using Penumbra.GameData.Files.AtchStructs; - -namespace Penumbra.UI.Tabs.Debug; - +using Penumbra.GameData.Files.AtchStructs; + +namespace Penumbra.UI.Tabs.Debug; + public static class AtchDrawer { public static void Draw(AtchFile file) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b7bc8edf..b4fa3b9f 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -108,6 +108,7 @@ public class DebugTab : Window, ITab, IUiService private readonly ObjectIdentification _objectIdentification; private readonly RenderTargetDrawer _renderTargetDrawer; private readonly ModMigratorDebug _modMigratorDebug; + private readonly ShapeInspector _shapeInspector; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -119,7 +120,7 @@ public class DebugTab : Window, ITab, IUiService Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, - ModMigratorDebug modMigratorDebug) + ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -162,6 +163,7 @@ public class DebugTab : Window, ITab, IUiService _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; _modMigratorDebug = modMigratorDebug; + _shapeInspector = shapeInspector; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -508,37 +510,50 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; - _objects.DrawDebug(); - - using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - DrawSpecial("Current Player", _actors.GetCurrentPlayer()); - DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); - DrawSpecial("Current Card", _actors.GetCardPlayer()); - DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - - foreach (var obj in _objects) + using (var objectTree = ImUtf8.TreeNode("Object Manager"u8)) { - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}"); - ImGui.TableNextColumn(); - if (obj.Address != nint.Zero) - ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); - var identifier = _actors.FromObject(obj, out _, false, true, false); - ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc - ? $"{identifier.DataId} | {obj.AsObject->BaseId}" - : identifier.DataId.ToString(); - ImGuiUtil.DrawTableColumn(id); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero - ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" - : "NULL"); + if (objectTree) + { + _objects.DrawDebug(); + + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + DrawSpecial("Current Player", _actors.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); + DrawSpecial("Current Card", _actors.GetCardPlayer()); + DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); + + foreach (var obj in _objects) + { + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address); + ImGui.TableNextColumn(); + if (obj.Address != nint.Zero) + Penumbra.Dynamis.DrawPointer((nint)((Character*)obj.Address)->GameObject.GetDrawObject()); + var identifier = _actors.FromObject(obj, out _, false, true, false); + ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); + var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->BaseId}" + : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn(id); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address != nint.Zero ? *(nint*)obj.Address : nint.Zero); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero + ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" + : "NULL"); + } + } + } + + using (var shapeTree = ImUtf8.TreeNode("Shape Inspector"u8)) + { + if (shapeTree) + _shapeInspector.Draw(); } return; @@ -1184,8 +1199,16 @@ public class DebugTab : Window, ITab, IUiService /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { - if (ImGui.CollapsingHeader("IPC")) - _ipcTester.Draw(); + if (!ImUtf8.CollapsingHeader("IPC"u8)) + return; + + using (var tree = ImUtf8.TreeNode("Dynamis"u8)) + { + if (tree) + Penumbra.Dynamis.DrawDebugInfo(); + } + + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs new file mode 100644 index 00000000..968bc484 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -0,0 +1,71 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.UI.Tabs.Debug; + +public class ShapeInspector(ObjectManager objects) : IUiService +{ + private int _objectIndex = 0; + + public unsafe void Draw() + { + ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); + var actor = objects[0]; + if (!actor.IsCharacter) + { + ImUtf8.Text("No valid character."u8); + return; + } + + var human = actor.Model; + if (!human.IsHuman) + { + ImUtf8.Text("No valid character."u8); + return; + } + + using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + foreach (var slot in Enum.GetValues()) + { + ImUtf8.DrawTableColumn($"{(uint)slot:D2}"); + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[(int)slot]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if ((idx % 8) < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index b1f82a91..7b3a3c8b 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -50,6 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly AttributeHooks _attributeHooks; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -61,7 +63,8 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + AttributeHooks attributeHooks) { _pluginInterface = pluginInterface; _config = config; @@ -86,6 +89,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _attributeHooks = attributeHooks; } public void DrawHeader() @@ -807,6 +811,8 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); + Checkbox("Enable Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + _config.EnableAttributeHooks, _attributeHooks.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index dda6b305..4a162f8f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -2,12 +2,6 @@ "version": 1, "dependencies": { "net9.0-windows7.0": { - "DalamudPackager": { - "type": "Direct", - "requested": "[12.0.0, )", - "resolved": "12.0.0", - "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" - }, "DotNet.ReproducibleBuilds": { "type": "Direct", "requested": "[1.2.25, )",