mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +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 HideRedrawBar { get; set; } = false;
|
||||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||||
public bool DefaultTemporaryMode { get; set; } = false;
|
public bool DefaultTemporaryMode { get; set; } = false;
|
||||||
|
public bool EnableCustomShapes { get; set; } = true;
|
||||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||||
|
|
@ -84,9 +85,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
[JsonProperty(Order = int.MaxValue)]
|
[JsonProperty(Order = int.MaxValue)]
|
||||||
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
|
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 bool OpenFoldersByDefault { get; set; } = false;
|
||||||
public int SingleGroupRadioMax { get; set; } = 2;
|
public int SingleGroupRadioMax { get; set; } = 2;
|
||||||
public string DefaultImportFolder { get; set; } = string.Empty;
|
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;
|
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)
|
private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam)
|
||||||
{
|
{
|
||||||
switch (slotIndex)
|
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 readonly Logger Log = new();
|
||||||
public static MessageService Messager { get; private set; } = null!;
|
public static MessageService Messager { get; private set; } = null!;
|
||||||
|
public static DynamisIpc Dynamis { get; private set; } = null!;
|
||||||
|
|
||||||
private readonly ValidityChecker _validityChecker;
|
private readonly ValidityChecker _validityChecker;
|
||||||
private readonly ResidentResourceManager _residentResources;
|
private readonly ResidentResourceManager _residentResources;
|
||||||
|
|
@ -60,6 +61,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
// Invoke the IPC Penumbra.Launching method before any hooks or other services are created.
|
// Invoke the IPC Penumbra.Launching method before any hooks or other services are created.
|
||||||
_services.GetService<IpcLaunchingProvider>();
|
_services.GetService<IpcLaunchingProvider>();
|
||||||
Messager = _services.GetService<MessageService>();
|
Messager = _services.GetService<MessageService>();
|
||||||
|
Dynamis = _services.GetService<DynamisIpc>();
|
||||||
_validityChecker = _services.GetService<ValidityChecker>();
|
_validityChecker = _services.GetService<ValidityChecker>();
|
||||||
_services.EnsureRequiredServices();
|
_services.EnsureRequiredServices();
|
||||||
|
|
||||||
|
|
@ -228,6 +230,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n");
|
sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n");
|
||||||
sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\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($"> **`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($"> **`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($"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n");
|
||||||
sb.Append(
|
sb.Append(
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ public class CommunicatorService : IDisposable, IService
|
||||||
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
|
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
|
||||||
public readonly ResolvedFileChanged ResolvedFileChanged = new();
|
public readonly ResolvedFileChanged ResolvedFileChanged = new();
|
||||||
|
|
||||||
|
/// <inheritdoc cref="Communication.ModelAttributeComputed"/>
|
||||||
|
public readonly ModelAttributeComputed ModelAttributeComputed = new();
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
CollectionChange.Dispose();
|
CollectionChange.Dispose();
|
||||||
|
|
@ -105,5 +108,6 @@ public class CommunicatorService : IDisposable, IService
|
||||||
ChangedItemClick.Dispose();
|
ChangedItemClick.Dispose();
|
||||||
SelectTab.Dispose();
|
SelectTab.Dispose();
|
||||||
ResolvedFileChanged.Dispose();
|
ResolvedFileChanged.Dispose();
|
||||||
|
ModelAttributeComputed.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
private readonly ObjectIdentification _objectIdentification;
|
private readonly ObjectIdentification _objectIdentification;
|
||||||
private readonly RenderTargetDrawer _renderTargetDrawer;
|
private readonly RenderTargetDrawer _renderTargetDrawer;
|
||||||
private readonly ModMigratorDebug _modMigratorDebug;
|
private readonly ModMigratorDebug _modMigratorDebug;
|
||||||
|
private readonly ShapeInspector _shapeInspector;
|
||||||
|
|
||||||
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects,
|
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects,
|
||||||
IClientState clientState, IDataManager dataManager,
|
IClientState clientState, IDataManager dataManager,
|
||||||
|
|
@ -119,7 +120,7 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
|
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
|
||||||
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
|
HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer,
|
||||||
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
|
SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer,
|
||||||
ModMigratorDebug modMigratorDebug)
|
ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector)
|
||||||
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
|
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
|
||||||
{
|
{
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
@ -162,6 +163,7 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
_objectIdentification = objectIdentification;
|
_objectIdentification = objectIdentification;
|
||||||
_renderTargetDrawer = renderTargetDrawer;
|
_renderTargetDrawer = renderTargetDrawer;
|
||||||
_modMigratorDebug = modMigratorDebug;
|
_modMigratorDebug = modMigratorDebug;
|
||||||
|
_shapeInspector = shapeInspector;
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_dataManager = dataManager;
|
_dataManager = dataManager;
|
||||||
|
|
@ -508,6 +510,10 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
if (!ImGui.CollapsingHeader("Actors"))
|
if (!ImGui.CollapsingHeader("Actors"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
using (var objectTree = ImUtf8.TreeNode("Object Manager"u8))
|
||||||
|
{
|
||||||
|
if (objectTree)
|
||||||
|
{
|
||||||
_objects.DrawDebug();
|
_objects.DrawDebug();
|
||||||
|
|
||||||
using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
|
using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit,
|
||||||
|
|
@ -524,22 +530,31 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
{
|
{
|
||||||
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL");
|
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL");
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}");
|
Penumbra.Dynamis.DrawPointer(obj.Address);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
if (obj.Address != nint.Zero)
|
if (obj.Address != nint.Zero)
|
||||||
ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}");
|
Penumbra.Dynamis.DrawPointer((nint)((Character*)obj.Address)->GameObject.GetDrawObject());
|
||||||
var identifier = _actors.FromObject(obj, out _, false, true, false);
|
var identifier = _actors.FromObject(obj, out _, false, true, false);
|
||||||
ImGuiUtil.DrawTableColumn(_actors.ToString(identifier));
|
ImGuiUtil.DrawTableColumn(_actors.ToString(identifier));
|
||||||
var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc
|
var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc
|
||||||
? $"{identifier.DataId} | {obj.AsObject->BaseId}"
|
? $"{identifier.DataId} | {obj.AsObject->BaseId}"
|
||||||
: identifier.DataId.ToString();
|
: identifier.DataId.ToString();
|
||||||
ImGuiUtil.DrawTableColumn(id);
|
ImGuiUtil.DrawTableColumn(id);
|
||||||
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL");
|
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 ? $"0x{obj.AsObject->EntityId:X}" : "NULL");
|
||||||
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero
|
ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero
|
||||||
? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character"
|
? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character"
|
||||||
: "NULL");
|
: "NULL");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var shapeTree = ImUtf8.TreeNode("Shape Inspector"u8))
|
||||||
|
{
|
||||||
|
if (shapeTree)
|
||||||
|
_shapeInspector.Draw();
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -1184,7 +1199,15 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
/// <summary> Draw information about IPC options and availability. </summary>
|
/// <summary> Draw information about IPC options and availability. </summary>
|
||||||
private void DrawDebugTabIpc()
|
private void DrawDebugTabIpc()
|
||||||
{
|
{
|
||||||
if (ImGui.CollapsingHeader("IPC"))
|
if (!ImUtf8.CollapsingHeader("IPC"u8))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (var tree = ImUtf8.TreeNode("Dynamis"u8))
|
||||||
|
{
|
||||||
|
if (tree)
|
||||||
|
Penumbra.Dynamis.DrawDebugInfo();
|
||||||
|
}
|
||||||
|
|
||||||
_ipcTester.Draw();
|
_ipcTester.Draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 OtterGui.Widgets;
|
||||||
using Penumbra.Api;
|
using Penumbra.Api;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
@ -50,6 +51,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
private readonly MigrationSectionDrawer _migrationDrawer;
|
private readonly MigrationSectionDrawer _migrationDrawer;
|
||||||
private readonly CollectionAutoSelector _autoSelector;
|
private readonly CollectionAutoSelector _autoSelector;
|
||||||
private readonly CleanupService _cleanupService;
|
private readonly CleanupService _cleanupService;
|
||||||
|
private readonly AttributeHooks _attributeHooks;
|
||||||
|
|
||||||
private int _minimumX = int.MaxValue;
|
private int _minimumX = int.MaxValue;
|
||||||
private int _minimumY = int.MaxValue;
|
private int _minimumY = int.MaxValue;
|
||||||
|
|
@ -61,7 +63,8 @@ public class SettingsTab : ITab, IUiService
|
||||||
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
||||||
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
||||||
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
||||||
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService)
|
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
|
||||||
|
AttributeHooks attributeHooks)
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
@ -86,6 +89,7 @@ public class SettingsTab : ITab, IUiService
|
||||||
_migrationDrawer = migrationDrawer;
|
_migrationDrawer = migrationDrawer;
|
||||||
_autoSelector = autoSelector;
|
_autoSelector = autoSelector;
|
||||||
_cleanupService = cleanupService;
|
_cleanupService = cleanupService;
|
||||||
|
_attributeHooks = attributeHooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawHeader()
|
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. "
|
"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.",
|
+ "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);
|
_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();
|
DrawWaitForPluginsReflection();
|
||||||
DrawEnableHttpApiBox();
|
DrawEnableHttpApiBox();
|
||||||
DrawEnableDebugModeBox();
|
DrawEnableDebugModeBox();
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,6 @@
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0-windows7.0": {
|
"net9.0-windows7.0": {
|
||||||
"DalamudPackager": {
|
|
||||||
"type": "Direct",
|
|
||||||
"requested": "[12.0.0, )",
|
|
||||||
"resolved": "12.0.0",
|
|
||||||
"contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw=="
|
|
||||||
},
|
|
||||||
"DotNet.ReproducibleBuilds": {
|
"DotNet.ReproducibleBuilds": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.2.25, )",
|
"requested": "[1.2.25, )",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue