diff --git a/CustomizePlus/Api/CustomizePlusIpc.cs b/CustomizePlus/Api/CustomizePlusIpc.cs index 5caafda..f74985f 100644 --- a/CustomizePlus/Api/CustomizePlusIpc.cs +++ b/CustomizePlus/Api/CustomizePlusIpc.cs @@ -226,7 +226,7 @@ public void OnLocalPlayerProfileUpdate() CharacterProfile? profile = Plugin.ProfileManager.GetProfileByCharacterName(name, true); PluginLog.Debug($"Sending local player update message: {profile?.ProfileName ?? "no profile"} - {profile?.CharacterName ?? "no profile"}"); - ProviderOnLocalPlayerProfileUpdate?.SendMessage(profile != null ? JsonConvert.SerializeObject(profile) : null); + ProviderOnLocalPlayerProfileUpdate?.SendMessage(profile != null ? profile.SerializeToJSON() : null); } } diff --git a/CustomizePlus/Api/VectorContractResolver.cs b/CustomizePlus/Api/VectorContractResolver.cs new file mode 100644 index 0000000..818e60d --- /dev/null +++ b/CustomizePlus/Api/VectorContractResolver.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace CustomizePlus.Api +{ + public class VectorContractResolver : DefaultContractResolver + { + public static VectorContractResolver Instance { get; } = new VectorContractResolver(); + + protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + if (typeof(FFXIVClientStructs.FFXIV.Common.Math.Vector3).IsAssignableFrom(member.DeclaringType) + && member.Name != nameof(FFXIVClientStructs.FFXIV.Common.Math.Vector3.X) + && member.Name != nameof(FFXIVClientStructs.FFXIV.Common.Math.Vector3.Y) + && member.Name != nameof(FFXIVClientStructs.FFXIV.Common.Math.Vector3.Z)) + { + property.Ignored = true; + } + return property; + } + } +} diff --git a/CustomizePlus/Data/Armature/Armature.cs b/CustomizePlus/Data/Armature/Armature.cs index 82bcca8..d47217a 100644 --- a/CustomizePlus/Data/Armature/Armature.cs +++ b/CustomizePlus/Data/Armature/Armature.cs @@ -4,16 +4,19 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; +using System.Runtime; + using CustomizePlus.Data.Profile; -using CustomizePlus.Extensions; using CustomizePlus.Helpers; + +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; + using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Dalamud.Game.ClientState.Objects.Types; using FFXIVClientStructs.Havok; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip; + +using CustomizePlus.Extensions; namespace CustomizePlus.Data.Armature { @@ -21,35 +24,26 @@ namespace CustomizePlus.Data.Armature /// 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 + public abstract unsafe class Armature : IBoneContainer { - /// - /// Gets the Customize+ profile for which this mockup applies transformations. - /// - public CharacterProfile Profile { 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; } - /// - /// Gets a value indicating whether or not this armature has successfully built itself with bone information. - /// - public bool IsBuilt { get; private set; } - /// /// For debugging purposes, each armature is assigned a globally-unique ID number upon creation. /// private static uint _nextGlobalId; - private readonly uint _localId; + protected readonly uint _localId; /// /// 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; + protected ModelBone[][] _partialSkeletons { get; set; } + protected Armature[] _subArmatures { get; set; } #region Bone Accessors ------------------------------------------------------------------------------- @@ -58,14 +52,6 @@ public unsafe class 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. /// @@ -74,11 +60,13 @@ public ModelBone[] this[int i] /// /// Get the bone at index 'j' within the partial skeleton at index 'i'. /// - public ModelBone this[int i, int j] + private ModelBone this[int i, int j] { get => _partialSkeletons[i][j]; } + protected int BoneCount() => _partialSkeletons.Sum(x => x.Length) + _subArmatures.Sum(x => x.BoneCount()); + /// /// Return the bone at the given indices, if it exists /// @@ -98,16 +86,10 @@ public ModelBone[] this[int i] /// public ModelBone GetRootBoneOfPartial(int partialIndex) => this[partialIndex, 0]; - public ModelBone MainRootBone => GetRootBoneOfPartial(0); - /// - /// Get the total number of bones in each partial skeleton combined. + /// Get all individual model bones making up this armature. /// - // 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() + public IEnumerable GetLocalBones() { for (int i = 0; i < _partialSkeletons.Length; ++i) { @@ -118,6 +100,19 @@ public IEnumerable GetAllBones() } } + public IEnumerable GetLocalAndDownstreamBones() + { + foreach (ModelBone mb in GetLocalBones()) yield return mb; + + foreach(Armature arm in _subArmatures) + { + foreach(ModelBone mbd in arm.GetLocalAndDownstreamBones()) + { + yield return mbd; + } + } + } + /// /// Gets a value indicating whether this armature has yet built its skeleton. /// @@ -130,170 +125,56 @@ public IEnumerable GetAllBones() /// Gets or sets a value indicating whether or not this armature should snap all of its bones to their reference "bindposes". /// i.e. force the character ingame to assume their "default" pose. /// - public bool SnapToReferencePose + public bool FrozenPose { - get => GetReferenceSnap(); - set => SetReferenceSnap(value); + get => GetFrozenStatus(); + set => SetFrozenStatus(value); } - private bool _snapToReference; + private bool _frozenInDefaultPose; - public Armature(CharacterProfile prof) + public Armature() { - _localId = _nextGlobalId++; + _localId = ++_nextGlobalId; _partialSkeletons = Array.Empty(); + _subArmatures = Array.Empty(); - Profile = prof; IsVisible = false; - - //cross-link the two, though I'm not positive the profile ever needs to refer back - Profile.Armature = this; - - TryLinkSkeleton(); - - PluginLog.LogDebug($"Instantiated {this}, attached to {Profile}"); - } - /// - /// Returns whether or not this armature was designed to apply to an object with the given name. - /// - public bool AppliesTo(string objectName) => Profile.AppliesTo(objectName); - /// - public override string ToString() - { - return Built - ? $"Armature (#{_localId}) on {Profile.CharacterName} with {TotalBoneCount} bone/s" - : $"Armature (#{_localId}) on {Profile.CharacterName} with no skeleton reference"; - } - - private bool GetReferenceSnap() - { - if (Profile != Plugin.ProfileManager.ProfileOpenInEditor) - _snapToReference = false; - - return _snapToReference; - } + public abstract override string ToString(); - private void SetReferenceSnap(bool value) + protected bool GetFrozenStatus() { - if (value && Profile == Plugin.ProfileManager.ProfileOpenInEditor) - _snapToReference = false; - - _snapToReference = value; + return _frozenInDefaultPose; } - /// - /// Returns whether or not a link can be established between the armature and an in-game object. - /// If unbuilt, the armature will use this opportunity to rebuild itself. - /// - public unsafe bool TryLinkSkeleton(bool forceRebuild = false) + protected virtual void SetFrozenStatus(bool value) { - try + _frozenInDefaultPose = value; + foreach(Armature arm in _subArmatures) { - foreach (var obj in DalamudServices.ObjectTable) - { - if(!Profile.AppliesTo(obj) || !GameDataHelper.IsValidGameObject(obj)) - continue; - - CharacterBase* cBase = obj.ToCharacterBase(); - - if (!Built || forceRebuild) - { - RebuildSkeleton(cBase); - } - else if (NewBonesAvailable(cBase)) - { - AugmentSkeleton(cBase); - } - return true; - } + arm.SetFrozenStatus(value); } - catch - { - PluginLog.LogError($"Error occured while attempting to link skeleton: {this}"); - } - - return false; } - private bool NewBonesAvailable(CharacterBase* cBase) - { - if (cBase == null) - { - return false; - } - else if (cBase->Skeleton->PartialSkeletonCount > _partialSkeletons.Length) - { - return true; - } - else - { - for (int i = 0; i < cBase->Skeleton->PartialSkeletonCount; ++i) - { - hkaPose* newPose = cBase->Skeleton->PartialSkeletons[i].GetHavokPose(Constants.TruePoseIndex); - if (newPose != null - && newPose->Skeleton->Bones.Length > _partialSkeletons[i].Length) - { - return true; - } - } - } - - return false; - } + public abstract void UpdateOrDeleteRecord(string recordKey, BoneTransform? trans); /// /// Rebuild the armature using the provided character base as a reference. /// - public void RebuildSkeleton(CharacterBase* cBase) - { - if (cBase == null) - return; - - List> newPartials = ParseBonesFromObject(this, cBase); - - _partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray(); - - PluginLog.LogDebug($"Rebuilt {this}"); - } + public abstract void RebuildSkeleton(CharacterBase* cBase); - public void AugmentSkeleton(CharacterBase* cBase) + protected static unsafe List> ParseBonesFromObject(Armature arm, CharacterBase* cBase, Dictionary? records) { - if (cBase == null) - return; - - List> oldPartials = _partialSkeletons.Select(x => x.ToList()).ToList(); - List> newPartials = ParseBonesFromObject(this, cBase); + List> newPartials = new(); - //for each of the new partial skeletons discovered... - for (int i = 0; i < newPartials.Count; ++i) + if (cBase == null) { - //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 - { - for (int j = oldPartials[i].Count; j < newPartials[i].Count; ++j) - { - oldPartials[i].Add(newPartials[i][j]); - } - } + return newPartials; } - _partialSkeletons = oldPartials.Select(x => x.ToArray()).ToArray(); - - PluginLog.LogDebug($"Augmented {this} with new bones"); - } - - private static unsafe List> ParseBonesFromObject(Armature arm, CharacterBase* cBase) - { - List> newPartials = new(); - try { //build the skeleton @@ -312,30 +193,49 @@ private static unsafe List> ParseBonesFromObject(Armature arm, C 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); + ModelBone newBone; - if (currentPose->Skeleton->ParentIndices[boneIndex] is short parentIndex - && parentIndex >= 0) + if (pSkeleIndex == 0 && boneIndex == 0) + { + newBone = new ModelRootBone(arm, boneName); + PluginLog.LogDebug($"Main root @ <{pSkeleIndex}, {boneIndex}> ({boneName})"); + } + else if (currentPartial.ConnectedBoneIndex == boneIndex) + { + ModelBone cloneOf = newPartials[0][currentPartial.ConnectedParentBoneIndex]; + newBone = new PartialRootBone(arm, cloneOf, boneName, pSkeleIndex); + PluginLog.LogDebug($"Partial root @ <{pSkeleIndex}, {boneIndex}> ({boneName})"); + } + else { - newBone.AddParent(pSkeleIndex, parentIndex); - newPartials[pSkeleIndex][parentIndex].AddChild(pSkeleIndex, boneIndex); + newBone = new ModelBone(arm, boneName, pSkeleIndex, boneIndex); } - foreach (ModelBone mb in newPartials.SelectMany(x => x)) + //skip adding parents/children/twins if it's the root bone + if (pSkeleIndex > 0 || boneIndex > 0) { - if (AreTwinnedNames(boneName, mb.BoneName)) + if (currentPose->Skeleton->ParentIndices[boneIndex] is short parentIndex + && parentIndex >= 0) { - newBone.AddTwin(mb.PartialSkeletonIndex, mb.BoneIndex); - mb.AddTwin(pSkeleIndex, boneIndex); - break; + newBone.AddParent(pSkeleIndex, parentIndex); + newPartials[pSkeleIndex][parentIndex].AddChild(pSkeleIndex, boneIndex); } - } - if (arm.Profile.Bones.TryGetValue(boneName, out BoneTransform? bt) - && bt != null) - { - newBone.UpdateModel(bt); + foreach (ModelBone mb in newPartials.SelectMany(x => x)) + { + if (AreTwinnedNames(boneName, mb.BoneName)) + { + newBone.AddTwin(mb.PartialSkeletonIndex, mb.BoneIndex); + mb.AddTwin(pSkeleIndex, boneIndex); + break; + } + } + + if (records != null && records.TryGetValue(boneName, out BoneTransform? bt) + && bt != null) + { + newBone.UpdateModel(bt); + } } newPartials.Last().Add(newBone); @@ -348,6 +248,20 @@ private static unsafe List> ParseBonesFromObject(Armature arm, C } BoneData.LogNewBones(newPartials.SelectMany(x => x.Select(y => y.BoneName)).ToArray()); + + if (newPartials.Any()) + { + PluginLog.LogDebug($"Rebuilt {arm}"); + PluginLog.LogDebug($"Height: {cBase->Height()}"); + PluginLog.LogDebug($"Attachment Info:"); + PluginLog.LogDebug($"\t Type: {cBase->AttachType()}"); + PluginLog.LogDebug($"\tTarget: {(cBase->AttachTarget() == null ? "N/A" : cBase->AttachTarget()->PartialSkeletonCount)} partial/s"); + PluginLog.LogDebug($"\tParent: {(cBase->AttachParent() == null ? "N/A" : cBase->AttachParent()->PartialSkeletonCount)} partial/s"); + PluginLog.LogDebug($"\t Count: {cBase->AttachCount()}"); + PluginLog.LogDebug($"\tBoneID: {cBase->AttachBoneID()}"); + PluginLog.LogDebug($"\t Scale: {cBase->AttachBoneScale()}"); + } + } catch (Exception ex) { @@ -356,101 +270,123 @@ private static unsafe List> ParseBonesFromObject(Armature arm, C return newPartials; } + //protected virtual bool NewBonesAvailable(CharacterBase* cBase) + //{ + // if (cBase == null) + // { + // return false; + // } + // else if (cBase->Skeleton->PartialSkeletonCount > _partialSkeletons.Length) + // { + // return true; + // } + // else + // { + // for (int i = 0; i < cBase->Skeleton->PartialSkeletonCount; ++i) + // { + // hkaPose* newPose = cBase->Skeleton->PartialSkeletons[i].GetHavokPose(Constants.TruePoseIndex); + // if (newPose != null + // && newPose->Skeleton->Bones.Length > _partialSkeletons[i].Length) + // { + // return true; + // } + // } + // } - public void UpdateBoneTransform(int partialIdx, int boneIdx, BoneTransform bt, bool mirror = false, bool propagate = false) - { - this[partialIdx, boneIdx].UpdateModel(bt, mirror, propagate); - } + // return false; + //} /// /// Iterate through this armature's model bones and apply their associated transformations - /// to all of their in-game siblings + /// to all of their in-game siblings. /// - public unsafe void ApplyTransformation(GameObject obj) + public virtual unsafe void ApplyTransformation(CharacterBase* cBase, bool applyScaling) { - CharacterBase* cBase = obj.ToCharacterBase(); - if (cBase != null) { - foreach (ModelBone mb in GetAllBones().Where(x => x.CustomizedTransform.IsEdited())) + for (int pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex) { - if (mb == MainRootBone) - { - //the main root bone's position information is handled by a different hook - //so there's no point in trying to update it here - //meanwhile root scaling has special rules + hkaPose* currentPose = cBase->Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(Constants.TruePoseIndex); - if (obj.HasScalableRoot()) + if (currentPose != null) + { + if (FrozenPose) { - mb.ApplyModelScale(cBase); + currentPose->SetToReferencePose(); + currentPose->SyncModelSpace(); } - mb.ApplyModelRotation(cBase); - } - else - { - mb.ApplyModelTransform(cBase); - } - } - } - } + for (int boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex) + { + if (GetBoneAt(pSkeleIndex, boneIndex) is ModelBone mb + && mb != null + && mb is not PartialRootBone + && (mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String + || mb.BoneName[3..] == currentPose->Skeleton->Bones[boneIndex].Name.String) + && mb.HasActiveTransform) + { + //Partial root bones aren't guaranteed to be parented the way that would + //logically make sense. For that reason, don't bother trying to transform them locally. + if (applyScaling) + { + mb.ApplyIndividualScale(cBase); + } - /// - /// 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 - /// - public unsafe void ApplyPiecewiseTransformation(GameObject obj) - { - CharacterBase* cBase = obj.ToCharacterBase(); + if (!GameStateHelper.GameInPosingModeWithFrozenRotation()) + { + mb.ApplyRotation(cBase, false); + } - if (cBase != null) - { - for (int pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex) - { - hkaPose* currentPose = cBase->Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(Constants.TruePoseIndex); + if (!GameStateHelper.GameInPosingModeWithFrozenPosition()) + { + mb.ApplyTranslationAtAngle(cBase, false); + } + + } + } + + currentPose->SyncModelSpace(); + currentPose->SyncLocalSpace(); - if (currentPose != null) - { for (int boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex) { if (GetBoneAt(pSkeleIndex, boneIndex) is ModelBone mb && mb != null - && mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String) + && mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String + && mb.HasActiveTransform) { - if (mb == MainRootBone) + if (mb is PartialRootBone prb) { - if (obj.HasScalableRoot()) - { - mb.ApplyModelScale(cBase); - } - - mb.ApplyModelRotation(cBase); + //In the case of partial root bones, simply copy the transform in model space + //wholesale from the bone that they're a copy of + prb.ApplyOriginalTransform(cBase); + continue; } - else if (GameStateHelper.GameInPosingMode()) + + if (!GameStateHelper.GameInPosingModeWithFrozenRotation()) { - mb.ApplyModelScale(cBase); + mb.ApplyRotation(cBase, true); } - else + else if (GameStateHelper.GameInPosingMode()) { - mb.ApplyModelTransform(cBase); + mb.ApplyTranslationAtAngle(cBase, true); } + } } } } - } - } - public void ApplyRootTranslation(CharacterBase* cBase) - { - if (cBase != null && _partialSkeletons.Any() && _partialSkeletons.First().Any()) - { - _partialSkeletons[0][0].ApplyStraightModelTranslation(cBase); + foreach(Armature subArm in _subArmatures) + { + //subs are responsible for figuring out how they want to parse the character base + subArm.ApplyTransformation(cBase, applyScaling); + } } } + private static bool AreTwinnedNames(string name1, string name2) { return (name1[^1] == 'r' ^ name2[^1] == 'r') @@ -458,73 +394,42 @@ private static bool AreTwinnedNames(string name1, string name2) && (name1[0..^1] == name2[0..^1]); } - //public void OverrideWithReferencePose() - //{ - // for (var pSkeleIndex = 0; pSkeleIndex < Skeleton->PartialSkeletonCount; ++pSkeleIndex) - // { - // for (var poseIndex = 0; poseIndex < 4; ++poseIndex) - // { - // var snapPose = Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(poseIndex); - - // if (snapPose != null) - // { - // snapPose->SetToReferencePose(); - // } - // } - // } - //} - - //public void OverrideRootParenting() - //{ - // var pSkeleNot = Skeleton->PartialSkeletons[0]; - - // for (var pSkeleIndex = 1; pSkeleIndex < Skeleton->PartialSkeletonCount; ++pSkeleIndex) - // { - // var partialSkele = Skeleton->PartialSkeletons[pSkeleIndex]; - - // for (var poseIndex = 0; poseIndex < 4; ++poseIndex) - // { - // var currentPose = partialSkele.GetHavokPose(poseIndex); - - // if (currentPose != null && partialSkele.ConnectedBoneIndex >= 0) - // { - // int boneIdx = partialSkele.ConnectedBoneIndex; - // int parentBoneIdx = partialSkele.ConnectedParentBoneIndex; - - // var transA = currentPose->AccessBoneModelSpace(boneIdx, 0); - // var transB = pSkeleNot.GetHavokPose(0)->AccessBoneModelSpace(parentBoneIdx, 0); - - // //currentPose->AccessBoneModelSpace(parentBoneIdx, hkaPose.PropagateOrNot.DontPropagate); + public virtual IEnumerable GetBoneTransformValues(BoneAttribute attribute, PosingSpace space) + { + foreach(ModelBone mb in GetLocalBones().Where(x => x is not PartialRootBone)) + { + yield return new TransformInfo(this, mb, attribute, space); + } - // for (var i = 0; i < currentPose->Skeleton->Bones.Length; ++i) - // { - // currentPose->ModelPose[i] = ApplyPropagatedTransform(currentPose->ModelPose[i], transB, - // transA->Translation, transB->Rotation); - // currentPose->ModelPose[i] = ApplyPropagatedTransform(currentPose->ModelPose[i], transB, - // transB->Translation, transA->Rotation); - // } - // } - // } - // } - //} + foreach(Armature arm in _subArmatures) + { + foreach(TransformInfo trInfo in arm.GetBoneTransformValues(attribute, space)) + { + yield return trInfo; + } + } + } - //private hkQsTransformf ApplyPropagatedTransform(hkQsTransformf init, hkQsTransformf* propTrans, - // hkVector4f initialPos, hkQuaternionf initialRot) - //{ - // var sourcePosition = propTrans->Translation.GetAsNumericsVector().RemoveWTerm(); - // var deltaRot = propTrans->Rotation.ToQuaternion() / initialRot.ToQuaternion(); - // var deltaPos = sourcePosition - initialPos.GetAsNumericsVector().RemoveWTerm(); + public void UpdateBoneTransformValue(TransformInfo newTransform, BoneAttribute attribute, bool mirrorChanges) + { + foreach(ModelBone mb in GetLocalBones().Where(x => x.BoneName == newTransform.BoneCodeName)) + { + BoneTransform oldTransform = mb.GetTransformation(); + oldTransform.UpdateAttribute(attribute, newTransform.TransformationValue); + mb.UpdateModel(oldTransform); - // hkQsTransformf output = new() - // { - // Translation = Vector3 - // .Transform(init.Translation.GetAsNumericsVector().RemoveWTerm() - sourcePosition, deltaRot) - // .ToHavokTranslation(), - // Rotation = deltaRot.ToHavokRotation(), - // Scale = init.Scale - // }; - - // return output; - //} + if (mirrorChanges && mb.TwinBone is ModelBone twin && twin != null) + { + if (BoneData.IsIVCSBone(twin.BoneName)) + { + twin.UpdateModel(oldTransform.GetSpecialReflection()); + } + else + { + twin.UpdateModel(oldTransform.GetStandardReflection()); + } + } + } + } } } \ No newline at end of file diff --git a/CustomizePlus/Data/Armature/ArmatureManager.cs b/CustomizePlus/Data/Armature/ArmatureManager.cs index 86ca942..9fb2f94 100644 --- a/CustomizePlus/Data/Armature/ArmatureManager.cs +++ b/CustomizePlus/Data/Armature/ArmatureManager.cs @@ -16,7 +16,7 @@ namespace CustomizePlus.Data.Armature public sealed class ArmatureManager { private Armature? _defaultArmature = null; - private readonly HashSet _armatures = new(); + private readonly HashSet _armatures = new(); public void RenderCharacterProfiles(params CharacterProfile[] profiles) { @@ -32,11 +32,18 @@ public void RenderCharacterProfiles(params CharacterProfile[] profiles) } } - public void ConstructArmatureForProfile(CharacterProfile newProfile) + public void ConstructArmatureForProfile(CharacterProfile newProfile, bool forceNew = false) { + if (forceNew + && _armatures.FirstOrDefault(x => x.Profile == newProfile) is CharacterArmature arm + && arm != null) + { + _armatures.Remove(arm); + } + if (!_armatures.Any(x => x.Profile == newProfile)) { - var newArm = new Armature(newProfile); + var newArm = new CharacterArmature(newProfile); _armatures.Add(newArm); PluginLog.LogDebug($"Added '{newArm}' to cache"); } @@ -59,11 +66,11 @@ private void RefreshActiveArmatures(params CharacterProfile[] profiles) } - private void RefreshArmatureVisibility() + private unsafe void RefreshArmatureVisibility() { foreach (var arm in _armatures) { - arm.IsVisible = arm.Profile.Enabled && arm.TryLinkSkeleton(); + arm.IsVisible = arm.Profile.Enabled && arm.TryLinkSkeleton() != null; } } @@ -79,7 +86,7 @@ private unsafe void ApplyArmatureTransforms() && prof.Armature != null && prof.Armature.IsVisible) { - prof.Armature.ApplyPiecewiseTransformation(obj); + prof.Armature.ApplyTransformation(obj.ToCharacterBase(), obj.HasScalableRoot()); } } } diff --git a/CustomizePlus/Data/Armature/CharacterArmature.cs b/CustomizePlus/Data/Armature/CharacterArmature.cs new file mode 100644 index 0000000..091ddf8 --- /dev/null +++ b/CustomizePlus/Data/Armature/CharacterArmature.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using CustomizePlus.Data.Profile; +using CustomizePlus.Extensions; +using CustomizePlus.Helpers; + +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; + +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace CustomizePlus.Data.Armature +{ + public unsafe class CharacterArmature : Armature + { + /// + /// Gets the Customize+ profile for which this mockup applies transformations. + /// + public CharacterProfile Profile { get; init; } + + public CharacterArmature(CharacterProfile prof) : base() + { + Profile = prof; + //cross-link the two, though I'm not positive the profile ever needs to refer back + Profile.Armature = this; + + TryLinkSkeleton(); + + PluginLog.LogDebug($"Instantiated {this}, attached to {Profile}"); + } + + /// + public override string ToString() + { + return Built + ? $"Armature (#{_localId:00000}) on {Profile.CharacterName} with {BoneCount()} bone/s" + : $"Armature (#{_localId:00000}) on {Profile.CharacterName} with no skeleton reference"; + } + + protected override void SetFrozenStatus(bool value) + { + base.SetFrozenStatus(value && Profile == Plugin.ProfileManager.ProfileOpenInEditor); + } + + /// + /// Returns whether or not a link can be established between the armature and an in-game object. + /// If unbuilt, the armature will use this opportunity to rebuild itself. + /// + public unsafe CharacterBase* TryLinkSkeleton(bool forceRebuild = false) + { + try + { + if (GameDataHelper.TryLookupCharacterBase(Profile.CharacterName, out CharacterBase* cBase) + && cBase != null) + { + if (!Built || forceRebuild) + { + RebuildSkeleton(cBase); + } + return cBase; + } + } + catch (Exception ex) + { + PluginLog.LogError($"Error occured while attempting to link skeleton '{this}': {ex}"); + } + + return null; + } + + public override void UpdateOrDeleteRecord(string recordKey, BoneTransform? trans) + { + if (trans == null) + { + Profile.Bones.Remove(recordKey); + } + else + { + Profile.Bones[recordKey] = trans; + } + } + public override unsafe void RebuildSkeleton(CharacterBase* cBase) + { + if (cBase == null) + return; + + List> newPartials = ParseBonesFromObject(this, cBase, Profile.Bones); + + _partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray(); + + List weapons = new(); + if (WeaponArmature.CreateMainHand(this, cBase) is WeaponArmature main && main != null) + weapons.Add(main); + if (WeaponArmature.CreateOffHand(this, cBase) is WeaponArmature off && off != null) + weapons.Add(off); + + if (weapons.Any()) _subArmatures = weapons.ToArray(); + } + } +} diff --git a/CustomizePlus/Data/Armature/ModelBone.cs b/CustomizePlus/Data/Armature/ModelBone.cs index 71f7902..39907c4 100644 --- a/CustomizePlus/Data/Armature/ModelBone.cs +++ b/CustomizePlus/Data/Armature/ModelBone.cs @@ -5,23 +5,29 @@ using System.Collections.Generic; using System.Linq; +using CustomizePlus.Extensions; + using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.Havok; //using CustomizePlus.Memory; namespace CustomizePlus.Data.Armature { + /// + /// Represents a frame of reference in which a model bone's transformations are being modified. + /// + public enum PosingSpace + { + Self, Parent, Character + } + /// /// Represents a single bone of an ingame character's skeleton. /// public unsafe class ModelBone { - public enum PoseType - { - Local, Model, BindPose, World - } - public readonly Armature MasterArmature; public readonly int PartialSkeletonIndex; @@ -31,8 +37,9 @@ public enum PoseType /// 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) /// - public ModelBone? ParentBone => (_parentPartialIndex >= 0 && _parentBoneIndex >= 0) - ? MasterArmature[_parentPartialIndex, _parentBoneIndex] + public ModelBone? ParentBone => (_parentPartialIndex >= 0 && _parentPartialIndex < MasterArmature.PartialSkeletonCount + && _parentBoneIndex >= 0 && _parentBoneIndex < MasterArmature.GetBoneCountOfPartial(_parentPartialIndex)) + ? MasterArmature.GetBoneAt(_parentPartialIndex, _parentBoneIndex) : null; private int _parentPartialIndex = -1; private int _parentBoneIndex = -1; @@ -41,7 +48,7 @@ public enum PoseType /// Gets each model bone for which this model bone corresponds to a direct parent thereof. /// A model bone may have zero children. /// - public IEnumerable ChildBones => _childPartialIndices.Zip(_childBoneIndices, (x, y) => MasterArmature[x, y]); + public IEnumerable ChildBones => _childPartialIndices.Zip(_childBoneIndices, MasterArmature.GetBoneAt); private List _childPartialIndices = new(); private List _childBoneIndices = new(); @@ -49,7 +56,7 @@ public enum PoseType /// Gets the model bone that forms a mirror image of this model bone, if one exists. /// public ModelBone? TwinBone => (_twinPartialIndex >= 0 && _twinBoneIndex >= 0) - ? MasterArmature[_twinPartialIndex, _twinBoneIndex] + ? MasterArmature.GetBoneAt(_twinPartialIndex, _twinBoneIndex) : null; private int _twinPartialIndex = -1; private int _twinBoneIndex = -1; @@ -57,13 +64,18 @@ public enum PoseType /// /// The name of the bone within the in-game skeleton. Referred to in some places as its "code name". /// - public string BoneName; + public string BoneName { get; } + public BoneData.BoneFamily FamilyName; /// /// The transform that this model bone will impart upon its in-game sibling when the master armature /// is applied to the in-game skeleton. /// - public BoneTransform CustomizedTransform { get; } + protected virtual BoneTransform CustomizedTransform { get; } + + public virtual bool HasActiveTransform { get => CustomizedTransform.IsEdited(); } + + #region Model Bone Construction public ModelBone(Armature arm, string codeName, int partialIdx, int boneIdx) { @@ -72,6 +84,7 @@ public ModelBone(Armature arm, string codeName, int partialIdx, int boneIdx) BoneIndex = boneIdx; BoneName = codeName; + FamilyName = BoneData.GetBoneFamily(codeName); CustomizedTransform = new(); } @@ -100,7 +113,7 @@ public void AddChild(int childPartialIdx, int childBoneIdx) } /// - /// Indicate a bone that acts as this model bone's mirror image, or "twin". + /// Indicate a bone that acts as this model bone's mirror image. /// public void AddTwin(int twinPartialIdx, int twinBoneIdx) { @@ -108,21 +121,16 @@ public void AddTwin(int twinPartialIdx, int twinBoneIdx) _twinBoneIndex = twinBoneIdx; } - private void UpdateTransformation(BoneTransform newTransform) + #endregion + + protected virtual void UpdateTransformation(BoneTransform newTransform) { //update the transform locally CustomizedTransform.UpdateToMatch(newTransform); - //these should be connected by reference already, I think? - //but I suppose it doesn't hurt...? - if (newTransform.IsEdited()) - { - MasterArmature.Profile.Bones[BoneName] = CustomizedTransform; - } - else - { - MasterArmature.Profile.Bones.Remove(BoneName); - } + //the model bones should(?) be the same, by reference + //but we still may need to delete them + MasterArmature.UpdateOrDeleteRecord(BoneName, newTransform.IsEdited() ? newTransform : null); } public override string ToString() @@ -131,90 +139,23 @@ public override string ToString() return $"{BoneName} ({BoneData.GetBoneDisplayName(BoneName)}) @ <{PartialSkeletonIndex}, {BoneIndex}>"; } - /// - /// Get the lineage of this model bone, going back to the skeleton's root bone. - /// - public IEnumerable GetAncestors(bool includeSelf = true) => includeSelf - ? GetAncestors(new List() { this }) - : GetAncestors(new List()); - - private IEnumerable GetAncestors(List tail) - { - tail.Add(this); - if (ParentBone is ModelBone mb && mb != null) - { - return mb.GetAncestors(tail); - } - else - { - return tail; - } - } - - /// - /// Gets all model bones with a lineage that contains this one. - /// - public IEnumerable GetDescendants(bool includeSelf = false) => includeSelf - ? GetDescendants(this) - : GetDescendants(null); - - private IEnumerable GetDescendants(ModelBone? first) - { - List output = first != null - ? new List() { first } - : new List(); - - output.AddRange(ChildBones); - - using (var iter = output.GetEnumerator()) - { - while (iter.MoveNext()) - { - output.AddRange(iter.Current.ChildBones); - yield return iter.Current; - } - } - } + public BoneTransform GetTransformation() => new(CustomizedTransform); /// /// Update the transformation associated with this model bone. Optionally extend the transformation /// to the model bone's twin (in which case it will be appropriately mirrored) and/or children. /// - public void UpdateModel(BoneTransform newTransform, bool mirror = false, bool propagate = false) + public virtual void UpdateModel(BoneTransform newTransform) { - if (mirror && TwinBone is ModelBone mb && mb != null) - { - BoneTransform mirroredTransform = BoneData.IsIVCSBone(BoneName) - ? newTransform.GetSpecialReflection() - : newTransform.GetStandardReflection(); - - mb.UpdateModel(mirroredTransform, false, propagate); - } - UpdateTransformation(newTransform); - UpdateClones(newTransform); - } - - /// - /// For each OTHER bone that shares the name of this one, direct - /// it to update its transform to match the one provided - /// - private void UpdateClones(BoneTransform newTransform) - { - foreach(ModelBone mb in MasterArmature.GetAllBones() - .Where(x => x.BoneName == this.BoneName && x != this)) - { - mb.UpdateTransformation(newTransform); - } } /// /// 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. + /// return the game's current transform value for the bone corresponding to this model bone (in model space). /// - public hkQsTransformf GetGameTransform(CharacterBase* cBase, PoseType refFrame) + public virtual hkQsTransformf GetGameTransform(CharacterBase* cBase, bool? modelSpace = null) { - FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* skelly = cBase->Skeleton; FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton pSkelly = skelly->PartialSkeletons[PartialSkeletonIndex]; hkaPose* targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex); @@ -222,80 +163,63 @@ public hkQsTransformf GetGameTransform(CharacterBase* cBase, PoseType refFrame) if (targetPose == null) return Constants.NullTransform; - return refFrame switch + if (modelSpace == null) { - 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); + return targetPose->AccessUnsyncedPoseLocalSpace()->Data[BoneIndex]; + } + else if (modelSpace == true) + { + return targetPose->AccessSyncedPoseModelSpace()->Data[BoneIndex]; + } + else + { + return targetPose->AccessSyncedPoseLocalSpace()->Data[BoneIndex]; + } } - private static void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, int partialIndex, int boneIndex, PoseType refFrame) + /// + /// Given a character base to which this model bone's master armature (presumably) applies, + /// change to the given transform value the value for the bone corresponding to this model bone. + /// + protected virtual void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, bool propagate) { FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* skelly = cBase->Skeleton; - FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton pSkelly = skelly->PartialSkeletons[partialIndex]; + FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton pSkelly = skelly->PartialSkeletons[PartialSkeletonIndex]; hkaPose* targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex); //hkaPose* targetPose = cBase->Skeleton->PartialSkeletons[PartialSkeletonIndex].GetHavokPose(Constants.TruePoseIndex); if (targetPose == null) return; - switch (refFrame) + if (propagate) { - 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 + targetPose->AccessSyncedPoseLocalSpace()->Data[BoneIndex] = transform; } - } - - /// - /// Apply this model bone's associated transformation to its in-game sibling within - /// the skeleton of the given character base. - /// - public void ApplyModelTransform(CharacterBase* cBase) - { - 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)) + else { - SetGameTransform(cBase, modTransform, PoseType.Model); + targetPose->AccessSyncedPoseModelSpace()->Data[BoneIndex] = transform; } } - 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); + public void ApplyIndividualScale(CharacterBase* cBase) => ApplyTransform(cBase, CustomizedTransform.ModifyScale, false); + public void ApplyRotation(CharacterBase* cBase, bool propagate) => ApplyTransform(cBase, + propagate ? CustomizedTransform.ModifyKinematicRotation : CustomizedTransform.ModifyRotation, propagate); + public void ApplyTranslationAtAngle(CharacterBase* cBase, bool propagate) => ApplyTransform(cBase, + propagate ? CustomizedTransform.ModifyKineTranslationWithRotation : CustomizedTransform.ModifyTranslationWithRotation, propagate); + public void ApplyTranslationAsIs(CharacterBase* cBase, bool propagate) => ApplyTransform(cBase, + propagate ? CustomizedTransform.ModifyKineTranslationAsIs : CustomizedTransform.ModifyTranslationAsIs, propagate); - private void ApplyTransFunc(CharacterBase* cBase, Func modTrans) + protected virtual void ApplyTransform(CharacterBase* cBase, Func modTrans, bool propagate) { if (cBase != null && CustomizedTransform.IsEdited() - && GetGameTransform(cBase, PoseType.Model) is hkQsTransformf gameTransform + && GetGameTransform(cBase, !propagate) is hkQsTransformf gameTransform && !gameTransform.Equals(Constants.NullTransform)) { hkQsTransformf modTransform = modTrans(gameTransform); if (!modTransform.Equals(gameTransform) && !modTransform.Equals(Constants.NullTransform)) { - SetGameTransform(cBase, modTransform, PoseType.Model); + SetGameTransform(cBase, modTransform, propagate); } } } diff --git a/CustomizePlus/Data/Armature/ModelRootBone.cs b/CustomizePlus/Data/Armature/ModelRootBone.cs new file mode 100644 index 0000000..d7b73df --- /dev/null +++ b/CustomizePlus/Data/Armature/ModelRootBone.cs @@ -0,0 +1,92 @@ +// © Customize+. +// Licensed under the MIT license. + +using System; +using CustomizePlus.Extensions; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Common.Math; +using FFXIVClientStructs.Havok; + +namespace CustomizePlus.Data.Armature +{ + /// + /// A fake model bone that doesn't actually correspond to a bone within a skeleton, + /// but instead some other data that can be nonetheless be transformed LIKE a bone. + /// + internal unsafe class ModelRootBone : ModelBone + { + //private Vector3 _cachedGamePosition = Vector3.Zero; + //private Quaternion _cachedGameRotation = Quaternion.Identity; + //private Vector3 _cachedGameScale = Vector3.One; + + //private Vector3 _moddedPosition; + //private Quaternion _moddedRotation; + //private Vector3 _moddedScale; + + public ModelRootBone(Armature arm, string codeName) : base(arm, codeName, 0, 0) + { + //_moddedPosition = _cachedGamePosition; + //_moddedRotation = _cachedGameRotation; + //_moddedScale = _cachedGameScale; + } + + public override unsafe hkQsTransformf GetGameTransform(CharacterBase* cBase, bool? modelSpace = null) + { + //return new hkQsTransformf() + //{ + // Translation = objPosition.ToHavokVector(), + // Rotation = objRotation.ToHavokQuaternion(), + // Scale = objScale.ToHavokVector() + //}; + + return new hkQsTransformf() + { + Translation = cBase->Skeleton->Transform.Position.ToHavokVector(), + Rotation = cBase->Skeleton->Transform.Rotation.ToHavokQuaternion(), + Scale = cBase->Skeleton->Transform.Scale.ToHavokVector() + }; + } + + protected override unsafe void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, bool propagate) + { + //if (_moddedPosition != transform.Translation.ToClientVector3()) + //{ + // _moddedPosition = transform.Translation.ToClientVector3(); + // cBase->DrawObject.Object.Position = _moddedPosition; + //} + + //cBase->DrawObject.Object.Position = transform.Translation.ToClientVector3(); + //cBase->DrawObject.Object.Rotation = transform.Rotation.ToClientQuaternion(); + //cBase->DrawObject.Object.Scale = transform.Scale.ToClientVector3(); + + FFXIVClientStructs.FFXIV.Client.Graphics.Transform tr = new FFXIVClientStructs.FFXIV.Client.Graphics.Transform() + { + Position = transform.Translation.ToClientVector3(), + Rotation = transform.Rotation.ToClientQuaternion(), + Scale = transform.Scale.ToClientVector3() + }; + + cBase->Skeleton->Transform = tr; + + //if (cBase->AttachType() == 4 && cBase->AttachCount() == 1) + //{ + // cBase->Skeleton->Transform.Scale *= cBase->AttachBoneScale() * cBase->AttachParent()->Owner->Height(); + //} + + //CharacterBase* child1 = (CharacterBase*)cBase->DrawObject.Object.ChildObject; + //if (child1 != null && child1->GetModelType() == CharacterBase.ModelType.Weapon) + //{ + // child1->Skeleton->Transform = tr; + + // CharacterBase* child2 = (CharacterBase*)child1->DrawObject.Object.NextSiblingObject; + // if (child2 != child1 && child2 != null && child2->GetModelType() == CharacterBase.ModelType.Weapon) + // { + // child2->Skeleton->Transform = tr; + // } + //} + + //? + //cBase->VfxScale = MathF.Max(MathF.Max(transform.Scale.X, transform.Scale.Y), transform.Scale.Z); + } + } +} diff --git a/CustomizePlus/Data/Armature/PartialRootBone.cs b/CustomizePlus/Data/Armature/PartialRootBone.cs new file mode 100644 index 0000000..5ea1466 --- /dev/null +++ b/CustomizePlus/Data/Armature/PartialRootBone.cs @@ -0,0 +1,42 @@ +// © Customize+. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok; + +namespace CustomizePlus.Data.Armature +{ + internal unsafe class PartialRootBone : ModelBone + { + private ModelBone PrimaryPartialBone; + + public PartialRootBone(Armature arm, ModelBone primaryBone, string codeName, int partialIdx) : base(arm, codeName, partialIdx, 0) + { + PrimaryPartialBone = primaryBone; + + //partial roots don't have ACTUAL parents, but for the sake of simplicty let's + //pretend that they're parented the same as their duplicates + if (PrimaryPartialBone.ParentBone is ModelBone pBone && pBone != null) + { + AddParent(pBone.PartialSkeletonIndex, pBone.BoneIndex); + } + } + + protected override BoneTransform CustomizedTransform { get => PrimaryPartialBone.GetTransformation(); } + + /// + /// Reference this partial root bone's duplicate model bone and copy its model space transform + /// wholesale. This presumes that the duplicate model bone has first completed its own spacial calcs. + /// + public void ApplyOriginalTransform(CharacterBase *cBase) + { + SetGameTransform(cBase, PrimaryPartialBone.GetGameTransform(cBase, true), true); + } + } +} diff --git a/CustomizePlus/Data/Armature/WeaponArmature.cs b/CustomizePlus/Data/Armature/WeaponArmature.cs new file mode 100644 index 0000000..c2337e7 --- /dev/null +++ b/CustomizePlus/Data/Armature/WeaponArmature.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using CustomizePlus.Data.Profile; +using CustomizePlus.Extensions; +using CustomizePlus.Helpers; + +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; + +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; + +namespace CustomizePlus.Data.Armature +{ + public unsafe class WeaponArmature : Armature + { + public CharacterArmature ParentArmature; + private bool _mainHand; + private bool _offHand => !_mainHand; + + private WeaponArmature(CharacterArmature chArm, bool mainHand) : base() + { + ParentArmature = chArm; + _mainHand = mainHand; + } + + public static WeaponArmature CreateMainHand(CharacterArmature arm, CharacterBase* cBase) + { + WeaponArmature output = new WeaponArmature(arm, true); + output.RebuildSkeleton(cBase); + return output; + } + public static WeaponArmature CreateOffHand(CharacterArmature arm, CharacterBase* cBase) + { + WeaponArmature output = new WeaponArmature(arm, false); + output.RebuildSkeleton(cBase); + return output; + } + + public override void UpdateOrDeleteRecord(string recordKey, BoneTransform? trans) + { + UpdateOrDeleteRecord(recordKey, trans, _mainHand + ? ParentArmature.Profile.MHBones + : ParentArmature.Profile.OHBones); + } + + private static void UpdateOrDeleteRecord(string key, BoneTransform? trans, Dictionary records) + { + if (trans == null) + records.Remove(key); + else + records[key] = trans; + } + + public override unsafe void RebuildSkeleton(CharacterBase* cBase) + { + if (cBase == null) + return; + + List> newPartials = _mainHand + ? ParseBonesFromObject(this, cBase->GetChild1(), ParentArmature.Profile.MHBones) + : ParseBonesFromObject(this, cBase->GetChild2(), ParentArmature.Profile.OHBones); + + _partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray(); + + foreach(ModelBone mb in newPartials.SelectMany(x => x)) + { + mb.FamilyName = _mainHand ? BoneData.BoneFamily.MainHand : BoneData.BoneFamily.OffHand; + } + } + + public override void ApplyTransformation(CharacterBase* cBase, bool applyScaling) + { + base.ApplyTransformation(_mainHand ? cBase->GetChild1() : cBase->GetChild2(), applyScaling); + } + + /// + public override string ToString() + { + string wep = _mainHand ? "MH" : "OH"; + string bInf = Built ? $"{BoneCount()} bone/s" : "no skeleton reference"; + + return $"Armature (#{_localId:00000}) on {ParentArmature.Profile.CharacterName}'s {wep} weapon with {bInf}"; + } + + public override IEnumerable GetBoneTransformValues(BoneAttribute attribute, PosingSpace space) + { + foreach(ModelBone mb in GetLocalAndDownstreamBones().Where(x => x is not PartialRootBone)) + { + TransformInfo trInfo = new(this, mb, attribute, space); + trInfo.BoneDisplayName = $"{(_mainHand ? "Main Hand" : "Off Hand")} {trInfo.BoneDisplayName}"; + trInfo.BoneFamilyName = _mainHand ? BoneData.BoneFamily.MainHand : BoneData.BoneFamily.OffHand; + + yield return trInfo; + } + } + } +} diff --git a/CustomizePlus/Data/BoneData.cs b/CustomizePlus/Data/BoneData.cs index e5e7716..7318aae 100644 --- a/CustomizePlus/Data/BoneData.cs +++ b/CustomizePlus/Data/BoneData.cs @@ -32,8 +32,10 @@ public enum BoneFamily Cape, Armor, Skirt, + MainHand, + OffHand, Equipment, - Unknown + Unknown, } //TODO move the csv data to an external (compressed?) file @@ -425,6 +427,8 @@ private static BoneFamily ParseFamilyName(string n) "armor" => BoneFamily.Armor, "skirt" => BoneFamily.Skirt, "equipment" => BoneFamily.Equipment, + "mainhand" => BoneFamily.MainHand, + "offhand" => BoneFamily.OffHand, _ => BoneFamily.Unknown }; diff --git a/CustomizePlus/Data/BoneTransform.cs b/CustomizePlus/Data/BoneTransform.cs index d26d9eb..5f789ed 100644 --- a/CustomizePlus/Data/BoneTransform.cs +++ b/CustomizePlus/Data/BoneTransform.cs @@ -2,10 +2,10 @@ // Licensed under the MIT license. using System; -using System.Numerics; using System.Runtime.Serialization; using CustomizePlus.Extensions; using FFXIVClientStructs.Havok; +using FFXIVClientStructs.FFXIV.Common.Math; namespace CustomizePlus.Data { @@ -15,7 +15,9 @@ public enum BoneAttribute //hard-coding the backing values for legacy purposes Position = 0, Rotation = 1, - Scale = 2 + Scale = 2, + FKPosition = 3, + FKRotation = 4 } [Serializable] @@ -29,82 +31,127 @@ public class BoneTransform public BoneTransform() { Translation = Vector3.Zero; + KinematicTranslation = Vector3.Zero; + Rotation = Vector3.Zero; + KinematicRotation = Vector3.Zero; + Scaling = Vector3.One; } - public BoneTransform(BoneTransform original) + public BoneTransform(BoneTransform original) : this() { UpdateToMatch(original); } - private Vector3 _translation; + private Vector3 _translation = Vector3.Zero; public Vector3 Translation { get => _translation; set => _translation = ClampVector(value); } - private Vector3 _rotation; + private Vector3 _rotation = Vector3.Zero; public Vector3 Rotation { get => _rotation; set => _rotation = ClampAngles(value); } - private Vector3 _scaling; + private Vector3 _scaling = Vector3.One; public Vector3 Scaling { get => _scaling; set => _scaling = ClampVector(value); } + private Vector3 _kinematicTranslation = Vector3.Zero; + public Vector3 KinematicTranslation + { + get => _kinematicTranslation; + set => _kinematicTranslation = ClampVector(value); + } + + private Vector3 _kinematicRotation = Vector3.Zero; + public Vector3 KinematicRotation + { + get => _kinematicRotation; + set => _kinematicRotation = ClampAngles(value); + } + [OnDeserialized] internal void OnDeserialized(StreamingContext context) { //Sanitize all values on deserialization - _translation = ClampToDefaultLimits(_translation); + _translation = ClampVector(_translation); _rotation = ClampAngles(_rotation); - _scaling = ClampToDefaultLimits(_scaling); + _scaling = ClampVector(_scaling); + + _kinematicTranslation = ClampVector(_kinematicTranslation); + _kinematicRotation = ClampAngles(_kinematicRotation); } + private const float VectorUnitEpsilon = 0.00001f; + private const float AngleUnitEpsilon = 0.1f; + public bool IsEdited() { - return !Translation.IsApproximately(Vector3.Zero, 0.00001f) - || !Rotation.IsApproximately(Vector3.Zero, 0.1f) - || !Scaling.IsApproximately(Vector3.One, 0.00001f); + return !Translation.IsApproximately(Vector3.Zero, VectorUnitEpsilon) + || !Rotation.IsApproximately(Vector3.Zero, AngleUnitEpsilon) + || !Scaling.IsApproximately(Vector3.One, VectorUnitEpsilon) + || !KinematicTranslation.IsApproximately(Vector3.Zero, VectorUnitEpsilon) + || !KinematicRotation.IsApproximately(Vector3.Zero, AngleUnitEpsilon); } public BoneTransform DeepCopy() { return new BoneTransform { - Translation = Translation, - Rotation = Rotation, - Scaling = Scaling + Translation = _translation, + Rotation = _rotation, + Scaling = _scaling, + KinematicTranslation = _kinematicTranslation, + KinematicRotation = _kinematicRotation }; } public void UpdateAttribute(BoneAttribute which, Vector3 newValue) { - if (which == BoneAttribute.Position) - { - Translation = newValue; - } - else if (which == BoneAttribute.Rotation) + switch (which) { - Rotation = newValue; - } - else - { - Scaling = newValue; + case BoneAttribute.Position: + Translation = newValue; + break; + + case BoneAttribute.FKPosition: + KinematicTranslation = newValue; + break; + + case BoneAttribute.Rotation: + Rotation = newValue; + break; + + case BoneAttribute.FKRotation: + KinematicRotation = newValue; + break; + + case BoneAttribute.Scale: + Scaling = newValue; + break; + + default: + throw new Exception("Invalid bone attribute!?"); } } public void UpdateToMatch(BoneTransform newValues) { Translation = newValues.Translation; + KinematicTranslation = newValues.KinematicTranslation; + Rotation = newValues.Rotation; + KinematicRotation = newValues.KinematicRotation; + Scaling = newValues.Scaling; } @@ -117,7 +164,11 @@ public BoneTransform GetStandardReflection() return new BoneTransform { Translation = new Vector3(Translation.X, Translation.Y, -1 * Translation.Z), + KinematicTranslation = new Vector3(_kinematicTranslation.X, _kinematicTranslation.Y, -1 * _kinematicTranslation.Z), + Rotation = new Vector3(-1 * Rotation.X, -1 * Rotation.Y, Rotation.Z), + KinematicRotation = new Vector3(-1 * _kinematicRotation.X, -1 * _kinematicRotation.Y, _kinematicRotation.Z), + Scaling = Scaling }; } @@ -131,7 +182,11 @@ public BoneTransform GetSpecialReflection() return new BoneTransform { Translation = new Vector3(Translation.X, -1 * Translation.Y, Translation.Z), + KinematicTranslation = new Vector3(_kinematicTranslation.X, -1 * _kinematicTranslation.Y, _kinematicTranslation.Z), + Rotation = new Vector3(Rotation.X, -1 * Rotation.Y, -1 * Rotation.Z), + KinematicRotation = new Vector3(_kinematicRotation.X, -1 * _kinematicRotation.Y, -1 * _kinematicRotation.Z), + Scaling = Scaling }; } @@ -142,14 +197,18 @@ public BoneTransform GetSpecialReflection() private void Sanitize() { _translation = ClampVector(_translation); + _kinematicTranslation = ClampVector(_kinematicTranslation); + _rotation = ClampAngles(_rotation); + _kinematicRotation = ClampAngles(_kinematicRotation); + _scaling = ClampVector(_scaling); } /// /// Clamp all vector values to be within allowed limits. /// - private Vector3 ClampVector(Vector3 vector) + private static Vector3 ClampVector(Vector3 vector) { return new Vector3 { @@ -161,29 +220,25 @@ private Vector3 ClampVector(Vector3 vector) private static Vector3 ClampAngles(Vector3 rotVec) { - static float Clamp(float angle) + static float Clamp_Helper(float angle) { - if (angle > 180) + while (angle > 180) angle -= 360; - else if (angle < -180) + + while (angle < -180) angle += 360; return angle; } - rotVec.X = Clamp(rotVec.X); - rotVec.Y = Clamp(rotVec.Y); - rotVec.Z = Clamp(rotVec.Z); + rotVec.X = Clamp_Helper(rotVec.X); + rotVec.Y = Clamp_Helper(rotVec.Y); + rotVec.Z = Clamp_Helper(rotVec.Z); return rotVec; } - public hkQsTransformf ModifyExistingTransform(hkQsTransformf tr) - { - return ModifyExistingTranslationWithRotation(ModifyExistingRotation(ModifyExistingScale(tr))); - } - - public hkQsTransformf ModifyExistingScale(hkQsTransformf tr) + public hkQsTransformf ModifyScale(hkQsTransformf tr) { tr.Scale.X *= Scaling.X; tr.Scale.Y *= Scaling.Y; @@ -192,9 +247,9 @@ public hkQsTransformf ModifyExistingScale(hkQsTransformf tr) return tr; } - public hkQsTransformf ModifyExistingRotation(hkQsTransformf tr) + public hkQsTransformf ModifyRotation(hkQsTransformf tr) { - var newRotation = Quaternion.Multiply(tr.Rotation.ToQuaternion(), Rotation.ToQuaternion()); + Quaternion newRotation = tr.Rotation.ToClientQuaternion() * Rotation.ToQuaternion(); tr.Rotation.X = newRotation.X; tr.Rotation.Y = newRotation.Y; tr.Rotation.Z = newRotation.Z; @@ -203,9 +258,33 @@ public hkQsTransformf ModifyExistingRotation(hkQsTransformf tr) return tr; } - public hkQsTransformf ModifyExistingTranslationWithRotation(hkQsTransformf tr) + public hkQsTransformf ModifyKinematicRotation(hkQsTransformf tr) { - var adjustedTranslation = Vector4.Transform(Translation, tr.Rotation.ToQuaternion()); + Quaternion newRotation = tr.Rotation.ToClientQuaternion() * KinematicRotation.ToQuaternion(); + tr.Rotation.X = newRotation.X; + tr.Rotation.Y = newRotation.Y; + tr.Rotation.Z = newRotation.Z; + tr.Rotation.W = newRotation.W; + + return tr; + } + + //public hkQsTransformf ModifyExistingRotationWithOffset(hkQsTransformf tr) + //{ + // Vector3 offset = BoneTranslation; + // tr.Translation = (tr.Translation.ToClientVector3() - offset).ToHavokVector(); + + // tr = ModifyBoneRotation(tr); + + // Vector3 modifiedOffset = Vector3.Transform(offset, BoneRotation.ToQuaternion()); + // tr.Translation = (tr.Translation.ToClientVector3() + modifiedOffset).ToHavokVector(); + + // return tr; + //} + + public hkQsTransformf ModifyTranslationWithRotation(hkQsTransformf tr) + { + var adjustedTranslation = Vector4.Transform(Translation, tr.Rotation.ToClientQuaternion()); tr.Translation.X += adjustedTranslation.X; tr.Translation.Y += adjustedTranslation.Y; tr.Translation.Z += adjustedTranslation.Z; @@ -214,7 +293,7 @@ public hkQsTransformf ModifyExistingTranslationWithRotation(hkQsTransformf tr) return tr; } - public hkQsTransformf ModifyExistingTranslation(hkQsTransformf tr) + public hkQsTransformf ModifyTranslationAsIs(hkQsTransformf tr) { tr.Translation.X += Translation.X; tr.Translation.Y += Translation.Y; @@ -223,16 +302,24 @@ public hkQsTransformf ModifyExistingTranslation(hkQsTransformf tr) return tr; } - /// - /// Clamp all vector values to be within allowed limits. - /// - private static Vector3 ClampToDefaultLimits(Vector3 vector) + public hkQsTransformf ModifyKineTranslationWithRotation(hkQsTransformf tr) + { + var adjustedTranslation = Vector4.Transform(KinematicTranslation, tr.Rotation.ToClientQuaternion()); + tr.Translation.X += adjustedTranslation.X; + tr.Translation.Y += adjustedTranslation.Y; + tr.Translation.Z += adjustedTranslation.Z; + tr.Translation.W += adjustedTranslation.W; + + return tr; + } + + public hkQsTransformf ModifyKineTranslationAsIs(hkQsTransformf tr) { - vector.X = Math.Clamp(vector.X, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit); - vector.Y = Math.Clamp(vector.Y, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit); - vector.Z = Math.Clamp(vector.Z, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit); + tr.Translation.X += KinematicTranslation.X; + tr.Translation.Y += KinematicTranslation.Y; + tr.Translation.Z += KinematicTranslation.Z; - return vector; + return tr; } } } \ No newline at end of file diff --git a/CustomizePlus/Data/Configuration/Version2/Version2BodyScale.cs b/CustomizePlus/Data/Configuration/Version2/Version2BodyScale.cs index fd9f7e8..e23b801 100644 --- a/CustomizePlus/Data/Configuration/Version2/Version2BodyScale.cs +++ b/CustomizePlus/Data/Configuration/Version2/Version2BodyScale.cs @@ -3,7 +3,8 @@ using System; using System.Collections.Generic; -using System.Numerics; + +using FFXIVClientStructs.FFXIV.Common.Math; namespace CustomizePlus.Data.Configuration.Version2 { diff --git a/CustomizePlus/Data/IBoneContainer.cs b/CustomizePlus/Data/IBoneContainer.cs new file mode 100644 index 0000000..d1de50b --- /dev/null +++ b/CustomizePlus/Data/IBoneContainer.cs @@ -0,0 +1,24 @@ +// © Customize+. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace CustomizePlus.Data +{ + /// + /// Represents a container of editable bones. + /// + public interface IBoneContainer + { + /// + /// For each bone in the container, retrieve the selected attribute within the given posing space. + /// + public IEnumerable GetBoneTransformValues(BoneAttribute attribute, Armature.PosingSpace space); + + /// + /// Given updated transformation info for a given bone (for the specific attribute, in the given posing space), + /// update that bone's transformation values to reflect the updated info. + /// + public void UpdateBoneTransformValue(TransformInfo newValue, BoneAttribute mode, bool mirrorChanges); + } +} diff --git a/CustomizePlus/Data/Profile/CharacterProfile.cs b/CustomizePlus/Data/Profile/CharacterProfile.cs index f916358..c9dcf46 100644 --- a/CustomizePlus/Data/Profile/CharacterProfile.cs +++ b/CustomizePlus/Data/Profile/CharacterProfile.cs @@ -2,7 +2,11 @@ // Licensed under the MIT license. using System; +using System.Linq; using System.Collections.Generic; + +using CustomizePlus.Data.Armature; + using Newtonsoft.Json; namespace CustomizePlus.Data.Profile @@ -12,13 +16,13 @@ namespace CustomizePlus.Data.Profile /// the information that gets saved to disk by the plugin. /// [Serializable] - public sealed class CharacterProfile + public sealed class CharacterProfile : IBoneContainer { [NonSerialized] private static int _nextGlobalId; [NonSerialized] private readonly int _localId; - [NonSerialized] public Armature.Armature? Armature; + [NonSerialized] public CharacterArmature? Armature; [NonSerialized] public string? OriginalFilePath; @@ -59,6 +63,8 @@ public CharacterProfile(CharacterProfile original) : this() [JsonIgnore] public int UniqueId => CreationDate.GetHashCode(); public Dictionary Bones { get; init; } = new(); + public Dictionary MHBones { get; init; } = new(); + public Dictionary OHBones { get; init; } = new(); /// /// Returns whether or not this profile applies to the object with the indicated name. @@ -93,5 +99,29 @@ public override int GetHashCode() { return UniqueId; } + + public IEnumerable GetBoneTransformValues(BoneAttribute attribute, PosingSpace space) + { + return Bones.Select(x => new TransformInfo(this, x.Key, x.Value, attribute, space)); + } + + public void UpdateBoneTransformValue(TransformInfo newTransform, BoneAttribute attribute, bool mirrorChanges) + { + if (!Bones.ContainsKey(newTransform.BoneCodeName)) + { + Bones[newTransform.BoneCodeName] = new BoneTransform(); + } + + Bones[newTransform.BoneCodeName].UpdateAttribute(attribute, newTransform.TransformationValue); + } + + public string SerializeToJSON() + { + return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings() + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ContractResolver = Api.VectorContractResolver.Instance + }); + } } } \ No newline at end of file diff --git a/CustomizePlus/Data/Profile/ProfileConverter.cs b/CustomizePlus/Data/Profile/ProfileConverter.cs index dca047e..7db6cb2 100644 --- a/CustomizePlus/Data/Profile/ProfileConverter.cs +++ b/CustomizePlus/Data/Profile/ProfileConverter.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Anamnesis; using CustomizePlus.Data.Configuration.Version0; using CustomizePlus.Data.Configuration.Version2; @@ -54,7 +54,7 @@ public static class ProfileConverter { var bt = new BoneTransform { - Scaling = kvp.Value.Scale!.GetAsNumericsVector() + Scaling = kvp.Value.Scale!.ToClientVector3() }; output.Bones[kvp.Key] = bt; @@ -65,14 +65,14 @@ public static class ProfileConverter var validRoot = pose.Bones.TryGetValue(Constants.RootBoneName, out var root) && root != null && root.Scale != null - && root.Scale.GetAsNumericsVector() != Vector3.Zero - && root.Scale.GetAsNumericsVector() != Vector3.One; + && root.Scale.ToClientVector3() != Vector3.Zero + && root.Scale.ToClientVector3() != Vector3.One; if (validRoot) { output.Bones[Constants.RootBoneName] = new BoneTransform { - Scaling = root.Scale!.GetAsNumericsVector() + Scaling = root.Scale!.ToClientVector3() }; } diff --git a/CustomizePlus/Data/Profile/ProfileManager.cs b/CustomizePlus/Data/Profile/ProfileManager.cs index 28e4b09..163d0cf 100644 --- a/CustomizePlus/Data/Profile/ProfileManager.cs +++ b/CustomizePlus/Data/Profile/ProfileManager.cs @@ -92,10 +92,14 @@ public void CheckForNewProfiles() /// saves it to disk. If the profile already exists (and it is not forced to be new) /// the given profile will overwrite the old one. /// - public void AddAndSaveProfile(CharacterProfile prof, bool forceNew = false) + public unsafe void AddAndSaveProfile(CharacterProfile prof, bool forceNew = false) { PruneIdempotentTransforms(prof); - prof.Armature = null; + if (prof.Armature != null + && prof.Armature.TryLinkSkeleton() == null) + { + prof.Armature = null; + } //if the profile is already in the list, simply replace it if (!forceNew && Profiles.Remove(prof)) @@ -124,10 +128,31 @@ public void AddAndSaveProfile(CharacterProfile prof, bool forceNew = false) } } + public void DuplicateProfile(CharacterProfile prof, string? newCharName, string? newProfName) + { + if (Profiles.Contains(prof)) + { + CharacterProfile dupe = new CharacterProfile(prof); + + if (newCharName != null) + { + dupe.CharacterName = newCharName; + } + + if (newProfName != null) + { + dupe.CharacterName = newProfName; + } + + AddAndSaveProfile(dupe); + } + } + public void DeleteProfile(CharacterProfile prof) { if (Profiles.Remove(prof)) { + Dalamud.Logging.PluginLog.LogInformation($"{prof} deleted"); ProfileReaderWriter.DeleteProfile(prof); } } @@ -162,68 +187,68 @@ public void AssertEnabledProfile(CharacterProfile activeProfile) /// Mark the given profile (if any) as currently being edited, and return /// a copy that can be safely mangled without affecting the old one. /// - public bool GetWorkingCopy(CharacterProfile prof, out CharacterProfile? copy) + public CharacterProfile? GetWorkingCopy(CharacterProfile prof) { - if (prof != null && ProfileOpenInEditor != prof) + if (prof != null && ProfileOpenInEditor == null) { Dalamud.Logging.PluginLog.LogInformation($"Creating new copy of {prof} for editing..."); - copy = new CharacterProfile(prof); + ProfileOpenInEditor = new CharacterProfile(prof); - PruneIdempotentTransforms(copy); - ProfileOpenInEditor = copy; - return true; + PruneIdempotentTransforms(ProfileOpenInEditor); + return ProfileOpenInEditor; } - copy = null; - return false; + return null; } - public void SaveWorkingCopy(CharacterProfile prof, bool editingComplete = false) + public void SaveWorkingCopy(bool editingComplete = false) { - if (ProfileOpenInEditor == prof) + if (ProfileOpenInEditor != null) { - Dalamud.Logging.PluginLog.LogInformation($"Saving changes to {prof} to manager..."); + Dalamud.Logging.PluginLog.LogInformation($"Saving changes to {ProfileOpenInEditor} to manager..."); + + AddAndSaveProfile(ProfileOpenInEditor); - AddAndSaveProfile(prof); + //Send OnProfileUpdate if this is profile of the current player and it's enabled + if (ProfileOpenInEditor.CharacterName == GameDataHelper.GetPlayerName() && ProfileOpenInEditor.Enabled) + Plugin.IPCManager.OnLocalPlayerProfileUpdate(); if (editingComplete) { - StopEditing(prof); + StopEditing(); } - - //Send OnProfileUpdate if this is profile of the current player and it's enabled - if (prof.CharacterName == GameDataHelper.GetPlayerName() && prof.Enabled) - Plugin.IPCManager.OnLocalPlayerProfileUpdate(); } } - public void RevertWorkingCopy(CharacterProfile prof) + public void RevertWorkingCopy() { - var original = GetProfileByUniqueId(prof.UniqueId); - Dalamud.Logging.PluginLog.LogInformation($"Reverting {prof} to its original state..."); - - if (original != null - && Profiles.Contains(prof) - && ProfileOpenInEditor == prof) + if (ProfileOpenInEditor != null) { - foreach (var kvp in prof.Bones) + var original = GetProfileByUniqueId(ProfileOpenInEditor.UniqueId); + + if (original != null) { - if (original.Bones.TryGetValue(kvp.Key, out var bt) && bt != null) - { - prof.Bones[kvp.Key].UpdateToMatch(bt); - } - else + Dalamud.Logging.PluginLog.LogInformation($"Reverting {ProfileOpenInEditor} to its original state..."); + + foreach (var kvp in ProfileOpenInEditor.Bones) { - prof.Bones.Remove(kvp.Key); + if (original.Bones.TryGetValue(kvp.Key, out var bt) && bt != null) + { + ProfileOpenInEditor.Bones[kvp.Key].UpdateToMatch(bt); + } + else + { + ProfileOpenInEditor.Bones.Remove(kvp.Key); + } } } } } - public void StopEditing(CharacterProfile prof) + public void StopEditing() { - Dalamud.Logging.PluginLog.LogInformation($"{prof} deleted"); + Dalamud.Logging.PluginLog.LogInformation($"{ProfileOpenInEditor} deleted"); ProfileOpenInEditor = null; } diff --git a/CustomizePlus/Data/Profile/ProfileReaderWriter.cs b/CustomizePlus/Data/Profile/ProfileReaderWriter.cs index 0903d78..66e8f67 100644 --- a/CustomizePlus/Data/Profile/ProfileReaderWriter.cs +++ b/CustomizePlus/Data/Profile/ProfileReaderWriter.cs @@ -45,7 +45,7 @@ public static void SaveProfile(CharacterProfile prof, bool archival = false) if (!archival) { - var json = JsonConvert.SerializeObject(prof, Formatting.Indented); + string json = prof.SerializeToJSON(); File.WriteAllText(newFilePath, json); } diff --git a/CustomizePlus/Data/TransformInfo.cs b/CustomizePlus/Data/TransformInfo.cs new file mode 100644 index 0000000..9ec6dff --- /dev/null +++ b/CustomizePlus/Data/TransformInfo.cs @@ -0,0 +1,84 @@ +// © Customize+. +// Licensed under the MIT license. + +using CustomizePlus.Data.Armature; + +using FFXIVClientStructs.FFXIV.Common.Math; + +namespace CustomizePlus.Data +{ + /// + /// Represents a chunk of editable information about a bone. + /// + public class TransformInfo + { + /// + /// The container from which this transformation information was retrieved. + /// + private IBoneContainer _sourceContainer; + + public string BoneCodeName { get; } + public string BoneDisplayName { get; set; } + public BoneData.BoneFamily BoneFamilyName { get; set; } + + public Vector3 TransformationValue { get; set; } + public BoneAttribute Attribute { get; } + public PosingSpace ReferenceFrame { get; } + + private TransformInfo(IBoneContainer container, string codename, BoneAttribute att, PosingSpace ps) + { + _sourceContainer = container; + BoneCodeName = codename; + Attribute = att; + ReferenceFrame = ps; + + BoneDisplayName = BoneData.GetBoneDisplayName(BoneCodeName); + BoneFamilyName = BoneData.GetBoneFamily(BoneCodeName); + } + + /// + /// Initializes a new instance of the class + /// by referencing values from a model bone. (i.e. instantiating from an armature). + /// + public TransformInfo(IBoneContainer container, ModelBone mb, BoneAttribute att, PosingSpace ps) + : this(container, mb.BoneName, att, ps) + { + BoneTransform bt = mb.GetTransformation(); + + TransformationValue = att switch + { + BoneAttribute.Position => bt.Translation, + BoneAttribute.FKPosition => bt.KinematicTranslation, + BoneAttribute.Rotation => bt.Rotation, + BoneAttribute.FKRotation => bt.KinematicRotation, + _ => bt.Scaling + }; + } + + /// + /// Initializes a new instance of the class + /// using raw transformation values and a given codename. (i.e. instantiating from a plain CharacterProfile). + /// + public TransformInfo(IBoneContainer container, string codename, BoneTransform tr, BoneAttribute att, PosingSpace ps) + : this(container, codename, att, ps) + { + TransformationValue = att switch + { + BoneAttribute.Position => tr.Translation, + BoneAttribute.FKPosition => tr.KinematicTranslation, + BoneAttribute.Rotation => tr.Rotation, + BoneAttribute.FKRotation => tr.KinematicRotation, + _ => tr.Scaling + }; + } + + /// + /// Push this transformation info back to its source container, updating it with any changes made + /// to the information since it was first retrieved. + /// + public void PushChanges(BoneAttribute attribute, bool mirrorChanges) + { + _sourceContainer.UpdateBoneTransformValue(this, attribute, mirrorChanges); + } + } +} diff --git a/CustomizePlus/Extensions/CharacterBaseExtensions.cs b/CustomizePlus/Extensions/CharacterBaseExtensions.cs new file mode 100644 index 0000000..5753b7e --- /dev/null +++ b/CustomizePlus/Extensions/CharacterBaseExtensions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using CustomizePlus.Data.Armature; +using Lumina.Excel.GeneratedSheets; + +namespace CustomizePlus.Extensions +{ + //Thanks to Ktisis contributors for discovering some of these previously-undocumented class members. + public static class CharacterBaseExtensions + { + public static unsafe CharacterBase* GetChild1(this CharacterBase cBase) + { + if (cBase.DrawObject.Object.ChildObject != null) + { + CharacterBase* child1 = (CharacterBase*)cBase.DrawObject.Object.ChildObject; + + if (child1 != null + && child1->GetModelType() == CharacterBase.ModelType.Weapon + && child1->Skeleton->PartialSkeletonCount > 0) + { + return child1; + } + } + + return null; + } + + public static unsafe CharacterBase* GetChild2(this CharacterBase cBase) + { + CharacterBase* child1 = cBase.GetChild1(); + + if (child1 != null) + { + CharacterBase* child2 = (CharacterBase*)child1->DrawObject.Object.NextSiblingObject; + + if (child2 != null + && child1 != child2 + && child2->GetModelType() == CharacterBase.ModelType.Weapon + && child2->Skeleton->PartialSkeletonCount > 0) + { + return child2; + } + } + + return null; + } + public static unsafe float Height(this CharacterBase cBase) + { + return *(float*)(new nint(&cBase) + 0x274); + } + + private unsafe static nint GetAttachPtr(CharacterBase cBase) + { + return new nint(&cBase) + 0x0D0; + } + + private unsafe static nint GetBoneAttachPtr(CharacterBase cBase) + { + return *(nint*)(GetAttachPtr(cBase) + 0x70); + } + + public static unsafe uint AttachType(this CharacterBase cBase) => *(uint*)(GetAttachPtr(cBase) + 0x50); + public static unsafe Skeleton* AttachTarget(this CharacterBase cBase) => *(Skeleton**)(GetAttachPtr(cBase) + 0x58); + public static unsafe Skeleton* AttachParent(this CharacterBase cBase) => *(Skeleton**)(GetAttachPtr(cBase) + 0x60); + public static unsafe uint AttachCount(this CharacterBase cBase) => *(uint*)(GetAttachPtr(cBase) + 0x68); + public static unsafe ushort AttachBoneID(this CharacterBase cBase) => *(ushort*)(GetBoneAttachPtr(cBase) + 0x02); + public static unsafe float AttachBoneScale(this CharacterBase cBase) => *(float*)(GetBoneAttachPtr(cBase) + 0x30); + } +} diff --git a/CustomizePlus/Extensions/TransformExtensions.cs b/CustomizePlus/Extensions/TransformExtensions.cs index a945f81..bc3e862 100644 --- a/CustomizePlus/Extensions/TransformExtensions.cs +++ b/CustomizePlus/Extensions/TransformExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using System; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Data; using FFXIVClientStructs.Havok; @@ -24,35 +24,13 @@ public static bool IsNull(this hkQsTransformf t) return t.Equals(Constants.NullTransform); } - public static hkQsTransformf ToHavokTransform(this BoneTransform bt) - { - return new hkQsTransformf - { - Translation = bt.Translation.ToHavokTranslation(), - Rotation = bt.Rotation.ToQuaternion().ToHavokRotation(), - Scale = bt.Scaling.ToHavokScaling() - }; - } - - public static BoneTransform ToBoneTransform(this hkQsTransformf t) - { - var rotVec = Quaternion.Divide(t.Translation.ToQuaternion(), t.Rotation.ToQuaternion()); - - return new BoneTransform - { - Translation = new Vector3(rotVec.X / rotVec.W, rotVec.Y / rotVec.W, rotVec.Z / rotVec.W), - Rotation = t.Rotation.ToQuaternion().ToEulerAngles(), - Scaling = new Vector3(t.Scale.X, t.Scale.Y, t.Scale.Z) - }; - } - - public static hkVector4f GetAttribute(this hkQsTransformf t, BoneAttribute att) + public static Vector4 GetAttribute(this hkQsTransformf t, BoneAttribute att) { return att switch { - BoneAttribute.Position => t.Translation, - BoneAttribute.Rotation => t.Rotation.ToQuaternion().GetAsNumericsVector().ToHavokVector(), - BoneAttribute.Scale => t.Scale, + BoneAttribute.Position => t.Translation.ToClientVector4(), + BoneAttribute.Rotation => t.Rotation.ToClientQuaternion().ToClientVector4(), + BoneAttribute.Scale => t.Scale.ToClientVector4(), _ => throw new NotImplementedException() }; } diff --git a/CustomizePlus/Extensions/VectorExtensions.cs b/CustomizePlus/Extensions/VectorExtensions.cs index b58f2dc..fb56712 100644 --- a/CustomizePlus/Extensions/VectorExtensions.cs +++ b/CustomizePlus/Extensions/VectorExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using System; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Anamnesis; using FFXIVClientStructs.Havok; @@ -10,18 +10,19 @@ namespace CustomizePlus.Extensions { internal static class VectorExtensions { - public static bool IsApproximately(this hkVector4f vector, Vector3 other, float errorMargin = 0.001f) + public static bool IsApproximately(this Vector3 vector, Vector3 other, float errorMargin = 0.001f) { return IsApproximately(vector.X, other.X, errorMargin) && IsApproximately(vector.Y, other.Y, errorMargin) && IsApproximately(vector.Z, other.Z, errorMargin); } - public static bool IsApproximately(this Vector3 vector, Vector3 other, float errorMargin = 0.001f) + public static bool IsApproximately(this Quaternion quat, Quaternion other, float errorMargin =0.001f) { - return IsApproximately(vector.X, other.X, errorMargin) - && IsApproximately(vector.Y, other.Y, errorMargin) - && IsApproximately(vector.Z, other.Z, errorMargin); + return IsApproximately(quat.X, other.X, errorMargin) + && IsApproximately(quat.Y, other.Y, errorMargin) + && IsApproximately(quat.Z, other.Z, errorMargin) + && IsApproximately(quat.W, other.W, errorMargin); } private static bool IsApproximately(float a, float b, float errorMargin) @@ -40,40 +41,15 @@ public static Quaternion ToQuaternion(this Vector3 rotation) public static Vector3 ToEulerAngles(this Quaternion q) { - var nq = Vector4.Normalize(q.GetAsNumericsVector()); - - var rollX = MathF.Atan2( - 2 * (nq.W * nq.X + nq.Y * nq.Z), - 1 - 2 * (nq.X * nq.X + nq.Y * nq.Y)); - - var pitchY = 2 * MathF.Atan2( - MathF.Sqrt(1 + 2 * (nq.W * nq.Y - nq.X * nq.Z)), - MathF.Sqrt(1 - 2 * (nq.W * nq.Y - nq.X * nq.Z))); - - var yawZ = MathF.Atan2( - 2 * (nq.W * nq.Z + nq.X * nq.Y), - 1 - 2 * (nq.Y * nq.Y + nq.Z * nq.Z)); - - return new Vector3(rollX, pitchY, yawZ); - } - - public static Quaternion ToQuaternion(this Vector4 rotation) - { - return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W); - } - - public static Quaternion ToQuaternion(this hkQuaternionf rotation) - { - return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W); + return q.EulerAngles; } - public static Quaternion ToQuaternion(this hkVector4f rotation) + public static Quaternion ToClientQuaternion(this hkQuaternionf rotation) { return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W); } - - public static hkQuaternionf ToHavokRotation(this Quaternion rotation) + public static hkQuaternionf ToHavokQuaternion(this Quaternion rotation) { return new hkQuaternionf { @@ -84,24 +60,18 @@ public static hkQuaternionf ToHavokRotation(this Quaternion rotation) }; } - public static hkVector4f ToHavokTranslation(this Vector3 translation) + public static Vector4 ToClientVector(this Quaternion quat) { - return new hkVector4f - { - X = translation.X, - Y = translation.Y, - Z = translation.Z, - W = 0.0f - }; + return new Vector4(quat.X, quat.Y, quat.Z, quat.W); } - public static hkVector4f ToHavokScaling(this Vector3 scaling) + public static hkVector4f ToHavokVector(this Vector3 vec) { return new hkVector4f { - X = scaling.X, - Y = scaling.Y, - Z = scaling.Z, + X = vec.X, + Y = vec.Y, + Z = vec.Z, W = 1.0f }; } @@ -117,24 +87,24 @@ public static hkVector4f ToHavokVector(this Vector4 vec) }; } - public static Vector3 GetAsNumericsVector(this PoseFile.Vector vec) + public static Vector3 ToClientVector3(this PoseFile.Vector vec) { return new Vector3(vec.X, vec.Y, vec.Z); } - public static Vector4 GetAsNumericsVector(this hkVector4f vec) + public static Vector3 ToClientVector3(this hkVector4f vec) { - return new Vector4(vec.X, vec.Y, vec.Z, vec.W); + return new Vector3(vec.X, vec.Y, vec.Z); } - public static Vector4 GetAsNumericsVector(this Quaternion q) + public static Vector4 ToClientVector4(this hkVector4f vec) { - return new Vector4(q.X, q.Y, q.Z, q.W); + return new Vector4(vec.X, vec.Y, vec.Z, vec.W); } - public static Vector3 RemoveWTerm(this Vector4 vec) + public static Vector4 ToClientVector4(this Quaternion q) { - return new Vector3(vec.X, vec.Y, vec.Z); + return new Vector4(q.X, q.Y, q.Z, q.W); } public static bool Equals(this hkVector4f first, hkVector4f second) diff --git a/CustomizePlus/Helpers/GameDataHelper.cs b/CustomizePlus/Helpers/GameDataHelper.cs index c2dc2f3..db1aff2 100644 --- a/CustomizePlus/Helpers/GameDataHelper.cs +++ b/CustomizePlus/Helpers/GameDataHelper.cs @@ -84,7 +84,9 @@ public static unsafe bool TryLookupCharacterBase(string name, out CharacterBase* cBase = anyObj.ToCharacterBase(); return true; } - else if (FindGameObjectByName(name) is DalamudObject obj) + else if (FindGameObjectByName(name) is DalamudObject obj + && obj.Address is nint objPtr + && objPtr != nint.Zero) { cBase = (CharacterBase*)((FFXIVClientObject*)obj.Address)->DrawObject; return true; @@ -209,7 +211,7 @@ public unsafe static string GetObjectName(DalamudObject obj) // // Check if in pvp intro sequence, which uses 240-244 for the 5 players, and only affect the first if so // // TODO: Ensure player side only. First group, where one of the node textures is blue. Alternately, look for hidden party list UI and get names from there. - // if (DalamudServices.GameGui.GetAddonByName("PvPMKSIntroduction", 1) == IntPtr.Zero) + // if (DalamudServices.GameGui.GetAddonByName("PvPMKSIntroduction", 1) == nint.Zero) // { // actualName = obj->ObjectIndex switch // { @@ -294,8 +296,8 @@ public unsafe static string GetObjectName(DalamudObject obj) var customize2 = ((FFXIVClientCharacter*)player.Address)->CustomizeData; for (var i = 0; i < 26; i++) { - var data1 = Marshal.ReadByte((IntPtr)customize1, i); - var data2 = Marshal.ReadByte((IntPtr)customize2, i); + var data1 = Marshal.ReadByte((nint)customize1, i); + var data2 = Marshal.ReadByte((nint)customize2, i); if (data1 != data2) { customizeEqual = false; @@ -309,7 +311,7 @@ public unsafe static string GetObjectName(DalamudObject obj) public static unsafe string? GetInspectName() { var addon = DalamudServices.GameGui.GetAddonByName("CharacterInspect"); - if (addon == IntPtr.Zero) + if (addon == nint.Zero) { return null; } @@ -354,7 +356,7 @@ public unsafe static string GetObjectName(DalamudObject obj) public static string? GetGlamourName() { var addon = DalamudServices.GameGui.GetAddonByName("MiragePrismMiragePlate"); - return addon == IntPtr.Zero ? null : GetPlayerName(); + return addon == nint.Zero ? null : GetPlayerName(); } public static string? GetPlayerName() @@ -370,8 +372,8 @@ public unsafe static string GetObjectName(DalamudObject obj) //and then make sure that the target in question is actually something with a skeleton if (target != null - && target.Address is IntPtr tgtPtr - && tgtPtr != IntPtr.Zero) + && target.Address is nint tgtPtr + && tgtPtr != nint.Zero) { var clientObj = (FFXIVClientObject*)tgtPtr; if (clientObj != null) diff --git a/CustomizePlus/Helpers/GameStateHelper.cs b/CustomizePlus/Helpers/GameStateHelper.cs index b951472..004bf4d 100644 --- a/CustomizePlus/Helpers/GameStateHelper.cs +++ b/CustomizePlus/Helpers/GameStateHelper.cs @@ -14,5 +14,16 @@ public static bool GameInPosingMode() || Services.PosingModeDetectService.Instance.IsInPosingMode; } + public static bool GameInPosingModeWithFrozenRotation() + { + return Services.GPoseService.Instance.GPoseState == Services.GPoseState.Inside + || Services.PosingModeDetectService.IsAnamnesisRotationFrozen; + } + + public static bool GameInPosingModeWithFrozenPosition() + { + return Services.GPoseService.Instance.GPoseState == Services.GPoseState.Inside + || Services.PosingModeDetectService.IsAnamnesisPositionFrozen; + } } } diff --git a/CustomizePlus/Plugin.cs b/CustomizePlus/Plugin.cs index 4a92dd5..d1c4a79 100644 --- a/CustomizePlus/Plugin.cs +++ b/CustomizePlus/Plugin.cs @@ -249,26 +249,11 @@ private static IntPtr OnRender(IntPtr a1, long a2, int a3, int a4) return original(a1, a2, a3, a4); } - //todo: doesn't work in cutscenes, something getting called after this and resets changes + //TODO: fully remove, later? private unsafe static void OnGameObjectMove(IntPtr gameObjectPtr) { // Call the original function. _gameObjectMovementHook.Original(gameObjectPtr); - - // If GPose and a 3rd-party posing service are active simultneously, abort - if (GameStateHelper.GameInPosingMode()) - { - return; - } - - if (DalamudServices.ObjectTable.CreateObjectReference(gameObjectPtr) is var obj - && obj != null - && ProfileManager.GetProfilesByGameObject(obj) .FirstOrDefault(x => x.Enabled) is CharacterProfile prof - && prof != null - && prof.Armature != null) - { - prof.Armature.ApplyRootTranslation(obj.ToCharacterBase()); - } } } } \ No newline at end of file diff --git a/CustomizePlus/Services/GPoseAmnesisKtisisWarningService.cs b/CustomizePlus/Services/GPoseAmnesisKtisisWarningService.cs index 9356607..3bfbd6d 100644 --- a/CustomizePlus/Services/GPoseAmnesisKtisisWarningService.cs +++ b/CustomizePlus/Services/GPoseAmnesisKtisisWarningService.cs @@ -1,7 +1,7 @@ // © Customize+. // Licensed under the MIT license. -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Core; using CustomizePlus.UI.Dialogs; diff --git a/CustomizePlus/UI/Windows/BoneEditWindow.cs b/CustomizePlus/UI/Windows/BoneEditWindow.cs index f62f791..68fe504 100644 --- a/CustomizePlus/UI/Windows/BoneEditWindow.cs +++ b/CustomizePlus/UI/Windows/BoneEditWindow.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Data; using CustomizePlus.Data.Armature; using CustomizePlus.Data.Profile; @@ -23,23 +23,14 @@ namespace CustomizePlus.UI.Windows public class BoneEditWindow : WindowBase { private bool _dirty; - private string? _originalCharName; - private string? _originalProfName; private int _precision = 3; - private Armature _targetArmature => _profileInProgress.Armature; - - /// - /// The character profile being edited. - /// - private CharacterProfile _profileInProgress = null!; - /// /// User-selected settings for this instance of the bone edit window. /// private EditorSessionSettings _settings; /// - protected override string Title => $"Edit Profile: {_profileInProgress.ProfileName}"; + protected override string Title => $"Edit Profile: {_settings.ProfileInProgress.ProfileName}"; /// protected override bool SingleInstance => true; @@ -62,37 +53,31 @@ public static void Show(CharacterProfile prof) { var editWnd = Plugin.InterfaceManager.Show(); - editWnd._profileInProgress = prof; - editWnd._originalCharName = prof.CharacterName; - editWnd._originalProfName = prof.ProfileName; - - //By having the armature manager to do its checks on this profile, - // we force it to generate and track a new armature for it - Plugin.ArmatureManager.ConstructArmatureForProfile(prof); - - editWnd._settings = new EditorSessionSettings(prof.Armature); - - //editWnd.ConfirmSkeletonConnection(); + editWnd._settings = new EditorSessionSettings(prof); } /// protected unsafe override void DrawContents() { CharacterBase* targetObject = null; - if (_profileInProgress.Enabled - && !GameDataHelper.TryLookupCharacterBase(_profileInProgress.CharacterName, out targetObject)) + + if (_settings.ArmatureInProgress != null) + { + targetObject = _settings.ArmatureInProgress.TryLinkSkeleton(); + } + + if (targetObject == null && _settings.ShowLiveBones) { - _profileInProgress.Enabled = false; + _settings.ToggleLiveBones(false); DisplayNoLinkMsg(); } - if (_profileInProgress.CharacterName != _originalCharName - || _profileInProgress.ProfileName != _originalProfName) + if (_settings.ProfileRenamed()) { _dirty = true; } - if (ImGui.BeginTable("##Save/Close", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) + if (ImGui.BeginTable("##ProfileSettings", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) { ImGui.TableSetupColumn("##CharName", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("##ProfName", ImGuiTableColumnFlags.WidthStretch); @@ -103,15 +88,15 @@ protected unsafe override void DrawContents() CtrlHelper.StaticLabel("Character Name", CtrlHelper.TextAlignment.Center); CtrlHelper.TextPropertyBox("##Character Name", - () => _profileInProgress.CharacterName, - (s) => _profileInProgress.CharacterName = s); + () => _settings.ProfileInProgress.CharacterName, + (s) => _settings.ProfileInProgress.CharacterName = s); ImGui.TableNextColumn(); CtrlHelper.StaticLabel("Profile Name", CtrlHelper.TextAlignment.Center); CtrlHelper.TextPropertyBox("##Profile Name", - () => _profileInProgress.ProfileName, - (s) => _profileInProgress.ProfileName = s); + () => _settings.ProfileInProgress.ProfileName, + (s) => _settings.ProfileInProgress.ProfileName = s); ImGui.TableNextColumn(); @@ -128,156 +113,126 @@ protected unsafe override void DrawContents() ImGui.Separator(); - int numColumns = Plugin.ConfigurationManager.Configuration.DebuggingModeEnabled ? 5 : 3; - - if (ImGui.BeginTable("Checkboxes", numColumns, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) + if (ImGui.BeginTable("EditingOptions", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) { - ImGui.TableSetupColumn("CheckEnabled", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("CheckLive", ImGuiTableColumnFlags.WidthStretch); - if (Plugin.ConfigurationManager.Configuration.DebuggingModeEnabled) - ImGui.TableSetupColumn("CheckAPose", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("CheckMirrored", ImGuiTableColumnFlags.WidthStretch); - if (Plugin.ConfigurationManager.Configuration.DebuggingModeEnabled) - ImGui.TableSetupColumn("CheckParented", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("RadioButtons", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Checkboxes", ImGuiTableColumnFlags.WidthFixed); ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); - var tempEnabled = _profileInProgress.Enabled; - if (CtrlHelper.Checkbox("Enable Preview", ref tempEnabled)) + if (GameStateHelper.GameInPosingMode()) ImGui.BeginDisabled(); + if (ImGui.RadioButton("Static Position", _settings.EditingAttribute == BoneAttribute.Position)) { - _profileInProgress.Enabled = tempEnabled; - ConfirmSkeletonConnection(); + _settings.EditingAttribute = BoneAttribute.Position; } - CtrlHelper.AddHoverText($"Hook the editor into the game to edit and preview live bone data"); - - ImGui.TableNextColumn(); + CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); - if (!_profileInProgress.Enabled) ImGui.BeginDisabled(); + ImGui.SameLine(); - if (CtrlHelper.Checkbox("Show Live Bones", ref _settings.ShowLiveBones)) + if (ImGui.RadioButton("Static Rotation", _settings.EditingAttribute == BoneAttribute.Rotation)) { - ConfirmSkeletonConnection(); + _settings.EditingAttribute = BoneAttribute.Rotation; } - CtrlHelper.AddHoverText($"If selected, present for editing all bones found in the game data,\nelse show only bones for which the profile already contains edits."); + CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); + if (GameStateHelper.GameInPosingMode()) ImGui.EndDisabled(); - if (Plugin.ConfigurationManager.Configuration.DebuggingModeEnabled) + ImGui.SameLine(); + if (ImGui.RadioButton("Scale", _settings.EditingAttribute == BoneAttribute.Scale)) { - ImGui.TableNextColumn(); - - var tempRefSnap = _targetArmature?.SnapToReferencePose ?? false; - if (_targetArmature != null && CtrlHelper.Checkbox("A-Pose", ref tempRefSnap)) - { - ConfirmSkeletonConnection(); - _targetArmature.SnapToReferencePose = tempRefSnap; - } - CtrlHelper.AddHoverText($"D: Force character into their default reference pose"); + _settings.EditingAttribute = BoneAttribute.Scale; } + ImGui.TableNextColumn(); ImGui.TableNextColumn(); - if (CtrlHelper.Checkbox("Mirror Mode", ref _settings.MirrorModeEnabled)) + if (CtrlHelper.Checkbox("Show Live Bones", ref _settings.ShowLiveBones)) { + _settings.ToggleLiveBones(_settings.ShowLiveBones); ConfirmSkeletonConnection(); } - CtrlHelper.AddHoverText($"Bone changes will be reflected from left to right and vice versa"); + CtrlHelper.AddHoverText($"If selected, present for editing all bones found in the game data,\nelse show only bones for which the profile already contains edits."); - if (Plugin.ConfigurationManager.Configuration.DebuggingModeEnabled) - { - ImGui.TableNextColumn(); + ImGui.SameLine(); - if (CtrlHelper.Checkbox("Parenting Mode", ref _settings.ParentingEnabled)) - { - ConfirmSkeletonConnection(); - } - CtrlHelper.AddHoverText($"D: Changes will propagate \"outward\" from edited bones"); + if (!_settings.ShowLiveBones || targetObject == null) ImGui.BeginDisabled(); + if (ImGui.Button("Reload Bone Data")) + { + _settings.ArmatureInProgress.RebuildSkeleton(targetObject); } + CtrlHelper.AddHoverText("Refresh the skeleton data obtained from in-game"); + if (!_settings.ShowLiveBones || targetObject == null) ImGui.EndDisabled(); - if (!_profileInProgress.Enabled) ImGui.EndDisabled(); - ImGui.EndTable(); - } - - ImGui.Separator(); - - if (ImGui.BeginTable("Misc", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) - { - ImGui.TableSetupColumn("Attributes", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("ReloadButton", ImGuiTableColumnFlags.WidthFixed); ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); if (GameStateHelper.GameInPosingMode()) ImGui.BeginDisabled(); - if (ImGui.RadioButton("Position", _settings.EditingAttribute == BoneAttribute.Position)) + + if (ImGui.RadioButton("Kinematic Position", _settings.EditingAttribute == BoneAttribute.FKPosition)) { - _settings.EditingAttribute = BoneAttribute.Position; + _settings.EditingAttribute = BoneAttribute.FKPosition; } CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); ImGui.SameLine(); - if (ImGui.RadioButton("Rotation", _settings.EditingAttribute == BoneAttribute.Rotation)) + + if (ImGui.RadioButton("Kinematic Rotation", _settings.EditingAttribute == BoneAttribute.FKRotation)) { - _settings.EditingAttribute = BoneAttribute.Rotation; + _settings.EditingAttribute = BoneAttribute.FKRotation; } CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); if (GameStateHelper.GameInPosingMode()) ImGui.EndDisabled(); - ImGui.SameLine(); - if (ImGui.RadioButton("Scale", _settings.EditingAttribute == BoneAttribute.Scale)) - { - _settings.EditingAttribute = BoneAttribute.Scale; - } - ImGui.TableNextColumn(); ImGui.TableNextColumn(); - if (!_profileInProgress.Enabled || targetObject == null) ImGui.BeginDisabled(); - if (ImGui.Button("Reload Bone Data")) + var tempRefSnap = _settings.ArmatureInProgress?.FrozenPose ?? false; + if (_settings.ArmatureInProgress != null && CtrlHelper.Checkbox("A-Pose", ref tempRefSnap)) { - _targetArmature.RebuildSkeleton(targetObject); + ConfirmSkeletonConnection(); + _settings.ArmatureInProgress.FrozenPose = tempRefSnap; } - CtrlHelper.AddHoverText("Refresh the skeleton data obtained from in-game"); - if (!_profileInProgress.Enabled || targetObject == null) ImGui.EndDisabled(); + CtrlHelper.AddHoverText($"Force character into their default reference pose"); - ImGui.EndTable(); - } + ImGui.SameLine(); + + if (!_settings.ShowLiveBones) ImGui.BeginDisabled(); - //if (!Settings.EditStack.UndoPossible()) ImGui.BeginDisabled(); - //if (ImGuiComponents.IconButton(FontAwesomeIcon.UndoAlt)) - //{ - // Settings.EditStack.Undo(); - //} - //CtrlHelper.AddHoverText("Undo last edit"); - //if (!Settings.EditStack.UndoPossible()) ImGui.EndDisabled(); + if (CtrlHelper.Checkbox("Mirror Mode", ref _settings.MirrorModeEnabled)) + { + ConfirmSkeletonConnection(); + } + CtrlHelper.AddHoverText($"Bone changes will be reflected from left to right and vice versa"); - //ImGui.SameLine(); + if (!_settings.ShowLiveBones) ImGui.EndDisabled(); + //ImGui.TableNextColumn(); - //if (!Settings.EditStack.RedoPossible()) ImGui.BeginDisabled(); - //if (ImGuiComponents.IconButton(FontAwesomeIcon.RedoAlt)) - //{ - // Settings.EditStack.Redo(); - //} - //CtrlHelper.AddHoverText("Redo next edit"); - //if (!Settings.EditStack.RedoPossible()) ImGui.EndDisabled(); + //if (CtrlHelper.Checkbox("Parenting Mode", ref _settings.ParentingEnabled)) + //{ + // ConfirmSkeletonConnection(); + //} + //CtrlHelper.AddHoverText($"Propagate changes \"outward\" from edited bones"); + ImGui.EndTable(); + } ImGui.Separator(); //CompleteBoneEditor("n_root"); - var col1Label = _settings.EditingAttribute == BoneAttribute.Rotation - ? "Roll" - : "X"; - var col2Label = _settings.EditingAttribute == BoneAttribute.Rotation - ? "Pitch" - : "Y"; - var col3Label = _settings.EditingAttribute == BoneAttribute.Rotation - ? "Yaw" - : "Z"; - var col4Label = _settings.EditingAttribute == BoneAttribute.Scale - ? "All" - : "N/A"; + string col1Label = "X"; + string col2Label = "Y"; + string col3Label = "Z"; + string col4Label = _settings.EditingAttribute == BoneAttribute.Scale ? "All" : "N/A"; + + if (_settings.EditingAttribute == BoneAttribute.Rotation || _settings.EditingAttribute == BoneAttribute.FKRotation) + { + col1Label = "Roll"; + col2Label = "Pitch"; + col3Label = "Yaw"; + } if (ImGui.BeginTable("Bones", 6, ImGuiTableFlags.BordersOuterH | ImGuiTableFlags.BordersV | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 56))) @@ -296,17 +251,18 @@ protected unsafe override void DrawContents() ImGui.TableHeadersRow(); - if (_profileInProgress != null || _targetArmature != null) + if (_settings.ArmatureInProgress != null || _settings.ProfileInProgress != null) { - IEnumerable relevantModelBones = _settings.ShowLiveBones && _targetArmature != null - ? _targetArmature.GetAllBones().DistinctBy(x => x.BoneName).Select(x => new EditRowParams(x)) - : _profileInProgress.Bones.Select(x => new EditRowParams(x.Key, x.Value)); + IBoneContainer container = _settings.ShowLiveBones && _settings.ArmatureInProgress != null + ? _settings.ArmatureInProgress + : _settings.ProfileInProgress; - var groupedBones = relevantModelBones.GroupBy(x => BoneData.GetBoneFamily(x.BoneCodeName)); + var groupedBones = container.GetBoneTransformValues(_settings.EditingAttribute, _settings.ReferenceFrame) + .GroupBy(x => x.BoneFamilyName).ToList(); foreach (var boneGroup in groupedBones.OrderBy(x => (int)x.Key)) { - //Hide root bone if it's not enabled in settings + //Hide root bone group if it's not enabled in settings if (boneGroup.Key == BoneData.BoneFamily.Root && !Plugin.ConfigurationManager.Configuration.RootPositionEditingEnabled) continue; @@ -338,9 +294,9 @@ protected unsafe override void DrawContents() if (expanded) { - foreach (EditRowParams erp in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneCodeName))) + foreach (TransformInfo trInfo in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneCodeName))) { - CompleteBoneEditor(erp); + CompleteBoneEditor(trInfo); } } @@ -369,7 +325,7 @@ protected unsafe override void DrawContents() { if (_dirty) { - Plugin.ProfileManager.SaveWorkingCopy(_profileInProgress, false); + Plugin.ProfileManager.SaveWorkingCopy(false); _dirty = false; } } @@ -381,7 +337,7 @@ protected unsafe override void DrawContents() { if (_dirty) { - Plugin.ProfileManager.SaveWorkingCopy(_profileInProgress, true); + Plugin.ProfileManager.SaveWorkingCopy(true); _dirty = false; } @@ -399,7 +355,10 @@ protected unsafe override void DrawContents() ConfirmationDialog.Show("Revert all unsaved work?", () => { - Plugin.ProfileManager.RevertWorkingCopy(_profileInProgress); + bool useAPose = _settings.ArmatureInProgress.FrozenPose; + Plugin.ProfileManager.RevertWorkingCopy(); + Plugin.ArmatureManager.ConstructArmatureForProfile(_settings.ProfileInProgress, true); + _settings.ArmatureInProgress.FrozenPose = useAPose; _dirty = false; }); } @@ -415,16 +374,14 @@ protected unsafe override void DrawContents() ConfirmationDialog.Show("Close editor and abandon all unsaved work?", () => { - Plugin.ProfileManager.RevertWorkingCopy(_profileInProgress); - Plugin.ProfileManager.StopEditing(_profileInProgress); - _dirty = false; + Plugin.ProfileManager.StopEditing(); Close(); }); } else { //convenient data handling means we just drop it - Plugin.ProfileManager.StopEditing(_profileInProgress); + Plugin.ProfileManager.StopEditing(); Close(); } } @@ -442,27 +399,21 @@ protected unsafe override void DrawContents() /// public unsafe void ConfirmSkeletonConnection() { - if (_targetArmature == null || !_targetArmature.TryLinkSkeleton()) - { - _profileInProgress.Enabled = false; - - _settings.ShowLiveBones = false; - _settings.MirrorModeEnabled = false; - _settings.ParentingEnabled = false; - DisplayNoLinkMsg(); - } - else if (!_profileInProgress.Enabled) + if (_settings.ArmatureInProgress == null || _settings.ArmatureInProgress.TryLinkSkeleton() == null) { - _settings.ShowLiveBones = false; - _settings.MirrorModeEnabled = false; - _settings.ParentingEnabled = false; + if (_settings.ShowLiveBones) + { + _settings.ToggleLiveBones(false); + _settings.MirrorModeEnabled = false; + DisplayNoLinkMsg(); + } } } public void DisplayNoLinkMsg() { var msg = - $"The editor can't find {_profileInProgress.CharacterName} or their bone data in the game's memory.\nCertain editing features will be unavailable."; + $"The editor can't find {_settings.ProfileInProgress.CharacterName} or their bone data in the game's memory.\nAs a result, certain editing features will be unavailable."; MessageDialog.Show(msg); } @@ -496,21 +447,28 @@ private bool RevertBoneButton(string codename, ref Vector3 value) { //if the backup scale doesn't contain bone values to revert TO, then just reset it - value = Plugin.ProfileManager.Profiles.TryGetValue(_profileInProgress, out var oldProf) + if (Plugin.ProfileManager.GetProfileByUniqueId(_settings.ProfileInProgress.UniqueId) is CharacterProfile oldProf && oldProf != null && oldProf.Bones.TryGetValue(codename, out var bec) - && bec != null - ? _settings.EditingAttribute switch + && bec != null) + { + value = _settings.EditingAttribute switch { BoneAttribute.Position => bec.Translation, + BoneAttribute.FKPosition => bec.KinematicTranslation, BoneAttribute.Rotation => bec.Rotation, + BoneAttribute.FKRotation => bec.KinematicRotation, _ => bec.Scaling - } - : _settings.EditingAttribute switch + }; + } + else + { + value = _settings.EditingAttribute switch { BoneAttribute.Scale => Vector3.One, _ => Vector3.Zero }; + } } return output; @@ -518,9 +476,9 @@ private bool RevertBoneButton(string codename, ref Vector3 value) private bool FullBoneSlider(string label, ref Vector3 value) { - float velocity = _settings.EditingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f; - float minValue = _settings.EditingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f; - float maxValue = _settings.EditingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f; + float velocity = _settings.EditingRotation ? 0.1f : 0.001f; + float minValue = _settings.EditingRotation ? -360.0f : -10.0f; + float maxValue = _settings.EditingRotation ? 360.0f : 10.0f; float temp = _settings.EditingAttribute switch { @@ -542,9 +500,9 @@ private bool FullBoneSlider(string label, ref Vector3 value) private bool SingleValueSlider(string label, ref float value) { - var velocity = _settings.EditingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f; - var minValue = _settings.EditingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f; - var maxValue = _settings.EditingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f; + var velocity = _settings.EditingRotation ? 0.1f : 0.001f; + var minValue = _settings.EditingRotation ? -360.0f : -10.0f; + var maxValue = _settings.EditingRotation ? 360.0f : 10.0f; ImGui.PushItemWidth(ImGui.GetColumnWidth()); var temp = value; @@ -557,20 +515,14 @@ private bool SingleValueSlider(string label, ref float value) return false; } - private void CompleteBoneEditor(EditRowParams bone) + private void CompleteBoneEditor(TransformInfo trInfo) { - string codename = bone.BoneCodeName; - string displayName = bone.BoneDisplayName; - BoneTransform transform = new BoneTransform(bone.Transform); + string codename = trInfo.BoneCodeName; + string displayName = trInfo.BoneDisplayName; bool flagUpdate = false; - Vector3 newVector = _settings.EditingAttribute switch - { - BoneAttribute.Position => transform.Translation, - BoneAttribute.Rotation => transform.Rotation, - _ => transform.Scaling - }; + Vector3 newVector = trInfo.TransformationValue; ImGui.PushID(codename); @@ -584,6 +536,9 @@ private void CompleteBoneEditor(EditRowParams bone) ImGui.SameLine(); flagUpdate |= RevertBoneButton(codename, ref newVector); + //TODO the sliders need to cache their value at the instant they're clicked into + //then transforms can be adjusted using the delta in relation to that cached value + //---------------------------------- ImGui.TableNextColumn(); flagUpdate |= SingleValueSlider($"##{displayName}-X", ref newVector.X); @@ -617,33 +572,9 @@ private void CompleteBoneEditor(EditRowParams bone) { _dirty = true; - //var whichValue = FrameStackManager.Axis.X; - //if (originalVector.Y != newVector.Y) - //{ - // whichValue = FrameStackManager.Axis.Y; - //} - - //if (originalVector.Z != newVector.Z) - //{ - // whichValue = FrameStackManager.Axis.Z; - //} - - //_settings.EditStack.Do(codename, Settings.EditingAttribute, whichValue, originalVector, newVector); - - transform.UpdateAttribute(_settings.EditingAttribute, newVector); - - //if we have access to the armature, then use it to push the values through - //as the bone information allows us to propagate them to siblings and children - //otherwise access them through the profile directly + trInfo.TransformationValue = newVector; - if (_profileInProgress.Enabled && _settings.ShowLiveBones) - { - bone.Basis.UpdateModel(transform, _settings.MirrorModeEnabled, _settings.ParentingEnabled); - } - else - { - _profileInProgress.Bones[codename].UpdateToMatch(transform); - } + trInfo.PushChanges(_settings.EditingAttribute, _settings.MirrorModeEnabled); } } @@ -652,43 +583,41 @@ private void CompleteBoneEditor(EditRowParams bone) public struct EditorSessionSettings { + public CharacterProfile ProfileInProgress { get; private set; } + private string? _originalCharName; + private string? _originalProfName; + + public CharacterArmature ArmatureInProgress => ProfileInProgress.Armature; + public bool ShowLiveBones = false; public bool MirrorModeEnabled = false; - public bool ParentingEnabled = false; + public BoneAttribute EditingAttribute = BoneAttribute.Scale; + public PosingSpace ReferenceFrame = PosingSpace.Self; public Dictionary GroupExpandedState = new(); - public EditorSessionSettings(Armature armRef) + public bool EditingRotation => EditingAttribute == BoneAttribute.Rotation || EditingAttribute == BoneAttribute.FKRotation; + + public void ToggleLiveBones(bool setTo) { - //EditStack = new FrameStackManager(armRef); - ShowLiveBones = armRef.Profile.Enabled; + ShowLiveBones = setTo; + ProfileInProgress.Enabled = setTo; } - } - /// - /// Simple structure for representing arguments to the editor table. - /// Can be constructed with or without access to a live armature. - /// - internal struct EditRowParams - { - public string BoneCodeName; - public string BoneDisplayName => BoneData.GetBoneDisplayName(BoneCodeName); - public BoneTransform Transform; - public ModelBone? Basis = null; + public bool ProfileRenamed() => _originalCharName != ProfileInProgress.CharacterName + || _originalProfName != ProfileInProgress.ProfileName; - public EditRowParams(ModelBone mb) + public EditorSessionSettings(CharacterProfile prof) { - BoneCodeName = mb.BoneName; - Transform = mb.CustomizedTransform; - Basis = mb; - } + ProfileInProgress = prof; + Plugin.ArmatureManager.ConstructArmatureForProfile(prof); - public EditRowParams(string codename, BoneTransform tr) - { - BoneCodeName = codename; - Transform = tr; - Basis = null; + _originalCharName = prof.CharacterName; + _originalProfName = prof.ProfileName; + + ProfileInProgress.Enabled = true; + ShowLiveBones = ProfileInProgress.Enabled; } } } \ No newline at end of file diff --git a/CustomizePlus/UI/Windows/Debug/BoneMonitorWindow.cs b/CustomizePlus/UI/Windows/Debug/BoneMonitorWindow.cs index f2fa870..86eed63 100644 --- a/CustomizePlus/UI/Windows/Debug/BoneMonitorWindow.cs +++ b/CustomizePlus/UI/Windows/Debug/BoneMonitorWindow.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using CustomizePlus.Data; using CustomizePlus.Data.Armature; using CustomizePlus.Data.Profile; @@ -22,9 +22,7 @@ namespace CustomizePlus.UI.Windows.Debug internal unsafe class BoneMonitorWindow : WindowBase { private readonly Dictionary _groupExpandedState = new(); - private readonly bool _modelFrozen = false; - private ModelBone.PoseType _targetPose; - private bool _aggregateDeforms; + private PosingSpace _targetPose; private BoneAttribute _targetAttribute; @@ -47,13 +45,14 @@ public static void Show(CharacterProfile prof) editWnd._targetProfile = prof; - Plugin.ArmatureManager.RenderCharacterProfiles(prof); + //Plugin.ArmatureManager.RenderCharacterProfiles(prof); editWnd._targetArmature = prof.Armature; } protected override void DrawContents() { - if (!GameDataHelper.TryLookupCharacterBase(_targetProfile.CharacterName, out CharacterBase* targetObject)) + if (!GameDataHelper.TryLookupCharacterBase(_targetProfile.CharacterName, out CharacterBase* targetObject) + && _targetProfile.Enabled) { _targetProfile.Enabled = false; @@ -61,74 +60,59 @@ protected override void DrawContents() //DisplayNoLinkMsg(); } - ImGui.TextUnformatted($"Character Name: {_targetProfile.CharacterName}"); - - ImGui.SameLine(); - ImGui.TextUnformatted("|"); - - ImGui.SameLine(); - ImGui.TextUnformatted($"Profile Name: {_targetProfile.ProfileName}"); - - ImGui.SameLine(); - ImGui.TextUnformatted("|"); - - ImGui.SameLine(); - var tempEnabled = _targetProfile.Enabled; - if (CtrlHelper.Checkbox("Live", ref tempEnabled)) + if (ImGui.BeginTable("Header", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip | ImGuiTableFlags.BordersInnerV)) { - _targetProfile.Enabled = tempEnabled; - } + ImGui.TableSetupColumn("AttributeType", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("ReferenceFrame", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reload", ImGuiTableColumnFlags.WidthFixed); - CtrlHelper.AddHoverText("Hook the editor into the game to edit and preview live bone data"); - - ImGui.Separator(); - - if (ImGui.RadioButton("Position", _targetAttribute == BoneAttribute.Position)) - _targetAttribute = BoneAttribute.Position; + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); - ImGui.SameLine(); - if (ImGui.RadioButton("Rotation", _targetAttribute == BoneAttribute.Rotation)) - _targetAttribute = BoneAttribute.Rotation; + if (ImGui.RadioButton("Position", _targetAttribute == BoneAttribute.Position)) + _targetAttribute = BoneAttribute.Position; - ImGui.SameLine(); - if (ImGui.RadioButton("Scale", _targetAttribute == BoneAttribute.Scale)) - _targetAttribute = BoneAttribute.Scale; + ImGui.SameLine(); + if (ImGui.RadioButton("Rotation", _targetAttribute == BoneAttribute.Rotation)) + _targetAttribute = BoneAttribute.Rotation; - ImGui.SameLine(); - ImGui.Spacing(); - ImGui.SameLine(); + ImGui.SameLine(); + if (ImGui.RadioButton("Scale", _targetAttribute == BoneAttribute.Scale)) + _targetAttribute = BoneAttribute.Scale; - ImGui.TextUnformatted("|"); + ImGui.TableNextColumn(); - ImGui.SameLine(); - ImGui.Spacing(); - ImGui.SameLine(); + if (ImGui.RadioButton("Local", _targetPose == PosingSpace.Self)) + _targetPose = PosingSpace.Self; - if (ImGui.RadioButton("Local", _targetPose == ModelBone.PoseType.Local)) - _targetPose = ModelBone.PoseType.Local; + ImGui.SameLine(); + if (ImGui.RadioButton("Model", _targetPose == PosingSpace.Parent)) + _targetPose = PosingSpace.Parent; - ImGui.SameLine(); - if (ImGui.RadioButton("Model", _targetPose == ModelBone.PoseType.Model)) - _targetPose = ModelBone.PoseType.Model; + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); - //ImGui.SameLine(); - //if (ImGui.RadioButton("Reference", _targetPose == ModelBone.PoseType.Reference)) - // _targetPose = ModelBone.PoseType.Reference; + if (!_targetProfile.Enabled) ImGui.BeginDisabled(); - //------------- + if (ImGui.Button("Reload Bone Data")) + _targetArmature.RebuildSkeleton(targetObject); - if (!_targetProfile.Enabled) - ImGui.BeginDisabled(); + if (!_targetProfile.Enabled) ImGui.EndDisabled(); - if (ImGui.Button("Reload Bone Data")) - _targetArmature.RebuildSkeleton(targetObject); + ImGui.SameLine(); + var tempEnabled = _targetProfile.Enabled; + if (CtrlHelper.Checkbox("Live", ref tempEnabled)) + { + _targetProfile.Enabled = tempEnabled; + } - if (!_targetProfile.Enabled) - ImGui.EndDisabled(); + ImGui.EndTable(); + } ImGui.Separator(); - if (ImGui.BeginTable("Bones", 9, + if (ImGui.BeginTable("Bones", 10, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 56))) { @@ -148,15 +132,18 @@ protected override void DrawContents() ImGui.TableSetupColumn("Bone Name", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Parent Bone", + ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); if (_targetArmature != null && targetObject != null) { - IEnumerable relevantModelBones = _targetArmature.GetAllBones(); + IEnumerable relevantModelBones = _targetArmature.GetLocalAndDownstreamBones(); - var groupedBones = relevantModelBones.GroupBy(x => BoneData.GetBoneFamily(x.BoneName)); + var groupedBones = relevantModelBones.GroupBy(x => x.FamilyName); foreach (var boneGroup in groupedBones.OrderBy(x => (int)x.Key)) { @@ -179,7 +166,9 @@ protected override void DrawContents() if (expanded) { - foreach (ModelBone mb in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneName))) + foreach (ModelBone mb in boneGroup + .OrderBy(x => x.PartialSkeletonIndex) + .ThenBy(x => x.BoneIndex)) { RenderTransformationInfo(mb, targetObject); } @@ -195,9 +184,34 @@ protected override void DrawContents() ImGui.Separator(); //---------------------------------- + if (ImGui.BeginTable("Footer", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoClip)) + { + ImGui.TableSetupColumn("HeightInfo", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("VarSpace", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Close", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Bumper", ImGuiTableColumnFlags.WidthFixed); + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + + if (targetObject != null) + { + CtrlHelper.StaticLabel($"Character Height: {targetObject->Height().ToString()}"); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + if (ImGui.Button("Close")) + Close(); + + ImGui.TableNextColumn(); + + ImGui.Dummy(new(CtrlHelper.IconButtonWidth, 0)); + + ImGui.EndTable(); + } - if (ImGui.Button("Cancel")) - Close(); } public void DisplayNoLinkMsg() @@ -218,11 +232,11 @@ private bool MysteryButton(string codename, ref Vector4 value) private void RenderTransformationInfo(ModelBone bone, CharacterBase* cBase) { - if (bone.GetGameTransform(cBase, _targetPose) is FFXIVClientStructs.Havok.hkQsTransformf deform) + if (bone.GetGameTransform(cBase, _targetPose == PosingSpace.Parent) is FFXIVClientStructs.Havok.hkQsTransformf deform) { var displayName = bone.ToString(); - var rowVector = deform.GetAttribute(_targetAttribute).GetAsNumericsVector(); + Vector4 rowVector = deform.GetAttribute(_targetAttribute); ImGui.PushID(bone.BoneName.GetHashCode()); @@ -261,6 +275,11 @@ private void RenderTransformationInfo(ModelBone bone, CharacterBase* cBase) ImGui.TableNextColumn(); CtrlHelper.StaticLabel(BoneData.GetBoneDisplayName(bone.BoneName)); + ImGui.TableNextColumn(); + CtrlHelper.StaticLabel(BoneData.GetBoneDisplayName(bone.ParentBone?.BoneName ?? "N/A"), + CtrlHelper.TextAlignment.Left, + bone.ParentBone?.ToString() ?? "N/A"); + ImGui.PopFont(); ImGui.PopID(); diff --git a/CustomizePlus/UI/Windows/MainWindow.cs b/CustomizePlus/UI/Windows/MainWindow.cs index 685b9b4..dc06278 100644 --- a/CustomizePlus/UI/Windows/MainWindow.cs +++ b/CustomizePlus/UI/Windows/MainWindow.cs @@ -4,7 +4,7 @@ using System; using System.IO; using System.Linq; -using System.Numerics; +using FFXIVClientStructs.FFXIV.Common.Math; using System.Windows.Forms; using CustomizePlus.Data; using CustomizePlus.Data.Profile; @@ -281,7 +281,7 @@ protected override void DrawContents() // Edit ImGui.TableNextColumn(); if (ImGuiComponents.IconButton(FontAwesomeIcon.Pen) - && Plugin.ProfileManager.GetWorkingCopy(prof, out var profCopy) + && Plugin.ProfileManager.GetWorkingCopy(prof) is CharacterProfile profCopy && profCopy != null) { BoneEditWindow.Show(profCopy); @@ -294,15 +294,10 @@ protected override void DrawContents() // Dupe ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy) - && Plugin.ProfileManager.GetWorkingCopy(prof, out var dupe) - && dupe != null) + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) { var newProfileName = ValidateProfileName(characterName, inputProfName); - dupe.ProfileName = newProfileName; - - Plugin.ProfileManager.StopEditing(dupe); - Plugin.ProfileManager.AddAndSaveProfile(dupe, true); + Plugin.ProfileManager.DuplicateProfile(prof, characterName, newProfileName); } CtrlHelper.AddHoverText("Duplicate Profile");