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");