mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add attribute handling, rework atr and shape caches.
This commit is contained in:
parent
3412786282
commit
d7dee39fab
23 changed files with 1187 additions and 300 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b
|
||||
Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1
|
||||
|
|
@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\group_single.json = schemas\structs\group_single.json
|
||||
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
||||
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
|
||||
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
|
||||
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
|
||||
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
||||
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
||||
|
|
|
|||
56
Penumbra/Collections/Cache/AtrCache.cs
Normal file
56
Penumbra/Collections/Cache/AtrCache.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtrIdentifier, AtrEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace);
|
||||
|
||||
public int DisabledCount { get; private set; }
|
||||
|
||||
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> Data
|
||||
=> _atrData;
|
||||
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _atrData = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_atrData.Clear();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Clear();
|
||||
|
||||
protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
{
|
||||
if (entry.Value)
|
||||
return;
|
||||
|
||||
value = [];
|
||||
_atrData.Add(identifier.Attribute, value);
|
||||
}
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value))
|
||||
++DisabledCount;
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(AtrIdentifier identifier)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false))
|
||||
{
|
||||
--DisabledCount;
|
||||
if (value.IsEmpty)
|
||||
_atrData.Remove(identifier.Attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable
|
|||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atr)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||
AddManipulation(mod, identifier, null!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
public readonly ImcCache Imc = new(manager, collection);
|
||||
public readonly AtchCache Atch = new(manager, collection);
|
||||
public readonly ShpCache Shp = new(manager, collection);
|
||||
public readonly AtrCache Atr = new(manager, collection);
|
||||
public readonly GlobalEqpCache GlobalEqp = new();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public int Count
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count;
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
|
||||
|
||||
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
|
||||
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
||||
|
|
@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
|
||||
|
||||
public void Reset()
|
||||
|
|
@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Imc.Reset();
|
||||
Atch.Reset();
|
||||
Shp.Reset();
|
||||
Atr.Reset();
|
||||
GlobalEqp.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Imc.Dispose();
|
||||
Atch.Dispose();
|
||||
Shp.Dispose();
|
||||
Atr.Dispose();
|
||||
}
|
||||
|
||||
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||
|
|
@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
||||
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
||||
ShpIdentifier i => Shp.RevertMod(i, out mod),
|
||||
AtrIdentifier i => Atr.RevertMod(i, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
|
||||
_ => (mod = null) != null,
|
||||
};
|
||||
|
|
@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
|
||||
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
|
||||
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
|
||||
AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
|
||||
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
|
||||
_ => false,
|
||||
};
|
||||
|
|
|
|||
123
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal file
123
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using System.Collections.Frozen;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong>
|
||||
{
|
||||
public static readonly IReadOnlyList<GenderRace> GenderRaceValues =
|
||||
[
|
||||
GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale,
|
||||
GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale,
|
||||
GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale,
|
||||
GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale,
|
||||
];
|
||||
|
||||
public static readonly FrozenDictionary<GenderRace, int> GenderRaceIndices =
|
||||
GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index);
|
||||
|
||||
private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count);
|
||||
|
||||
public bool this[HumanSlot slot]
|
||||
=> slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count];
|
||||
|
||||
public bool this[GenderRace genderRace]
|
||||
=> GenderRaceIndices.TryGetValue(genderRace, out var index)
|
||||
&& _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index];
|
||||
|
||||
public bool this[HumanSlot slot, GenderRace genderRace]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
|
||||
return false;
|
||||
|
||||
if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index])
|
||||
return true;
|
||||
|
||||
return _allIds[(int)slot * GenderRaceIndices.Count + index];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
|
||||
return;
|
||||
|
||||
var genderRaceCount = GenderRaceValues.Count;
|
||||
if (slot is HumanSlot.Unknown)
|
||||
_allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value;
|
||||
else
|
||||
_allIds[(int)slot * genderRaceCount + index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool All
|
||||
=> _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count];
|
||||
|
||||
public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> GenderRaceIndices.TryGetValue(genderRace, out var index)
|
||||
&& TryGetValue((slot, id), out var flags)
|
||||
&& (flags & (1ul << index)) is not 0;
|
||||
|
||||
public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value)
|
||||
{
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
|
||||
return false;
|
||||
|
||||
if (!id.HasValue)
|
||||
{
|
||||
var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot;
|
||||
var old = _allIds[slotIndex * GenderRaceIndices.Count + index];
|
||||
_allIds[slotIndex * GenderRaceIndices.Count + index] = value;
|
||||
return old != value;
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
if (TryGetValue((slot, id.Value), out var flags))
|
||||
{
|
||||
var newFlags = flags | (1ul << index);
|
||||
if (newFlags == flags)
|
||||
return false;
|
||||
|
||||
this[(slot, id.Value)] = newFlags;
|
||||
return true;
|
||||
}
|
||||
|
||||
this[(slot, id.Value)] = 1ul << index;
|
||||
return true;
|
||||
}
|
||||
else if (TryGetValue((slot, id.Value), out var flags))
|
||||
{
|
||||
var newFlags = flags & ~(1ul << index);
|
||||
if (newFlags == flags)
|
||||
return false;
|
||||
|
||||
if (newFlags is 0)
|
||||
{
|
||||
Remove((slot, id.Value));
|
||||
return true;
|
||||
}
|
||||
|
||||
this[(slot, id.Value)] = newFlags;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
_allIds.SetAll(false);
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
=> !_allIds.HasAnySet() && Count is 0;
|
||||
}
|
||||
|
|
@ -7,10 +7,10 @@ namespace Penumbra.Collections.Cache;
|
|||
|
||||
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id)
|
||||
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id);
|
||||
public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace);
|
||||
|
||||
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State(ShapeConnectorCondition connector)
|
||||
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> State(ShapeConnectorCondition connector)
|
||||
=> connector switch
|
||||
{
|
||||
ShapeConnectorCondition.None => _shpData,
|
||||
|
|
@ -22,73 +22,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
|||
|
||||
public int EnabledCount { get; private set; }
|
||||
|
||||
public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
|
||||
{
|
||||
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
|
||||
|
||||
public bool All
|
||||
{
|
||||
get => _allIds[^1];
|
||||
set => _allIds[^1] = value;
|
||||
}
|
||||
|
||||
public bool this[HumanSlot slot]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
return All;
|
||||
|
||||
return _allIds[(int)slot];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
_allIds[^1] = value;
|
||||
else
|
||||
_allIds[(int)slot] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(HumanSlot slot, PrimaryId id)
|
||||
=> All || this[slot] || Contains((slot, id));
|
||||
|
||||
public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value)
|
||||
{
|
||||
if (slot is HumanSlot.Unknown)
|
||||
{
|
||||
var old = All;
|
||||
All = value.Value;
|
||||
return old != value.Value;
|
||||
}
|
||||
|
||||
if (!id.HasValue)
|
||||
{
|
||||
var old = this[slot];
|
||||
this[slot] = value.Value;
|
||||
return old != value.Value;
|
||||
}
|
||||
|
||||
if (value.Value)
|
||||
return Add((slot, id.Value));
|
||||
|
||||
return Remove((slot, id.Value));
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
_allIds.SetAll(false);
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
=> !_allIds.HasAnySet() && Count is 0;
|
||||
}
|
||||
|
||||
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
|
||||
private readonly Dictionary<ShapeString, ShpHashSet> _wristConnectors = [];
|
||||
private readonly Dictionary<ShapeString, ShpHashSet> _waistConnectors = [];
|
||||
private readonly Dictionary<ShapeString, ShpHashSet> _ankleConnectors = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _wristConnectors = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _waistConnectors = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _ankleConnectors = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
|
|
@ -114,7 +51,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
|||
|
||||
return;
|
||||
|
||||
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
||||
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||
{
|
||||
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||
{
|
||||
|
|
@ -125,7 +62,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
|||
dict.Add(identifier.Shape, value);
|
||||
}
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, entry))
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value))
|
||||
++EnabledCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -142,12 +79,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection)
|
|||
|
||||
return;
|
||||
|
||||
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
||||
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||
{
|
||||
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False))
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false))
|
||||
{
|
||||
--EnabledCount;
|
||||
if (value.IsEmpty)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollect
|
|||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="ShapeManager.OnAttributeComputed"/>
|
||||
ShapeManager = 0,
|
||||
/// <seealso cref="ShapeAttributeManager.OnAttributeComputed"/>
|
||||
ShapeAttributeManager = 0,
|
||||
}
|
||||
|
||||
private readonly CollectionResolver _resolver;
|
||||
|
|
|
|||
145
Penumbra/Meta/Manipulations/AtrIdentifier.cs
Normal file
145
Penumbra/Meta/Manipulations/AtrIdentifier.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ public enum MetaManipulationType : byte
|
|||
GlobalEqp = 7,
|
||||
Atch = 8,
|
||||
Shp = 9,
|
||||
Atr = 10,
|
||||
}
|
||||
|
||||
public interface IMetaIdentifier
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
|
||||
var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition);
|
||||
return identifier.Validate() ? identifier : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
153
Penumbra/Meta/ShapeAttributeManager.cs
Normal file
153
Penumbra/Meta/ShapeAttributeManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
274
Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs
Normal file
274
Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.UI.Classes;
|
||||
|
||||
namespace Penumbra.UI.AdvancedWindow.Meta;
|
||||
|
||||
public sealed class AtrMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles)
|
||||
: MetaDrawer<AtrIdentifier, AtrEntry>(editor, metaFiles), IService
|
||||
{
|
||||
public override ReadOnlySpan<byte> Label
|
||||
=> "Attributes(ATR)###ATR"u8;
|
||||
|
||||
private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("atrx_"u8, out var s) ? s : ShapeAttributeString.Empty;
|
||||
private bool _identifierValid;
|
||||
|
||||
public override int NumColumns
|
||||
=> 7;
|
||||
|
||||
public override float ColumnHeight
|
||||
=> ImUtf8.FrameHeightSpacing;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
Identifier = new AtrIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, GenderRace.Unknown);
|
||||
Entry = AtrEntry.True;
|
||||
}
|
||||
|
||||
protected override void DrawNew()
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton("Copy all current ATR manipulations to clipboard."u8,
|
||||
new Lazy<JToken?>(() => MetaDictionary.SerializeTo([], Editor.Atr)));
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = !Editor.Contains(Identifier) && _identifierValid;
|
||||
var tt = canAdd
|
||||
? "Stage this edit."u8
|
||||
: _identifierValid
|
||||
? "This entry does not contain a valid attribute."u8
|
||||
: "This entry is already edited."u8;
|
||||
if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd))
|
||||
Editor.Changes |= Editor.TryAdd(Identifier, AtrEntry.False);
|
||||
|
||||
DrawIdentifierInput(ref Identifier);
|
||||
DrawEntry(ref Entry, true);
|
||||
}
|
||||
|
||||
protected override void DrawEntry(AtrIdentifier identifier, AtrEntry entry)
|
||||
{
|
||||
DrawMetaButtons(identifier, entry);
|
||||
DrawIdentifier(identifier);
|
||||
|
||||
if (DrawEntry(ref entry, false))
|
||||
Editor.Changes |= Editor.Update(identifier, entry);
|
||||
}
|
||||
|
||||
protected override IEnumerable<(AtrIdentifier, AtrEntry)> Enumerate()
|
||||
=> Editor.Atr
|
||||
.OrderBy(kvp => kvp.Key.Attribute)
|
||||
.ThenBy(kvp => kvp.Key.Slot)
|
||||
.ThenBy(kvp => kvp.Key.Id)
|
||||
.Select(kvp => (kvp.Key, kvp.Value));
|
||||
|
||||
protected override int Count
|
||||
=> Editor.Atr.Count;
|
||||
|
||||
private bool DrawIdentifierInput(ref AtrIdentifier identifier)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
var changes = DrawHumanSlot(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawGenderRaceConditionInput(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawPrimaryId(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawAttributeKeyInput(ref identifier, ref _buffer, ref _identifierValid);
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static void DrawIdentifier(AtrIdentifier identifier)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImUtf8.TextFramed(ShpMetaDrawer.SlotName(identifier.Slot), FrameColor);
|
||||
ImUtf8.HoverTooltip("Model Slot"u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (identifier.GenderRaceCondition is not GenderRace.Unknown)
|
||||
{
|
||||
ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor);
|
||||
ImUtf8.HoverTooltip("Gender & Race Code for this attribute to be set.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (identifier.Id.HasValue)
|
||||
ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor);
|
||||
else
|
||||
ImUtf8.TextFramed("All IDs"u8, FrameColor);
|
||||
ImUtf8.HoverTooltip("Primary ID"u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImUtf8.TextFramed(identifier.Attribute.AsSpan, FrameColor);
|
||||
}
|
||||
|
||||
private static bool DrawEntry(ref AtrEntry entry, bool disabled)
|
||||
{
|
||||
using var dis = ImRaii.Disabled(disabled);
|
||||
ImGui.TableNextColumn();
|
||||
var value = entry.Value;
|
||||
var changes = ImUtf8.Checkbox("##atrEntry"u8, ref value);
|
||||
if (changes)
|
||||
entry = new AtrEntry(value);
|
||||
ImUtf8.HoverTooltip("Whether to enable or disable this attribute for the selected items.");
|
||||
return changes;
|
||||
}
|
||||
|
||||
public static bool DrawPrimaryId(ref AtrIdentifier identifier, float unscaledWidth = 100)
|
||||
{
|
||||
var allSlots = identifier.Slot is HumanSlot.Unknown;
|
||||
var all = !identifier.Id.HasValue;
|
||||
var ret = false;
|
||||
using (ImRaii.Disabled(allSlots))
|
||||
{
|
||||
if (ImUtf8.Checkbox("##atrAll"u8, ref all))
|
||||
{
|
||||
identifier = identifier with { Id = all ? null : 0 };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(allSlots
|
||||
? "When using all slots, you also need to use all IDs."u8
|
||||
: "Enable this attribute for all model IDs."u8);
|
||||
|
||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
if (all)
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f));
|
||||
ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f),
|
||||
new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1;
|
||||
if (IdInput("##atrPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false))
|
||||
{
|
||||
identifier = identifier with { Id = setId };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool DrawHumanSlot(ref AtrIdentifier identifier, float unscaledWidth = 150)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
using (var combo = ImUtf8.Combo("##atrSlot"u8, ShpMetaDrawer.SlotName(identifier.Slot)))
|
||||
{
|
||||
if (combo)
|
||||
foreach (var slot in ShpMetaDrawer.AvailableSlots)
|
||||
{
|
||||
if (!ImUtf8.Selectable(ShpMetaDrawer.SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot)
|
||||
continue;
|
||||
|
||||
ret = true;
|
||||
if (slot is HumanSlot.Unknown)
|
||||
{
|
||||
identifier = identifier with
|
||||
{
|
||||
Id = null,
|
||||
Slot = slot,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
identifier = identifier with
|
||||
{
|
||||
Id = identifier.Id.HasValue
|
||||
? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0,
|
||||
slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1)
|
||||
: null,
|
||||
Slot = slot,
|
||||
};
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Model Slot"u8);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawGenderRaceConditionInput(ref AtrIdentifier identifier, float unscaledWidth = 250)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
|
||||
using (var combo = ImUtf8.Combo("##shpGenderRace"u8,
|
||||
identifier.GenderRaceCondition is GenderRace.Unknown
|
||||
? "Any Gender & Race"
|
||||
: $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})"))
|
||||
{
|
||||
if (combo)
|
||||
{
|
||||
if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown)
|
||||
&& identifier.GenderRaceCondition is not GenderRace.Unknown)
|
||||
{
|
||||
identifier = identifier with { GenderRaceCondition = GenderRace.Unknown };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1))
|
||||
{
|
||||
if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr)
|
||||
&& identifier.GenderRaceCondition != gr)
|
||||
{
|
||||
identifier = identifier with { GenderRaceCondition = gr };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(
|
||||
"Only activate this attribute for this gender & race code."u8);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static unsafe bool DrawAttributeKeyInput(ref AtrIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid,
|
||||
float unscaledWidth = 150)
|
||||
{
|
||||
var ret = false;
|
||||
var ptr = Unsafe.AsPointer(ref buffer);
|
||||
var span = new Span<byte>(ptr, ShapeAttributeString.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("##atrAttribute"u8, span, out int newLength, "Attribute..."u8))
|
||||
{
|
||||
buffer.ForceLength((byte)newLength);
|
||||
valid = buffer.ValidateCustomAttributeString();
|
||||
if (valid)
|
||||
identifier = identifier with { Attribute = buffer };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip("Supported attribute need to have the format `atrx_*` and a maximum length of 30 characters."u8);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,8 @@ public class MetaDrawers(
|
|||
ImcMetaDrawer imc,
|
||||
RspMetaDrawer rsp,
|
||||
AtchMetaDrawer atch,
|
||||
ShpMetaDrawer shp) : IService
|
||||
ShpMetaDrawer shp,
|
||||
AtrMetaDrawer atr) : IService
|
||||
{
|
||||
public readonly EqdpMetaDrawer Eqdp = eqdp;
|
||||
public readonly EqpMetaDrawer Eqp = eqp;
|
||||
|
|
@ -23,6 +24,7 @@ public class MetaDrawers(
|
|||
public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp;
|
||||
public readonly AtchMetaDrawer Atch = atch;
|
||||
public readonly ShpMetaDrawer Shp = shp;
|
||||
public readonly AtrMetaDrawer Atr = atr;
|
||||
|
||||
public IMetaDrawer? Get(MetaManipulationType type)
|
||||
=> type switch
|
||||
|
|
@ -35,6 +37,7 @@ public class MetaDrawers(
|
|||
MetaManipulationType.Rsp => Rsp,
|
||||
MetaManipulationType.Atch => Atch,
|
||||
MetaManipulationType.Shp => Shp,
|
||||
MetaManipulationType.Atr => Atr,
|
||||
MetaManipulationType.GlobalEqp => GlobalEqp,
|
||||
_ => null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
|
|
@ -20,18 +21,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
public override ReadOnlySpan<byte> Label
|
||||
=> "Shape Keys (SHP)###SHP"u8;
|
||||
|
||||
private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty;
|
||||
private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("shpx_"u8, out var s) ? s : ShapeAttributeString.Empty;
|
||||
private bool _identifierValid;
|
||||
|
||||
public override int NumColumns
|
||||
=> 7;
|
||||
=> 8;
|
||||
|
||||
public override float ColumnHeight
|
||||
=> ImUtf8.FrameHeightSpacing;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None);
|
||||
Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, ShapeConnectorCondition.None, GenderRace.Unknown);
|
||||
}
|
||||
|
||||
protected override void DrawNew()
|
||||
|
|
@ -79,6 +80,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
ImGui.TableNextColumn();
|
||||
var changes = DrawHumanSlot(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawGenderRaceConditionInput(ref identifier);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
changes |= DrawPrimaryId(ref identifier);
|
||||
|
||||
|
|
@ -97,6 +101,17 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor);
|
||||
ImUtf8.HoverTooltip("Model Slot"u8);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (identifier.GenderRaceCondition is not GenderRace.Unknown)
|
||||
{
|
||||
ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor);
|
||||
ImUtf8.HoverTooltip("Gender & Race Code for this shape key to be set.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (identifier.Id.HasValue)
|
||||
ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor);
|
||||
|
|
@ -165,7 +180,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
return ret;
|
||||
}
|
||||
|
||||
public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150)
|
||||
public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 170)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
|
|
@ -212,18 +227,19 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
return ret;
|
||||
}
|
||||
|
||||
public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150)
|
||||
public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid,
|
||||
float unscaledWidth = 200)
|
||||
{
|
||||
var ret = false;
|
||||
var ptr = Unsafe.AsPointer(ref buffer);
|
||||
var span = new Span<byte>(ptr, ShapeString.MaxLength + 1);
|
||||
var span = new Span<byte>(ptr, ShapeAttributeString.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("##shpShape"u8, span, out int newLength, "Shape Key..."u8))
|
||||
{
|
||||
buffer.ForceLength((byte)newLength);
|
||||
valid = ShpIdentifier.ValidateCustomShapeString(buffer);
|
||||
valid = buffer.ValidateCustomShapeString();
|
||||
if (valid)
|
||||
identifier = identifier with { Shape = buffer };
|
||||
ret = true;
|
||||
|
|
@ -234,7 +250,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
return ret;
|
||||
}
|
||||
|
||||
public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150)
|
||||
private static bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 80)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
|
|
@ -271,7 +287,43 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
return ret;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<HumanSlot> AvailableSlots
|
||||
private static bool DrawGenderRaceConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 250)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale);
|
||||
|
||||
using (var combo = ImUtf8.Combo("##shpGenderRace"u8, identifier.GenderRaceCondition is GenderRace.Unknown
|
||||
? "Any Gender & Race"
|
||||
: $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})"))
|
||||
{
|
||||
if (combo)
|
||||
{
|
||||
if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown)
|
||||
&& identifier.GenderRaceCondition is not GenderRace.Unknown)
|
||||
{
|
||||
identifier = identifier with { GenderRaceCondition = GenderRace.Unknown };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1))
|
||||
{
|
||||
if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr)
|
||||
&& identifier.GenderRaceCondition != gr)
|
||||
{
|
||||
identifier = identifier with { GenderRaceCondition = gr };
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImUtf8.HoverTooltip(
|
||||
"Only activate this shape key for this gender & race code."u8);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static ReadOnlySpan<HumanSlot> AvailableSlots
|
||||
=>
|
||||
[
|
||||
HumanSlot.Unknown,
|
||||
|
|
@ -291,7 +343,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile
|
|||
HumanSlot.Ear,
|
||||
];
|
||||
|
||||
private static ReadOnlySpan<byte> SlotName(HumanSlot slot)
|
||||
public static ReadOnlySpan<byte> SlotName(HumanSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
HumanSlot.Unknown => "All Slots"u8,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ public partial class ModEditWindow
|
|||
DrawEditHeader(MetaManipulationType.Rsp);
|
||||
DrawEditHeader(MetaManipulationType.Atch);
|
||||
DrawEditHeader(MetaManipulationType.Shp);
|
||||
DrawEditHeader(MetaManipulationType.Atr);
|
||||
DrawEditHeader(MetaManipulationType.GlobalEqp);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,27 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
|
||||
DrawCollectionShapeCache(actor);
|
||||
DrawCharacterShapes(human);
|
||||
DrawCollectionAttributeCache(actor);
|
||||
DrawCharacterAttributes(human);
|
||||
}
|
||||
|
||||
private unsafe void DrawCollectionAttributeCache(Actor actor)
|
||||
{
|
||||
var data = resolver.IdentifyCollection(actor.AsObject, true);
|
||||
using var treeNode1 = ImUtf8.TreeNode($"Collection Attribute Cache ({data.ModCollection})");
|
||||
if (!treeNode1.Success || !data.ModCollection.HasCache)
|
||||
return;
|
||||
|
||||
using var table = ImUtf8.Table("##aCache"u8, 2, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data)
|
||||
DrawShapeAttribute(attribute, set);
|
||||
}
|
||||
|
||||
private unsafe void DrawCollectionShapeCache(Actor actor)
|
||||
|
|
@ -44,7 +65,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
if (!treeNode1.Success || !data.ModCollection.HasCache)
|
||||
return;
|
||||
|
||||
using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg);
|
||||
using var table = ImUtf8.Table("##sCache"u8, 3, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
|
|
@ -58,14 +79,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition))
|
||||
{
|
||||
ImUtf8.DrawTableColumn(condition.ToString());
|
||||
DrawShape(shape, set);
|
||||
DrawShapeAttribute(shape, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set)
|
||||
private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set)
|
||||
{
|
||||
ImUtf8.DrawTableColumn(shape.AsSpan);
|
||||
ImUtf8.DrawTableColumn(shapeAttribute.AsSpan);
|
||||
if (set.All)
|
||||
{
|
||||
ImUtf8.DrawTableColumn("All"u8);
|
||||
|
|
@ -73,7 +94,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
else
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var slot in ShapeManager.UsedModels)
|
||||
foreach (var slot in ShapeAttributeManager.UsedModels)
|
||||
{
|
||||
if (!set[slot])
|
||||
continue;
|
||||
|
|
@ -82,11 +103,53 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
|
||||
foreach (var item in set.Where(i => !set[i.Slot]))
|
||||
foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1))
|
||||
{
|
||||
ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, ");
|
||||
if (set[gr])
|
||||
{
|
||||
ImUtf8.Text($"All {gr.ToName()}, ");
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var slot in ShapeAttributeManager.UsedModels)
|
||||
{
|
||||
if (!set[slot, gr])
|
||||
continue;
|
||||
|
||||
ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, ");
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var ((slot, id), flags) in set)
|
||||
{
|
||||
if ((flags & 1ul) is not 0)
|
||||
{
|
||||
if (set[slot])
|
||||
continue;
|
||||
|
||||
ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, ");
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentFlags = flags >> 1;
|
||||
var currentIndex = BitOperations.TrailingZeroCount(currentFlags);
|
||||
while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count)
|
||||
{
|
||||
var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex];
|
||||
if (set[slot, gr])
|
||||
continue;
|
||||
|
||||
ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, ");
|
||||
currentFlags >>= currentIndex;
|
||||
currentIndex = BitOperations.TrailingZeroCount(currentFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
if (!treeNode2)
|
||||
return;
|
||||
|
||||
using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg);
|
||||
using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
|
|
@ -140,4 +203,55 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawCharacterAttributes(Model human)
|
||||
{
|
||||
using var treeNode2 = ImUtf8.TreeNode("Character Model Attributes"u8);
|
||||
if (!treeNode2)
|
||||
return;
|
||||
|
||||
using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale);
|
||||
ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14);
|
||||
ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8);
|
||||
ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||
for (var i = 0; i < human.AsHuman->SlotCount; ++i)
|
||||
{
|
||||
ImUtf8.DrawTableColumn($"{(uint)i:D2}");
|
||||
ImUtf8.DrawTableColumn(((HumanSlot)i).ToName());
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var model = human.AsHuman->Models[i];
|
||||
Penumbra.Dynamis.DrawPointer((nint)model);
|
||||
if (model is not null)
|
||||
{
|
||||
var mask = model->EnabledAttributeIndexMask;
|
||||
ImUtf8.DrawTableColumn($"{mask:X8}");
|
||||
ImGui.TableNextColumn();
|
||||
foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes)
|
||||
{
|
||||
var disabled = (mask & (1u << idx)) is 0;
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled);
|
||||
ImUtf8.Text(attribute.AsSpan());
|
||||
ImGui.SameLine(0, 0);
|
||||
ImUtf8.Text(", "u8);
|
||||
if (idx % 8 < 7)
|
||||
ImGui.SameLine(0, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ]
|
||||
"enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp", "Atr" ]
|
||||
},
|
||||
"Manipulation": {
|
||||
"type": "object"
|
||||
|
|
@ -100,6 +100,16 @@
|
|||
"$ref": "meta_shp.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"Type": {
|
||||
"const": "Atr"
|
||||
},
|
||||
"Manipulation": {
|
||||
"$ref": "meta_atr.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
27
schemas/structs/meta_atr.json
Normal file
27
schemas/structs/meta_atr.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Entry": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Slot": {
|
||||
"$ref": "meta_enums.json#HumanSlot"
|
||||
},
|
||||
"Id": {
|
||||
"$ref": "meta_enums.json#U16"
|
||||
},
|
||||
"Attribute": {
|
||||
"type": "string",
|
||||
"minLength": 5,
|
||||
"maxLength": 30,
|
||||
"pattern": "^atrx_"
|
||||
},
|
||||
"GenderRaceCondition": {
|
||||
"enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Attribute"
|
||||
]
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@
|
|||
},
|
||||
"ConnectorCondition": {
|
||||
"$ref": "meta_enums.json#ShapeConnectorCondition"
|
||||
},
|
||||
"GenderRaceCondition": {
|
||||
"enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue