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, )",