mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add initial support for custom shapes.
This commit is contained in:
parent
0fe4a3671a
commit
0adec35848
15 changed files with 502 additions and 75 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2
|
||||
Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 002260d9815e571f1496c50374f5b712818e9880
|
||||
Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6
|
||||
24
Penumbra/Communication/ModelAttributeComputed.cs
Normal file
24
Penumbra/Communication/ModelAttributeComputed.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using OtterGui.Classes;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever a model recomputes its attribute masks.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the game object that recomputed its attributes. </item>
|
||||
/// <item>Parameter is the draw object on which the recomputation was called. </item>
|
||||
/// <item>Parameter is the collection associated with the game object. </item>
|
||||
/// <item>Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. </item>
|
||||
/// </list> </summary>
|
||||
public sealed class ModelAttributeComputed()
|
||||
: EventWrapper<Actor, Model, ModCollection, HumanSlot, ModelAttributeComputed.Priority>(nameof(ModelAttributeComputed))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Meta.ShapeManager.OnAttributeComputed"/>
|
||||
ShapeManager = 0,
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Mod> SortMode = ISortMode<Mod>.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;
|
||||
|
|
|
|||
110
Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs
Normal file
110
Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs
Normal file
|
|
@ -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<Hook<AttributeUpdate>> _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<AttributeUpdate>("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour,
|
||||
_config.EnableCustomShapes);
|
||||
}
|
||||
|
||||
private class AttributeHook
|
||||
{
|
||||
private readonly AttributeHooks _parent;
|
||||
public readonly string Name;
|
||||
public readonly Task<Hook<SetupAttributes>> 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<SetupAttributes>(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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
127
Penumbra/Meta/ShapeManager.cs
Normal file
127
Penumbra/Meta/ShapeManager.cs
Normal file
|
|
@ -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<byte> UsedModels
|
||||
=> [1, 2, 3, 4];
|
||||
|
||||
public ShapeManager(CommunicatorService communicator)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager);
|
||||
}
|
||||
|
||||
private readonly Dictionary<ShapeString, short>[] _temporaryIndices =
|
||||
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeString, short>()).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<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Penumbra/Meta/ShapeString.cs
Normal file
89
Penumbra/Meta/ShapeString.cs
Normal file
|
|
@ -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<ShapeString>
|
||||
{
|
||||
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<byte> 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<char> 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<ShapeString>
|
||||
{
|
||||
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<string>(reader);
|
||||
if (!TryRead(value, out existingValue))
|
||||
throw new JsonReaderException($"Could not parse {value} into ShapeString.");
|
||||
|
||||
return existingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IpcLaunchingProvider>();
|
||||
Messager = _services.GetService<MessageService>();
|
||||
_validityChecker = _services.GetService<ValidityChecker>();
|
||||
Messager = _services.GetService<MessageService>();
|
||||
Dynamis = _services.GetService<DynamisIpc>();
|
||||
_validityChecker = _services.GetService<ValidityChecker>();
|
||||
_services.EnsureRequiredServices();
|
||||
|
||||
var startup = _services.GetService<DalamudConfigService>()
|
||||
|
|
@ -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<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n");
|
||||
sb.Append(
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ public class CommunicatorService : IDisposable, IService
|
|||
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
|
||||
public readonly ResolvedFileChanged ResolvedFileChanged = new();
|
||||
|
||||
/// <inheritdoc cref="Communication.ModelAttributeComputed"/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <summary> Draw information about IPC options and availability. </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary> Helper to print a property and its value in a 2-column table. </summary>
|
||||
|
|
|
|||
71
Penumbra/UI/Tabs/Debug/ShapeInspector.cs
Normal file
71
Penumbra/UI/Tabs/Debug/ShapeInspector.cs
Normal file
|
|
@ -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<HumanSlot>())
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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, )",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue