mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add conditional connector shapes.
This commit is contained in:
parent
c0dcfdd835
commit
f1448ed947
7 changed files with 360 additions and 120 deletions
|
|
@ -13,7 +13,23 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
||||||
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State
|
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State
|
||||||
=> _shpData;
|
=> _shpData;
|
||||||
|
|
||||||
internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
|
internal IEnumerable<(ShapeString, IReadOnlyDictionary<ShapeString, ShpHashSet>)> ConditionState
|
||||||
|
=> _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary<ShapeString, ShpHashSet>)kvp.Value));
|
||||||
|
|
||||||
|
public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary<ShapeString, ShpHashSet>? dict)
|
||||||
|
{
|
||||||
|
if (_conditionalSet.TryGetValue(condition, out var d))
|
||||||
|
{
|
||||||
|
dict = d;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dict = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
|
||||||
{
|
{
|
||||||
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
|
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
|
||||||
|
|
||||||
|
|
@ -77,11 +93,13 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
|
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
|
||||||
|
private readonly Dictionary<ShapeString, Dictionary<ShapeString, ShpHashSet>> _conditionalSet = [];
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
_shpData.Clear();
|
_shpData.Clear();
|
||||||
|
_conditionalSet.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool _)
|
protected override void Dispose(bool _)
|
||||||
|
|
@ -89,16 +107,56 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
||||||
|
|
||||||
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
||||||
{
|
{
|
||||||
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
if (identifier.ShapeCondition.Length > 0)
|
||||||
{
|
{
|
||||||
|
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
|
||||||
|
{
|
||||||
|
if (!entry.Value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
shapes = new Dictionary<ShapeString, ShpHashSet>();
|
||||||
|
_conditionalSet.Add(identifier.ShapeCondition, shapes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Func(shapes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Func(_shpData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
||||||
|
{
|
||||||
|
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||||
|
{
|
||||||
|
if (!entry.Value)
|
||||||
|
return;
|
||||||
|
|
||||||
value = [];
|
value = [];
|
||||||
_shpData.Add(identifier.Shape, value);
|
dict.Add(identifier.Shape, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
value.TrySet(identifier.Slot, identifier.Id, entry);
|
value.TrySet(identifier.Slot, identifier.Id, entry);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void RevertModInternal(ShpIdentifier identifier)
|
protected override void RevertModInternal(ShpIdentifier identifier)
|
||||||
|
{
|
||||||
|
if (identifier.ShapeCondition.Length > 0)
|
||||||
|
{
|
||||||
|
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Func(shapes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Func(_shpData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
||||||
{
|
{
|
||||||
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
||||||
return;
|
return;
|
||||||
|
|
@ -106,4 +164,5 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
||||||
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty)
|
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty)
|
||||||
_shpData.Remove(identifier.Shape);
|
_shpData.Remove(identifier.Shape);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Meta.Manipulations;
|
namespace Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape)
|
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition)
|
||||||
: IComparable<ShpIdentifier>, IMetaIdentifier
|
: IComparable<ShpIdentifier>, IMetaIdentifier
|
||||||
{
|
{
|
||||||
public int CompareTo(ShpIdentifier other)
|
public int CompareTo(ShpIdentifier other)
|
||||||
|
|
@ -34,12 +34,39 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shapeComparison = Shape.CompareTo(other.Shape);
|
||||||
|
if (shapeComparison is not 0)
|
||||||
|
return shapeComparison;
|
||||||
|
|
||||||
return Shape.CompareTo(other.Shape);
|
return ShapeCondition.CompareTo(other.ShapeCondition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}";
|
{
|
||||||
|
var sb = new StringBuilder(64);
|
||||||
|
sb.Append("Shp - ")
|
||||||
|
.Append(Shape);
|
||||||
|
if (Slot is HumanSlot.Unknown)
|
||||||
|
{
|
||||||
|
sb.Append(" - All Slots & IDs");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(" - ")
|
||||||
|
.Append(Slot.ToName())
|
||||||
|
.Append(" - ");
|
||||||
|
if (Id.HasValue)
|
||||||
|
sb.Append(Id.Value.Id);
|
||||||
|
else
|
||||||
|
sb.Append("All IDs");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShapeCondition.Length > 0)
|
||||||
|
sb.Append(" - ")
|
||||||
|
.Append(ShapeCondition);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +84,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
if (Slot is HumanSlot.Unknown && Id is not null)
|
if (Slot is HumanSlot.Unknown && Id is not null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return ValidateCustomShapeString(Shape);
|
if (!ValidateCustomShapeString(Shape))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ShapeCondition.Length is 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!ValidateCustomShapeString(ShapeCondition))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Slot switch
|
||||||
|
{
|
||||||
|
HumanSlot.Hands when ShapeCondition.IsWrist() => true,
|
||||||
|
HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true,
|
||||||
|
HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
|
||||||
|
HumanSlot.Feet when ShapeCondition.IsAnkle() => true,
|
||||||
|
HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe bool ValidateCustomShapeString(byte* shape)
|
public static unsafe bool ValidateCustomShapeString(byte* shape)
|
||||||
|
|
@ -101,18 +145,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
if (Id.HasValue)
|
if (Id.HasValue)
|
||||||
jObj["Id"] = Id.Value.Id.ToString();
|
jObj["Id"] = Id.Value.Id.ToString();
|
||||||
jObj["Shape"] = Shape.ToString();
|
jObj["Shape"] = Shape.ToString();
|
||||||
|
if (ShapeCondition.Length > 0)
|
||||||
|
jObj["ShapeCondition"] = ShapeCondition.ToString();
|
||||||
return jObj;
|
return jObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ShpIdentifier? FromJson(JObject jObj)
|
public static ShpIdentifier? FromJson(JObject jObj)
|
||||||
{
|
{
|
||||||
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
|
||||||
var id = jObj["Id"]?.ToObject<ushort>();
|
|
||||||
var shape = jObj["Shape"]?.ToObject<string>();
|
var shape = jObj["Shape"]?.ToObject<string>();
|
||||||
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
|
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var identifier = new ShpIdentifier(slot, id, shapeString);
|
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
||||||
|
var id = jObj["Id"]?.ToObject<ushort>();
|
||||||
|
var shapeCondition = jObj["ShapeCondition"]?.ToObject<string>();
|
||||||
|
var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s;
|
||||||
|
var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString);
|
||||||
return identifier.Validate() ? identifier : null;
|
return identifier.Validate() ? identifier : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using System.Reflection.Metadata.Ecma335;
|
using System.Reflection.Metadata.Ecma335;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
|
@ -32,13 +34,17 @@ public class ShapeManager : IRequiredService, IDisposable
|
||||||
|
|
||||||
private readonly uint[] _temporaryMasks = new uint[NumSlots];
|
private readonly uint[] _temporaryMasks = new uint[NumSlots];
|
||||||
private readonly uint[] _temporaryValues = new uint[NumSlots];
|
private readonly uint[] _temporaryValues = new uint[NumSlots];
|
||||||
|
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> _attributeHook.Unsubscribe(OnAttributeComputed);
|
=> _attributeHook.Unsubscribe(OnAttributeComputed);
|
||||||
|
|
||||||
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
|
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
|
||||||
{
|
{
|
||||||
ComputeCache(model, collection);
|
if (!collection.HasCache)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ComputeCache(model, collection.MetaCache!.Shp);
|
||||||
for (var i = 0; i < NumSlots; ++i)
|
for (var i = 0; i < NumSlots; ++i)
|
||||||
{
|
{
|
||||||
if (_temporaryMasks[i] is 0)
|
if (_temporaryMasks[i] is 0)
|
||||||
|
|
@ -52,11 +58,8 @@ public class ShapeManager : IRequiredService, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void ComputeCache(Model human, ModCollection collection)
|
private unsafe void ComputeCache(Model human, ShpCache cache)
|
||||||
{
|
{
|
||||||
if (!collection.HasCache)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (var i = 0; i < NumSlots; ++i)
|
for (var i = 0; i < NumSlots; ++i)
|
||||||
{
|
{
|
||||||
_temporaryMasks[i] = 0;
|
_temporaryMasks[i] = 0;
|
||||||
|
|
@ -68,6 +71,8 @@ public class ShapeManager : IRequiredService, IDisposable
|
||||||
if (model is null || model->ModelResourceHandle is null)
|
if (model is null || model->ModelResourceHandle is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
_ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set;
|
||||||
|
|
||||||
ref var shapes = ref model->ModelResourceHandle->Shapes;
|
ref var shapes = ref model->ModelResourceHandle->Shapes;
|
||||||
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
|
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
|
||||||
{
|
{
|
||||||
|
|
@ -75,8 +80,8 @@ public class ShapeManager : IRequiredService, IDisposable
|
||||||
{
|
{
|
||||||
_temporaryIndices[i].TryAdd(shapeString, index);
|
_temporaryIndices[i].TryAdd(shapeString, index);
|
||||||
_temporaryMasks[i] |= (ushort)(1 << index);
|
_temporaryMasks[i] |= (ushort)(1 << index);
|
||||||
if (collection.MetaCache!.Shp.State.Count > 0
|
if (cache.State.Count > 0
|
||||||
&& collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set))
|
&& cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
|
||||||
_temporaryValues[i] |= (ushort)(1 << index);
|
_temporaryValues[i] |= (ushort)(1 << index);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -86,37 +91,54 @@ public class ShapeManager : IRequiredService, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateDefaultMasks();
|
UpdateDefaultMasks(cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateDefaultMasks()
|
private void UpdateDefaultMasks(ShpCache cache)
|
||||||
{
|
{
|
||||||
foreach (var (shape, topIndex) in _temporaryIndices[1])
|
foreach (var (shape, topIndex) in _temporaryIndices[1])
|
||||||
{
|
{
|
||||||
if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
|
if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
|
||||||
{
|
{
|
||||||
_temporaryValues[1] |= 1u << topIndex;
|
_temporaryValues[1] |= 1u << topIndex;
|
||||||
_temporaryValues[2] |= 1u << handIndex;
|
_temporaryValues[2] |= 1u << handIndex;
|
||||||
|
CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
|
if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
|
||||||
{
|
{
|
||||||
_temporaryValues[1] |= 1u << topIndex;
|
_temporaryValues[1] |= 1u << topIndex;
|
||||||
_temporaryValues[3] |= 1u << legIndex;
|
_temporaryValues[3] |= 1u << legIndex;
|
||||||
|
CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
|
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
|
||||||
{
|
{
|
||||||
if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
|
if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
|
||||||
{
|
{
|
||||||
_temporaryValues[3] |= 1u << bottomIndex;
|
_temporaryValues[3] |= 1u << bottomIndex;
|
||||||
_temporaryValues[4] |= 1u << footIndex;
|
_temporaryValues[4] |= 1u << footIndex;
|
||||||
}
|
CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
return;
|
||||||
private static bool CheckCenter(in ShapeString shape, char first, char second)
|
|
||||||
=> shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_';
|
void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
|
||||||
|
{
|
||||||
|
if (!cache.CheckConditionState(shape, out var dict))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var (subShape, set) in dict)
|
||||||
|
{
|
||||||
|
if (set.Contains(slot1, _ids[idx1]))
|
||||||
|
if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex))
|
||||||
|
_temporaryValues[idx1] |= 1u << subIndex;
|
||||||
|
if (set.Contains(slot2, _ids[idx2]))
|
||||||
|
if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex))
|
||||||
|
_temporaryValues[idx2] |= 1u << subIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,22 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public bool IsAnkle()
|
||||||
|
=> CheckCenter('a', 'n');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public bool IsWaist()
|
||||||
|
=> CheckCenter('w', 'a');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public bool IsWrist()
|
||||||
|
=> CheckCenter('w', 'r');
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private bool CheckCenter(char first, char second)
|
||||||
|
=> Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_';
|
||||||
|
|
||||||
public bool Equals(ShapeString other)
|
public bool Equals(ShapeString other)
|
||||||
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
|
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,19 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
=> "Shape Keys (SHP)###SHP"u8;
|
=> "Shape Keys (SHP)###SHP"u8;
|
||||||
|
|
||||||
private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty;
|
private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty;
|
||||||
|
private ShapeString _conditionBuffer = ShapeString.Empty;
|
||||||
private bool _identifierValid;
|
private bool _identifierValid;
|
||||||
|
private bool _conditionValid = true;
|
||||||
|
|
||||||
public override int NumColumns
|
public override int NumColumns
|
||||||
=> 6;
|
=> 7;
|
||||||
|
|
||||||
public override float ColumnHeight
|
public override float ColumnHeight
|
||||||
=> ImUtf8.FrameHeightSpacing;
|
=> ImUtf8.FrameHeightSpacing;
|
||||||
|
|
||||||
protected override void Initialize()
|
protected override void Initialize()
|
||||||
{
|
{
|
||||||
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty);
|
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawNew()
|
protected override void DrawNew()
|
||||||
|
|
@ -40,7 +42,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Shp)));
|
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Shp)));
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
var canAdd = !Editor.Contains(Identifier) && _identifierValid;
|
var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid;
|
||||||
var tt = canAdd
|
var tt = canAdd
|
||||||
? "Stage this edit."u8
|
? "Stage this edit."u8
|
||||||
: _identifierValid
|
: _identifierValid
|
||||||
|
|
@ -67,6 +69,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
.OrderBy(kvp => kvp.Key.Shape)
|
.OrderBy(kvp => kvp.Key.Shape)
|
||||||
.ThenBy(kvp => kvp.Key.Slot)
|
.ThenBy(kvp => kvp.Key.Slot)
|
||||||
.ThenBy(kvp => kvp.Key.Id)
|
.ThenBy(kvp => kvp.Key.Id)
|
||||||
|
.ThenBy(kvp => kvp.Key.ShapeCondition)
|
||||||
.Select(kvp => (kvp.Key, kvp.Value));
|
.Select(kvp => (kvp.Key, kvp.Value));
|
||||||
|
|
||||||
protected override int Count
|
protected override int Count
|
||||||
|
|
@ -82,6 +85,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid);
|
changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid);
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +107,13 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor);
|
ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if (identifier.ShapeCondition.Length > 0)
|
||||||
|
{
|
||||||
|
ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor);
|
||||||
|
ImUtf8.HoverTooltip("Connector condition for this shape to be activated.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool DrawEntry(ref ShpEntry entry, bool disabled)
|
private static bool DrawEntry(ref ShpEntry entry, bool disabled)
|
||||||
|
|
@ -154,7 +167,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150)
|
public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150)
|
||||||
{
|
{
|
||||||
var ret = false;
|
var ret = false;
|
||||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||||
|
|
@ -168,13 +181,37 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
|
|
||||||
ret = true;
|
ret = true;
|
||||||
if (slot is HumanSlot.Unknown)
|
if (slot is HumanSlot.Unknown)
|
||||||
|
{
|
||||||
identifier = identifier with
|
identifier = identifier with
|
||||||
{
|
{
|
||||||
Id = null,
|
Id = null,
|
||||||
Slot = slot,
|
Slot = slot,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
else
|
else
|
||||||
identifier = identifier with { Slot = slot };
|
{
|
||||||
|
if (_conditionBuffer.Length > 0
|
||||||
|
&& (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs
|
||||||
|
|| _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body
|
||||||
|
|| _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs))
|
||||||
|
{
|
||||||
|
identifier = identifier with
|
||||||
|
{
|
||||||
|
Slot = slot,
|
||||||
|
ShapeCondition = ShapeString.Empty,
|
||||||
|
};
|
||||||
|
_conditionValid = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
identifier = identifier with
|
||||||
|
{
|
||||||
|
Slot = slot,
|
||||||
|
ShapeCondition = _conditionBuffer,
|
||||||
|
};
|
||||||
|
_conditionValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +241,33 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid,
|
||||||
|
float unscaledWidth = 150)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
var ptr = Unsafe.AsPointer(ref buffer);
|
||||||
|
var span = new Span<byte>(ptr, ShapeString.MaxLength + 1);
|
||||||
|
using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid))
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||||
|
if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8))
|
||||||
|
{
|
||||||
|
buffer.ForceLength((byte)newLength);
|
||||||
|
valid = ShpIdentifier.ValidateCustomShapeString(buffer)
|
||||||
|
&& (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs
|
||||||
|
|| buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs
|
||||||
|
|| buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands);
|
||||||
|
if (valid)
|
||||||
|
identifier = identifier with { ShapeCondition = buffer };
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImUtf8.HoverTooltip(
|
||||||
|
"Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
private static ReadOnlySpan<HumanSlot> AvailableSlots
|
private static ReadOnlySpan<HumanSlot> AvailableSlots
|
||||||
=>
|
=>
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using Dalamud.Interface.Utility.Raii;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Interop;
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
@ -12,9 +13,9 @@ namespace Penumbra.UI.Tabs.Debug;
|
||||||
|
|
||||||
public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService
|
public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService
|
||||||
{
|
{
|
||||||
private int _objectIndex = 0;
|
private int _objectIndex;
|
||||||
|
|
||||||
public unsafe void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
ImUtf8.InputScalar("Object Index"u8, ref _objectIndex);
|
ImUtf8.InputScalar("Object Index"u8, ref _objectIndex);
|
||||||
var actor = objects[0];
|
var actor = objects[0];
|
||||||
|
|
@ -31,19 +32,43 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DrawCollectionShapeCache(actor);
|
||||||
|
DrawCharacterShapes(human);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void DrawCollectionShapeCache(Actor actor)
|
||||||
|
{
|
||||||
var data = resolver.IdentifyCollection(actor.AsObject, true);
|
var data = resolver.IdentifyCollection(actor.AsObject, true);
|
||||||
using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"))
|
using var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})");
|
||||||
{
|
if (!treeNode1.Success || !data.ModCollection.HasCache)
|
||||||
if (treeNode1.Success && data.ModCollection.HasCache)
|
return;
|
||||||
{
|
|
||||||
using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg);
|
using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg);
|
||||||
if (!table)
|
if (!table)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||||
ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch);
|
ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||||
|
ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||||
|
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State)
|
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
DrawShape(shape, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState)
|
||||||
|
{
|
||||||
|
foreach (var (shape, set) in dict)
|
||||||
|
{
|
||||||
|
ImUtf8.DrawTableColumn(condition.AsSpan);
|
||||||
|
DrawShape(shape, set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set)
|
||||||
{
|
{
|
||||||
ImUtf8.DrawTableColumn(shape.AsSpan);
|
ImUtf8.DrawTableColumn(shape.AsSpan);
|
||||||
if (set.All)
|
if (set.All)
|
||||||
|
|
@ -69,22 +94,24 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8))
|
private unsafe void DrawCharacterShapes(Model human)
|
||||||
{
|
|
||||||
if (treeNode2)
|
|
||||||
{
|
{
|
||||||
|
using var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8);
|
||||||
|
if (!treeNode2)
|
||||||
|
return;
|
||||||
|
|
||||||
using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg);
|
using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg);
|
||||||
if (!table)
|
if (!table)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale);
|
ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale);
|
||||||
ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||||
ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14);
|
ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14);
|
||||||
ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8);
|
ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8);
|
||||||
ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch);
|
ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||||
|
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||||
for (var i = 0; i < human.AsHuman->SlotCount; ++i)
|
for (var i = 0; i < human.AsHuman->SlotCount; ++i)
|
||||||
|
|
@ -118,6 +145,4 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@
|
||||||
"minLength": 5,
|
"minLength": 5,
|
||||||
"maxLength": 30,
|
"maxLength": 30,
|
||||||
"pattern": "^shpx_"
|
"pattern": "^shpx_"
|
||||||
|
},
|
||||||
|
"ShapeCondition": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 8,
|
||||||
|
"maxLength": 30,
|
||||||
|
"pattern": "^shpx_(wa|an|wr)_"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue