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; /// /// 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. /// public unsafe class Armature { /// /// Gets the Customize+ profile for which this mockup applies transformations. /// public Profile Profile { get; set; } /// /// Static identifier of the actor associated with this armature /// public ActorIdentifier ActorIdentifier { get; init; } /// /// Gets or sets a value indicating whether or not this armature has any renderable objects on which it should act. /// public bool IsVisible { get; set; } /// /// Represents date and time when actor associated with this armature was last seen. /// Implemented mostly as a armature cleanup protection hack for mare and penumbra. /// public DateTime LastSeen { get; private set; } /// /// Gets a value indicating whether or not this armature has successfully built itself with bone information. /// public bool IsBuilt => _partialSkeletons.Any(); /// /// Internal flag telling ArmatureManager that it should attempt to rebind profile to (another) profile whenever possible. /// public bool IsPendingProfileRebind { get; set; } /// /// For debugging purposes, each armature is assigned a globally-unique ID number upon creation. /// private static uint _nextGlobalId; private readonly uint _localId; /// /// Binding telling which bones are bound to each template for this armature. Built from template list in profile. /// public Dictionary BoneTemplateBinding { get; init; } /// /// 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. /// private ModelBone[][] _partialSkeletons; #region Bone Accessors ------------------------------------------------------------------------------- /// /// Gets the number of partial skeletons contained in this armature. /// public int PartialSkeletonCount => _partialSkeletons.Length; /// /// Get the list of bones belonging to the partial skeleton at the given index. /// public ModelBone[] this[int i] { get => _partialSkeletons[i]; } /// /// Returns the number of bones contained within the partial skeleton with the given index. /// public int GetBoneCountOfPartial(int partialIndex) => _partialSkeletons[partialIndex].Length; /// /// Get the bone at index 'j' within the partial skeleton at index 'i'. /// public ModelBone this[int i, int j] { get => _partialSkeletons[i][j]; } /// /// Return the bone at the given indices, if it exists /// public ModelBone? GetBoneAt(int partialIndex, int boneIndex) { if (_partialSkeletons.Length > partialIndex && _partialSkeletons[partialIndex].Length > boneIndex) { return this[partialIndex, boneIndex]; } return null; } /// /// Returns the root bone of the partial skeleton with the given index. /// public ModelBone GetRootBoneOfPartial(int partialIndex) => this[partialIndex, 0]; public ModelBone MainRootBone => GetRootBoneOfPartial(0); /// /// Get the total number of bones in each partial skeleton combined. /// // 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 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(); BoneTemplateBinding = new Dictionary(); ActorIdentifier = actorIdentifier; Profile = profile; IsVisible = false; UpdateLastSeen(); Profile.Armatures.Add(this); Plugin.Logger.Debug($"Instantiated {this}, attached to {Profile}"); } /// 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 IsSkeletonUpdated(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; //todo: compare bones for hair partial skeleton [2] } } return false; } /// /// Rebuild the armature using the provided character base as a reference. /// public void RebuildSkeleton(CharacterBase* cBase) { if (cBase == null) return; var newPartials = ParseBonesFromObject(this, cBase); _partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray(); RebuildBoneTemplateBinding(); //todo: intentionally not calling ArmatureChanged.Type.Updated because this is pending rewrite Plugin.Logger.Debug($"Rebuilt {this}"); } 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; } /// /// Update last time actor for this armature was last seen in the game /// public void UpdateLastSeen(DateTime? dateTime = null) { if(dateTime == null) dateTime = DateTime.UtcNow; LastSeen = (DateTime)dateTime; } private static unsafe List> ParseBonesFromObject(Armature arm, CharacterBase* cBase) { List> 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.Verbose($"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]; } }