Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View 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];
}
}

View 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;
}
}