Add attribute handling, rework atr and shape caches.

This commit is contained in:
Ottermandias 2025-05-21 15:44:26 +02:00
parent 3412786282
commit d7dee39fab
23 changed files with 1187 additions and 300 deletions

View file

@ -0,0 +1,145 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
namespace Penumbra.Meta.Manipulations;
public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition)
: IComparable<AtrIdentifier>, IMetaIdentifier
{
public int CompareTo(AtrIdentifier other)
{
var slotComparison = Slot.CompareTo(other.Slot);
if (slotComparison is not 0)
return slotComparison;
if (Id.HasValue)
{
if (other.Id.HasValue)
{
var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id);
if (idComparison is not 0)
return idComparison;
}
else
{
return -1;
}
}
else if (other.Id.HasValue)
{
return 1;
}
var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
if (genderRaceComparison is not 0)
return genderRaceComparison;
return Attribute.CompareTo(other.Attribute);
}
public override string ToString()
{
var sb = new StringBuilder(64);
sb.Append("Shp - ")
.Append(Attribute);
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 (GenderRaceCondition is not GenderRace.Unknown)
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
return sb.ToString();
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
// Nothing for now since it depends entirely on the shape key.
}
public MetaIndex FileIndex()
=> (MetaIndex)(-1);
public bool Validate()
{
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
return false;
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
return false;
if (Slot is HumanSlot.Unknown && Id is not null)
return false;
if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue })
return false;
if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
return false;
return Attribute.ValidateCustomAttributeString();
}
public JObject AddToJson(JObject jObj)
{
if (Slot is not HumanSlot.Unknown)
jObj["Slot"] = Slot.ToString();
if (Id.HasValue)
jObj["Id"] = Id.Value.Id.ToString();
jObj["Attribute"] = Attribute.ToString();
if (GenderRaceCondition is not GenderRace.Unknown)
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
return jObj;
}
public static AtrIdentifier? FromJson(JObject jObj)
{
var attribute = jObj["Attribute"]?.ToObject<string>();
if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString))
return null;
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition);
return identifier.Validate() ? identifier : null;
}
public MetaManipulationType Type
=> MetaManipulationType.Atr;
}
[JsonConverter(typeof(Converter))]
public readonly record struct AtrEntry(bool Value)
{
public static readonly AtrEntry True = new(true);
public static readonly AtrEntry False = new(false);
private class Converter : JsonConverter<AtrEntry>
{
public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer)
=> serializer.Serialize(writer, value.Value);
public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue,
JsonSerializer serializer)
=> new(serializer.Deserialize<bool>(reader));
}
}

View file

@ -16,6 +16,7 @@ public enum MetaManipulationType : byte
GlobalEqp = 7,
Atch = 8,
Shp = 9,
Atr = 10,
}
public interface IMetaIdentifier

View file

@ -21,6 +21,7 @@ public class MetaDictionary
public readonly Dictionary<GmpIdentifier, GmpEntry> Gmp = [];
public readonly Dictionary<AtchIdentifier, AtchEntry> Atch = [];
public readonly Dictionary<ShpIdentifier, ShpEntry> Shp = [];
public readonly Dictionary<AtrIdentifier, AtrEntry> Atr = [];
public Wrapper()
{ }
@ -35,6 +36,7 @@ public class MetaDictionary
Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
foreach (var geqp in cache.GlobalEqp.Keys)
Add(geqp);
}
@ -66,6 +68,9 @@ public class MetaDictionary
public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp
=> _data?.Shp ?? [];
public IReadOnlyDictionary<AtrIdentifier, AtrEntry> Atr
=> _data?.Atr ?? [];
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
=> _data ?? [];
@ -84,6 +89,7 @@ public class MetaDictionary
MetaManipulationType.Rsp => _data.Rsp.Count,
MetaManipulationType.Atch => _data.Atch.Count,
MetaManipulationType.Shp => _data.Shp.Count,
MetaManipulationType.Atr => _data.Atr.Count,
MetaManipulationType.GlobalEqp => _data.Count,
_ => 0,
};
@ -100,6 +106,7 @@ public class MetaDictionary
ImcIdentifier i => _data.Imc.ContainsKey(i),
AtchIdentifier i => _data.Atch.ContainsKey(i),
ShpIdentifier i => _data.Shp.ContainsKey(i),
AtrIdentifier i => _data.Atr.ContainsKey(i),
RspIdentifier i => _data.Rsp.ContainsKey(i),
_ => false,
};
@ -115,13 +122,13 @@ public class MetaDictionary
if (_data is null)
return;
if (_data.Count is 0 && Shp.Count is 0)
if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0)
{
_data = null;
Count = 0;
}
Count = GlobalEqp.Count + Shp.Count;
Count = GlobalEqp.Count + Shp.Count + Atr.Count;
_data!.Imc.Clear();
_data!.Eqp.Clear();
_data!.Eqdp.Clear();
@ -147,6 +154,7 @@ public class MetaDictionary
&& _data.Gmp.SetEquals(other._data!.Gmp)
&& _data.Atch.SetEquals(other._data!.Atch)
&& _data.Shp.SetEquals(other._data!.Shp)
&& _data.Atr.SetEquals(other._data!.Atr)
&& _data.SetEquals(other._data!);
}
@ -161,6 +169,7 @@ public class MetaDictionary
.Concat(_data!.Rsp.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Atch.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Shp.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Atr.Keys.Cast<IMetaIdentifier>())
.Concat(_data!.Cast<IMetaIdentifier>());
#region TryAdd
@ -251,6 +260,16 @@ public class MetaDictionary
return true;
}
public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry)
{
_data ??= [];
if (!_data!.Atr.TryAdd(identifier, entry))
return false;
++Count;
return true;
}
public bool TryAdd(GlobalEqpManipulation identifier)
{
_data ??= [];
@ -343,6 +362,15 @@ public class MetaDictionary
return true;
}
public bool Update(AtrIdentifier identifier, in AtrEntry entry)
{
if (_data is null || !_data.Atr.ContainsKey(identifier))
return false;
_data.Atr[identifier] = entry;
return true;
}
#endregion
#region TryGetValue
@ -371,6 +399,9 @@ public class MetaDictionary
public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value)
=> _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value);
public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value)
=> _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetDefault<T>(out T? value)
{
@ -396,6 +427,7 @@ public class MetaDictionary
RspIdentifier i => _data.Rsp.Remove(i),
AtchIdentifier i => _data.Atch.Remove(i),
ShpIdentifier i => _data.Shp.Remove(i),
AtrIdentifier i => _data.Atr.Remove(i),
_ => false,
};
if (ret && --Count is 0)
@ -436,6 +468,9 @@ public class MetaDictionary
foreach (var (identifier, entry) in manips._data!.Shp)
TryAdd(identifier, entry);
foreach (var (identifier, entry) in manips._data!.Atr)
TryAdd(identifier, entry);
foreach (var identifier in manips._data!)
TryAdd(identifier);
}
@ -498,6 +533,12 @@ public class MetaDictionary
return false;
}
foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
{
failedIdentifier = identifier;
return false;
}
foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier)))
{
failedIdentifier = identifier;
@ -526,6 +567,7 @@ public class MetaDictionary
_data!.Gmp.SetTo(other._data!.Gmp);
_data!.Atch.SetTo(other._data!.Atch);
_data!.Shp.SetTo(other._data!.Shp);
_data!.Atr.SetTo(other._data!.Atr);
_data!.SetTo(other._data!);
Count = other.Count;
}
@ -544,6 +586,7 @@ public class MetaDictionary
_data!.Gmp.UpdateTo(other._data!.Gmp);
_data!.Atch.UpdateTo(other._data!.Atch);
_data!.Shp.UpdateTo(other._data!.Shp);
_data!.Atr.UpdateTo(other._data!.Atr);
_data!.UnionWith(other._data!);
Count = _data!.Imc.Count
+ _data!.Eqp.Count
@ -651,6 +694,16 @@ public class MetaDictionary
}),
};
public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry)
=> new()
{
["Type"] = MetaManipulationType.Atr.ToString(),
["Manipulation"] = identifier.AddToJson(new JObject
{
["Entry"] = entry.Value,
}),
};
public static JObject Serialize(GlobalEqpManipulation identifier)
=> new()
{
@ -682,6 +735,8 @@ public class MetaDictionary
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry))
return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry));
if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry))
return Serialize(Unsafe.As<TIdentifier, AtrIdentifier>(ref identifier), Unsafe.As<TEntry, AtrEntry>(ref entry));
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
@ -730,6 +785,7 @@ public class MetaDictionary
SerializeTo(array, value._data!.Gmp);
SerializeTo(array, value._data!.Atch);
SerializeTo(array, value._data!.Shp);
SerializeTo(array, value._data!.Atr);
SerializeTo(array, value._data!);
}
@ -839,6 +895,16 @@ public class MetaDictionary
Penumbra.Log.Warning("Invalid SHP Manipulation encountered.");
break;
}
case MetaManipulationType.Atr:
{
var identifier = AtrIdentifier.FromJson(manip);
var entry = new AtrEntry(manip["Entry"]?.Value<bool>() ?? true);
if (identifier.HasValue)
dict.TryAdd(identifier.Value, entry);
else
Penumbra.Log.Warning("Invalid ATR Manipulation encountered.");
break;
}
case MetaManipulationType.GlobalEqp:
{
var identifier = GlobalEqpManipulation.FromJson(manip);

View file

@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -18,7 +19,12 @@ public enum ShapeConnectorCondition : byte
Ankles = 3,
}
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition)
public readonly record struct ShpIdentifier(
HumanSlot Slot,
PrimaryId? Id,
ShapeAttributeString Shape,
ShapeConnectorCondition ConnectorCondition,
GenderRace GenderRaceCondition)
: IComparable<ShpIdentifier>, IMetaIdentifier
{
public int CompareTo(ShpIdentifier other)
@ -49,6 +55,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (conditionComparison is not 0)
return conditionComparison;
var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
if (genderRaceComparison is not 0)
return genderRaceComparison;
return Shape.CompareTo(other.Shape);
}
@ -80,6 +90,9 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break;
}
if (GenderRaceCondition is not GenderRace.Unknown)
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
return sb.ToString();
}
@ -96,6 +109,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
return false;
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
return false;
if (!Enum.IsDefined(ConnectorCondition))
return false;
if (Slot is HumanSlot.Unknown && Id is not null)
return false;
@ -105,10 +124,7 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
return false;
if (!ValidateCustomShapeString(Shape))
return false;
if (!Enum.IsDefined(ConnectorCondition))
if (!Shape.ValidateCustomShapeString())
return false;
return ConnectorCondition switch
@ -121,40 +137,6 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
};
}
public static unsafe bool ValidateCustomShapeString(byte* shape)
{
// "shpx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'s'
|| *shape++ is not (byte)'h'
|| *shape++ is not (byte)'p'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public static bool ValidateCustomShapeString(in ShapeString shape)
{
// "shpx_*"
if (shape.Length < 6)
return false;
var span = shape.AsSpan;
if (span[0] is not (byte)'s'
|| span[1] is not (byte)'h'
|| span[2] is not (byte)'p'
|| span[3] is not (byte)'x'
|| span[4] is not (byte)'_')
return false;
return true;
}
public JObject AddToJson(JObject jObj)
{
if (Slot is not HumanSlot.Unknown)
@ -164,19 +146,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
jObj["Shape"] = Shape.ToString();
if (ConnectorCondition is not ShapeConnectorCondition.None)
jObj["ConnectorCondition"] = ConnectorCondition.ToString();
if (GenderRaceCondition is not GenderRace.Unknown)
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
return jObj;
}
public static ShpIdentifier? FromJson(JObject jObj)
{
var shape = jObj["Shape"]?.ToObject<string>();
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString))
return null;
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var connectorCondition = jObj["ConnectorCondition"]?.ToObject<ShapeConnectorCondition>() ?? ShapeConnectorCondition.None;
var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition);
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
var id = jObj["Id"]?.ToObject<ushort>();
var connectorCondition = jObj["ConnectorCondition"]?.ToObject<ShapeConnectorCondition>() ?? ShapeConnectorCondition.None;
var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition);
return identifier.Validate() ? identifier : null;
}

View file

@ -0,0 +1,153 @@
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta;
public unsafe class ShapeAttributeManager : IRequiredService, IDisposable
{
public const int NumSlots = 14;
public const int ModelSlotSize = 18;
private readonly AttributeHook _attributeHook;
public static ReadOnlySpan<HumanSlot> UsedModels
=>
[
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
];
public ShapeAttributeManager(AttributeHook attributeHook)
{
_attributeHook = attributeHook;
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager);
}
private readonly Dictionary<ShapeAttributeString, short>[] _temporaryShapes =
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeAttributeString, short>()).ToArray();
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
private HumanSlot _modelIndex;
private int _slotIndex;
private GenderRace _genderRace;
private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model;
public void Dispose()
=> _attributeHook.Unsubscribe(OnAttributeComputed);
private void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
{
if (!collection.HasCache)
return;
_genderRace = (GenderRace)model.AsHuman->RaceSexId;
for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex)
{
_modelIndex = UsedModels[_slotIndex];
_model = model.AsHuman->Models[_modelIndex.ToIndex()];
if (_model is null || _model->ModelResourceHandle is null)
continue;
_ids[(int)_modelIndex] = model.GetModelId(_modelIndex);
CheckShapes(collection.MetaCache!.Shp);
CheckAttributes(collection.MetaCache!.Atr);
}
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
}
private void CheckAttributes(AtrCache attributeCache)
{
if (attributeCache.DisabledCount is 0)
return;
ref var attributes = ref _model->ModelResourceHandle->Attributes;
foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value)))
{
if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString))
{
// Mask out custom attributes if they are disabled. Attributes are enabled by default.
if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
_model->EnabledAttributeIndexMask &= (ushort)~(1 << index);
}
else
{
Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}.");
}
}
}
private void CheckShapes(ShpCache shapeCache)
{
_temporaryShapes[_slotIndex].Clear();
ref var shapes = ref _model->ModelResourceHandle->Shapes;
foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value)))
{
if (ShapeAttributeString.TryRead(shape.Value, out var shapeString))
{
_temporaryShapes[_slotIndex].TryAdd(shapeString, index);
// Add custom shapes if they are enabled. Shapes are disabled by default.
if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
_model->EnabledShapeKeyIndexMask |= (ushort)(1 << index);
}
else
{
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
}
}
}
private void UpdateDefaultMasks(Model human, ShpCache cache)
{
foreach (var (shape, topIndex) in _temporaryShapes[1])
{
if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex))
{
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex))
{
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
foreach (var (shape, bottomIndex) in _temporaryShapes[3])
{
if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex))
{
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex;
human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> dict, HumanSlot slot1,
HumanSlot slot2, int idx1, int idx2)
{
if (dict.Count is 0)
return;
foreach (var (shape, set) in dict)
{
if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1))
human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1;
if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2))
human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2;
}
}
}
}

View file

@ -6,11 +6,11 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta;
[JsonConverter(typeof(Converter))]
public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
public struct ShapeAttributeString : IEquatable<ShapeAttributeString>, IComparable<ShapeAttributeString>
{
public const int MaxLength = 30;
public static readonly ShapeString Empty = new();
public static readonly ShapeAttributeString Empty = new();
private FixedString32 _buffer;
@ -37,6 +37,72 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
}
}
public static unsafe bool ValidateCustomShapeString(byte* shape)
{
// "shpx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'s'
|| *shape++ is not (byte)'h'
|| *shape++ is not (byte)'p'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public bool ValidateCustomShapeString()
{
// "shpx_*"
if (Length < 6)
return false;
if (_buffer[0] is not (byte)'s'
|| _buffer[1] is not (byte)'h'
|| _buffer[2] is not (byte)'p'
|| _buffer[3] is not (byte)'x'
|| _buffer[4] is not (byte)'_')
return false;
return true;
}
public static unsafe bool ValidateCustomAttributeString(byte* shape)
{
// "atrx_*"
if (shape is null)
return false;
if (*shape++ is not (byte)'a'
|| *shape++ is not (byte)'t'
|| *shape++ is not (byte)'r'
|| *shape++ is not (byte)'x'
|| *shape++ is not (byte)'_'
|| *shape is 0)
return false;
return true;
}
public bool ValidateCustomAttributeString()
{
// "atrx_*"
if (Length < 6)
return false;
if (_buffer[0] is not (byte)'a'
|| _buffer[1] is not (byte)'t'
|| _buffer[2] is not (byte)'r'
|| _buffer[3] is not (byte)'x'
|| _buffer[4] is not (byte)'_')
return false;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsAnkle()
=> CheckCenter('a', 'n');
@ -53,28 +119,28 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
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(ShapeAttributeString other)
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
public override bool Equals(object? obj)
=> obj is ShapeString other && Equals(other);
=> obj is ShapeAttributeString other && Equals(other);
public override int GetHashCode()
=> (int)Crc32.Get(_buffer[..Length]);
public static bool operator ==(ShapeString left, ShapeString right)
public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right)
=> left.Equals(right);
public static bool operator !=(ShapeString left, ShapeString right)
public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right)
=> !left.Equals(right);
public static unsafe bool TryRead(byte* pointer, out ShapeString ret)
public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret)
{
var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer);
return TryRead(span, out ret);
}
public unsafe int CompareTo(ShapeString other)
public unsafe int CompareTo(ShapeAttributeString other)
{
fixed (void* lhs = &this)
{
@ -82,7 +148,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
}
}
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeString ret)
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeAttributeString ret)
{
if (utf8.Length is 0 or > MaxLength)
{
@ -97,7 +163,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
return true;
}
public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeString ret)
public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeAttributeString ret)
{
ret = Empty;
if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written))
@ -116,19 +182,20 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
_buffer[31] = length;
}
private sealed class Converter : JsonConverter<ShapeString>
private sealed class Converter : JsonConverter<ShapeAttributeString>
{
public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue,
public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString 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.");
throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString.");
return existingValue;
}

View file

@ -1,140 +0,0 @@
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Meta;
public class ShapeManager : IRequiredService, IDisposable
{
public const int NumSlots = 14;
public const int ModelSlotSize = 18;
private readonly AttributeHook _attributeHook;
public static ReadOnlySpan<HumanSlot> UsedModels
=>
[
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
];
public ShapeManager(AttributeHook attributeHook)
{
_attributeHook = attributeHook;
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.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 readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
public void Dispose()
=> _attributeHook.Unsubscribe(OnAttributeComputed);
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
{
if (!collection.HasCache)
return;
ComputeCache(model, collection.MetaCache!.Shp);
for (var i = 0; i < NumSlots; ++i)
{
if (_temporaryMasks[i] is 0)
continue;
var modelIndex = UsedModels[i];
var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask;
var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i];
Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}.");
model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask;
}
}
private unsafe void ComputeCache(Model human, ShpCache cache)
{
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.ToIndex()];
if (model is null || model->ModelResourceHandle is null)
continue;
_ids[(int)modelIndex] = human.GetModelId(modelIndex);
ref var shapes = ref model->ModelResourceHandle->Shapes;
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
{
if (ShapeString.TryRead(shape.Value, out var shapeString))
{
_temporaryIndices[i].TryAdd(shapeString, index);
_temporaryMasks[i] |= (ushort)(1 << index);
if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
_temporaryValues[i] |= (ushort)(1 << index);
}
else
{
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
}
}
}
UpdateDefaultMasks(cache);
}
private void UpdateDefaultMasks(ShpCache cache)
{
foreach (var (shape, topIndex) in _temporaryIndices[1])
{
if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[2] |= 1u << handIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2);
}
if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
{
_temporaryValues[1] |= 1u << topIndex;
_temporaryValues[3] |= 1u << legIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3);
}
}
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
{
if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
{
_temporaryValues[3] |= 1u << bottomIndex;
_temporaryValues[4] |= 1u << footIndex;
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4);
}
}
return;
void CheckCondition(IReadOnlyDictionary<ShapeString, ShpCache.ShpHashSet> dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
{
if (dict.Count is 0)
return;
foreach (var (shape, set) in dict)
{
if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1))
_temporaryValues[idx1] |= 1u << index1;
if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2))
_temporaryValues[idx2] |= 1u << index2;
}
}
}
}