Code commit
This commit is contained in:
355
CustomizePlus/Armatures/Data/Armature.cs
Normal file
355
CustomizePlus/Armatures/Data/Armature.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.GameData.Actors;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Armatures.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "copy" of the ingame skeleton upon which the linked character profile is meant to operate.
|
||||
/// Acts as an interface by which the in-game skeleton can be manipulated on a bone-by-bone basis.
|
||||
/// </summary>
|
||||
public unsafe class Armature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Customize+ profile for which this mockup applies transformations.
|
||||
/// </summary>
|
||||
public Profile Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Static identifier of the actor associated with this armature
|
||||
/// </summary>
|
||||
public ActorIdentifier ActorIdentifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this armature has any renderable objects on which it should act.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this armature has successfully built itself with bone information.
|
||||
/// </summary>
|
||||
public bool IsBuilt => _partialSkeletons.Any();
|
||||
|
||||
/// <summary>
|
||||
/// Internal flag telling ArmatureManager that it should attempt to rebind profile to (another) profile whenever possible.
|
||||
/// </summary>
|
||||
public bool IsPendingProfileRebind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents date and time until which any kind of removal protections will not be applying to this armature.
|
||||
/// Implemented mostly as a armature cleanup protection hack due to how mare works when downloading files for the first time
|
||||
/// </summary>
|
||||
public DateTime ProtectedUntil { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For debugging purposes, each armature is assigned a globally-unique ID number upon creation.
|
||||
/// </summary>
|
||||
private static uint _nextGlobalId;
|
||||
private readonly uint _localId;
|
||||
|
||||
/// <summary>
|
||||
/// Binding telling which bones are bound to each template for this armature. Built from template list in profile.
|
||||
/// </summary>
|
||||
public Dictionary<string, Template> BoneTemplateBinding { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Each skeleton is made up of several smaller "partial" skeletons.
|
||||
/// Each partial skeleton has its own list of bones, with a root bone at index zero.
|
||||
/// The root bone of a partial skeleton may also be a regular bone in a different partial skeleton.
|
||||
/// </summary>
|
||||
private ModelBone[][] _partialSkeletons;
|
||||
|
||||
#region Bone Accessors -------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of partial skeletons contained in this armature.
|
||||
/// </summary>
|
||||
public int PartialSkeletonCount => _partialSkeletons.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of bones belonging to the partial skeleton at the given index.
|
||||
/// </summary>
|
||||
public ModelBone[] this[int i]
|
||||
{
|
||||
get => _partialSkeletons[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of bones contained within the partial skeleton with the given index.
|
||||
/// </summary>
|
||||
public int GetBoneCountOfPartial(int partialIndex) => _partialSkeletons[partialIndex].Length;
|
||||
|
||||
/// <summary>
|
||||
/// Get the bone at index 'j' within the partial skeleton at index 'i'.
|
||||
/// </summary>
|
||||
public ModelBone this[int i, int j]
|
||||
{
|
||||
get => _partialSkeletons[i][j];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the bone at the given indices, if it exists
|
||||
/// </summary>
|
||||
public ModelBone? GetBoneAt(int partialIndex, int boneIndex)
|
||||
{
|
||||
if (_partialSkeletons.Length > partialIndex
|
||||
&& _partialSkeletons[partialIndex].Length > boneIndex)
|
||||
{
|
||||
return this[partialIndex, boneIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the root bone of the partial skeleton with the given index.
|
||||
/// </summary>
|
||||
public ModelBone GetRootBoneOfPartial(int partialIndex) => this[partialIndex, 0];
|
||||
|
||||
public ModelBone MainRootBone => GetRootBoneOfPartial(0);
|
||||
|
||||
/// <summary>
|
||||
/// Get the total number of bones in each partial skeleton combined.
|
||||
/// </summary>
|
||||
// In exactly one partial skeleton will the root bone be an independent bone. In all others, it's a reference to a separate, real bone.
|
||||
// For that reason we must subtract the number of duplicate bones
|
||||
public int TotalBoneCount => _partialSkeletons.Sum(x => x.Length);
|
||||
|
||||
public IEnumerable<ModelBone> GetAllBones()
|
||||
{
|
||||
for (var i = 0; i < _partialSkeletons.Length; ++i)
|
||||
{
|
||||
for (var j = 0; j < _partialSkeletons[i].Length; ++j)
|
||||
{
|
||||
yield return this[i, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
#endregion
|
||||
|
||||
public Armature(ActorIdentifier actorIdentifier, Profile profile)
|
||||
{
|
||||
_localId = _nextGlobalId++;
|
||||
|
||||
_partialSkeletons = Array.Empty<ModelBone[]>();
|
||||
|
||||
BoneTemplateBinding = new Dictionary<string, Template>();
|
||||
|
||||
ActorIdentifier = actorIdentifier;
|
||||
Profile = profile;
|
||||
IsVisible = false;
|
||||
|
||||
ProtectFromRemoval();
|
||||
|
||||
Profile.Armatures.Add(this);
|
||||
|
||||
Plugin.Logger.Debug($"Instantiated {this}, attached to {Profile}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return IsBuilt
|
||||
? $"Armature (#{_localId}) on {ActorIdentifier.IncognitoDebug()} ({Profile}) with {TotalBoneCount} bone/s"
|
||||
: $"Armature (#{_localId}) on {ActorIdentifier.IncognitoDebug()} ({Profile}) with no skeleton reference";
|
||||
}
|
||||
|
||||
public bool NewBonesAvailable(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (cBase->Skeleton->PartialSkeletonCount > _partialSkeletons.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < cBase->Skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
var newPose = cBase->Skeleton->PartialSkeletons[i].GetHavokPose(Constants.TruePoseIndex);
|
||||
if (newPose != null
|
||||
&& newPose->Skeleton->Bones.Length > _partialSkeletons[i].Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild the armature using the provided character base as a reference.
|
||||
/// </summary>
|
||||
public void RebuildSkeleton(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
return;
|
||||
|
||||
var newPartials = ParseBonesFromObject(this, cBase);
|
||||
|
||||
_partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray();
|
||||
|
||||
RebuildBoneTemplateBinding();
|
||||
|
||||
Plugin.Logger.Debug($"Rebuilt {this}");
|
||||
}
|
||||
|
||||
public void AugmentSkeleton(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
return;
|
||||
|
||||
var oldPartials = _partialSkeletons.Select(x => x.ToList()).ToList();
|
||||
var newPartials = ParseBonesFromObject(this, cBase);
|
||||
|
||||
//for each of the new partial skeletons discovered...
|
||||
for (var i = 0; i < newPartials.Count; ++i)
|
||||
{
|
||||
//if the old skeleton doesn't contain the new partial at all, add the whole thing
|
||||
if (i > oldPartials.Count)
|
||||
{
|
||||
oldPartials.Add(newPartials[i]);
|
||||
}
|
||||
//otherwise, add every model bone the new partial has that the old one doesn't
|
||||
else
|
||||
{
|
||||
//Case: get carbuncle, enable profile for it, turn carbuncle into human via glamourer
|
||||
if (oldPartials.Count <= i)
|
||||
oldPartials.Add(new List<ModelBone>());
|
||||
|
||||
for (var j = oldPartials[i].Count; j < newPartials[i].Count; ++j)
|
||||
{
|
||||
oldPartials[i].Add(newPartials[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_partialSkeletons = oldPartials.Select(x => x.ToArray()).ToArray();
|
||||
|
||||
RebuildBoneTemplateBinding();
|
||||
|
||||
Plugin.Logger.Debug($"Augmented {this} with new bones");
|
||||
}
|
||||
|
||||
public BoneTransform? GetAppliedBoneTransform(string boneName)
|
||||
{
|
||||
if (BoneTemplateBinding.TryGetValue(boneName, out var template)
|
||||
&& template != null)
|
||||
{
|
||||
if (template.Bones.TryGetValue(boneName, out var boneTransform))
|
||||
return boneTransform;
|
||||
else
|
||||
Plugin.Logger.Error($"Bone {boneName} is null in template {template.UniqueId}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply removal protection for 30 seconds starting from current time. For the most part this is a hack for mare.
|
||||
/// </summary>
|
||||
public void ProtectFromRemoval()
|
||||
{
|
||||
ProtectedUntil = DateTime.UtcNow.AddSeconds(30);
|
||||
}
|
||||
|
||||
private static unsafe List<List<ModelBone>> ParseBonesFromObject(Armature arm, CharacterBase* cBase)
|
||||
{
|
||||
List<List<ModelBone>> newPartials = new();
|
||||
|
||||
try
|
||||
{
|
||||
//build the skeleton
|
||||
for (var pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex)
|
||||
{
|
||||
var currentPartial = cBase->Skeleton->PartialSkeletons[pSkeleIndex];
|
||||
var currentPose = currentPartial.GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
newPartials.Add(new());
|
||||
|
||||
if (currentPose == null)
|
||||
continue;
|
||||
|
||||
for (var boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex)
|
||||
{
|
||||
if (currentPose->Skeleton->Bones[boneIndex].Name.String is string boneName &&
|
||||
boneName != null)
|
||||
{
|
||||
//time to build a new bone
|
||||
ModelBone newBone = new(arm, boneName, pSkeleIndex, boneIndex);
|
||||
Plugin.Logger.Debug($"Created new bone: {boneName} on {pSkeleIndex}->{boneIndex} arm: {arm._localId}");
|
||||
|
||||
if (currentPose->Skeleton->ParentIndices[boneIndex] is short parentIndex
|
||||
&& parentIndex >= 0)
|
||||
{
|
||||
newBone.AddParent(pSkeleIndex, parentIndex);
|
||||
newPartials[pSkeleIndex][parentIndex].AddChild(pSkeleIndex, boneIndex);
|
||||
}
|
||||
|
||||
foreach (var mb in newPartials.SelectMany(x => x))
|
||||
{
|
||||
if (AreTwinnedNames(boneName, mb.BoneName))
|
||||
{
|
||||
newBone.AddTwin(mb.PartialSkeletonIndex, mb.BoneIndex);
|
||||
mb.AddTwin(pSkeleIndex, boneIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//linking is performed later
|
||||
|
||||
newPartials.Last().Add(newBone);
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.Logger.Error($"Failed to process bone @ <{pSkeleIndex}, {boneIndex}> while parsing bones from {cBase->ToString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BoneData.LogNewBones(newPartials.SelectMany(x => x.Select(y => y.BoneName)).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Error parsing armature skeleton from {cBase->ToString()}:\n\t{ex}");
|
||||
}
|
||||
|
||||
return newPartials;
|
||||
}
|
||||
|
||||
public void RebuildBoneTemplateBinding()
|
||||
{
|
||||
BoneTemplateBinding.Clear();
|
||||
|
||||
foreach (var template in Profile.Templates)
|
||||
{
|
||||
foreach (var kvPair in template.Bones)
|
||||
{
|
||||
BoneTemplateBinding[kvPair.Key] = template;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bone in GetAllBones())
|
||||
bone.LinkToTemplate(BoneTemplateBinding.ContainsKey(bone.BoneName) ? BoneTemplateBinding[bone.BoneName] : null);
|
||||
|
||||
Plugin.Logger.Debug($"Rebuilt template binding for armature {_localId}");
|
||||
}
|
||||
|
||||
private static bool AreTwinnedNames(string name1, string name2)
|
||||
{
|
||||
return name1[^1] == 'r' ^ name2[^1] == 'r'
|
||||
&& name1[^1] == 'l' ^ name2[^1] == 'l'
|
||||
&& name1[0..^1] == name2[0..^1];
|
||||
}
|
||||
}
|
||||
301
CustomizePlus/Armatures/Data/ModelBone.cs
Normal file
301
CustomizePlus/Armatures/Data/ModelBone.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Armatures.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single bone of an ingame character's skeleton.
|
||||
/// </summary>
|
||||
public unsafe class ModelBone
|
||||
{
|
||||
public enum PoseType
|
||||
{
|
||||
Local, Model, BindPose, World
|
||||
}
|
||||
|
||||
public readonly Armature MasterArmature;
|
||||
|
||||
public readonly int PartialSkeletonIndex;
|
||||
public readonly int BoneIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bone corresponding to this model bone's parent, if it exists.
|
||||
/// (It should in all cases but the root of the skeleton)
|
||||
/// </summary>
|
||||
public ModelBone? ParentBone => _parentPartialIndex >= 0 && _parentBoneIndex >= 0
|
||||
? MasterArmature[_parentPartialIndex, _parentBoneIndex]
|
||||
: null;
|
||||
private int _parentPartialIndex = -1;
|
||||
private int _parentBoneIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets each model bone for which this model bone corresponds to a direct parent thereof.
|
||||
/// A model bone may have zero children.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> ChildBones => _childPartialIndices.Zip(_childBoneIndices, (x, y) => MasterArmature[x, y]);
|
||||
private List<int> _childPartialIndices = new();
|
||||
private List<int> _childBoneIndices = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bone that forms a mirror image of this model bone, if one exists.
|
||||
/// </summary>
|
||||
public ModelBone? TwinBone => _twinPartialIndex >= 0 && _twinBoneIndex >= 0
|
||||
? MasterArmature[_twinPartialIndex, _twinBoneIndex]
|
||||
: null;
|
||||
private int _twinPartialIndex = -1;
|
||||
private int _twinBoneIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the bone within the in-game skeleton. Referred to in some places as its "code name".
|
||||
/// </summary>
|
||||
public string BoneName;
|
||||
|
||||
/// <summary>
|
||||
/// The transform that this model bone will impart upon its in-game sibling when the master armature
|
||||
/// is applied to the in-game skeleton. Reference to transform contained in top most template in profile applied to character.
|
||||
/// </summary>
|
||||
public BoneTransform? CustomizedTransform { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if bone is linked to any template
|
||||
/// </summary>
|
||||
public bool IsActive => CustomizedTransform != null;
|
||||
|
||||
public ModelBone(Armature arm, string codeName, int partialIdx, int boneIdx)
|
||||
{
|
||||
MasterArmature = arm;
|
||||
PartialSkeletonIndex = partialIdx;
|
||||
BoneIndex = boneIdx;
|
||||
|
||||
BoneName = codeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link bone to specific template, unlinks if null is passed
|
||||
/// </summary>
|
||||
/// <param name="template"></param>
|
||||
/// <returns></returns>
|
||||
public bool LinkToTemplate(Template? template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
if (CustomizedTransform == null)
|
||||
return false;
|
||||
|
||||
CustomizedTransform = null;
|
||||
|
||||
Plugin.Logger.Information($"Unlinked {BoneName} from all templates");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!template.Bones.ContainsKey(BoneName))
|
||||
return false;
|
||||
|
||||
Plugin.Logger.Information($"Linking {BoneName} to {template.Name}");
|
||||
CustomizedTransform = template.Bones[BoneName];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate a bone to act as this model bone's "parent".
|
||||
/// </summary>
|
||||
public void AddParent(int parentPartialIdx, int parentBoneIdx)
|
||||
{
|
||||
if (_parentPartialIndex != -1 || _parentBoneIndex != -1)
|
||||
{
|
||||
throw new Exception($"Tried to add redundant parent to model bone -- {this}");
|
||||
}
|
||||
|
||||
_parentPartialIndex = parentPartialIdx;
|
||||
_parentBoneIndex = parentBoneIdx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that a bone is one of this model bone's "children".
|
||||
/// </summary>
|
||||
public void AddChild(int childPartialIdx, int childBoneIdx)
|
||||
{
|
||||
_childPartialIndices.Add(childPartialIdx);
|
||||
_childBoneIndices.Add(childBoneIdx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate a bone that acts as this model bone's mirror image, or "twin".
|
||||
/// </summary>
|
||||
public void AddTwin(int twinPartialIdx, int twinBoneIdx)
|
||||
{
|
||||
_twinPartialIndex = twinPartialIdx;
|
||||
_twinBoneIndex = twinBoneIdx;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
//string numCopies = _copyIndices.Count > 0 ? $" ({_copyIndices.Count} copies)" : string.Empty;
|
||||
return $"{BoneName} ({BoneData.GetBoneDisplayName(BoneName)}) @ <{PartialSkeletonIndex}, {BoneIndex}>";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the lineage of this model bone, going back to the skeleton's root bone.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> GetAncestors(bool includeSelf = true) => includeSelf
|
||||
? GetAncestors(new List<ModelBone>() { this })
|
||||
: GetAncestors(new List<ModelBone>());
|
||||
|
||||
private IEnumerable<ModelBone> GetAncestors(List<ModelBone> tail)
|
||||
{
|
||||
tail.Add(this);
|
||||
if (ParentBone is ModelBone mb && mb != null)
|
||||
{
|
||||
return mb.GetAncestors(tail);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tail;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all model bones with a lineage that contains this one.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> GetDescendants(bool includeSelf = false) => includeSelf
|
||||
? GetDescendants(this)
|
||||
: GetDescendants(null);
|
||||
|
||||
private IEnumerable<ModelBone> GetDescendants(ModelBone? first)
|
||||
{
|
||||
var output = first != null
|
||||
? new List<ModelBone>() { first }
|
||||
: new List<ModelBone>();
|
||||
|
||||
output.AddRange(ChildBones);
|
||||
|
||||
using (var iter = output.GetEnumerator())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
output.AddRange(iter.Current.ChildBones);
|
||||
yield return iter.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a character base to which this model bone's master armature (presumably) applies,
|
||||
/// return the game's transform value for this model's in-game sibling within the given reference frame.
|
||||
/// </summary>
|
||||
public hkQsTransformf GetGameTransform(CharacterBase* cBase, PoseType refFrame)
|
||||
{
|
||||
|
||||
var skelly = cBase->Skeleton;
|
||||
var pSkelly = skelly->PartialSkeletons[PartialSkeletonIndex];
|
||||
var targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex);
|
||||
//hkaPose* targetPose = cBase->Skeleton->PartialSkeletons[PartialSkeletonIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (targetPose == null) return Constants.NullTransform;
|
||||
|
||||
return refFrame switch
|
||||
{
|
||||
PoseType.Local => targetPose->LocalPose[BoneIndex],
|
||||
PoseType.Model => targetPose->ModelPose[BoneIndex],
|
||||
_ => Constants.NullTransform
|
||||
//TODO properly implement the other options
|
||||
};
|
||||
}
|
||||
|
||||
private void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, PoseType refFrame)
|
||||
{
|
||||
SetGameTransform(cBase, transform, PartialSkeletonIndex, BoneIndex, refFrame);
|
||||
}
|
||||
|
||||
private static void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, int partialIndex, int boneIndex, PoseType refFrame)
|
||||
{
|
||||
var skelly = cBase->Skeleton;
|
||||
var pSkelly = skelly->PartialSkeletons[partialIndex];
|
||||
var targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex);
|
||||
//hkaPose* targetPose = cBase->Skeleton->PartialSkeletons[PartialSkeletonIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (targetPose == null || targetPose->ModelInSync == 0) return;
|
||||
|
||||
switch (refFrame)
|
||||
{
|
||||
case PoseType.Local:
|
||||
targetPose->LocalPose.Data[boneIndex] = transform;
|
||||
return;
|
||||
|
||||
case PoseType.Model:
|
||||
targetPose->ModelPose.Data[boneIndex] = transform;
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
|
||||
//TODO properly implement the other options
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this model bone's associated transformation to its in-game sibling within
|
||||
/// the skeleton of the given character base.
|
||||
/// </summary>
|
||||
public void ApplyModelTransform(CharacterBase* cBase)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (cBase != null
|
||||
&& CustomizedTransform.IsEdited()
|
||||
&& GetGameTransform(cBase, PoseType.Model) is hkQsTransformf gameTransform
|
||||
&& !gameTransform.Equals(Constants.NullTransform)
|
||||
&& CustomizedTransform.ModifyExistingTransform(gameTransform) is hkQsTransformf modTransform
|
||||
&& !modTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
SetGameTransform(cBase, modTransform, PoseType.Model);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyModelScale(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingScale);
|
||||
public void ApplyModelRotation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingRotation);
|
||||
public void ApplyModelFullTranslation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingTranslationWithRotation);
|
||||
public void ApplyStraightModelTranslation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingTranslation);
|
||||
|
||||
private void ApplyTransFunc(CharacterBase* cBase, Func<hkQsTransformf, hkQsTransformf> modTrans)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (cBase != null
|
||||
&& CustomizedTransform.IsEdited()
|
||||
&& GetGameTransform(cBase, PoseType.Model) is hkQsTransformf gameTransform
|
||||
&& !gameTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
var modTransform = modTrans(gameTransform);
|
||||
|
||||
if (!modTransform.Equals(gameTransform) && !modTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
SetGameTransform(cBase, modTransform, PoseType.Model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks for a non-zero and non-identity (root) scale.
|
||||
/// </summary>
|
||||
/// <param name="mb">The bone to check</param>
|
||||
/// <returns>If the scale should be applied.</returns>
|
||||
public bool IsModifiedScale()
|
||||
{
|
||||
if (!IsActive)
|
||||
return false;
|
||||
return CustomizedTransform.Scaling.X != 0 && CustomizedTransform.Scaling.X != 1 ||
|
||||
CustomizedTransform.Scaling.Y != 0 && CustomizedTransform.Scaling.Y != 1 ||
|
||||
CustomizedTransform.Scaling.Z != 0 && CustomizedTransform.Scaling.Z != 1;
|
||||
}
|
||||
}
|
||||
30
CustomizePlus/Armatures/Events/ArmatureChanged.cs
Normal file
30
CustomizePlus/Armatures/Events/ArmatureChanged.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Armatures.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when armature is changed
|
||||
/// </summary>
|
||||
public sealed class ArmatureChanged() : EventWrapper<ArmatureChanged.Type, Armature?, object?, ArmatureChanged.Priority>(nameof(ArmatureChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
//Created,
|
||||
Deleted
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
ProfileManager
|
||||
}
|
||||
|
||||
public enum DeletionReason
|
||||
{
|
||||
Gone,
|
||||
NoActiveProfiles,
|
||||
ProfileManagerEvent,
|
||||
TemplateEditorEvent
|
||||
}
|
||||
}
|
||||
535
CustomizePlus/Armatures/Services/ArmatureManager.cs
Normal file
535
CustomizePlus/Armatures/Services/ArmatureManager.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.GameData.Data;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Armatures.Services;
|
||||
|
||||
public unsafe sealed class ArmatureManager : IDisposable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ProfileChanged _profileChangedEvent;
|
||||
private readonly Logger _logger;
|
||||
private readonly FrameworkManager _framework;
|
||||
private readonly ObjectManager _objectManager;
|
||||
private readonly ActorService _actorService;
|
||||
private readonly ArmatureChanged _event;
|
||||
|
||||
public Dictionary<ActorIdentifier, Armature> Armatures { get; private set; } = new();
|
||||
|
||||
public ArmatureManager(
|
||||
ProfileManager profileManager,
|
||||
IObjectTable objectTable,
|
||||
GameObjectService gameObjectService,
|
||||
TemplateChanged templateChangedEvent,
|
||||
ProfileChanged profileChangedEvent,
|
||||
Logger logger,
|
||||
FrameworkManager framework,
|
||||
ObjectManager objectManager,
|
||||
ActorService actorService,
|
||||
ArmatureChanged @event)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_objectTable = objectTable;
|
||||
_gameObjectService = gameObjectService;
|
||||
_templateChangedEvent = templateChangedEvent;
|
||||
_profileChangedEvent = profileChangedEvent;
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_objectManager = objectManager;
|
||||
_actorService = actorService;
|
||||
_event = @event;
|
||||
|
||||
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ArmatureManager);
|
||||
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.ArmatureManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChangedEvent.Unsubscribe(OnTemplateChange);
|
||||
_profileChangedEvent.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main rendering function, called from rendering hook
|
||||
/// </summary>
|
||||
public void OnRender()
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshArmatures();
|
||||
ApplyArmatureTransforms();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Exception while rendering armatures:\n\t{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function called when game object movement is detected
|
||||
/// </summary>
|
||||
public void OnGameObjectMove(Actor actor)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
if (Armatures.TryGetValue(identifier, out var armature) && armature.IsBuilt && armature.IsVisible)
|
||||
ApplyRootTranslation(armature, actor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes armatures which no longer have actor associated with them and creates armatures for new actors
|
||||
/// </summary>
|
||||
private void RefreshArmatures()
|
||||
{
|
||||
_objectManager.Update();
|
||||
|
||||
var currentTime = DateTime.UtcNow;
|
||||
foreach (var kvPair in Armatures.ToList())
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (!_objectManager.ContainsKey(kvPair.Value.ActorIdentifier) &&
|
||||
currentTime > armature.ProtectedUntil) //Only remove armatures which are no longer protected
|
||||
{
|
||||
_logger.Debug($"Removing armature {armature} because {kvPair.Key.IncognitoDebug()} is gone");
|
||||
RemoveArmature(armature, ArmatureChanged.DeletionReason.Gone);
|
||||
}
|
||||
}
|
||||
|
||||
Profile? GetProfileForActor(ActorIdentifier identifier)
|
||||
{
|
||||
foreach (var profile in _profileManager.GetEnabledProfilesByActor(identifier))
|
||||
{
|
||||
if (profile.LimitLookupToOwnedObjects &&
|
||||
identifier.Type == IdentifierType.Owned &&
|
||||
identifier.PlayerName != _objectManager.PlayerData.Identifier.PlayerName)
|
||||
continue;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectManager)
|
||||
{
|
||||
if (!Armatures.ContainsKey(obj.Key))
|
||||
{
|
||||
var activeProfile = GetProfileForActor(obj.Key);
|
||||
if (activeProfile == null)
|
||||
continue;
|
||||
|
||||
var newArm = new Armature(obj.Key, activeProfile);
|
||||
TryLinkSkeleton(newArm);
|
||||
Armatures.Add(obj.Key, newArm);
|
||||
_logger.Debug($"Added '{newArm}' for {obj.Key.IncognitoDebug()} to cache");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var armature = Armatures[obj.Key];
|
||||
|
||||
if (armature.IsPendingProfileRebind)
|
||||
{
|
||||
_logger.Debug($"Armature {armature} is pending profile rebind, rebinding...");
|
||||
armature.IsPendingProfileRebind = false;
|
||||
|
||||
var activeProfile = GetProfileForActor(obj.Key);
|
||||
if (activeProfile == armature.Profile)
|
||||
continue;
|
||||
|
||||
if (activeProfile == null)
|
||||
{
|
||||
_logger.Debug($"Removing armature {armature} because it doesn't have any active profiles");
|
||||
RemoveArmature(armature, ArmatureChanged.DeletionReason.NoActiveProfiles);
|
||||
continue;
|
||||
}
|
||||
|
||||
armature.Profile.Armatures.Remove(armature);
|
||||
armature.Profile = activeProfile;
|
||||
activeProfile.Armatures.Add(armature);
|
||||
armature.RebuildBoneTemplateBinding();
|
||||
|
||||
}
|
||||
|
||||
armature.IsVisible = armature.Profile.Enabled && TryLinkSkeleton(armature); //todo: remove armatures which are not visible?
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ApplyArmatureTransforms()
|
||||
{
|
||||
foreach (var kvPair in Armatures)
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (armature.IsBuilt && armature.IsVisible && _objectManager.ContainsKey(armature.ActorIdentifier))
|
||||
{
|
||||
foreach (var actor in _objectManager[armature.ActorIdentifier].Objects)
|
||||
ApplyPiecewiseTransformation(armature, actor, armature.ActorIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not a link can be established between the armature and an in-game object.
|
||||
/// If unbuilt, the armature will be rebuilded.
|
||||
/// </summary>
|
||||
private bool TryLinkSkeleton(Armature armature, bool forceRebuild = false)
|
||||
{
|
||||
_objectManager.Update();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_objectManager.ContainsKey(armature.ActorIdentifier))
|
||||
return false;
|
||||
|
||||
var actor = _objectManager[armature.ActorIdentifier].Objects[0];
|
||||
|
||||
if (!armature.IsBuilt || forceRebuild)
|
||||
{
|
||||
armature.RebuildSkeleton(actor.Model.AsCharacterBase);
|
||||
}
|
||||
else if (armature.NewBonesAvailable(actor.Model.AsCharacterBase))
|
||||
{
|
||||
armature.AugmentSkeleton(actor.Model.AsCharacterBase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is on wait until isse #191 on Github responds. Keeping it in code, delete it if I forget and this is longer then a month ago.
|
||||
|
||||
// Disabling this if its any Default Profile due to Log spam. A bit crazy but hey, if its for me id Remove Default profiles all together so this is as much as ill do for now! :)
|
||||
//if(!(Profile.CharacterName.Equals(Constants.DefaultProfileCharacterName) || Profile.CharacterName.Equals("DefaultCutscene"))) {
|
||||
_logger.Error($"Error occured while attempting to link skeleton: {armature}");
|
||||
throw;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterate through the skeleton of the given character base, and apply any transformations
|
||||
/// for which this armature contains corresponding model bones. This method of application
|
||||
/// is safer but more computationally costly
|
||||
/// </summary>
|
||||
private void ApplyPiecewiseTransformation(Armature armature, Actor actor, ActorIdentifier actorIdentifier)
|
||||
{
|
||||
var cBase = actor.Model.AsCharacterBase;
|
||||
|
||||
var isMount = actorIdentifier.Type == IdentifierType.Owned &&
|
||||
actorIdentifier.Kind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.MountType;
|
||||
|
||||
Actor? mountOwner = null;
|
||||
Armature? mountOwnerArmature = null;
|
||||
if (isMount)
|
||||
{
|
||||
(var ident, mountOwner) = _gameObjectService.FindActorsByName(actorIdentifier.PlayerName.ToString()).FirstOrDefault();
|
||||
Armatures.TryGetValue(ident, out mountOwnerArmature);
|
||||
}
|
||||
|
||||
if (cBase != null)
|
||||
{
|
||||
for (var pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex)
|
||||
{
|
||||
var currentPose = cBase->Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (currentPose != null)
|
||||
{
|
||||
for (var boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex)
|
||||
{
|
||||
if (armature.GetBoneAt(pSkeleIndex, boneIndex) is ModelBone mb
|
||||
&& mb != null
|
||||
&& mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String)
|
||||
{
|
||||
if (mb == armature.MainRootBone)
|
||||
{
|
||||
if (_gameObjectService.IsActorHasScalableRoot(actor) && mb.IsModifiedScale())
|
||||
{
|
||||
cBase->DrawObject.Object.Scale = mb.CustomizedTransform!.Scaling;
|
||||
|
||||
//Fix mount owner's scale if needed
|
||||
//todo: always keep owner's scale proper instead of scaling with mount if no armature found
|
||||
if (isMount && mountOwner != null && mountOwnerArmature != null)
|
||||
{
|
||||
var ownerDrawObject = cBase->DrawObject.Object.ChildObject;
|
||||
|
||||
//limit to only modified scales because that is just easier to handle
|
||||
//because we don't need to hook into dismount code to reset character scale
|
||||
//todo: hook into dismount
|
||||
//https://github.com/Cytraen/SeatedSidekickSpectator/blob/main/SetModeHook.cs?
|
||||
if (cBase->DrawObject.Object.ChildObject == mountOwner.Value.Model &&
|
||||
mountOwnerArmature.MainRootBone.IsModifiedScale())
|
||||
{
|
||||
var baseScale = mountOwnerArmature.MainRootBone.CustomizedTransform!.Scaling;
|
||||
|
||||
ownerDrawObject->Scale = new Vector3(Math.Abs(baseScale.X / cBase->DrawObject.Object.Scale.X),
|
||||
Math.Abs(baseScale.Y / cBase->DrawObject.Object.Scale.Y),
|
||||
Math.Abs(baseScale.Z / cBase->DrawObject.Object.Scale.Z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mb.ApplyModelTransform(cBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRootTranslation(Armature arm, Actor actor)
|
||||
{
|
||||
//I'm honestly not sure if we should or even can check if cBase->DrawObject or cBase->DrawObject.Object is a valid object
|
||||
//So for now let's assume we don't need to check for that
|
||||
|
||||
var cBase = actor.Model.AsCharacterBase;
|
||||
if (cBase != null)
|
||||
{
|
||||
var rootBoneTransform = arm.GetAppliedBoneTransform("n_root");
|
||||
if (rootBoneTransform == null)
|
||||
return;
|
||||
|
||||
if (rootBoneTransform.Translation.X == 0 &&
|
||||
rootBoneTransform.Translation.Y == 0 &&
|
||||
rootBoneTransform.Translation.Z == 0)
|
||||
return;
|
||||
|
||||
if (!cBase->DrawObject.IsVisible)
|
||||
return;
|
||||
|
||||
var newPosition = new FFXIVClientStructs.FFXIV.Common.Math.Vector3
|
||||
{
|
||||
X = cBase->DrawObject.Object.Position.X + MathF.Max(rootBoneTransform.Translation.X, 0.01f),
|
||||
Y = cBase->DrawObject.Object.Position.Y + MathF.Max(rootBoneTransform.Translation.Y, 0.01f),
|
||||
Z = cBase->DrawObject.Object.Position.Z + MathF.Max(rootBoneTransform.Translation.Z, 0.01f)
|
||||
};
|
||||
|
||||
cBase->DrawObject.Object.Position = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveArmature(Armature armature, ArmatureChanged.DeletionReason reason)
|
||||
{
|
||||
armature.Profile.Armatures.Remove(armature);
|
||||
Armatures.Remove(armature.ActorIdentifier);
|
||||
_logger.Debug($"Armature {armature} removed from cache");
|
||||
|
||||
_event.Invoke(ArmatureChanged.Type.Deleted, armature, reason);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Templates.Data.Template? template, object? arg3)
|
||||
{
|
||||
if (type is not TemplateChanged.Type.NewBone &&
|
||||
type is not TemplateChanged.Type.DeletedBone &&
|
||||
type is not TemplateChanged.Type.EditorCharacterChanged &&
|
||||
type is not TemplateChanged.Type.EditorEnabled &&
|
||||
type is not TemplateChanged.Type.EditorDisabled)
|
||||
return;
|
||||
|
||||
if (type == TemplateChanged.Type.NewBone ||
|
||||
type == TemplateChanged.Type.DeletedBone) //type == TemplateChanged.Type.EditorCharacterChanged?
|
||||
{
|
||||
//In case a lot of events are triggered at the same time for the same template this should limit the amount of times bindings are unneccessary rebuilt
|
||||
_framework.RegisterImportant($"TemplateRebuild @ {template.UniqueId}", () =>
|
||||
{
|
||||
foreach (var profile in _profileManager.GetProfilesUsingTemplate(template))
|
||||
{
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange New/Deleted bone or character changed: {type}, template: {template.Name.Text.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}->{profile.Armatures.Count} armatures");
|
||||
if (!profile.Enabled || profile.Armatures.Count == 0)
|
||||
continue;
|
||||
|
||||
profile.Armatures.ForEach(x => x.RebuildBoneTemplateBinding());
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == TemplateChanged.Type.EditorCharacterChanged)
|
||||
{
|
||||
(var characterName, var profile) = ((string, Profile))arg3;
|
||||
|
||||
foreach (var armature in GetArmaturesForCharacterName(characterName))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, {armature}");
|
||||
}
|
||||
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
//Rebuild armatures for previous character
|
||||
foreach (var armature in profile.Armatures)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}, new name: {characterName.Incognify()}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == TemplateChanged.Type.EditorEnabled ||
|
||||
type == TemplateChanged.Type.EditorDisabled)
|
||||
{
|
||||
foreach (var armature in GetArmaturesForCharacterName((string)arg3!))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange template editor enabled/disabled: {type}, pending profile set for {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3)
|
||||
{
|
||||
if (type is not ProfileChanged.Type.AddedTemplate &&
|
||||
type is not ProfileChanged.Type.RemovedTemplate &&
|
||||
type is not ProfileChanged.Type.MovedTemplate &&
|
||||
type is not ProfileChanged.Type.ChangedTemplate &&
|
||||
type is not ProfileChanged.Type.Toggled &&
|
||||
type is not ProfileChanged.Type.Deleted &&
|
||||
type is not ProfileChanged.Type.TemporaryProfileAdded &&
|
||||
type is not ProfileChanged.Type.TemporaryProfileDeleted &&
|
||||
type is not ProfileChanged.Type.ChangedCharacterName &&
|
||||
type is not ProfileChanged.Type.ChangedDefaultProfile &&
|
||||
type is not ProfileChanged.Type.LimitLookupToOwnedChanged)
|
||||
return;
|
||||
|
||||
if (type == ProfileChanged.Type.ChangedDefaultProfile)
|
||||
{
|
||||
var oldProfile = (Profile?)arg3;
|
||||
|
||||
if (oldProfile == null || oldProfile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var armature in oldProfile.Armatures)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange Profile no longer default, armatures rebind scheduled: {type}, old profile: {oldProfile.Name.Text.Incognify()}->{oldProfile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
_logger.Error($"ArmatureManager.OnProfileChange Invalid input for event: {type}, profile is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.Toggled)
|
||||
{
|
||||
if (!profile.Enabled && profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
if (profile == _profileManager.DefaultProfile)
|
||||
{
|
||||
foreach (var kvPair in Armatures)
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (armature.Profile == profile)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange default profile toggled, planning rebind for armature {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profile.CharacterName))
|
||||
return;
|
||||
|
||||
foreach (var armature in GetArmaturesForCharacterName(profile.CharacterName))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.TemporaryProfileAdded)
|
||||
{
|
||||
if (!profile.TemporaryActor.IsValid || !Armatures.ContainsKey(profile.TemporaryActor))
|
||||
return;
|
||||
|
||||
var armature = Armatures[profile.TemporaryActor];
|
||||
if (armature.Profile == profile)
|
||||
return;
|
||||
|
||||
armature.ProtectFromRemoval();
|
||||
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange TemporaryProfileAdded, calling rebind for existing armature: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.ChangedCharacterName ||
|
||||
type == ProfileChanged.Type.Deleted ||
|
||||
type == ProfileChanged.Type.TemporaryProfileDeleted ||
|
||||
type == ProfileChanged.Type.LimitLookupToOwnedChanged)
|
||||
{
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var armature in profile.Armatures)
|
||||
{
|
||||
if (type == ProfileChanged.Type.TemporaryProfileDeleted)
|
||||
armature.ProtectFromRemoval(); //just to be safe
|
||||
|
||||
armature.IsPendingProfileRebind = true;
|
||||
}
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange CCN/DEL/TPD/LLTOC, armature rebind scheduled: {type}, data payload: {arg3?.ToString()?.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//todo: shouldn't happen, but happens sometimes? I think?
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange Added/Deleted/Moved/Changed template: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name}->{profile.Enabled}->{profile.Armatures.Count} armatures");
|
||||
|
||||
profile!.Armatures.ForEach(x => x.RebuildBoneTemplateBinding());
|
||||
}
|
||||
|
||||
private IEnumerable<Armature> GetArmaturesForCharacterName(string characterName)
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(characterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
yield break;
|
||||
|
||||
foreach (var actorData in actors)
|
||||
{
|
||||
if (!Armatures.TryGetValue(actorData.Item1, out var armature))
|
||||
continue;
|
||||
|
||||
yield return armature;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user