mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
227 lines
9.2 KiB
C#
227 lines
9.2 KiB
C#
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);
|
|
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
|
|
AccessoryImcCheck(model);
|
|
}
|
|
|
|
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
|
|
}
|
|
|
|
private void AccessoryImcCheck(Model model)
|
|
{
|
|
var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex));
|
|
|
|
Span<byte> attr =
|
|
[
|
|
(byte)'a',
|
|
(byte)'t',
|
|
(byte)'r',
|
|
(byte)'_',
|
|
AccessoryByte(_modelIndex),
|
|
(byte)'v',
|
|
(byte)'_',
|
|
(byte)'a',
|
|
0,
|
|
];
|
|
for (var i = 1; i < 10; ++i)
|
|
{
|
|
var flag = (ushort)(1 << i);
|
|
if ((imcMask & flag) is not 0)
|
|
continue;
|
|
|
|
attr[^2] = (byte)('a' + i);
|
|
|
|
foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes)
|
|
{
|
|
if (!EqualAttribute(attr, attribute.Value))
|
|
continue;
|
|
|
|
_model->EnabledAttributeIndexMask &= ~(1u << index);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
|
private static bool EqualAttribute(Span<byte> needle, byte* haystack)
|
|
{
|
|
foreach (var character in needle)
|
|
{
|
|
if (*haystack++ != character)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static byte AccessoryByte(HumanSlot slot)
|
|
=> slot switch
|
|
{
|
|
HumanSlot.Head => (byte)'m',
|
|
HumanSlot.Ears => (byte)'e',
|
|
HumanSlot.Neck => (byte)'n',
|
|
HumanSlot.Wrists => (byte)'w',
|
|
HumanSlot.RFinger => (byte)'r',
|
|
HumanSlot.LFinger => (byte)'r',
|
|
_ => 0,
|
|
};
|
|
|
|
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 &= ~(1u << 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 |= 1u << index;
|
|
}
|
|
else
|
|
{
|
|
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateDefaultMasks(Model human, ShpCache cache)
|
|
{
|
|
var genderRace = (GenderRace)human.AsHuman->RaceSexId;
|
|
foreach (var (shape, topIndex) in _temporaryShapes[1])
|
|
{
|
|
if (shape.IsWrist()
|
|
&& _temporaryShapes[2].TryGetValue(shape, out var handIndex)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace)
|
|
&& human.AsHuman->Models[1] is not null
|
|
&& human.AsHuman->Models[2] is not null)
|
|
{
|
|
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
|
|
human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex;
|
|
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2);
|
|
}
|
|
|
|
if (shape.IsWaist()
|
|
&& _temporaryShapes[3].TryGetValue(shape, out var legIndex)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
|
|
&& human.AsHuman->Models[1] is not null
|
|
&& human.AsHuman->Models[3] is not null)
|
|
{
|
|
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
|
|
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex;
|
|
CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3);
|
|
}
|
|
}
|
|
|
|
foreach (var (shape, bottomIndex) in _temporaryShapes[3])
|
|
{
|
|
if (shape.IsAnkle()
|
|
&& _temporaryShapes[4].TryGetValue(shape, out var footIndex)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
|
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace)
|
|
&& human.AsHuman->Models[3] is not null
|
|
&& human.AsHuman->Models[4] is not null)
|
|
{
|
|
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex;
|
|
human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex;
|
|
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
|
|
}
|
|
}
|
|
|
|
return;
|
|
|
|
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> dict, GenderRace genderRace, HumanSlot slot1,
|
|
HumanSlot slot2, int idx1, int idx2)
|
|
{
|
|
foreach (var (shape, set) in dict)
|
|
{
|
|
if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _temporaryShapes[idx1].TryGetValue(shape, out var index1))
|
|
human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1;
|
|
if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2))
|
|
human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2;
|
|
}
|
|
}
|
|
}
|
|
}
|