From e4fdf8983c1197db4291cc5a35102496ec4e69d9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:38:11 +1000 Subject: [PATCH 01/19] Attach stuff --- FFXIVClientStructs | 2 +- Meddle/Meddle.Plugin/Models/AttachSet.cs | 23 ++++ Meddle/Meddle.Plugin/Models/Groups.cs | 2 - Meddle/Meddle.Plugin/Skeleton/Attach.cs | 38 ++++-- .../Meddle.Plugin/Skeleton/SkeletonUtils.cs | 111 ++++++++++++++-- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 125 +++++++++++++----- Meddle/Meddle.Plugin/Utils/ExportUtil.cs | 32 +++-- Meddle/Meddle.Utils/BoneNodeBuilder.cs | 24 ++-- 8 files changed, 268 insertions(+), 89 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Models/AttachSet.cs diff --git a/FFXIVClientStructs b/FFXIVClientStructs index a6c867e..6247b3c 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a6c867e7ee35b4a402956a95bae419b58b56cc0c +Subproject commit 6247b3cfcdaffbb6c6751d794e7b54dc6273c9e8 diff --git a/Meddle/Meddle.Plugin/Models/AttachSet.cs b/Meddle/Meddle.Plugin/Models/AttachSet.cs new file mode 100644 index 0000000..69ae224 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/AttachSet.cs @@ -0,0 +1,23 @@ +using Meddle.Plugin.Skeleton; +using SharpGLTF.Transforms; + +namespace Meddle.Plugin.Models; + + +public class AttachSet +{ + public AttachSet(string id, Attach attach, Skeleton.Skeleton ownerSkeleton, AffineTransform transform, string? ownerId) + { + Id = id; + Attach = attach; + OwnerSkeleton = ownerSkeleton; + Transform = transform; + OwnerId = ownerId; + } + + public string Id { get; set; } + public string? OwnerId { get; set; } + public Attach Attach { get; set; } + public Skeleton.Skeleton OwnerSkeleton { get; set; } + public AffineTransform Transform { get; set; } +} diff --git a/Meddle/Meddle.Plugin/Models/Groups.cs b/Meddle/Meddle.Plugin/Models/Groups.cs index 8505bc3..b3fe589 100644 --- a/Meddle/Meddle.Plugin/Models/Groups.cs +++ b/Meddle/Meddle.Plugin/Models/Groups.cs @@ -20,5 +20,3 @@ public record TexResourceGroup(string MtrlPath, string Path, TextureResource Res public record SklbFileGroup(string Path, SklbFile File); public record Resource(string MdlPath, Vector3 Position, Quaternion Rotation, Vector3 Scale); public record DeformerGroup(string Path, ushort RaceSexId, ushort DeformerId); -public record AnimationFrameData(DateTime Time, Skeleton.Skeleton Skeleton, AffineTransform Transform, AttachedSkeleton[] Attachments); -public record AttachedSkeleton(string AttachId, Skeleton.Skeleton Skeleton, Attach Attach); diff --git a/Meddle/Meddle.Plugin/Skeleton/Attach.cs b/Meddle/Meddle.Plugin/Skeleton/Attach.cs index b20bac0..8249e44 100644 --- a/Meddle/Meddle.Plugin/Skeleton/Attach.cs +++ b/Meddle/Meddle.Plugin/Skeleton/Attach.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.Interop; using Meddle.Utils.Skeletons; using CSAttach = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Attach; +using CSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton; namespace Meddle.Plugin.Skeleton; @@ -18,7 +19,6 @@ public Attach(CSAttach attach) case 0: // nothing to do here return; - // 1/2 -> not sure, seem to be transformed to a root item on exec case 3: { if (attach.OwnerCharacter->Skeleton != null) @@ -29,17 +29,33 @@ public Attach(CSAttach attach) { var transform = attach.SkeletonBoneAttachments[0]; OffsetTransform = new Transform(transform.ChildTransform); - - PartialSkeletonIdx = transform.SkeletonIdx; - // not really sure how correct this is just yet - if (OwnerSkeleton!.PartialSkeletons[PartialSkeletonIdx].BoneCount <= transform.BoneIdx) + PartialSkeletonIdx = transform.BoneIndexMask.SkeletonIdx; + + var ownerSkeleton = attach.OwnerCharacter->Skeleton; + + CSSkeleton.Bone? foundBone = null; + var foundBoneIdx = 0; + for (var i = 0; i < ownerSkeleton->AttachBoneCount; i++) { - BoneIdx = 0; // TODO: sub_14041DCA0 + var bone = ownerSkeleton->AttachBonesSpan[i]; + if (bone.BoneIndex == transform.BoneIndexMask.BoneIdx) + { + foundBone = bone; + foundBoneIdx = i; + break; + } } - else + + if (foundBone == null) { - BoneIdx = transform.BoneIdx; + // should default but gonna throw for now + throw new InvalidOperationException("Bone not found"); } + + var boneMask = ownerSkeleton->BoneMasksSpan[foundBoneIdx]; + // some case for if boneMask == -1 but meh + PartialSkeletonIdx = boneMask.SkeletonIdx; + BoneIdx = boneMask.BoneIdx; } break; } @@ -53,8 +69,8 @@ public Attach(CSAttach attach) var att = attach.SkeletonBoneAttachments[0]; OffsetTransform = new Transform(att.ChildTransform); - PartialSkeletonIdx = att.SkeletonIdx; - BoneIdx = att.BoneIdx; + PartialSkeletonIdx = att.BoneIndexMask.SkeletonIdx; + BoneIdx = att.BoneIndexMask.BoneIdx; } break; } @@ -71,5 +87,5 @@ public Attach(CSAttach attach) public Skeleton? OwnerSkeleton { get; } public Transform? OffsetTransform { get; } public byte PartialSkeletonIdx { get; } - public ushort BoneIdx { get; } + public uint BoneIdx { get; } } diff --git a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs index ec579bf..a570587 100644 --- a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs +++ b/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs @@ -2,6 +2,7 @@ using Meddle.Plugin.Models; using Meddle.Plugin.Skeleton; using SharpGLTF.Scenes; +using SharpGLTF.Transforms; namespace Meddle.Utils.Skeletons; @@ -44,8 +45,9 @@ public static List GetBoneMap( var bone = new BoneNodeBuilder(name) { - PartialSkeletonIndex = partialIdx, - BoneIndex = i + BoneIndex = i, + PartialSkeletonHandle = partial.HandlePath ?? throw new InvalidOperationException($"No handle path for {name} [{partialIdx},{i}]"), + PartialSkeletonIndex = partialIdx }; if (pose != null && includePose) { @@ -81,13 +83,13 @@ public static List GetBoneMap( return boneMap; } - public static List GetBoneMap(Skeleton skeleton, out BoneNodeBuilder? root) + public static List GetBoneMap(Skeleton skeleton, bool includePose, out BoneNodeBuilder? root) { - return GetBoneMap(skeleton.PartialSkeletons, true, out root); + return GetBoneMap(skeleton.PartialSkeletons, includePose, out root); } - public static List GetAnimatedBoneMap( - List animation, bool includePositionalData, out BoneNodeBuilder? root) + /*public static List GetAnimatedBoneMap( + List<(DateTime, FrameData)> animation, bool includePositionalData, out BoneNodeBuilder? root) { root = null; @@ -154,7 +156,7 @@ public static List GetAnimatedBoneMap( continue; var attachName = da.AttachFrame.Skeleton.PartialSkeletons[da.DistinctAttach.Attach.PartialSkeletonIdx] - .HkSkeleton!.BoneNames[da.DistinctAttach.Attach.BoneIdx]; + .HkSkeleton!.BoneNames[(int)da.DistinctAttach.Attach.BoneIdx]; var attachPointBone = boneMap.FirstOrDefault(x => x.BoneName.Equals(attachName, StringComparison.OrdinalIgnoreCase)); if (attachPointBone == null) continue; @@ -221,7 +223,7 @@ private static Dictionary> GetA return attachBonePoseMap; } - private static Dictionary> GetBonePoseMap(List boneMap, List animation) + private static Dictionary> GetBonePoseMap(List boneMap, List<(DateTime time, FrameData data)> animation) { var bonePoseMap = new Dictionary>(); @@ -232,15 +234,15 @@ private static Dictionary> GetB foreach (var frame in animation) { - var pose = frame.Skeleton.PartialSkeletons[bone.PartialSkeletonIndex.Value].Poses.FirstOrDefault(); + var pose = frame.data.Skeleton.PartialSkeletons[bone.PartialSkeletonIndex.Value].Poses.FirstOrDefault(); if (pose == null) continue; var transform = pose.Pose[bone.BoneIndex.Value]; - if (!bonePoseMap.TryGetValue(frame.Time, out var boneTransforms)) + if (!bonePoseMap.TryGetValue(frame.time, out var boneTransforms)) { boneTransforms = new Dictionary(); - bonePoseMap.Add(frame.Time, boneTransforms); + bonePoseMap.Add(frame.time, boneTransforms); } boneTransforms.TryAdd(bone, transform); @@ -248,6 +250,91 @@ private static Dictionary> GetB } return bonePoseMap; - } + }*/ + + public static Dictionary Bones, BoneNodeBuilder? Root)> GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) + { + var attachDict = new Dictionary Bones, BoneNodeBuilder? Root)>(); + var attachTimelines = new Dictionary>(); + foreach (var frame in frames) + { + foreach (var attach in frame.Attaches) + { + if (!attachTimelines.TryGetValue(attach.Id, out var timeline)) + { + timeline = new List<(DateTime Time, AttachSet Attach)>(); + attachTimelines.Add(attach.Id, timeline); + } + timeline.Add((frame.Time, attach)); + } + } + + var allTimes = frames.Select(x => x.Time).ToArray(); + + var startTime = frames.Min(x => x.Time); + foreach (var (attachId, timeline) in attachTimelines) + { + var firstAttach = timeline.First().Attach; + if (!attachDict.TryGetValue(attachId, out var attachBoneMap)) + { + attachBoneMap = ([], null); + attachDict.Add(attachId, attachBoneMap); + } + + foreach (var time in allTimes) + { + var frame = timeline.FirstOrDefault(x => x.Time == time); + var frameTime = (float)(time - startTime).TotalSeconds; + if (frame != default) + { + var newMap = GetBoneMap(frame.Attach.OwnerSkeleton, false, out var attachRoot); + if (attachRoot == null) + continue; + + attachBoneMap.Root ??= attachRoot; + + foreach (var attachBone in newMap) + { + var bone = attachBoneMap.Bones.FirstOrDefault( + x => x.BoneName.Equals(attachBone.BoneName, StringComparison.OrdinalIgnoreCase)); + if (bone == null) + { + attachBoneMap.Bones.Add(attachBone); + bone = attachBone; + } + + var partial = frame.Attach.OwnerSkeleton.PartialSkeletons[attachBone.PartialSkeletonIndex]; + if (partial.Poses.Count == 0) + continue; + + var transform = partial.Poses[0].Pose[bone.BoneIndex]; + bone.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, transform.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Translation); + } + + var firstTranslation = firstAttach.Transform.Translation; + attachRoot.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Scale); + attachRoot.UseRotation().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Rotation); + attachRoot.UseTranslation().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Translation - firstTranslation); + + attachDict[attachId] = attachBoneMap; + } + } + + foreach (var time in allTimes) + { + var frame = timeline.FirstOrDefault(x => x.Time == time); + if (frame != default) continue; + // set scaling to 0 when not present + foreach (var bone in attachBoneMap.Bones) + { + bone.UseScale().UseTrackBuilder("pose").WithPoint((float)(time - startTime).TotalSeconds, Vector3.Zero); + } + } + } + + return attachDict; + } } diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index 071bc65..e5422ea 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -4,7 +4,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.Havok.Common.Base.Math.QsTransform; -using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; using Meddle.Plugin.Services; @@ -13,7 +12,6 @@ using Microsoft.Extensions.Logging; using SharpGLTF.Transforms; using Attach = Meddle.Plugin.Skeleton.Attach; -using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace Meddle.Plugin.UI; @@ -31,7 +29,7 @@ public class AnimationTab : ITab public bool DisplayTab => true; private bool captureAnimation; private ICharacter? selectedCharacter; - private readonly List frames = new(); + private readonly List<(DateTime Time, AttachSet[])> frames = []; private bool includePositionalData; public AnimationTab(IFramework framework, ILogger logger, @@ -136,7 +134,7 @@ public unsafe void Draw() ImGui.Separator(); - if (ImGui.CollapsingHeader("Frames")) + /*if (ImGui.CollapsingHeader("Frames")) { // render frames foreach (var frame in frames.ToArray()) @@ -164,7 +162,7 @@ public unsafe void Draw() } } } - } + }*/ if (ImGui.CollapsingHeader("Skeleton")) { @@ -177,7 +175,6 @@ private unsafe void Capture() if (!captureAnimation) return; if (selectedCharacter == null) return; - // 60fps if (frames.Count > 0 && DateTime.UtcNow - frames[^1].Time < TimeSpan.FromMilliseconds(100)) { return; @@ -190,53 +187,91 @@ private unsafe void Capture() captureAnimation = false; return; } - var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; - if (cBase == null) + var root = (CharacterBase*)charPtr->GameObject.DrawObject; + if (root == null) { logger.LogWarning("CharacterBase is null"); captureAnimation = false; return; } - var skeleton = cBase->Skeleton; - if (skeleton == null) + var attachCollection = new List(); + var rootSkeleton = new Skeleton.Skeleton(root->Skeleton); + string rootName; + if (root->Attach.ExecuteType == 3) + { + var owner = root->Attach.OwnerCharacter; + var rootAttach = new Attach(root->Attach); + var ownerSkeleton = new Skeleton.Skeleton(owner->Skeleton); + var attachBoneName = ownerSkeleton.PartialSkeletons[rootAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)rootAttach.BoneIdx] ?? "Bone"; + rootName = $"{(nint)root:X8}_{attachBoneName}"; + var rootAttachSet = new AttachSet(rootName, rootAttach, rootSkeleton, GetTransform(root), $"{(nint)owner:X8}"); + attachCollection.Add(rootAttachSet); + attachCollection.Add(new AttachSet($"{(nint)owner:X8}", new Attach(owner->Attach), ownerSkeleton, GetTransform(owner), null)); + } + else { - logger.LogWarning("Skeleton is null"); - captureAnimation = false; - return; + rootName = $"{(nint)root:X8}"; + var rootAttach = new AttachSet(rootName, new Attach(root->Attach), rootSkeleton, GetTransform(root), null); + attachCollection.Add(rootAttach); } - var mSkele = new Skeleton.Skeleton(skeleton); - var position = cBase->Position; - var rotation = cBase->Rotation; - var scale = cBase->Scale; - var transform = new AffineTransform(scale, rotation, position); - - var attachments = new List(); + foreach (var characterAttach in GetAttachData(charPtr, rootSkeleton, rootName)) + { + // skip ie. mount may be the owner of the character already so we don't want to duplicate + if (attachCollection.Any(a => a.Id == characterAttach.Id)) + { + continue; + } + attachCollection.Add(characterAttach); + } + frames.Add((DateTime.UtcNow, attachCollection.ToArray())); + } + + public static unsafe AffineTransform GetTransform(CharacterBase* character) + { + var position = character->Position; + var rotation = character->Rotation; + var scale = character->Scale; + return new AffineTransform(scale, rotation, position); + } + + private unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Skeleton ownerSkeleton, string ownerId) + { + var attachments = new List(); var ornament = charPtr->OrnamentData.OrnamentObject; var companion = charPtr->CompanionData.CompanionObject; var mount = charPtr->Mount.MountObject; var weaponData = charPtr->DrawData.WeaponData; - + if (ornament != null && ornament->DrawObject != null && ornament->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var ornamentBase = (CharacterBase*)ornament->DrawObject; - attachments.Add(new AttachedSkeleton($"{(nint)ornamentBase:X8}", new Skeleton.Skeleton(ornamentBase->Skeleton), new Attach(ornamentBase->Attach))); + var ornamentAttach = new Attach(ornamentBase->Attach); + var attachBoneName = ownerSkeleton.PartialSkeletons[ornamentAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)ornamentAttach.BoneIdx] ?? "Bone"; + attachments.Add(new ($"{(nint)ornamentBase:X8}_{attachBoneName}", ornamentAttach, + new Skeleton.Skeleton(ornamentBase->Skeleton), GetTransform(ornamentBase), ownerId)); } - + if (companion != null && companion->DrawObject != null && companion->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var companionBase = (CharacterBase*)companion->DrawObject; - attachments.Add(new AttachedSkeleton($"{(nint)companionBase:X8}", new Skeleton.Skeleton(companionBase->Skeleton), new Attach(companionBase->Attach))); + var companionAttach = new Attach(companionBase->Attach); + var attachBoneName = ownerSkeleton.PartialSkeletons[companionAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)companionAttach.BoneIdx] ?? "Bone"; + attachments.Add(new ($"{(nint)companionBase:X8}_{attachBoneName}", companionAttach, + new Skeleton.Skeleton(companionBase->Skeleton), GetTransform(companionBase), ownerId)); } - + if (mount != null && mount->DrawObject != null && mount->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var mountBase = (CharacterBase*)mount->DrawObject; - attachments.Add(new AttachedSkeleton($"{(nint)mountBase:X8}", new Skeleton.Skeleton(mountBase->Skeleton), new Attach(mountBase->Attach))); + var mountAttach = new Attach(mountBase->Attach); + var attachBoneName = ownerSkeleton.PartialSkeletons[mountAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)mountAttach.BoneIdx] ?? "Bone"; + attachments.Add(new ($"{(nint)mountBase:X8}_{attachBoneName}", mountAttach, + new Skeleton.Skeleton(mountBase->Skeleton), GetTransform(mountBase), ownerId)); } - + if (weaponData != null) { for (var i = 0; i < weaponData.Length; ++i) @@ -245,13 +280,17 @@ private unsafe void Capture() if (weapon.DrawObject != null && weapon.DrawObject->GetObjectType() == ObjectType.CharacterBase) { var weaponBase = (CharacterBase*)weapon.DrawObject; - attachments.Add(new AttachedSkeleton($"{(nint)weaponBase:X8}", new Skeleton.Skeleton(weaponBase->Skeleton), new Attach(weaponBase->Attach))); + var weaponAttach = new Attach(weaponBase->Attach); + var attachBoneName = ownerSkeleton.PartialSkeletons[weaponAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)weaponAttach.BoneIdx] ?? "Bone"; + attachments.Add(new ($"{(nint)weaponBase:X8}_{attachBoneName}", weaponAttach, + new Skeleton.Skeleton(weaponBase->Skeleton), GetTransform(weaponBase), ownerId)); } } } - frames.Add(new AnimationFrameData(DateTime.UtcNow, mSkele, transform, attachments.ToArray())); + return attachments.ToArray(); } + private void DrawSkeleton(Skeleton.Skeleton skeleton) { @@ -477,10 +516,10 @@ private unsafe void DrawCharacterBase(CharacterBase* character, string name) var modelType = character->GetModelType(); var attachHeader = $"[{modelType}]{name} Attach Pose ({attachPoint.ExecuteType},{attachPoint.AttachmentCount})"; - if (character->Attach.ExecuteType > 3) + if (character->Attach.ExecuteType >= 3) { var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; - var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[attachPoint.BoneIdx]; + var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[(int)attachPoint.BoneIdx]; attachHeader += $" at {boneName}"; } else if (character->Attach.ExecuteType == 3) @@ -488,7 +527,7 @@ private unsafe void DrawCharacterBase(CharacterBase* character, string name) var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; if (attachedPartialSkeleton.HkSkeleton != null && attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) { - var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[attachPoint.BoneIdx]; + var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[(int)attachPoint.BoneIdx]; attachHeader += $" at {boneName}"; } else @@ -543,14 +582,30 @@ private unsafe void DrawAttachInfo(CharacterBase* character, Attach attachPoint) private unsafe void DrawModels(CharacterBase* character) { + using var modelIndent = ImRaii.PushIndent(); var models = character->ModelsSpan; foreach (var model in models) { if (model == null) continue; - - var boneCount = model.Value->BoneCount; - ImGui.Text($"Model at: {model.Value->SlotIndex} Bone Count: {boneCount}"); + if (model.Value->ModelResourceHandle == null) + continue; + var fileName = model.Value->ModelResourceHandle->FileName.ToString(); + if (string.IsNullOrEmpty(fileName)) + continue; + using var id = ImRaii.PushId($"{(nint)model.Value:X8}"); + if (ImGui.CollapsingHeader($"Model: {fileName}")) + { + ImGui.Text($"Slot Index: {model.Value->SlotIndex}"); + ImGui.Text($"Bone Count: {model.Value->BoneCount}"); + ImGui.Text($"Material Count: {model.Value->MaterialCount}"); + ImGui.Text($"Enabled Attribute Index Mask: {model.Value->EnabledAttributeIndexMask}"); + ImGui.Text($"Enabled Shape Key Index Mask: {model.Value->EnabledShapeKeyIndexMask}"); + if (model.Value->Skeleton != null) + { + DrawSkeleton(model.Value->Skeleton, fileName); + } + } } } diff --git a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs index bfb2139..f8294fd 100644 --- a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs @@ -14,6 +14,7 @@ using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; using SharpGLTF.Scenes; +using SharpGLTF.Transforms; using SkiaSharp; using Material = Meddle.Utils.Export.Material; using Model = Meddle.Utils.Export.Model; @@ -122,20 +123,21 @@ public void ExportRawTextures(CharacterGroup characterGroup, CancellationToken t } } - public void ExportAnimation(List frames, bool includePositionalData, CancellationToken token = default) + public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePositionalData, CancellationToken token = default) { try { using var activity = ActivitySource.StartActivity(); var scene = new SceneBuilder(); - var bones = SkeletonUtils.GetAnimatedBoneMap(frames, includePositionalData, out var root); - var armature = new NodeBuilder("Armature"); - if (root != null) + var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray()); + + foreach (var (id, boneSet) in boneSets) { - armature.AddNode(root); + if (boneSet.Root == null) throw new InvalidOperationException("Root bone not found"); + logger.LogInformation("Adding bone set {Id}", id); + scene.AddNode(boneSet.Root); + scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, boneSet.Bones.Cast().ToArray()); } - scene.AddSkinnedMesh(GetDummyMesh(), Matrix4x4.Identity, bones.Cast().ToArray()); - scene.AddNode(armature); var sceneGraph = scene.ToGltf2(); var folder = GetPathForOutput(); @@ -152,8 +154,8 @@ public void ExportAnimation(List frames, bool includePositio } // https://github.com/0ceal0t/Dalamud-VFXEditor/blob/be00131b93b3c6dd4014a4f27c2661093daf3a85/VFXEditor/Utils/Gltf/GltfSkeleton.cs#L132 - public static MeshBuilder GetDummyMesh() { - var dummyMesh = new MeshBuilder( "DUMMY_MESH" ); + public static MeshBuilder GetDummyMesh(string name = "DUMMY_MESH") { + var dummyMesh = new MeshBuilder( name ); var material = new MaterialBuilder( "material" ); var p1 = new VertexPosition @@ -184,7 +186,7 @@ public void Export(CharacterGroup characterGroup, CancellationToken token = defa { using var activity = ActivitySource.StartActivity(); var scene = new SceneBuilder(); - var bones = SkeletonUtils.GetBoneMap(characterGroup.Skeleton, out var root); + var bones = SkeletonUtils.GetBoneMap(characterGroup.Skeleton, true, out var root); //var bones = XmlUtils.GetBoneMap(characterGroup.Skeletons, out var root); if (root != null) { @@ -219,8 +221,8 @@ public void Export(CharacterGroup characterGroup, CancellationToken token = defa { var attachedModelGroup = characterGroup.AttachedModelGroups[i]; var attachName = characterGroup.Skeleton.PartialSkeletons[attachedModelGroup.Attach.PartialSkeletonIdx] - .HkSkeleton!.BoneNames[attachedModelGroup.Attach.BoneIdx]; - var attachBones = SkeletonUtils.GetBoneMap(attachedModelGroup.Skeleton, out var attachRoot); + .HkSkeleton!.BoneNames[(int)attachedModelGroup.Attach.BoneIdx]; + var attachBones = SkeletonUtils.GetBoneMap(attachedModelGroup.Skeleton, true, out var attachRoot); if (attachRoot == null) { throw new InvalidOperationException("Failed to get attach root"); @@ -380,11 +382,7 @@ private MaterialBuilder HandleMaterial(CharacterGroup characterGroup, Material m { logger.LogInformation("Adding bone {BoneName} from mesh {MeshPath}", boneName, mdlGroup.Path); - var bone = new BoneNodeBuilder(boneName) - { - IsGenerated = true - }; - + var bone = new BoneNodeBuilder(boneName); if (root == null) throw new InvalidOperationException("Root bone not found"); root.AddNode(bone); logger.LogInformation("Added bone {BoneName} to {ParentBone}", boneName, root.BoneName); diff --git a/Meddle/Meddle.Utils/BoneNodeBuilder.cs b/Meddle/Meddle.Utils/BoneNodeBuilder.cs index 7d5c295..53ff447 100644 --- a/Meddle/Meddle.Utils/BoneNodeBuilder.cs +++ b/Meddle/Meddle.Utils/BoneNodeBuilder.cs @@ -5,10 +5,9 @@ namespace Meddle.Utils; public class BoneNodeBuilder(string name) : NodeBuilder(name) { public string BoneName { get; } = name; - public int? Suffix { get; private set; } - public int? PartialSkeletonIndex { get; set; } - public int? BoneIndex { get; set; } - public bool IsGenerated { get; set; } = false; + public string PartialSkeletonHandle { get; set; } + public int BoneIndex { get; set; } + public int PartialSkeletonIndex { get; set; } /// /// Sets the suffix of this bone and all its children. @@ -17,15 +16,18 @@ public class BoneNodeBuilder(string name) : NodeBuilder(name) /// public void SetSuffixRecursively(int? suffix) { - Suffix = suffix; - if (suffix is { } val) - { - Name = $"{BoneName}_{val}"; - } - else + Name = suffix != null ? $"{BoneName}_{suffix}" : BoneName; + + foreach (var child in VisualChildren) { - Name = BoneName; + if (child is BoneNodeBuilder boneChild) + boneChild.SetSuffixRecursively(suffix); } + } + + public void SetSuffixRecursively(string? suffix) + { + Name = suffix != null ? $"{BoneName}_{suffix}" : BoneName; foreach (var child in VisualChildren) { From 4ebbf936e303377532788fd879525870ddca5cd0 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 00:02:44 +1000 Subject: [PATCH 02/19] New Character tab work --- Meddle/Meddle.Plugin/Plugin.cs | 2 + Meddle/Meddle.Plugin/Services/TextureCache.cs | 98 +++ Meddle/Meddle.Plugin/UI/AnimationTab.cs | 341 +---------- Meddle/Meddle.Plugin/UI/DebugTab.cs | 93 +++ Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 579 ++++++++++++++++++ Meddle/Meddle.Plugin/Utils/DXHelper.cs | 32 + Meddle/Meddle.Plugin/Utils/ExportUtil.cs | 32 +- Meddle/Meddle.Plugin/Utils/ParseUtil.cs | 42 +- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 279 ++++++++- 9 files changed, 1127 insertions(+), 371 deletions(-) create mode 100644 Meddle/Meddle.Plugin/Services/TextureCache.cs create mode 100644 Meddle/Meddle.Plugin/UI/DebugTab.cs create mode 100644 Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs diff --git a/Meddle/Meddle.Plugin/Plugin.cs b/Meddle/Meddle.Plugin/Plugin.cs index c582dfd..7bf671c 100644 --- a/Meddle/Meddle.Plugin/Plugin.cs +++ b/Meddle/Meddle.Plugin/Plugin.cs @@ -3,6 +3,7 @@ using Dalamud.IoC; using Dalamud.Plugin; using Meddle.Plugin.Services; +using Meddle.Plugin.UI; using Meddle.Plugin.Utils; using Meddle.Utils.Files.SqPack; using Microsoft.Extensions.DependencyInjection; @@ -47,6 +48,7 @@ public Plugin(IDalamudPluginInterface pluginInterface) service.AddServices(services); services.AddSingleton(config) .AddUi() + .AddSingleton() .AddSingleton(pluginInterface) .AddSingleton() .AddSingleton() diff --git a/Meddle/Meddle.Plugin/Services/TextureCache.cs b/Meddle/Meddle.Plugin/Services/TextureCache.cs new file mode 100644 index 0000000..e2fc035 --- /dev/null +++ b/Meddle/Meddle.Plugin/Services/TextureCache.cs @@ -0,0 +1,98 @@ +using Dalamud.Interface.Textures.TextureWraps; +using Microsoft.Extensions.Logging; + +namespace Meddle.Plugin.Services; + +public class CachedTexture : IDisposable +{ + public CachedTexture(IDalamudTextureWrap wrap) + { + Wrap = wrap; + LastAccessTime = DateTime.Now; + } + + public IDalamudTextureWrap Wrap { get; set; } + public DateTime LastAccessTime { get; set; } + + public void Dispose() + { + Wrap.Dispose(); + } +} + +public sealed class TextureCache : IDisposable +{ + private readonly Dictionary cache = new(); + private readonly TimeSpan expirationTime; + private readonly ILogger logger; + private readonly Timer cleanupTimer; + + public TextureCache(ILogger logger) + { + this.expirationTime = TimeSpan.FromSeconds(10); + this.logger = logger; + cleanupTimer = new Timer(CleanupExpiredTextures, null, expirationTime, expirationTime); + } + + public IDalamudTextureWrap GetOrAdd(string key, Func createWrap) + { + if (cache.TryGetValue(key, out var cachedTexture)) + { + cachedTexture.LastAccessTime = DateTime.Now; + return cachedTexture.Wrap; + } + + var wrap = createWrap(); + cache[key] = new CachedTexture(wrap); + return wrap; + } + + private void CleanupExpiredTextures(object? state) + { + var now = DateTime.Now; + var keysToRemove = new List(); + + foreach (var kvp in cache) + { + if (now - kvp.Value.LastAccessTime > expirationTime) + { + keysToRemove.Add(kvp.Key); + logger.LogDebug("Removing expired texture: {Key}", kvp.Key); + } + } + + foreach (var key in keysToRemove) + { + cache[key].Dispose(); + cache.Remove(key); + } + } + + private void ReleaseUnmanagedResources() + { + foreach (var kvp in cache) + { + kvp.Value.Dispose(); + } + } + + private void Dispose(bool disposing) + { + ReleaseUnmanagedResources(); + if (disposing) + { + cleanupTimer.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~TextureCache() + { + Dispose(false); + } +} diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index e5422ea..4e3a60c 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -133,36 +133,6 @@ public unsafe void Draw() } ImGui.Separator(); - - /*if (ImGui.CollapsingHeader("Frames")) - { - // render frames - foreach (var frame in frames.ToArray()) - { - if (ImGui.CollapsingHeader($"Frame: {frame.Time}##{frame.GetHashCode()}")) - { - using var frameIndent = ImRaii.PushIndent(); - foreach (var partial in frame.Skeleton.PartialSkeletons) - { - if (ImGui.CollapsingHeader($"Partial: {partial.HandlePath}##{partial.GetHashCode()}")) - { - ImGui.Text($"Connected Bone Index: {partial.ConnectedBoneIndex}"); - var poseData = partial.Poses.FirstOrDefault(); - if (poseData == null) continue; - for (int i = 0; i < poseData.Pose.Count; i++) - { - var transform = poseData.Pose[i]; - var boneName = partial.HkSkeleton?.BoneNames[i] ?? "Bone"; - ImGui.Text($"[{i}]{boneName} " + - $"Scale: {transform.Scale} " + - $"Rotation: {transform.Rotation} " + - $"Translation: {transform.Translation}"); - } - } - } - } - } - }*/ if (ImGui.CollapsingHeader("Skeleton")) { @@ -237,7 +207,7 @@ public static unsafe AffineTransform GetTransform(CharacterBase* character) return new AffineTransform(scale, rotation, position); } - private unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Skeleton ownerSkeleton, string ownerId) + private static unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Skeleton ownerSkeleton, string ownerId) { var attachments = new List(); var ornament = charPtr->OrnamentData.OrnamentObject; @@ -290,73 +260,6 @@ private unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Skeleton o return attachments.ToArray(); } - - - private void DrawSkeleton(Skeleton.Skeleton skeleton) - { - using var skeletonIndent = ImRaii.PushIndent(); - ImGui.Text($"Partial Skeletons: {skeleton.PartialSkeletons.Count}"); - ImGui.Text($"Transform: {skeleton.Transform}"); - for (int i = 0; i < skeleton.PartialSkeletons.Count; i++) - { - var partial = skeleton.PartialSkeletons[i]; - if (partial.HandlePath == null) - { - continue; - } - using var partialIndent = ImRaii.PushIndent(); - using var partialId = ImRaii.PushId(i); - if (ImGui.CollapsingHeader($"[{i}]Partial: {partial.HandlePath}")) - { - ImGui.Text($"Connected Bone Index: {partial.ConnectedBoneIndex}"); - var poseData = partial.Poses.FirstOrDefault(); - if (poseData == null) continue; - for (int j = 0; j < poseData.Pose.Count; j++) - { - var transform = poseData.Pose[j]; - var boneName = partial.HkSkeleton?.BoneNames[j] ?? "Bone"; - ImGui.Text($"[{j}]{boneName} {transform}"); - } - } - } - } - - private unsafe void DrawSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* sk, string context) - { - using var skeletonIndent = ImRaii.PushIndent(); - using var skeletonId = ImRaii.PushId($"{(nint)sk:X8}"); - ImGui.Text($"Partial Skeletons: {sk->PartialSkeletonCount}"); - ImGui.Text($"Transform: {new Transform(sk->Transform)}"); - var mainPose = GetPose(sk); - - for (var i = 0; i < sk->PartialSkeletonCount; ++i) - { - using var partialId = ImRaii.PushId($"PartialSkeleton_{i}"); - var handle = sk->PartialSkeletons[i].SkeletonResourceHandle; - if (handle == null) - { - continue; - } - - if (ImGui.CollapsingHeader($"Partial {i}: {handle->FileName.ToString()}")) - { - var p = sk->PartialSkeletons[i].GetHavokPose(0); - if (p != null && p->Skeleton != null) - { - for (var j = 0; j < p->Skeleton->Bones.Length; ++j) - { - var boneName = p->Skeleton->Bones[j].Name.String ?? $"Bone {j}"; - ImGui.TextUnformatted($"[{i}, {j}] => {boneName}"); - if (mainPose != null && mainPose.TryGetValue(boneName, out var transform)) - { - ImGui.SameLine(); - ImGui.Text($" {new Transform(transform)}"); - } - } - } - } - } - } private unsafe void DrawSelectedCharacter() { @@ -366,247 +269,7 @@ private unsafe void DrawSelectedCharacter() var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; if (cBase == null) return; - DrawCharacterAttaches(charPtr); - } - - private unsafe void DrawCharacterAttaches(Character* charPtr) - { - var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; - - - DrawCharacterBase(cBase, "Main"); - DrawOrnamentContainer(charPtr->OrnamentData); - DrawCompanionContainer(charPtr->CompanionData); - DrawMountContainer(charPtr->Mount); - DrawDrawDataContainer(charPtr->DrawData); - } - - private unsafe void DrawDrawDataContainer(DrawDataContainer drawDataContainer) - { - if (drawDataContainer.OwnerObject == null) - { - ImGui.Text($"[DrawDataContainer] Owner is null"); - return; - } - - var ownerObject = drawDataContainer.OwnerObject; - if (ownerObject == null) - { - ImGui.Text($"[DrawDataContainer] Owner is null"); - return; - } - - var weaponData = drawDataContainer.WeaponData; - foreach (var weapon in weaponData) - { - var weaponDrawObject = weapon.DrawObject; - if (weaponDrawObject == null) - { - continue; - } - - var objectType = weaponDrawObject->GetObjectType(); - if (objectType != ObjectType.CharacterBase) - { - ImGui.Text($"[Weapon:{weapon.ModelId.Id}] Weapon is not a CharacterBase ({objectType})"); - return; - } - - DrawCharacterBase((CharacterBase*)weaponDrawObject, "Weapon"); - } - } - - private unsafe void DrawCompanionContainer(CompanionContainer companionContainer) - { - var owner = companionContainer.OwnerObject; - if (owner == null) - { - ImGui.Text($"[Companion:{companionContainer.CompanionId}] Owner is null"); - return; - } - var companion = companionContainer.CompanionObject; - if (companion == null) - { - return; - } - - var objectType = companion->DrawObject->GetObjectType(); - if (objectType != ObjectType.CharacterBase) - { - ImGui.Text($"[Companion:{companionContainer.CompanionId}] Companion is not a CharacterBase ({objectType})"); - return; - } - - DrawCharacterBase((CharacterBase*)companion->DrawObject, "Companion"); - } - - private unsafe void DrawMountContainer(MountContainer mountContainer) - { - var owner = mountContainer.OwnerObject; - if (owner == null) - { - ImGui.Text($"[Mount:{mountContainer.MountId}] Owner is null"); - return; - } - var mount = mountContainer.MountObject; - if (mount == null) - { - return; - } - - var drawObject = mount->DrawObject; - if (drawObject == null) - { - ImGui.Text($"[Mount:{mountContainer.MountId}] DrawObject is null"); - return; - } - - var objectType = drawObject->GetObjectType(); - if (objectType != ObjectType.CharacterBase) - { - ImGui.Text($"[Mount:{mountContainer.MountId}] Mount is not a CharacterBase ({objectType})"); - return; - } - - DrawCharacterBase((CharacterBase*)drawObject, "Mount"); - } - - private unsafe void DrawOrnamentContainer(OrnamentContainer ornamentContainer) - { - var owner = ornamentContainer.OwnerObject; - if (owner == null) - { - ImGui.Text($"[Ornament:{ornamentContainer.OrnamentId}] Owner is null"); - return; - } - var ornament = ornamentContainer.OrnamentObject; - if (ornament == null) - { - return; - } - - DrawCharacterBase((CharacterBase*)ornament->DrawObject, "Ornament"); - } - - private unsafe void DrawCharacterBase(CharacterBase* character, string name) - { - if (character == null) - return; - var skeleton = character->Skeleton; - if (skeleton == null) - return; - - Attach attachPoint; - try - { - attachPoint = new Attach(character->Attach); - } - catch (Exception e) - { - ImGui.Text($"Failed to parse attach: {e}"); - return; - } - - var pose = GetPose(skeleton); - if (pose == null) - { - ImGui.Text("No pose data"); - return; - } - - var modelType = character->GetModelType(); - var attachHeader = $"[{modelType}]{name} Attach Pose ({attachPoint.ExecuteType},{attachPoint.AttachmentCount})"; - if (character->Attach.ExecuteType >= 3) - { - var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; - var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[(int)attachPoint.BoneIdx]; - attachHeader += $" at {boneName}"; - } - else if (character->Attach.ExecuteType == 3) - { - var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; - if (attachedPartialSkeleton.HkSkeleton != null && attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) - { - var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[(int)attachPoint.BoneIdx]; - attachHeader += $" at {boneName}"; - } - else - { - attachHeader += $" at {attachPoint.BoneIdx} > {attachedPartialSkeleton.HandlePath}"; - } - } - - if (ImGui.CollapsingHeader(attachHeader)) - { - using var attachId = ImRaii.PushId($"{(nint)character:X8}_Attach"); - DrawAttachInfo(character, attachPoint); - using var attachIndent = ImRaii.PushIndent(); - if (attachPoint.TargetSkeleton != null && ImGui.CollapsingHeader($"Target Skeleton {(nint)character->Attach.TargetSkeleton:X8}")) - { - using var id = ImRaii.PushId($"{(nint)character:X8}_Target"); - DrawSkeleton(attachPoint.TargetSkeleton); - } - - if (attachPoint.OwnerSkeleton != null && ImGui.CollapsingHeader($"Owner Skeleton {(nint)character->Attach.OwnerSkeleton:X8}")) - { - using var id = ImRaii.PushId($"{(nint)character:X8}_Owner"); - DrawSkeleton(attachPoint.OwnerSkeleton); - } - } - } - - private unsafe void DrawAttachInfo(CharacterBase* character, Attach attachPoint) - { - var position = character->Position; - var rotation = character->Rotation; - var scale = character->Scale; - var aTransform = new AffineTransform(scale, rotation, position); - var transform = new Transform(aTransform); - ImGui.Text($"Attachment Count: {attachPoint.AttachmentCount}"); - ImGui.Text($"ExecuteType: {attachPoint.ExecuteType}"); - ImGui.Text($"SkeletonIdx: {attachPoint.PartialSkeletonIdx}"); - ImGui.Text($"BoneIdx: {attachPoint.BoneIdx}"); - ImGui.Text($"World Transform: {transform}"); - ImGui.Text($"Root: {attachPoint.OffsetTransform?.ToString() ?? "None"}"); - if (attachPoint.TargetSkeleton != null) - { - DrawSkeleton(attachPoint.TargetSkeleton); - } - else - { - var characterSkeleton = new Skeleton.Skeleton(character->Skeleton); - DrawSkeleton(characterSkeleton); - } - DrawModels(character); - } - - private unsafe void DrawModels(CharacterBase* character) - { - using var modelIndent = ImRaii.PushIndent(); - var models = character->ModelsSpan; - foreach (var model in models) - { - if (model == null) - continue; - if (model.Value->ModelResourceHandle == null) - continue; - var fileName = model.Value->ModelResourceHandle->FileName.ToString(); - if (string.IsNullOrEmpty(fileName)) - continue; - using var id = ImRaii.PushId($"{(nint)model.Value:X8}"); - if (ImGui.CollapsingHeader($"Model: {fileName}")) - { - ImGui.Text($"Slot Index: {model.Value->SlotIndex}"); - ImGui.Text($"Bone Count: {model.Value->BoneCount}"); - ImGui.Text($"Material Count: {model.Value->MaterialCount}"); - ImGui.Text($"Enabled Attribute Index Mask: {model.Value->EnabledAttributeIndexMask}"); - ImGui.Text($"Enabled Shape Key Index Mask: {model.Value->EnabledShapeKeyIndexMask}"); - if (model.Value->Skeleton != null) - { - DrawSkeleton(model.Value->Skeleton, fileName); - } - } - } + UIUtil.DrawCharacterAttaches(charPtr); } private static unsafe Dictionary? GetPose(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* skeleton) diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs new file mode 100644 index 0000000..ac9d785 --- /dev/null +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -0,0 +1,93 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using ImGuiNET; +using Meddle.Plugin.Utils; + +namespace Meddle.Plugin.UI; + +public class DebugTab : ITab +{ + private readonly Configuration config; + private readonly IClientState clientState; + private readonly IObjectTable objectTable; + + public DebugTab(Configuration config, IClientState clientState, IObjectTable objectTable) + { + this.config = config; + this.clientState = clientState; + this.objectTable = objectTable; + } + + public void Dispose() + { + // TODO release managed resources here + } + + public string Name => "Debug"; + public int Order => int.MaxValue; + public bool DisplayTab => config.ShowDebug; + public void Draw() + { + ICharacter[] objects; + if (clientState.LocalPlayer != null) + { + objects = objectTable.OfType() + .Where(obj => obj.IsValid() && obj.IsValidCharacterBase()) + .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) + .ToArray(); + } + else + { + // login/char creator produces "invalid" characters but are still usable I guess + objects = objectTable.OfType() + .Where(obj => obj.IsValidHuman()) + .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) + .ToArray(); + } + + selectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; + + ImGui.Text("Select Character"); + var preview = selectedCharacter != null ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) : "None"; + using (var combo = ImRaii.Combo("##Character", preview)) + { + if (combo) + { + foreach (var character in objects) + { + if (ImGui.Selectable(clientState.GetCharacterDisplayText(character, config.PlayerNameOverride))) + { + selectedCharacter = character; + } + } + } + } + + DrawDebugMenu(); + } + + private void DrawDebugMenu() + { + if (selectedCharacter == null) + { + ImGui.Text("No characters found"); + return; + } + + // player address + ImGui.Text($"Address: {selectedCharacter.Address:X8}"); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Click to copy"); + } + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText($"{selectedCharacter.Address:X8}"); + } + + } + + private ICharacter? selectedCharacter; + +} diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs new file mode 100644 index 0000000..3f6ba45 --- /dev/null +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -0,0 +1,579 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Common.Math; +using ImGuiNET; +using Meddle.Plugin.Models; +using Meddle.Plugin.Services; +using Meddle.Plugin.Utils; +using Meddle.Utils; +using Meddle.Utils.Export; +using Meddle.Utils.Files; +using Meddle.Utils.Files.SqPack; +using Microsoft.Extensions.Logging; +using SkiaSharp; +using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using CSCharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; +using CSHuman = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Human; +using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; +using CSMaterial = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material; +using CSModel = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model; + +namespace Meddle.Plugin.UI; + +public unsafe class LiveCharacterTab : ITab +{ + private readonly IClientState clientState; + private readonly ExportUtil exportUtil; + private readonly ITextureProvider textureProvider; + private readonly ILogger log; + private readonly IObjectTable objectTable; + private readonly ParseUtil parseUtil; + private readonly DXHelper dxHelper; + private readonly TextureCache textureCache; + private readonly SqPack pack; + private readonly Configuration config; + private readonly PluginState pluginState; + private ICharacter? selectedCharacter; + + private readonly FileDialogManager fileDialog = new FileDialogManager + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking + }; + + public LiveCharacterTab( + IObjectTable objectTable, + IClientState clientState, + ILogger log, + PluginState pluginState, + ExportUtil exportUtil, + ITextureProvider textureProvider, + ParseUtil parseUtil, + DXHelper dxHelper, + TextureCache textureCache, + SqPack pack, + Configuration config) + { + this.log = log; + this.pluginState = pluginState; + this.exportUtil = exportUtil; + this.textureProvider = textureProvider; + this.parseUtil = parseUtil; + this.dxHelper = dxHelper; + this.textureCache = textureCache; + this.pack = pack; + this.config = config; + this.objectTable = objectTable; + this.clientState = clientState; + } + + public string Name => "CharacterAlt"; + public int Order => 1; + public bool DisplayTab => true; + + public void Draw() + { + if (!pluginState.InteropResolved) + { + ImGui.Text("Waiting for game data..."); + return; + } + + DrawObjectPicker(); + } + + + private bool IsDisposed { get; set; } + + public void Dispose() + { + if (!IsDisposed) + { + log.LogDebug("Disposing CharacterTabAlt"); + IsDisposed = true; + } + } + + private void DrawObjectPicker() + { + // Warning text: + ImGui.TextWrapped("NOTE: Exported models use a rudimentary approximation of the games pixel shaders, " + + "they will likely not match 1:1 to the in-game appearance."); + + ICharacter[] objects; + if (clientState.LocalPlayer != null) + { + objects = objectTable.OfType() + .Where(obj => obj.IsValid() && obj.IsValidCharacterBase()) + .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) + .ToArray(); + } + else + { + // login/char creator produces "invalid" characters but are still usable I guess + objects = objectTable.OfType() + .Where(obj => obj.IsValidHuman()) + .OrderBy(c => clientState.GetDistanceToLocalPlayer(c).LengthSquared()) + .ToArray(); + } + + selectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; + + ImGui.Text("Select Character"); + var preview = selectedCharacter != null + ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) + : "None"; + using (var combo = ImRaii.Combo("##Character", preview)) + { + if (combo) + { + foreach (var character in objects) + { + if (ImGui.Selectable(clientState.GetCharacterDisplayText(character, config.PlayerNameOverride))) + { + selectedCharacter = character; + } + } + } + } + + DrawCharacterGroup(); + fileDialog.Draw(); + } + + private void DrawCharacterGroup() + { + if (selectedCharacter == null) + { + ImGui.Text("No character selected"); + return; + } + + var charPtr = (CSCharacter*)selectedCharacter.Address; + if (charPtr == null) + { + ImGui.Text("Character is null"); + return; + } + + var drawObject = charPtr->GameObject.DrawObject; + if (drawObject == null) + { + ImGui.Text("Character has no draw object"); + return; + } + + var objectType = drawObject->Object.GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + ImGui.Text("Selected object is not a character"); + return; + } + + var cBase = (CSCharacterBase*)drawObject; + var modelType = cBase->GetModelType(); + CustomizeParameter? customizeParams = null; + CustomizeData? customizeData = null; + GenderRace genderRace = GenderRace.Unknown; + if (modelType == CSCharacterBase.ModelType.Human) + { + DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); + } + + using var modelTable = ImRaii.Table("##Models", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Character Data", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + foreach (var modelPtr in cBase->ModelsSpan) + { + if (modelPtr == null) + { + continue; + } + + DrawModel(cBase, modelPtr.Value, customizeParams, customizeData, genderRace); + } + } + + private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + { + if (cBase == null) + { + return; + } + + if (model == null || model->ModelResourceHandle == null) + { + return; + } + + using var modelId = ImRaii.PushId($"{(nint)model}"); + ImGui.TableNextRow(); + var fileName = model->ModelResourceHandle->FileName.ToString(); + var modelName = cBase->ResolveMdlPath(model->SlotIndex); + //var actualModelName = gamePathHandler.ClassifyMdlGamePath(modelName); + + ImGui.TableSetColumnIndex(0); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.FileExport.ToIconString())) + { + ImGui.OpenPopup("ExportModelPopup"); + } + } + + // popup for export options + if (ImGui.BeginPopupContextItem("ExportModelPopup")) + { + if (ImGui.MenuItem("Export as mdl")) + { + var defaultFileName = Path.GetFileName(fileName); + fileDialog.SaveFileDialog("Save Model", "Model File{.mdl}", defaultFileName, ".mdl", + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(fileName); + if (data == null) + { + log.LogError("Failed to get model data from pack or disk for {FileName}", fileName); + return; + } + + File.WriteAllBytes(path, data); + }); + } + + if (ImGui.MenuItem("Export as glTF")) + { + var folderName = Path.GetFileNameWithoutExtension(fileName); + fileDialog.SaveFolderDialog("Save Model", folderName, + (result, path) => + { + if (!result) return; + var colorTableTextures = parseUtil.ParseColorTableTextures(cBase); + var modelData = parseUtil.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); + if (modelData == null) + { + log.LogError("Failed to get model data for {FileName}", fileName); + return; + } + + var skeleton = new Skeleton.Skeleton(model->Skeleton); + var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, [modelData], skeleton, []); + + + Task.Run(() => { exportUtil.Export(cGroup, path); }); + }, Plugin.TempDirectory); + } + + ImGui.EndPopup(); + } + + ImGui.TableSetColumnIndex(1); + + if (ImGui.CollapsingHeader($"[{model->SlotIndex}] {modelName}")) + { + ImGui.Text($"Game File Name: {modelName}"); + ImGui.Text($"File Name: {fileName}"); + ImGui.Text($"Slot Index: {model->SlotIndex}"); + var modelShapeAttributes = parseUtil.ParseModelShapeAttributes(model); + DrawShapeAttributeTable(modelShapeAttributes); + + for (var materialIdx = 0; materialIdx < model->MaterialsSpan.Length; materialIdx++) + { + var materialPtr = model->MaterialsSpan[materialIdx]; + if (materialPtr == null || materialPtr.Value == null) + { + continue; + } + + DrawMaterial(cBase, model, materialPtr.Value, materialIdx); + } + } + } + + private void DrawShapeAttributeTable(Model.ShapeAttributeGroup shapeAttributeGroup) + { + if (shapeAttributeGroup.AttributeMasks.Length == 0 && shapeAttributeGroup.ShapeMasks.Length == 0) + { + return; + } + + var enabledShapes = Model.GetEnabledValues(shapeAttributeGroup.EnabledShapeMask, + shapeAttributeGroup.ShapeMasks).ToArray(); + var enabledAttributes = Model.GetEnabledValues(shapeAttributeGroup.EnabledAttributeMask, + shapeAttributeGroup.AttributeMasks).ToArray(); + + if (ImGui.BeginTable("ShapeAttributeTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Enabled", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + foreach (var shape in shapeAttributeGroup.ShapeMasks) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text("Shape"); + ImGui.TableSetColumnIndex(1); + ImGui.Text($"[{shape.id}] {shape.name}"); + ImGui.TableSetColumnIndex(2); + ImGui.Text(enabledShapes.Contains(shape.name) ? "Yes" : "No"); + } + + foreach (var attribute in shapeAttributeGroup.AttributeMasks) + { + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text("Attribute"); + ImGui.TableSetColumnIndex(1); + ImGui.Text($"[{attribute.id}] {attribute.name}"); + ImGui.TableSetColumnIndex(2); + ImGui.Text(enabledAttributes.Contains(attribute.name) ? "Yes" : "No"); + } + + ImGui.EndTable(); + } + } + + private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* material, int materialIdx) + { + if (cBase == null) + { + return; + } + + if (model == null) + { + return; + } + + if (material == null || material->MaterialResourceHandle == null) + { + return; + } + + using var materialId = ImRaii.PushId($"{(nint)material}"); + var materialFileName = material->MaterialResourceHandle->FileName.ToString(); + var materialName = model->ModelResourceHandle->GetMaterialFileName((uint)materialIdx); + + // in same row as model export button, draw button for export material + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.FileExport.ToIconString())) + { + ImGui.OpenPopup("ExportMaterialPopup"); + } + } + + // popup for export options + if (ImGui.BeginPopupContextItem("ExportMaterialPopup")) + { + if (ImGui.MenuItem("Export raw textures as pngs")) + { + var textureBuffer = new Dictionary(); + for (int i = 0; i < material->TexturesSpan.Length; i++) + { + var textureEntry = material->TexturesSpan[i]; + if (textureEntry.Texture == null) + { + continue; + } + + if (i < material->MaterialResourceHandle->TextureCount) + { + var textureName = material->MaterialResourceHandle->TexturePathString(i); + var gpuTex = dxHelper.ExportTextureResource(textureEntry.Texture->Texture); + var textureData = gpuTex.Resource.ToBitmap(); + textureBuffer[textureName] = textureData; + } + } + + var materialNameNoExt = Path.GetFileNameWithoutExtension(materialFileName); + fileDialog.SaveFolderDialog("Save Textures", materialNameNoExt, + (result, path) => + { + if (!result) return; + Directory.CreateDirectory(path); + + foreach (var (name, texture) in textureBuffer) + { + var fileName = Path.GetFileNameWithoutExtension(name); + var filePath = Path.Combine(path, $"{fileName}.png"); + using var str = new SKDynamicMemoryWStream(); + texture.Encode(str, SKEncodedImageFormat.Png, 100); + var imageData = str.DetachAsData().AsSpan(); + File.WriteAllBytes(filePath, imageData.ToArray()); + } + }, Plugin.TempDirectory); + } + + + ImGui.EndPopup(); + } + + ImGui.TableSetColumnIndex(1); + if (ImGui.CollapsingHeader(materialName)) + { + ImGui.Text($"Game File Name: {materialName}"); + ImGui.Text($"File Name: {materialFileName}"); + ImGui.Text($"Material Index: {materialIdx}"); + ImGui.Text($"Texture Count: {material->TextureCount}"); + var shpkName = material->MaterialResourceHandle->ShpkNameString; + ImGui.Text($"Shader Package: {shpkName}"); + + var colorTableTexturePtr = + cBase->ColorTableTexturesSpan[((int)model->SlotIndex * CSCharacterBase.MaterialsPerSlot) + materialIdx]; + if (colorTableTexturePtr != null && colorTableTexturePtr.Value != null && + ImGui.CollapsingHeader("Color Table")) + { + var colorTableTexture = colorTableTexturePtr.Value; + var colorTable = parseUtil.ParseColorTableTexture(colorTableTexture); + UIUtil.DrawColorTable(colorTable); + } + + for (var texIdx = 0; texIdx < material->TextureCount; texIdx++) + { + var textureEntry = material->TexturesSpan[texIdx]; + DrawTexture(material, textureEntry, texIdx); + } + } + } + + private void DrawTexture(CSMaterial* material, CSMaterial.TextureEntry textureEntry, int texIdx) + { + if (textureEntry.Texture == null) + { + return; + } + + using var textureId = ImRaii.PushId($"{(nint)textureEntry.Texture}"); + string? textureName = null; + if (texIdx < material->MaterialResourceHandle->TextureCount) + textureName = material->MaterialResourceHandle->TexturePathString(texIdx); + var textureFileName = textureEntry.Texture->FileName.ToString(); + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.FileExport.ToIconString())) + { + ImGui.OpenPopup("ExportTexturePopup"); + } + } + + // popup for export options + if (ImGui.BeginPopupContextItem("ExportTexturePopup")) + { + if (ImGui.MenuItem("Export as png")) + { + var defaultFileName = Path.GetFileName(textureFileName); + defaultFileName = Path.ChangeExtension(defaultFileName, ".png"); + var gpuTex = dxHelper.ExportTextureResource(textureEntry.Texture->Texture); + var textureData = gpuTex.Resource.ToBitmap(); + + fileDialog.SaveFileDialog("Save Texture", "PNG Image{.png}", defaultFileName, ".png", + (result, path) => + { + if (!result) return; + using var str = new SKDynamicMemoryWStream(); + textureData.Encode(str, SKEncodedImageFormat.Png, 100); + var imageData = str.DetachAsData().AsSpan(); + File.WriteAllBytes(path, imageData.ToArray()); + }, Plugin.TempDirectory); + } + + if (ImGui.MenuItem("Export as tex")) + { + var defaultFileName = Path.GetFileName(textureFileName); + fileDialog.SaveFileDialog("Save Texture", "TEX File{.tex}", defaultFileName, ".tex", + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(textureFileName); + if (data == null) + { + log.LogError("Failed to get texture data from pack or disk for {TextureFileName}", + textureFileName); + return; + } + + File.WriteAllBytes(path, data); + }, Plugin.TempDirectory); + } + + ImGui.EndPopup(); + } + + ImGui.TableSetColumnIndex(1); + if (ImGui.CollapsingHeader(textureName ?? textureFileName)) + { + ImGui.Text($"Game File Name: {textureName}"); + ImGui.Text($"File Name: {textureFileName}"); + ImGui.Text($"Id: {textureEntry.Id}"); + + var availableWidth = ImGui.GetContentRegionAvail().X; + float displayWidth = textureEntry.Texture->Texture->Width; + float displayHeight = textureEntry.Texture->Texture->Height; + if (displayWidth > availableWidth) + { + var ratio = availableWidth / displayWidth; + displayWidth *= ratio; + displayHeight *= ratio; + } + + var wrap = textureCache.GetOrAdd($"{(nint)textureEntry.Texture->Texture}", () => + { + var gpuTex = dxHelper.ExportTextureResource(textureEntry.Texture->Texture); + var textureData = gpuTex.Resource.ToBitmap().GetPixelSpan(); + var wrap = textureProvider.CreateFromRaw( + RawImageSpecification.Rgba32(gpuTex.Resource.Width, gpuTex.Resource.Height), textureData, + $"Meddle_{(nint)textureEntry.Texture->Texture}_{textureFileName}"); + return wrap; + }); + + ImGui.Image(wrap.ImGuiHandle, new Vector2(displayWidth, displayHeight)); + } + } + + private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, out CustomizeParameter customizeParams, out GenderRace genderRace) + { + var customizeCBuf = cBase->CustomizeParameterCBuffer->TryGetBuffer()[0]; + customizeParams = new CustomizeParameter + { + SkinColor = customizeCBuf.SkinColor, + MuscleTone = customizeCBuf.MuscleTone, + SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, + LipColor = customizeCBuf.LipColor, + MainColor = customizeCBuf.MainColor, + FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, + HairFresnelValue0 = customizeCBuf.HairFresnelValue0, + MeshColor = customizeCBuf.MeshColor, + FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, + LeftColor = customizeCBuf.LeftColor, + RightColor = customizeCBuf.RightColor, + OptionColor = customizeCBuf.OptionColor + }; + customizeData = new CustomizeData + { + LipStick = cBase->Customize.Lipstick, + Highlights = cBase->Customize.Highlights + }; + genderRace = (GenderRace)cBase->RaceSexId; + + if (ImGui.CollapsingHeader("Customize Options")) + { + UIUtil.DrawCustomizeParams(ref customizeParams); + UIUtil.DrawCustomizeData(customizeData); + ImGui.Text(genderRace.ToString()); + } + } +} diff --git a/Meddle/Meddle.Plugin/Utils/DXHelper.cs b/Meddle/Meddle.Plugin/Utils/DXHelper.cs index 320a97b..5fe9805 100644 --- a/Meddle/Meddle.Plugin/Utils/DXHelper.cs +++ b/Meddle/Meddle.Plugin/Utils/DXHelper.cs @@ -1,4 +1,6 @@ using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Meddle.Utils.Export; using Microsoft.Extensions.Logging; using OtterTex; @@ -36,6 +38,36 @@ public DXHelper(ILogger log) return ret; } + + public byte[] ExportVertexBuffer(VertexBuffer* buffer) + { + if (buffer->DxPtr1 == nint.Zero) + throw new ArgumentException("Buffer's DX data is null"); + + using var res = new ID3D11Buffer(buffer->DxPtr1); + res.AddRef(); + + var ret = GetResourceData(res, + CloneBuffer, + (r, map) => + { + var ret = new byte[r.Description.ByteWidth]; + Marshal.Copy(map.DataPointer, ret, 0, ret.Length); + return ret; + }); + + return ret; + } + + private ID3D11Buffer CloneBuffer(ID3D11Buffer r) + { + var desc = r.Description with + { + Usage = ResourceUsage.Staging, BindFlags = 0, CPUAccessFlags = CpuAccessFlags.Read, MiscFlags = 0, + }; + + return r.Device.CreateBuffer(desc); + } private (TextureResource Resource, int RowPitch) GetData(ID3D11Texture2D1 r, MappedSubresource map) { diff --git a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs index f8294fd..d49f566 100644 --- a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ExportUtil.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Numerics; -using FFXIVClientStructs.FFXIV.Common.Lua; using Meddle.Plugin.Models; using Meddle.Utils; using Meddle.Utils.Export; @@ -14,7 +13,6 @@ using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; using SharpGLTF.Scenes; -using SharpGLTF.Transforms; using SkiaSharp; using Material = Meddle.Utils.Export.Material; using Model = Meddle.Utils.Export.Model; @@ -80,7 +78,7 @@ public static void ExportTexture(SKBitmap bitmap, string path) var folder = GetPathForOutput(); var outputPath = Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(path)}.png"); - var str = new SKDynamicMemoryWStream(); + using var str = new SKDynamicMemoryWStream(); bitmap.Encode(str, SKEncodedImageFormat.Png, 100); var data = str.DetachAsData().AsSpan(); @@ -128,21 +126,21 @@ public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePo try { using var activity = ActivitySource.StartActivity(); - var scene = new SceneBuilder(); var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray()); + var folder = GetPathForOutput(); foreach (var (id, boneSet) in boneSets) { + var scene = new SceneBuilder(); if (boneSet.Root == null) throw new InvalidOperationException("Root bone not found"); logger.LogInformation("Adding bone set {Id}", id); scene.AddNode(boneSet.Root); scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, boneSet.Bones.Cast().ToArray()); + var sceneGraph = scene.ToGltf2(); + var outputPath = Path.Combine(folder, $"motion_{id}.gltf"); + sceneGraph.SaveGLTF(outputPath); } - var sceneGraph = scene.ToGltf2(); - var folder = GetPathForOutput(); - var outputPath = Path.Combine(folder, "motion.gltf"); - sceneGraph.SaveGLTF(outputPath); Process.Start("explorer.exe", folder); logger.LogInformation("Export complete"); } @@ -180,7 +178,7 @@ public static MeshBuilder GetDummyMe return dummyMesh; } - public void Export(CharacterGroup characterGroup, CancellationToken token = default) + public void Export(CharacterGroup characterGroup, string? outputFolder = null, CancellationToken token = default) { try { @@ -286,7 +284,12 @@ public void Export(CharacterGroup characterGroup, CancellationToken token = defa } var sceneGraph = scene.ToGltf2(); - var folder = GetPathForOutput(); + if (outputFolder != null) + { + Directory.CreateDirectory(outputFolder); + } + + var folder = outputFolder ?? GetPathForOutput(); var outputPath = Path.Combine(folder, "character.gltf"); sceneGraph.SaveGLTF(outputPath); Process.Start("explorer.exe", folder); @@ -546,6 +549,15 @@ private MaterialBuilder BuildAndLogFallbackMaterial(Material material, string na logger.LogWarning("Using fallback material for {Path}", material.HandlePath); return MaterialUtility.BuildFallback(material, name); } + + private void ExportTextureFromPath(string path) + { + var data = pack.GetFileOrReadFromDisk(path); + if (data == null) throw new InvalidOperationException($"Failed to get texture {path}"); + var texFile = new TexFile(data); + var texture = new Texture(Texture.GetResource(texFile), path, null, null, null); + ExportTexture(texture.ToTexture().Bitmap, path); + } public void Dispose() { diff --git a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs b/Meddle/Meddle.Plugin/Utils/ParseUtil.cs index d84cd54..bb351a8 100644 --- a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ParseUtil.cs @@ -142,6 +142,28 @@ public unsafe CharacterGroup HandleCharacterGroup( attachGroups.ToArray()); } + public unsafe Model.ShapeAttributeGroup ParseModelShapeAttributes( + FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* model) + { + var shapesMask = model->EnabledShapeKeyIndexMask; + var shapes = new List<(string, short)>(); + foreach (var shape in model->ModelResourceHandle->Shapes) + { + shapes.Add((MemoryHelper.ReadStringNullTerminated((nint)shape.Item1.Value), shape.Item2)); + } + + var attributeMask = model->EnabledAttributeIndexMask; + var attributes = new List<(string, short)>(); + foreach (var attribute in model->ModelResourceHandle->Attributes) + { + attributes.Add((MemoryHelper.ReadStringNullTerminated((nint)attribute.Item1.Value), attribute.Item2)); + } + + var shapeAttributeGroup = new Model.ShapeAttributeGroup(shapesMask, attributeMask, shapes.ToArray(), attributes.ToArray()); + + return shapeAttributeGroup; + } + public unsafe MdlFileGroup? HandleModelPtr( CharacterBase* characterBase, int slotIdx, Dictionary colorTables) { @@ -163,24 +185,8 @@ public unsafe CharacterGroup HandleCharacterGroup( logger.LogWarning("Model file {MdlFileName} not found", mdlFileName); return null; } - - var shapesMask = model->EnabledShapeKeyIndexMask; - var shapes = new List<(string, short)>(); - foreach (var shape in model->ModelResourceHandle->Shapes) - { - shapes.Add((MemoryHelper.ReadStringNullTerminated((nint)shape.Item1.Value), shape.Item2)); - } - - var attributeMask = model->EnabledAttributeIndexMask; - var attributes = new List<(string, short)>(); - foreach (var attribute in model->ModelResourceHandle->Attributes) - { - attributes.Add((MemoryHelper.ReadStringNullTerminated((nint)attribute.Item1.Value), attribute.Item2)); - } - - var shapeAttributeGroup = - new Model.ShapeAttributeGroup(shapesMask, attributeMask, shapes.ToArray(), attributes.ToArray()); - + + var shapeAttributeGroup = ParseModelShapeAttributes(model); var mdlFile = new MdlFile(mdlFileResource); var mtrlFileNames = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); var mtrlGroups = new List(); diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index bb1a479..8494709 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -1,8 +1,14 @@ using System.Numerics; +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; +using Meddle.Utils.Skeletons; +using SharpGLTF.Transforms; +using Attach = Meddle.Plugin.Skeleton.Attach; using CustomizeData = Meddle.Utils.Export.CustomizeData; namespace Meddle.Plugin.Utils; @@ -42,6 +48,11 @@ public static void DrawCustomizeData(CustomizeData customize) } public static void DrawColorTable(ColorTable table, ColorDyeTable? dyeTable = null) + { + DrawColorTable(table.Rows, dyeTable); + } + + public static void DrawColorTable(ColorTableRow[] tableRows, ColorDyeTable? dyeTable = null) { if (ImGui.BeginTable("ColorTable", 9, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) { @@ -56,9 +67,9 @@ public static void DrawColorTable(ColorTable table, ColorDyeTable? dyeTable = nu ImGui.TableSetupColumn("Tile Set", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableHeadersRow(); - for (var i = 0; i < table.Rows.Length; i++) + for (var i = 0; i < tableRows.Length; i++) { - DrawRow(i, table, dyeTable); + DrawRow(i, ref tableRows[i], dyeTable); } ImGui.EndTable(); @@ -78,9 +89,8 @@ public static void DrawColorTable(MtrlFile file) DrawColorTable(file.ColorTable, file.HasDyeTable ? file.ColorDyeTable : null); } - private static void DrawRow(int i, ColorTable table, ColorDyeTable? dyeTable) + private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTable) { - ref var row = ref table.Rows[i]; ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); ImGui.Text($"{i}"); @@ -136,4 +146,265 @@ private static void DrawRow(int i, ColorTable table, ColorDyeTable? dyeTable) ImGui.TableSetColumnIndex(8); ImGui.Text($"{row.TileIndex}"); } + + public static unsafe void DrawCharacterAttaches(Character* charPtr) + { + var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; + DrawCharacterBase(cBase, "Main"); + DrawOrnamentContainer(charPtr->OrnamentData); + DrawCompanionContainer(charPtr->CompanionData); + DrawMountContainer(charPtr->Mount); + DrawDrawDataContainer(charPtr->DrawData); + } + + private static unsafe void DrawDrawDataContainer(DrawDataContainer drawDataContainer) + { + if (drawDataContainer.OwnerObject == null) + { + ImGui.Text($"[DrawDataContainer] Owner is null"); + return; + } + + var ownerObject = drawDataContainer.OwnerObject; + if (ownerObject == null) + { + ImGui.Text($"[DrawDataContainer] Owner is null"); + return; + } + + var weaponData = drawDataContainer.WeaponData; + foreach (var weapon in weaponData) + { + var weaponDrawObject = weapon.DrawObject; + if (weaponDrawObject == null) + { + continue; + } + + var objectType = weaponDrawObject->GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + ImGui.Text($"[Weapon:{weapon.ModelId.Id}] Weapon is not a CharacterBase ({objectType})"); + return; + } + + DrawCharacterBase((CharacterBase*)weaponDrawObject, "Weapon"); + } + } + + private static unsafe void DrawCompanionContainer(CompanionContainer companionContainer) + { + var owner = companionContainer.OwnerObject; + if (owner == null) + { + ImGui.Text($"[Companion:{companionContainer.CompanionId}] Owner is null"); + return; + } + var companion = companionContainer.CompanionObject; + if (companion == null) + { + return; + } + + var objectType = companion->DrawObject->GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + ImGui.Text($"[Companion:{companionContainer.CompanionId}] Companion is not a CharacterBase ({objectType})"); + return; + } + + DrawCharacterBase((CharacterBase*)companion->DrawObject, "Companion"); + } + + private static unsafe void DrawMountContainer(MountContainer mountContainer) + { + var owner = mountContainer.OwnerObject; + if (owner == null) + { + ImGui.Text($"[Mount:{mountContainer.MountId}] Owner is null"); + return; + } + var mount = mountContainer.MountObject; + if (mount == null) + { + return; + } + + var drawObject = mount->DrawObject; + if (drawObject == null) + { + ImGui.Text($"[Mount:{mountContainer.MountId}] DrawObject is null"); + return; + } + + var objectType = drawObject->GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + ImGui.Text($"[Mount:{mountContainer.MountId}] Mount is not a CharacterBase ({objectType})"); + return; + } + + DrawCharacterBase((CharacterBase*)drawObject, "Mount"); + } + + private static unsafe void DrawOrnamentContainer(OrnamentContainer ornamentContainer) + { + var owner = ornamentContainer.OwnerObject; + if (owner == null) + { + ImGui.Text($"[Ornament:{ornamentContainer.OrnamentId}] Owner is null"); + return; + } + var ornament = ornamentContainer.OrnamentObject; + if (ornament == null) + { + return; + } + + DrawCharacterBase((CharacterBase*)ornament->DrawObject, "Ornament"); + } + + private static unsafe void DrawCharacterBase(CharacterBase* character, string name) + { + if (character == null) + return; + var skeleton = character->Skeleton; + if (skeleton == null) + return; + + Skeleton.Attach attachPoint; + try + { + attachPoint = new Skeleton.Attach(character->Attach); + } + catch (Exception e) + { + ImGui.Text($"Failed to parse attach: {e}"); + return; + } + + var modelType = character->GetModelType(); + var attachHeader = $"[{modelType}]{name} Attach Pose ({attachPoint.ExecuteType},{attachPoint.AttachmentCount})"; + if (character->Attach.ExecuteType >= 3) + { + var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; + var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[(int)attachPoint.BoneIdx]; + attachHeader += $" at {boneName}"; + } + else if (character->Attach.ExecuteType == 3) + { + var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; + if (attachedPartialSkeleton.HkSkeleton != null && attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) + { + var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[(int)attachPoint.BoneIdx]; + attachHeader += $" at {boneName}"; + } + else + { + attachHeader += $" at {attachPoint.BoneIdx} > {attachedPartialSkeleton.HandlePath}"; + } + } + + if (ImGui.CollapsingHeader(attachHeader)) + { + using var attachId = ImRaii.PushId($"{(nint)character:X8}_Attach"); + DrawAttachInfo(character, attachPoint); + using var attachIndent = ImRaii.PushIndent(); + if (attachPoint.TargetSkeleton != null && ImGui.CollapsingHeader($"Target Skeleton {(nint)character->Attach.TargetSkeleton:X8}")) + { + using var id = ImRaii.PushId($"{(nint)character:X8}_Target"); + DrawSkeleton(attachPoint.TargetSkeleton); + } + + if (attachPoint.OwnerSkeleton != null && ImGui.CollapsingHeader($"Owner Skeleton {(nint)character->Attach.OwnerSkeleton:X8}")) + { + using var id = ImRaii.PushId($"{(nint)character:X8}_Owner"); + DrawSkeleton(attachPoint.OwnerSkeleton); + } + } + } + + private static unsafe void DrawAttachInfo(CharacterBase* character, Attach attachPoint) + { + var position = character->Position; + var rotation = character->Rotation; + var scale = character->Scale; + var aTransform = new AffineTransform(scale, rotation, position); + var transform = new Transform(aTransform); + ImGui.Text($"Attachment Count: {attachPoint.AttachmentCount}"); + ImGui.Text($"ExecuteType: {attachPoint.ExecuteType}"); + ImGui.Text($"SkeletonIdx: {attachPoint.PartialSkeletonIdx}"); + ImGui.Text($"BoneIdx: {attachPoint.BoneIdx}"); + ImGui.Text($"World Transform: {transform}"); + ImGui.Text($"Root: {attachPoint.OffsetTransform?.ToString() ?? "None"}"); + if (attachPoint.TargetSkeleton != null) + { + DrawSkeleton(attachPoint.TargetSkeleton); + } + else + { + var characterSkeleton = new Skeleton.Skeleton(character->Skeleton); + DrawSkeleton(characterSkeleton); + } + //DrawModels(character); + } + + public static void DrawSkeleton(Skeleton.Skeleton skeleton) + { + using var skeletonIndent = ImRaii.PushIndent(); + ImGui.Text($"Partial Skeletons: {skeleton.PartialSkeletons.Count}"); + ImGui.Text($"Transform: {skeleton.Transform}"); + for (int i = 0; i < skeleton.PartialSkeletons.Count; i++) + { + var partial = skeleton.PartialSkeletons[i]; + if (partial.HandlePath == null) + { + continue; + } + using var partialIndent = ImRaii.PushIndent(); + using var partialId = ImRaii.PushId(i); + if (ImGui.CollapsingHeader($"[{i}]Partial: {partial.HandlePath}")) + { + ImGui.Text($"Connected Bone Index: {partial.ConnectedBoneIndex}"); + var poseData = partial.Poses.FirstOrDefault(); + if (poseData == null) continue; + for (int j = 0; j < poseData.Pose.Count; j++) + { + var transform = poseData.Pose[j]; + var boneName = partial.HkSkeleton?.BoneNames[j] ?? "Bone"; + ImGui.Text($"[{j}]{boneName} {transform}"); + } + } + } + } + + private static unsafe void DrawSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* sk, string context) + { + using var skeletonIndent = ImRaii.PushIndent(); + using var skeletonId = ImRaii.PushId($"{(nint)sk:X8}"); + ImGui.Text($"Partial Skeletons: {sk->PartialSkeletonCount}"); + ImGui.Text($"Transform: {new Transform(sk->Transform)}"); + for (var i = 0; i < sk->PartialSkeletonCount; ++i) + { + using var partialId = ImRaii.PushId($"PartialSkeleton_{i}"); + var handle = sk->PartialSkeletons[i].SkeletonResourceHandle; + if (handle == null) + { + continue; + } + + if (ImGui.CollapsingHeader($"Partial {i}: {handle->FileName.ToString()}")) + { + var p = sk->PartialSkeletons[i].GetHavokPose(0); + if (p != null && p->Skeleton != null) + { + for (var j = 0; j < p->Skeleton->Bones.Length; ++j) + { + var boneName = p->Skeleton->Bones[j].Name.String ?? $"Bone {j}"; + ImGui.TextUnformatted($"[{i}, {j}] => {boneName}"); + } + } + } + } + } } From c51d024f9750942798d7c523d017c3f589b203cb Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 00:04:00 +1000 Subject: [PATCH 03/19] Update DXHelper.cs --- Meddle/Meddle.Plugin/Utils/DXHelper.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Meddle/Meddle.Plugin/Utils/DXHelper.cs b/Meddle/Meddle.Plugin/Utils/DXHelper.cs index 5fe9805..f8ef583 100644 --- a/Meddle/Meddle.Plugin/Utils/DXHelper.cs +++ b/Meddle/Meddle.Plugin/Utils/DXHelper.cs @@ -1,6 +1,4 @@ using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Meddle.Utils.Export; using Microsoft.Extensions.Logging; using OtterTex; @@ -39,7 +37,7 @@ public DXHelper(ILogger log) return ret; } - public byte[] ExportVertexBuffer(VertexBuffer* buffer) + /*public byte[] ExportVertexBuffer(VertexBuffer* buffer) { if (buffer->DxPtr1 == nint.Zero) throw new ArgumentException("Buffer's DX data is null"); @@ -67,7 +65,7 @@ private ID3D11Buffer CloneBuffer(ID3D11Buffer r) }; return r.Device.CreateBuffer(desc); - } + }*/ private (TextureResource Resource, int RowPitch) GetData(ID3D11Texture2D1 r, MappedSubresource map) { From 796975fbb2bd19a61c53e6823c37d3040fa3e476 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 00:06:58 +1000 Subject: [PATCH 04/19] Update FFXIVClientStructs --- FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 6247b3c..933c607 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6247b3cfcdaffbb6c6751d794e7b54dc6273c9e8 +Subproject commit 933c607fd973b05a07b8bbbee5fff58b1e2e2fa5 From 71e4782b0f786c0e931f8803a7f8479af66cbad3 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:09:15 +1000 Subject: [PATCH 05/19] Update FFXIVClientStructs --- FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 933c607..6d6c894 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 933c607fd973b05a07b8bbbee5fff58b1e2e2fa5 +Subproject commit 6d6c894f4d82f99f57dddb212bb80536754fa648 From 2b9f40860ce16d674007383d14c83c458ec6b905 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:53:38 +1000 Subject: [PATCH 06/19] Update FFXIVClientStructs --- FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 6d6c894..dbe63f0 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6d6c894f4d82f99f57dddb212bb80536754fa648 +Subproject commit dbe63f040b3d4dc1d5dc949af13b0a7a7a6bbda8 From d56d3b508acdbc59a2094fb93653c0619b3bbd7e Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:54:38 +1000 Subject: [PATCH 07/19] Specify applicable version --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2a9221b..bbcc89b 100644 --- a/repo.json +++ b/repo.json @@ -7,8 +7,8 @@ "AssemblyVersion": "0.1.9", "TestingAssemblyVersion": "0.1.9", "RepoUrl": "https://github.com/PassiveModding/Meddle", - "IconUrl": "https://github.com/PassiveModding/Meddle/raw/main/icon.png", - "ApplicableVersion": "any", + "IconUrl": "https://github.com/PassiveModding/Meddle/raw/main/icon.png", + "ApplicableVersion": "2024.08.02.0000.0000", "DalamudApiLevel": 10, "Tags": [ "Xande", From 6ae8d4b3fcaa033ec86e126ae493a6624864a5fd Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:55:10 +1000 Subject: [PATCH 08/19] Util stuff and restructure --- Meddle/Meddle.Plugin/Plugin.cs | 4 +- Meddle/Meddle.Plugin/Service.cs | 2 + .../ExportService.cs} | 11 +-- .../ParseUtil.cs => Services/ParseService.cs} | 61 ++----------- .../{Utils => Services}/PbdHooks.cs | 12 +-- Meddle/Meddle.Plugin/Skeleton/Skeletons.cs | 7 +- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 10 +-- Meddle/Meddle.Plugin/UI/CharacterTab.cs | 40 ++++----- Meddle/Meddle.Plugin/UI/DebugTab.cs | 89 +++++++++++++++++- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 28 +++--- Meddle/Meddle.Plugin/UI/WorldTab.cs | 8 +- Meddle/Meddle.Plugin/Utils/HkUtil.cs | 90 ------------------- Meddle/Meddle.Plugin/Utils/PoseUtil.cs | 24 +++++ Meddle/Meddle.Plugin/Utils/UIUtil.cs | 6 +- 14 files changed, 188 insertions(+), 204 deletions(-) rename Meddle/Meddle.Plugin/{Utils/ExportUtil.cs => Services/ExportService.cs} (98%) rename Meddle/Meddle.Plugin/{Utils/ParseUtil.cs => Services/ParseService.cs} (86%) rename Meddle/Meddle.Plugin/{Utils => Services}/PbdHooks.cs (87%) delete mode 100644 Meddle/Meddle.Plugin/Utils/HkUtil.cs create mode 100644 Meddle/Meddle.Plugin/Utils/PoseUtil.cs diff --git a/Meddle/Meddle.Plugin/Plugin.cs b/Meddle/Meddle.Plugin/Plugin.cs index 7bf671c..baf6078 100644 --- a/Meddle/Meddle.Plugin/Plugin.cs +++ b/Meddle/Meddle.Plugin/Plugin.cs @@ -50,8 +50,8 @@ public Plugin(IDalamudPluginInterface pluginInterface) .AddUi() .AddSingleton() .AddSingleton(pluginInterface) - .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(new SqPack(Environment.CurrentDirectory)) diff --git a/Meddle/Meddle.Plugin/Service.cs b/Meddle/Meddle.Plugin/Service.cs index c8c36ba..12bf507 100644 --- a/Meddle/Meddle.Plugin/Service.cs +++ b/Meddle/Meddle.Plugin/Service.cs @@ -2,6 +2,7 @@ using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using Meddle.Plugin.Utils; using Microsoft.Extensions.DependencyInjection; namespace Meddle.Plugin; @@ -54,5 +55,6 @@ public void AddServices(IServiceCollection services) services.AddSingleton(DataManager); services.AddSingleton(TextureProvider); services.AddSingleton(NotificationManager); + PoseUtil.SigScanner = SigScanner; } } diff --git a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs similarity index 98% rename from Meddle/Meddle.Plugin/Utils/ExportUtil.cs rename to Meddle/Meddle.Plugin/Services/ExportService.cs index d49f566..db102fa 100644 --- a/Meddle/Meddle.Plugin/Utils/ExportUtil.cs +++ b/Meddle/Meddle.Plugin/Services/ExportService.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Numerics; using Meddle.Plugin.Models; +using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; @@ -17,23 +18,23 @@ using Material = Meddle.Utils.Export.Material; using Model = Meddle.Utils.Export.Model; -namespace Meddle.Plugin.Utils; +namespace Meddle.Plugin.Services; -public class ExportUtil : IDisposable +public class ExportService : IDisposable { private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ExportUtil"); private readonly TexFile catchlightTex; private readonly TexFile tileNormTex; private readonly TexFile tileOrbTex; - private readonly EventLogger logger; + private readonly EventLogger logger; public event Action? OnLogEvent; private readonly SqPack pack; private readonly PbdFile pbdFile; - public ExportUtil(SqPack pack, ILogger logger) + public ExportService(SqPack pack, ILogger logger) { this.pack = pack; - this.logger = new EventLogger(logger); + this.logger = new EventLogger(logger); this.logger.OnLogEvent += OnLog; // chara/xls/boneDeformer/human.pbd diff --git a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs similarity index 86% rename from Meddle/Meddle.Plugin/Utils/ParseUtil.cs rename to Meddle/Meddle.Plugin/Services/ParseService.cs index bb351a8..4e9b7f7 100644 --- a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -3,42 +3,40 @@ using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; +using Meddle.Plugin.Models; +using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Meddle.Utils.Files.Structs.Material; -using Meddle.Plugin.Models; using Meddle.Utils.Models; -using Meddle.Utils.Skeletons.Havok; -using Meddle.Utils.Skeletons.Havok.Models; using Microsoft.Extensions.Logging; using Attach = Meddle.Plugin.Skeleton.Attach; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; -namespace Meddle.Plugin.Utils; +namespace Meddle.Plugin.Services; -public class ParseUtil : IDisposable +public class ParseService : IDisposable { private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ParseUtil"); private readonly DXHelper dxHelper; private readonly PbdHooks pbdHooks; - private readonly EventLogger logger; + private readonly EventLogger logger; private readonly IFramework framework; private readonly SqPack pack; public event Action? OnLogEvent; private readonly Dictionary shpkCache = new(); - public ParseUtil(SqPack pack, IFramework framework, DXHelper dxHelper, PbdHooks pbdHooks, ILogger logger) + public ParseService(SqPack pack, IFramework framework, DXHelper dxHelper, PbdHooks pbdHooks, ILogger logger) { this.pack = pack; this.framework = framework; this.dxHelper = dxHelper; this.pbdHooks = pbdHooks; - this.logger = new EventLogger(logger); + this.logger = new EventLogger(logger); this.logger.OnLogEvent += OnLog; } @@ -327,50 +325,7 @@ public unsafe AttachedModelGroup HandleAttachGroup( var attachGroup = new AttachedModelGroup(attach, models.ToArray(), skeleton); return attachGroup; } - - private unsafe List ParseSkeletons(Human* human) - { - var skeletonResourceHandles = - new Span>(human->Skeleton->SkeletonResourceHandles, - human->Skeleton->PartialSkeletonCount); - var skeletons = new List(); - foreach (var skeletonPtr in skeletonResourceHandles) - { - var skeletonResourceHandle = skeletonPtr.Value; - if (skeletonResourceHandle == null) - { - continue; - } - - var fileName = skeletonResourceHandle->ResourceHandle.FileName.ToString(); - var skeletonFileResource = pack.GetFileOrReadFromDisk(fileName); - if (skeletonFileResource == null) - { - continue; - } - - var sklbFile = new SklbFile(skeletonFileResource); - var tempFile = Path.GetTempFileName(); - try - { - File.WriteAllBytes(tempFile, sklbFile.Skeleton.ToArray()); - var hkXml = framework.RunOnTick(() => - { - var xml = HkUtil.HkxToXml(tempFile); - return xml; - }).GetAwaiter().GetResult(); - var havokXml = HavokUtils.ParseHavokXml(hkXml); - - skeletons.Add(havokXml); - } finally - { - File.Delete(tempFile); - } - } - - return skeletons; - } - + public void Dispose() { logger.LogDebug("Disposing ParseUtil"); diff --git a/Meddle/Meddle.Plugin/Utils/PbdHooks.cs b/Meddle/Meddle.Plugin/Services/PbdHooks.cs similarity index 87% rename from Meddle/Meddle.Plugin/Utils/PbdHooks.cs rename to Meddle/Meddle.Plugin/Services/PbdHooks.cs index 39a68a8..c2543e4 100644 --- a/Meddle/Meddle.Plugin/Utils/PbdHooks.cs +++ b/Meddle/Meddle.Plugin/Services/PbdHooks.cs @@ -5,16 +5,16 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Microsoft.Extensions.Logging; -namespace Meddle.Plugin.Utils; +namespace Meddle.Plugin.Services; public class PbdHooks : IDisposable { private readonly ISigScanner sigScanner; private readonly IGameInteropProvider gameInterop; private readonly ILogger logger; - public const string Human_CreateDeformerSig = "40 53 48 83 EC 20 4C 8B C1 83 FA 0D"; - private delegate nint Human_CreateDeformerDelegate(nint humanPtr, uint slot); - private Hook? humanCreateDeformerHook; + public const string HumanCreateDeformerSig = "40 53 48 83 EC 20 4C 8B C1 83 FA 0D"; + private delegate nint HumanCreateDeformerDelegate(nint humanPtr, uint slot); + private Hook? humanCreateDeformerHook; private readonly Dictionary> deformerCache = new(); public PbdHooks(ISigScanner sigScanner, IGameInteropProvider gameInterop, ILogger logger) @@ -26,10 +26,10 @@ public PbdHooks(ISigScanner sigScanner, IGameInteropProvider gameInterop, ILogge public void Setup() { - if (sigScanner.TryScanText(Human_CreateDeformerSig, out var humanCreateDeformerPtr)) + if (sigScanner.TryScanText(HumanCreateDeformerSig, out var humanCreateDeformerPtr)) { logger.LogDebug("Found Human::CreateDeformer at {ptr:X}", humanCreateDeformerPtr); - humanCreateDeformerHook = gameInterop.HookFromAddress(humanCreateDeformerPtr, Human_CreateDeformerDetour); + humanCreateDeformerHook = gameInterop.HookFromAddress(humanCreateDeformerPtr, Human_CreateDeformerDetour); humanCreateDeformerHook.Enable(); } else diff --git a/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs b/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs index 3bbe0ae..047cd83 100644 --- a/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs +++ b/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using FFXIVClientStructs.Havok.Animation.Rig; using FFXIVClientStructs.Interop; +using Meddle.Plugin.Utils; using Meddle.Utils.Skeletons; using PartialCSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton; using CSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton; @@ -115,7 +116,11 @@ public unsafe SkeletonPose(hkaPose* pose) var boneCount = pose->LocalPose.Length; for (var i = 0; i < boneCount; ++i) { - var localSpace = pose->AccessBoneLocalSpace(i); + var localSpace = PoseUtil.AccessBoneLocalSpace(pose, i); + if (localSpace == null) + { + throw new Exception("Failed to access bone local space"); + } transforms.Add(new Transform(*localSpace)); } diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index 4e3a60c..13b1103 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -21,7 +21,7 @@ public class AnimationTab : ITab private readonly ILogger logger; private readonly IClientState clientState; private readonly IObjectTable objectTable; - private readonly ExportUtil exportUtil; + private readonly ExportService exportService; private readonly PluginState pluginState; private readonly Configuration config; public string Name => "Animation"; @@ -35,7 +35,7 @@ public class AnimationTab : ITab public AnimationTab(IFramework framework, ILogger logger, IClientState clientState, IObjectTable objectTable, - ExportUtil exportUtil, + ExportService exportService, PluginState pluginState, Configuration config) { @@ -43,7 +43,7 @@ public AnimationTab(IFramework framework, ILogger logger, this.logger = logger; this.clientState = clientState; this.objectTable = objectTable; - this.exportUtil = exportUtil; + this.exportService = exportService; this.pluginState = pluginState; this.config = config; this.framework.Update += OnFrameworkUpdate; @@ -121,7 +121,7 @@ public unsafe void Draw() ImGui.Text($"Frames: {frameCount}"); if (ImGui.Button("Export")) { - exportUtil.ExportAnimation(frames, includePositionalData); + exportService.ExportAnimation(frames, includePositionalData); } ImGui.SameLine(); @@ -269,7 +269,7 @@ private unsafe void DrawSelectedCharacter() var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; if (cBase == null) return; - UIUtil.DrawCharacterAttaches(charPtr); + UiUtil.DrawCharacterAttaches(charPtr); } private static unsafe Dictionary? GetPose(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* skeleton) diff --git a/Meddle/Meddle.Plugin/UI/CharacterTab.cs b/Meddle/Meddle.Plugin/UI/CharacterTab.cs index 84826f7..94d1b0c 100644 --- a/Meddle/Meddle.Plugin/UI/CharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/CharacterTab.cs @@ -28,10 +28,10 @@ public unsafe class CharacterTab : ITab private readonly Dictionary channelCache = new(); private readonly IClientState clientState; - private readonly ExportUtil exportUtil; + private readonly ExportService exportService; private readonly ILogger log; private readonly IObjectTable objectTable; - private readonly ParseUtil parseUtil; + private readonly ParseService parseService; private readonly Configuration config; private readonly PluginState pluginState; @@ -47,15 +47,15 @@ public CharacterTab( IClientState clientState, ILogger log, PluginState pluginState, - ExportUtil exportUtil, + ExportService exportService, ITextureProvider textureProvider, - ParseUtil parseUtil, + ParseService parseService, Configuration config) { this.log = log; this.pluginState = pluginState; - this.exportUtil = exportUtil; - this.parseUtil = parseUtil; + this.exportService = exportService; + this.parseService = parseService; this.config = config; this.objectTable = objectTable; this.clientState = clientState; @@ -182,12 +182,12 @@ private void DrawCharacterGroup() ImGui.Separator(); // draw customizeparams var customizeParams = characterGroup.CustomizeParams; - UIUtil.DrawCustomizeParams(ref customizeParams); + UiUtil.DrawCustomizeParams(ref customizeParams); ImGui.NextColumn(); // draw customize data var customizeData = characterGroup.CustomizeData; - UIUtil.DrawCustomizeData(customizeData); + UiUtil.DrawCustomizeData(customizeData); ImGui.Text(characterGroup.GenderRace.ToString()); ImGui.NextColumn(); @@ -219,7 +219,7 @@ private void DrawCharacterGroup() { try { - exportUtil.Export(characterGroup with {MdlGroups = [mdlGroup], AttachedModelGroups = []}); + exportService.Export(characterGroup with {MdlGroups = [mdlGroup], AttachedModelGroups = []}); } catch (Exception e) { @@ -290,7 +290,7 @@ private void DrawCharacterGroup() { try { - exportUtil.Export(characterGroup with {AttachedModelGroups = [attachedModelGroup], MdlGroups = []}); + exportService.Export(characterGroup with {AttachedModelGroups = [attachedModelGroup], MdlGroups = []}); } catch (Exception e) { @@ -362,7 +362,7 @@ private void DrawExportOptions() { try { - exportUtil.Export(characterGroup, default); + exportService.Export(characterGroup, default); } catch (Exception e) { @@ -385,7 +385,7 @@ private void DrawExportOptions() { try { - exportUtil.Export(selectedSetGroup); + exportService.Export(selectedSetGroup); } catch (Exception e) { @@ -402,7 +402,7 @@ private void DrawExportOptions() { try { - exportUtil.ExportRawTextures(characterGroup); + exportService.ExportRawTextures(characterGroup); } catch (Exception e) { @@ -471,7 +471,7 @@ private Task ParseCharacter(ICharacter character) genderRace = GenderRace.Unknown; } - var colorTableTextures = parseUtil.ParseColorTableTextures(characterBase); + var colorTableTextures = parseService.ParseColorTableTextures(characterBase); var attachDict = new Dictionary, Dictionary>(); if (charPtr->Mount.MountObject != null) @@ -480,7 +480,7 @@ private Task ParseCharacter(ICharacter character) if (mountDrawObject != null && mountDrawObject->Object.GetObjectType() == ObjectType.CharacterBase) { var mountBase = (CharacterBase*)mountDrawObject; - var mountColorTableTextures = parseUtil.ParseColorTableTextures(mountBase); + var mountColorTableTextures = parseService.ParseColorTableTextures(mountBase); attachDict[mountBase] = mountColorTableTextures; } } @@ -491,7 +491,7 @@ private Task ParseCharacter(ICharacter character) if (ornamentDrawObject != null && ornamentDrawObject->Object.GetObjectType() == ObjectType.CharacterBase) { var ornamentBase = (CharacterBase*)ornamentDrawObject; - var ornamentColorTableTextures = parseUtil.ParseColorTableTextures(ornamentBase); + var ornamentColorTableTextures = parseService.ParseColorTableTextures(ornamentBase); attachDict[ornamentBase] = ornamentColorTableTextures; } } @@ -513,7 +513,7 @@ private Task ParseCharacter(ICharacter character) } var weaponBase = (CharacterBase*)draw; - var weaponColorTableTextures = parseUtil.ParseColorTableTextures(weaponBase); + var weaponColorTableTextures = parseService.ParseColorTableTextures(weaponBase); attachDict[weaponBase] = weaponColorTableTextures; } } @@ -521,7 +521,7 @@ private Task ParseCharacter(ICharacter character) // begin background work try { - characterGroup = parseUtil.HandleCharacterGroup(characterBase, colorTableTextures, attachDict, + characterGroup = parseService.HandleCharacterGroup(characterBase, colorTableTextures, attachDict, customizeParams, customizeData, genderRace); selectedSetGroup = characterGroup; } @@ -706,7 +706,7 @@ private void DrawMtrlGroup(MtrlFileGroup mtrlGroup) if (ImGui.CollapsingHeader($"Color Table##{mtrlGroup.GetHashCode()}")) { - UIUtil.DrawColorTable(mtrlGroup.MtrlFile); + UiUtil.DrawColorTable(mtrlGroup.MtrlFile); } foreach (var texGroup in mtrlGroup.TexFiles) @@ -860,7 +860,7 @@ private void DrawTexFile(string path, TextureResource file) if (ImGui.Button("Export as .png")) { - ExportUtil.ExportTexture(textureImage.Bitmap.Bitmap, path); + ExportService.ExportTexture(textureImage.Bitmap.Bitmap, path); } } diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs index ac9d785..0843ee4 100644 --- a/Meddle/Meddle.Plugin/UI/DebugTab.cs +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -1,8 +1,11 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Meddle.Plugin.Utils; +using Meddle.Utils.Skeletons; namespace Meddle.Plugin.UI; @@ -67,7 +70,7 @@ public void Draw() DrawDebugMenu(); } - private void DrawDebugMenu() + private unsafe void DrawDebugMenu() { if (selectedCharacter == null) { @@ -86,7 +89,91 @@ private void DrawDebugMenu() ImGui.SetClipboardText($"{selectedCharacter.Address:X8}"); } + + var character = (Character*)selectedCharacter.Address; + if (character == null) + { + ImGui.Text("Character is null"); + return; + } + + ImGui.Text($"Character Name: {character->NameString}"); + + var drawObject = character->DrawObject; + if (drawObject == null) + { + ImGui.Text("DrawObject is null"); + return; + } + + ImGui.Text($"DrawObject Address: {(nint)drawObject:X8}"); + + var objectType = drawObject->GetObjectType(); + ImGui.Text($"Object Type: {objectType}"); + if (objectType != ObjectType.CharacterBase) + { + return; + } + + var cBase = (CharacterBase*)drawObject; + var skeleton = cBase->Skeleton; + if (skeleton == null) + { + ImGui.Text("Skeleton is null"); + return; + } + + // imgui select partial skeleton by index + ImGui.Text($"Partial Skeleton Count: {skeleton->PartialSkeletonCount}"); + if (ImGui.InputInt("##PartialSkeletonIndex", ref selectedPartialSkeletonIndex)) + { + if (selectedPartialSkeletonIndex < 0) + { + selectedPartialSkeletonIndex = 0; + } + else if (selectedPartialSkeletonIndex >= skeleton->PartialSkeletonCount) + { + selectedPartialSkeletonIndex = skeleton->PartialSkeletonCount - 1; + } + } + + var partialSkeleton = skeleton->PartialSkeletons[selectedPartialSkeletonIndex]; + + ImGui.Text($"Partial Skeleton Bone Count: {partialSkeleton.BoneCount}"); + if (ImGui.InputInt("##BoneIndex", ref selectedBoneIndex)) + { + if (selectedBoneIndex < 0) + { + selectedBoneIndex = 0; + } + else if (selectedBoneIndex >= partialSkeleton.BoneCount) + { + selectedBoneIndex = (int)partialSkeleton.BoneCount - 1; + } + } + + var pose = partialSkeleton.GetHavokPose(0); + if (pose == null) + { + ImGui.Text("Pose is null"); + return; + } + + var localPoseValue = pose->LocalPose[selectedBoneIndex]; + var transform = new Transform(localPoseValue); + ImGui.Text($"Transform: {transform}"); + var poseBone = PoseUtil.AccessBoneLocalSpace(pose, selectedBoneIndex); + if (poseBone == null) + { + ImGui.Text("Pose Bone is null"); + return; + } + var boneTransform = new Transform(*poseBone); + ImGui.Text($"Bone Transform: {boneTransform}"); } + + private int selectedPartialSkeletonIndex; + private int selectedBoneIndex; private ICharacter? selectedCharacter; diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 3f6ba45..10e4b6c 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -28,11 +28,11 @@ namespace Meddle.Plugin.UI; public unsafe class LiveCharacterTab : ITab { private readonly IClientState clientState; - private readonly ExportUtil exportUtil; + private readonly ExportService exportService; private readonly ITextureProvider textureProvider; private readonly ILogger log; private readonly IObjectTable objectTable; - private readonly ParseUtil parseUtil; + private readonly ParseService parseService; private readonly DXHelper dxHelper; private readonly TextureCache textureCache; private readonly SqPack pack; @@ -50,9 +50,9 @@ public LiveCharacterTab( IClientState clientState, ILogger log, PluginState pluginState, - ExportUtil exportUtil, + ExportService exportService, ITextureProvider textureProvider, - ParseUtil parseUtil, + ParseService parseService, DXHelper dxHelper, TextureCache textureCache, SqPack pack, @@ -60,9 +60,9 @@ public LiveCharacterTab( { this.log = log; this.pluginState = pluginState; - this.exportUtil = exportUtil; + this.exportService = exportService; this.textureProvider = textureProvider; - this.parseUtil = parseUtil; + this.parseService = parseService; this.dxHelper = dxHelper; this.textureCache = textureCache; this.pack = pack; @@ -255,8 +255,8 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete (result, path) => { if (!result) return; - var colorTableTextures = parseUtil.ParseColorTableTextures(cBase); - var modelData = parseUtil.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); + var colorTableTextures = parseService.ParseColorTableTextures(cBase); + var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); if (modelData == null) { log.LogError("Failed to get model data for {FileName}", fileName); @@ -267,7 +267,7 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, [modelData], skeleton, []); - Task.Run(() => { exportUtil.Export(cGroup, path); }); + Task.Run(() => { exportService.Export(cGroup, path); }); }, Plugin.TempDirectory); } @@ -281,7 +281,7 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete ImGui.Text($"Game File Name: {modelName}"); ImGui.Text($"File Name: {fileName}"); ImGui.Text($"Slot Index: {model->SlotIndex}"); - var modelShapeAttributes = parseUtil.ParseModelShapeAttributes(model); + var modelShapeAttributes = parseService.ParseModelShapeAttributes(model); DrawShapeAttributeTable(modelShapeAttributes); for (var materialIdx = 0; materialIdx < model->MaterialsSpan.Length; materialIdx++) @@ -436,8 +436,8 @@ private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* ma ImGui.CollapsingHeader("Color Table")) { var colorTableTexture = colorTableTexturePtr.Value; - var colorTable = parseUtil.ParseColorTableTexture(colorTableTexture); - UIUtil.DrawColorTable(colorTable); + var colorTable = parseService.ParseColorTableTexture(colorTableTexture); + UiUtil.DrawColorTable(colorTable); } for (var texIdx = 0; texIdx < material->TextureCount; texIdx++) @@ -571,8 +571,8 @@ private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, if (ImGui.CollapsingHeader("Customize Options")) { - UIUtil.DrawCustomizeParams(ref customizeParams); - UIUtil.DrawCustomizeData(customizeData); + UiUtil.DrawCustomizeParams(ref customizeParams); + UiUtil.DrawCustomizeData(customizeData); ImGui.Text(genderRace.ToString()); } } diff --git a/Meddle/Meddle.Plugin/UI/WorldTab.cs b/Meddle/Meddle.Plugin/UI/WorldTab.cs index 4735e1c..7d4a37e 100644 --- a/Meddle/Meddle.Plugin/UI/WorldTab.cs +++ b/Meddle/Meddle.Plugin/UI/WorldTab.cs @@ -16,7 +16,7 @@ public class WorldTab : ITab { private readonly IClientState clientState; private readonly Configuration config; - private readonly ExportUtil exportUtil; + private readonly ExportService exportService; private readonly ILogger log; private readonly List objects = new(); @@ -25,12 +25,12 @@ public class WorldTab : ITab private Task exportTask = Task.CompletedTask; public WorldTab( - PluginState pluginState, IClientState clientState, ExportUtil exportUtil, ILogger log, + PluginState pluginState, IClientState clientState, ExportService exportService, ILogger log, Configuration config) { this.pluginState = pluginState; this.clientState = clientState; - this.exportUtil = exportUtil; + this.exportService = exportService; this.log = log; this.config = config; } @@ -129,7 +129,7 @@ public void Draw() } } - exportTask = Task.Run(() => exportUtil.ExportResource(resources.ToArray(), position)); + exportTask = Task.Run(() => exportService.ExportResource(resources.ToArray(), position)); } ImGui.EndDisabled(); diff --git a/Meddle/Meddle.Plugin/Utils/HkUtil.cs b/Meddle/Meddle.Plugin/Utils/HkUtil.cs deleted file mode 100644 index eaedc2d..0000000 --- a/Meddle/Meddle.Plugin/Utils/HkUtil.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Text; -using FFXIVClientStructs.Havok.Common.Base.System.IO.OStream; -using FFXIVClientStructs.Havok.Common.Base.Types; -using FFXIVClientStructs.Havok.Common.Serialize.Resource; -using FFXIVClientStructs.Havok.Common.Serialize.Util; - -namespace Meddle.Plugin.Utils; - -public static class HkUtil -{ - private static unsafe hkResource* Read(string filePath) - { - var path = Encoding.UTF8.GetBytes(filePath); - var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); - - var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; - loadOptions->Flags = new hkFlags - {Storage = (int)hkSerializeUtil.LoadOptionBits.Default}; - loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); - loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); - - return hkSerializeUtil.LoadFromFile(path, null, loadOptions); - } - - public static unsafe string HkxToXml(string pathToHkx) - { - const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers - | hkSerializeUtil.SaveOptionBits.TextFormat - | hkSerializeUtil.SaveOptionBits.WriteAttributes; - - var resource = Read(pathToHkx); - - if (resource == null) - throw new Exception("Failed to read havok file."); - - var file = Write(resource, options); - file.Close(); - - var contents = File.ReadAllText(file.Name); - File.Delete(file.Name); - - return contents; - } - - private static unsafe FileStream Write( - hkResource* resource, - hkSerializeUtil.SaveOptionBits optionBits - ) - { - var tempFileName = Path.GetTempFileName(); - var tempFile = Path.ChangeExtension(tempFileName, ".hkx"); - var path = Encoding.UTF8.GetBytes(tempFile); - var oStream = new hkOstream(); - oStream.Ctor(path); - - var result = stackalloc hkResult[1]; - - var saveOptions = new hkSerializeUtil.SaveOptions - { - Flags = new hkFlags {Storage = (int)optionBits} - }; - - var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); - var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); - var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); - - try - { - const string name = "hkRootLevelContainer"; - - var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); - if (resourcePtr == null) - throw new Exception("Failed to retrieve havok root level container resource."); - - var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); - if (hkRootLevelContainerClass == null) - throw new Exception("Failed to retrieve havok root level container type."); - - hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); - } finally - { - oStream.Dtor(); - } - - if (result->Result == hkResult.hkResultEnum.Failure) - throw new Exception("Failed to serialize havok file."); - - return new FileStream(tempFile, FileMode.Open); - } -} diff --git a/Meddle/Meddle.Plugin/Utils/PoseUtil.cs b/Meddle/Meddle.Plugin/Utils/PoseUtil.cs new file mode 100644 index 0000000..9779ce8 --- /dev/null +++ b/Meddle/Meddle.Plugin/Utils/PoseUtil.cs @@ -0,0 +1,24 @@ +using Dalamud.Game; +using FFXIVClientStructs.Havok.Animation.Rig; +using FFXIVClientStructs.Havok.Common.Base.Math.QsTransform; + +namespace Meddle.Plugin.Utils; + +public static class PoseUtil +{ + public static ISigScanner? SigScanner { get; set; } = null!; + + public static unsafe hkQsTransformf* AccessBoneLocalSpace(hkaPose* pose, int boneIdx) + { + if (SigScanner == null) + throw new Exception("SigScanner not set"); + + if (SigScanner.TryScanText("4C 8B DC 53 55 56 57 41 54 41 56 48 81 EC", out var accessBoneLocalSpacePtr)) + { + var accessBoneLocalSpace = (delegate* unmanaged)accessBoneLocalSpacePtr; + return accessBoneLocalSpace(pose, boneIdx); + } + + throw new Exception("Failed to find hkaPose::AccessBoneLocalSpace"); + } +} diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 8494709..a50407b 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -13,7 +13,7 @@ namespace Meddle.Plugin.Utils; -public static class UIUtil +public static class UiUtil { public static void DrawCustomizeParams(ref CustomizeParameter customize) { @@ -272,10 +272,10 @@ private static unsafe void DrawCharacterBase(CharacterBase* character, string na if (skeleton == null) return; - Skeleton.Attach attachPoint; + Attach attachPoint; try { - attachPoint = new Skeleton.Attach(character->Attach); + attachPoint = new Attach(character->Attach); } catch (Exception e) { From a457050ebf9d205b8e542bccafc15ee60e8afdc7 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 21:11:42 +1000 Subject: [PATCH 09/19] Add select option for individual models --- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 10e4b6c..cb55825 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -6,6 +6,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Common.Math; +using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; using Meddle.Plugin.Services; @@ -39,6 +40,7 @@ public unsafe class LiveCharacterTab : ITab private readonly Configuration config; private readonly PluginState pluginState; private ICharacter? selectedCharacter; + private Dictionary, bool> selectedModels = new(); private readonly FileDialogManager fileDialog = new FileDialogManager { @@ -225,6 +227,20 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete { ImGui.OpenPopup("ExportModelPopup"); } + + ImGui.SameLine(); + var selected = selectedModels.ContainsKey(model); + if (ImGui.Checkbox("##Selected", ref selected)) + { + if (selected) + { + selectedModels[model] = true; + } + else + { + selectedModels.Remove(model); + } + } } // popup for export options From 5deb0902c0c1b0288063659a56a962746c96e2a9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:00:39 +1000 Subject: [PATCH 10/19] Add attached objects to character view --- Meddle/Meddle.Plugin/UI/DebugTab.cs | 1 - Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 148 ++++++++++++++++++-- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 13 ++ 3 files changed, 146 insertions(+), 16 deletions(-) diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs index 0843ee4..9485125 100644 --- a/Meddle/Meddle.Plugin/UI/DebugTab.cs +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -176,5 +176,4 @@ private unsafe void DrawDebugMenu() private int selectedBoneIndex; private ICharacter? selectedCharacter; - } diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index cb55825..2288a47 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -40,9 +40,9 @@ public unsafe class LiveCharacterTab : ITab private readonly Configuration config; private readonly PluginState pluginState; private ICharacter? selectedCharacter; - private Dictionary, bool> selectedModels = new(); + private readonly Dictionary, bool> selectedModels = new(); - private readonly FileDialogManager fileDialog = new FileDialogManager + private readonly FileDialogManager fileDialog = new() { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; @@ -96,6 +96,7 @@ public void Dispose() if (!IsDisposed) { log.LogDebug("Disposing CharacterTabAlt"); + selectedModels.Clear(); IsDisposed = true; } } @@ -143,11 +144,11 @@ private void DrawObjectPicker() } } - DrawCharacterGroup(); + DrawSelectedCharacter(); fileDialog.Draw(); } - private void DrawCharacterGroup() + private void DrawSelectedCharacter() { if (selectedCharacter == null) { @@ -156,26 +157,60 @@ private void DrawCharacterGroup() } var charPtr = (CSCharacter*)selectedCharacter.Address; - if (charPtr == null) + DrawCharacter(charPtr); + } + + private void DrawCharacter(CSCharacter* character) + { + if (character == null) { ImGui.Text("Character is null"); return; } - var drawObject = charPtr->GameObject.DrawObject; + var drawObject = character->GameObject.DrawObject; + DrawDrawObject(drawObject); + + if (character->Mount.MountObject != null) + { + DrawCharacter(character->Mount.MountObject); + } + + if (character->CompanionData.CompanionObject != null) + { + DrawDrawObject(character->CompanionData.CompanionObject->DrawObject); + } + + if (character->OrnamentData.OrnamentObject != null) + { + DrawDrawObject(character->OrnamentData.OrnamentObject->DrawObject); + } + + foreach (var weaponData in character->DrawData.WeaponData) + { + if (weaponData.DrawObject != null) + { + DrawDrawObject(weaponData.DrawObject); + } + } + } + + private void DrawDrawObject(DrawObject* drawObject) + { if (drawObject == null) { - ImGui.Text("Character has no draw object"); + ImGui.Text("Draw object is null"); return; } - + var objectType = drawObject->Object.GetObjectType(); if (objectType != ObjectType.CharacterBase) { - ImGui.Text("Selected object is not a character"); + ImGui.Text($"Draw object is not a character base ({objectType})"); return; } - + + using var drawObjectId = ImRaii.PushId($"{(nint)drawObject}"); var cBase = (CSCharacterBase*)drawObject; var modelType = cBase->GetModelType(); CustomizeParameter? customizeParams = null; @@ -186,6 +221,70 @@ private void DrawCharacterGroup() DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); } + if (ImGui.Button("Export All Models")) + { + var colorTableTextures = parseService.ParseColorTableTextures(cBase); + var models = new List(); + foreach (var modelPtr in cBase->ModelsSpan) + { + if (modelPtr == null) continue; + var model = modelPtr.Value; + if (model == null) continue; + var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); + if (modelData == null) continue; + models.Add(modelData); + } + + var skeleton = new Skeleton.Skeleton(cBase->Skeleton); + var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), + customizeData ?? new CustomizeData(), + genderRace, models.ToArray(), + skeleton, []); + fileDialog.SaveFolderDialog("Save Model", "Character", + (result, path) => + { + if (!result) return; + + Task.Run(() => { exportService.Export(cGroup, path); }); + }, Plugin.TempDirectory); + } + + ImGui.SameLine(); + var selectedModelCount = cBase->ModelsSpan.ToArray().Count(modelPtr => + { + if (modelPtr == null) return false; + return selectedModels.ContainsKey(modelPtr.Value) && selectedModels[modelPtr.Value]; + }); + using (var disable = ImRaii.Disabled(selectedModelCount == 0)) + { + if (ImGui.Button($"Export Selected Models ({selectedModelCount})") && selectedModelCount > 0) + { + var colorTableTextures = parseService.ParseColorTableTextures(cBase); + var models = new List(); + foreach (var modelPtr in cBase->ModelsSpan) + { + if (modelPtr == null) continue; + if (!selectedModels.TryGetValue(modelPtr, out var isSelected) || !isSelected) continue; + var model = modelPtr.Value; + if (model == null) continue; + var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); + if (modelData == null) continue; + models.Add(modelData); + } + + var skeleton = new Skeleton.Skeleton(cBase->Skeleton); + var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, models.ToArray(), skeleton, []); + + fileDialog.SaveFolderDialog("Save Model", "Character", + (result, path) => + { + if (!result) return; + + Task.Run(() => { exportService.Export(cGroup, path); }); + }, Plugin.TempDirectory); + } + } + using var modelTable = ImRaii.Table("##Models", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableSetupColumn("Character Data", ImGuiTableColumnFlags.WidthStretch); @@ -218,7 +317,6 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete ImGui.TableNextRow(); var fileName = model->ModelResourceHandle->FileName.ToString(); var modelName = cBase->ResolveMdlPath(model->SlotIndex); - //var actualModelName = gamePathHandler.ClassifyMdlGamePath(modelName); ImGui.TableSetColumnIndex(0); using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -294,9 +392,10 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete if (ImGui.CollapsingHeader($"[{model->SlotIndex}] {modelName}")) { - ImGui.Text($"Game File Name: {modelName}"); - ImGui.Text($"File Name: {fileName}"); + UiUtil.Text($"Game File Name: {modelName}", modelName); + UiUtil.Text($"File Name: {fileName}", fileName); ImGui.Text($"Slot Index: {model->SlotIndex}"); + UiUtil.Text($"Skeleton Ptr: {(nint)model->Skeleton:X8}", $"{(nint)model->Skeleton:X8}"); var modelShapeAttributes = parseService.ParseModelShapeAttributes(model); DrawShapeAttributeTable(modelShapeAttributes); @@ -393,6 +492,25 @@ private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* ma // popup for export options if (ImGui.BeginPopupContextItem("ExportMaterialPopup")) { + if (ImGui.MenuItem("Export as mtrl")) + { + var defaultFileName = Path.GetFileName(materialName); + fileDialog.SaveFileDialog("Save Material", "Material File{.mtrl}", defaultFileName, ".mtrl", + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(materialFileName); + if (data == null) + { + log.LogError("Failed to get material data from pack or disk for {MaterialFileName}", + materialFileName); + return; + } + + File.WriteAllBytes(path, data); + }); + } + if (ImGui.MenuItem("Export raw textures as pngs")) { var textureBuffer = new Dictionary(); @@ -532,8 +650,8 @@ private void DrawTexture(CSMaterial* material, CSMaterial.TextureEntry textureEn ImGui.TableSetColumnIndex(1); if (ImGui.CollapsingHeader(textureName ?? textureFileName)) { - ImGui.Text($"Game File Name: {textureName}"); - ImGui.Text($"File Name: {textureFileName}"); + UiUtil.Text($"Game File Name: {textureName}", textureName); + UiUtil.Text($"File Name: {textureFileName}", textureFileName); ImGui.Text($"Id: {textureEntry.Id}"); var availableWidth = ImGui.GetContentRegionAvail().X; diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index a50407b..43959e0 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -15,6 +15,19 @@ namespace Meddle.Plugin.Utils; public static class UiUtil { + public static void Text(string text, string? copyValue) + { + ImGui.Text(text); + if (ImGui.IsItemHovered() && copyValue != null) + { + ImGui.SetTooltip($"Click to copy {copyValue}"); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText(copyValue); + } + } + } + public static void DrawCustomizeParams(ref CustomizeParameter customize) { ImGui.ColorEdit3("Skin Color", ref customize.SkinColor); From 37d23997e80975ab5f61ef86d6ff30bcf1bf1482 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:05:22 +1000 Subject: [PATCH 11/19] Update PluginLoggerProvider.cs --- .../Meddle.Plugin/Services/PluginLoggerProvider.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs b/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs index a849034..cf4d0f2 100644 --- a/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs +++ b/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs @@ -55,6 +55,8 @@ public bool IsEnabled(LogLevel logLevel) return null; } + private readonly List notifications = new(); + private void LogNotification(string message, LogLevel level) { if (level < config.MinimumNotificationLogLevel) return; @@ -79,7 +81,15 @@ private void LogNotification(string message, LogLevel level) InitialDuration = TimeSpan.FromSeconds(2) }; - notificationManager.AddNotification(notification); + var notif = notificationManager.AddNotification(notification); + notifications.Add(notif); + + if (notifications.Count > 5) + { + var toRemove = notifications[0]; + notifications.RemoveAt(0); + toRemove.DismissNow(); + } } public void Log( From 4817ffa5425da144237636975fa8a59570f2811d Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:07:14 +1000 Subject: [PATCH 12/19] Only apply name override to players --- Meddle/Meddle.Plugin/Utils/ObjectUtil.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs index ac7bae7..f3b7cbe 100644 --- a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Meddle.Plugin.Utils; @@ -60,8 +61,10 @@ public static unsafe string GetCharacterDisplayText(this IClientState clientStat return "Invalid Character"; var modelType = ((CharacterBase*)drawObject)->GetModelType(); - - var name = string.IsNullOrWhiteSpace(overrideName) ? obj.Name.TextValue : overrideName; + + var name = obj.Name.TextValue; + if (obj.ObjectKind == ObjectKind.Player && !string.IsNullOrWhiteSpace(overrideName)) + name = overrideName; return $"[{obj.Address:X8}:{obj.GameObjectId:X}][{obj.ObjectKind}][{modelType}] - {(string.IsNullOrWhiteSpace(name) ? "Unnamed" : name)} - {clientState.GetDistanceToLocalPlayer(obj).Length():0.00}y##{obj.GameObjectId}"; } From eeae419774049990e2865d630516f738214fc030 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:31:47 +1000 Subject: [PATCH 13/19] Update ObjectUtil.cs --- Meddle/Meddle.Plugin/Utils/ObjectUtil.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs index f3b7cbe..c2e4368 100644 --- a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs @@ -66,6 +66,8 @@ public static unsafe string GetCharacterDisplayText(this IClientState clientStat if (obj.ObjectKind == ObjectKind.Player && !string.IsNullOrWhiteSpace(overrideName)) name = overrideName; return - $"[{obj.Address:X8}:{obj.GameObjectId:X}][{obj.ObjectKind}][{modelType}] - {(string.IsNullOrWhiteSpace(name) ? "Unnamed" : name)} - {clientState.GetDistanceToLocalPlayer(obj).Length():0.00}y##{obj.GameObjectId}"; + $"[{obj.Address:X8}:{obj.GameObjectId:X}][{obj.ObjectKind}][{modelType}] - " + + $"{(string.IsNullOrWhiteSpace(name) ? "Unnamed" : name)} - " + + $"{clientState.GetDistanceToLocalPlayer(obj).Length():0.00}y##{obj.GameObjectId}"; } } From 39ab5ea65d250e17aaa3145de8f0320cbe7a104a Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:32:36 +1000 Subject: [PATCH 14/19] Cache customize data option + add pbd info to models + display text before different sets --- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 106 +++++++++++++++----- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 2288a47..8af92e7 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -37,6 +37,7 @@ public unsafe class LiveCharacterTab : ITab private readonly DXHelper dxHelper; private readonly TextureCache textureCache; private readonly SqPack pack; + private readonly PbdHooks pbd; private readonly Configuration config; private readonly PluginState pluginState; private ICharacter? selectedCharacter; @@ -58,6 +59,7 @@ public LiveCharacterTab( DXHelper dxHelper, TextureCache textureCache, SqPack pack, + PbdHooks pbd, Configuration config) { this.log = log; @@ -68,6 +70,7 @@ public LiveCharacterTab( this.dxHelper = dxHelper; this.textureCache = textureCache; this.pack = pack; + this.pbd = pbd; this.config = config; this.objectTable = objectTable; this.clientState = clientState; @@ -97,6 +100,7 @@ public void Dispose() { log.LogDebug("Disposing CharacterTabAlt"); selectedModels.Clear(); + humanCustomizeData.Clear(); IsDisposed = true; } } @@ -173,23 +177,32 @@ private void DrawCharacter(CSCharacter* character) if (character->Mount.MountObject != null) { + ImGui.Separator(); + ImGui.Text("Mount"); DrawCharacter(character->Mount.MountObject); } if (character->CompanionData.CompanionObject != null) { - DrawDrawObject(character->CompanionData.CompanionObject->DrawObject); + ImGui.Separator(); + ImGui.Text("Companion"); + DrawCharacter(&character->CompanionData.CompanionObject->Character); } if (character->OrnamentData.OrnamentObject != null) { - DrawDrawObject(character->OrnamentData.OrnamentObject->DrawObject); + ImGui.Separator(); + ImGui.Text("Ornament"); + DrawCharacter(&character->OrnamentData.OrnamentObject->Character); } - foreach (var weaponData in character->DrawData.WeaponData) + for (var weaponIdx = 0; weaponIdx < character->DrawData.WeaponData.Length; weaponIdx++) { + var weaponData = character->DrawData.WeaponData[weaponIdx]; if (weaponData.DrawObject != null) { + ImGui.Separator(); + ImGui.Text($"Weapon {weaponIdx}"); DrawDrawObject(weaponData.DrawObject); } } @@ -286,7 +299,7 @@ private void DrawDrawObject(DrawObject* drawObject) } using var modelTable = ImRaii.Table("##Models", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); - ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 80); ImGui.TableSetupColumn("Character Data", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -396,6 +409,18 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete UiUtil.Text($"File Name: {fileName}", fileName); ImGui.Text($"Slot Index: {model->SlotIndex}"); UiUtil.Text($"Skeleton Ptr: {(nint)model->Skeleton:X8}", $"{(nint)model->Skeleton:X8}"); + var deformerInfo = pbd.TryGetDeformer((nint)cBase, model->SlotIndex); + if (deformerInfo != null) + { + ImGui.Text($"Deformer Id: {(GenderRace)deformerInfo.Value.DeformerId} ({deformerInfo.Value.DeformerId})"); + ImGui.Text($"RaceSex Id: {(GenderRace)deformerInfo.Value.RaceSexId} ({deformerInfo.Value.RaceSexId})"); + ImGui.Text($"Pbd Path: {deformerInfo.Value.PbdPath}"); + } + else + { + ImGui.Text("No deformer info found"); + } + var modelShapeAttributes = parseService.ParseModelShapeAttributes(model); DrawShapeAttributeTable(modelShapeAttributes); @@ -677,35 +702,62 @@ private void DrawTexture(CSMaterial* material, CSMaterial.TextureEntry textureEn ImGui.Image(wrap.ImGuiHandle, new Vector2(displayWidth, displayHeight)); } } - + + private Dictionary, (CustomizeData, CustomizeParameter)> humanCustomizeData = new(); + private bool cacheHumanCustomizeData; private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, out CustomizeParameter customizeParams, out GenderRace genderRace) { - var customizeCBuf = cBase->CustomizeParameterCBuffer->TryGetBuffer()[0]; - customizeParams = new CustomizeParameter - { - SkinColor = customizeCBuf.SkinColor, - MuscleTone = customizeCBuf.MuscleTone, - SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, - LipColor = customizeCBuf.LipColor, - MainColor = customizeCBuf.MainColor, - FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, - HairFresnelValue0 = customizeCBuf.HairFresnelValue0, - MeshColor = customizeCBuf.MeshColor, - FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, - LeftColor = customizeCBuf.LeftColor, - RightColor = customizeCBuf.RightColor, - OptionColor = customizeCBuf.OptionColor - }; - customizeData = new CustomizeData - { - LipStick = cBase->Customize.Lipstick, - Highlights = cBase->Customize.Highlights - }; - genderRace = (GenderRace)cBase->RaceSexId; + if (cacheHumanCustomizeData && humanCustomizeData.TryGetValue(cBase, out var data)) + { + customizeData = data.Item1; + customizeParams = data.Item2; + genderRace = (GenderRace)cBase->RaceSexId; + } + else + { + var customizeCBuf = cBase->CustomizeParameterCBuffer->TryGetBuffer()[0]; + customizeParams = new CustomizeParameter + { + SkinColor = customizeCBuf.SkinColor, + MuscleTone = customizeCBuf.MuscleTone, + SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, + LipColor = customizeCBuf.LipColor, + MainColor = customizeCBuf.MainColor, + FacePaintUVMultiplier = customizeCBuf.FacePaintUVMultiplier, + HairFresnelValue0 = customizeCBuf.HairFresnelValue0, + MeshColor = customizeCBuf.MeshColor, + FacePaintUVOffset = customizeCBuf.FacePaintUVOffset, + LeftColor = customizeCBuf.LeftColor, + RightColor = customizeCBuf.RightColor, + OptionColor = customizeCBuf.OptionColor + }; + customizeData = new CustomizeData + { + LipStick = cBase->Customize.Lipstick, + Highlights = cBase->Customize.Highlights + }; + genderRace = (GenderRace)cBase->RaceSexId; + humanCustomizeData[cBase] = (customizeData, customizeParams); + } if (ImGui.CollapsingHeader("Customize Options")) { + if (ImGui.Checkbox("Cache Human Customize Data", ref cacheHumanCustomizeData)) + { + humanCustomizeData.Clear(); + } + + var width = ImGui.GetContentRegionAvail().X; + using var disable = ImRaii.Disabled(!cacheHumanCustomizeData); + using var table = ImRaii.Table("##CustomizeTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Params", ImGuiTableColumnFlags.WidthFixed, width * 0.75f); + ImGui.TableSetupColumn("Data"); + ImGui.TableHeadersRow(); + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); UiUtil.DrawCustomizeParams(ref customizeParams); + ImGui.TableSetColumnIndex(1); UiUtil.DrawCustomizeData(customizeData); ImGui.Text(genderRace.ToString()); } From 86e365353956320dc7b8c7773396ac874e39f0e7 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:45:34 +1000 Subject: [PATCH 15/19] Add animation positional data + fix floating point precision stuff --- .../Meddle.Plugin/Services/ExportService.cs | 20 ++++++++++++++++++- .../Meddle.Plugin/Skeleton/SkeletonUtils.cs | 19 +++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Meddle/Meddle.Plugin/Services/ExportService.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs index db102fa..7cb6ffe 100644 --- a/Meddle/Meddle.Plugin/Services/ExportService.cs +++ b/Meddle/Meddle.Plugin/Services/ExportService.cs @@ -128,13 +128,31 @@ public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePo { using var activity = ActivitySource.StartActivity(); var boneSets = SkeletonUtils.GetAnimatedBoneMap(frames.ToArray()); - + var startTime = frames.Min(x => x.Item1); var folder = GetPathForOutput(); foreach (var (id, boneSet) in boneSets) { var scene = new SceneBuilder(); if (boneSet.Root == null) throw new InvalidOperationException("Root bone not found"); logger.LogInformation("Adding bone set {Id}", id); + + if (includePositionalData) + { + var startPos = boneSet.Timeline.First().Attach.Transform.Translation; + foreach (var frameTime in boneSet.Timeline) + { + if (token.IsCancellationRequested) return; + var pos = frameTime.Attach.Transform.Translation; + var rot = frameTime.Attach.Transform.Rotation; + var scale = frameTime.Attach.Transform.Scale; + var time = SkeletonUtils.TotalSeconds(frameTime.Time, startTime); + var root = boneSet.Root; + root.UseTranslation().UseTrackBuilder("pose").WithPoint(time, pos - startPos); + root.UseRotation().UseTrackBuilder("pose").WithPoint(time, rot); + root.UseScale().UseTrackBuilder("pose").WithPoint(time, scale); + } + } + scene.AddNode(boneSet.Root); scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, boneSet.Bones.Cast().ToArray()); var sceneGraph = scene.ToGltf2(); diff --git a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs index a570587..19fbf4c 100644 --- a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs +++ b/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs @@ -252,9 +252,9 @@ private static Dictionary> GetB return bonePoseMap; }*/ - public static Dictionary Bones, BoneNodeBuilder? Root)> GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) + public static Dictionary Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline)> GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) { - var attachDict = new Dictionary Bones, BoneNodeBuilder? Root)>(); + var attachDict = new Dictionary Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline)>(); var attachTimelines = new Dictionary>(); foreach (var frame in frames) { @@ -278,14 +278,14 @@ private static Dictionary> GetB var firstAttach = timeline.First().Attach; if (!attachDict.TryGetValue(attachId, out var attachBoneMap)) { - attachBoneMap = ([], null); + attachBoneMap = ([], null, timeline); attachDict.Add(attachId, attachBoneMap); } foreach (var time in allTimes) { var frame = timeline.FirstOrDefault(x => x.Time == time); - var frameTime = (float)(time - startTime).TotalSeconds; + var frameTime = TotalSeconds(time, startTime); if (frame != default) { var newMap = GetBoneMap(frame.Attach.OwnerSkeleton, false, out var attachRoot); @@ -330,11 +330,20 @@ private static Dictionary> GetB // set scaling to 0 when not present foreach (var bone in attachBoneMap.Bones) { - bone.UseScale().UseTrackBuilder("pose").WithPoint((float)(time - startTime).TotalSeconds, Vector3.Zero); + bone.UseScale().UseTrackBuilder("pose").WithPoint(TotalSeconds(time, startTime), Vector3.Zero); } } } return attachDict; } + + public static float TotalSeconds(DateTime time, DateTime startTime) + { + var value = (float)(time - startTime).TotalSeconds; + // handle really close to 0 values + if (value < 0.0001f) + return 0; + return value; + } } From 686947a7608c56136870872f66a08ead60b8043b Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:56:51 +1000 Subject: [PATCH 16/19] Cleanup a lot of stuff - remove custom ClientStructs - Move additional fields to structs folder - Add pbd info to debug tab - general cleanup - rename some classes to ParsedXX --- .gitmodules | 3 - FFXIVClientStructs | 1 - Meddle.sln | 30 -- Meddle/Meddle.Plugin/Meddle.Plugin.csproj | 3 + Meddle/Meddle.Plugin/Models/AttachSet.cs | 12 +- Meddle/Meddle.Plugin/Models/Groups.cs | 32 +- .../Models/Skeletons/HkSkeleton.cs | 35 ++ .../Skeletons/ParsedAttach.cs} | 28 +- .../Models/Skeletons/ParsedPartialSkeleton.cs | 46 +++ .../Models/Skeletons/ParsedSkeleton.cs | 32 ++ .../Models/Skeletons/SkeletonPose.cs | 34 ++ .../Models/Skeletons/SkeletonUtils.cs | 191 ++++++++++ .../Skeletons}/Transform.cs | 0 .../Meddle.Plugin/Models/StructExtensions.cs | 108 ++++++ Meddle/Meddle.Plugin/Models/Structs/Attach.cs | 60 +++ .../{ => Structs}/CustomizeParameter.cs | 2 +- .../Meddle.Plugin/Models/Structs/Deformer.cs | 24 ++ .../Models/Structs/ModelResourceHandle.cs | 76 ++++ .../Meddle.Plugin/Models/Structs/Skeleton.cs | 50 +++ Meddle/Meddle.Plugin/Plugin.cs | 10 +- Meddle/Meddle.Plugin/Service.cs | 2 +- .../Meddle.Plugin/Services/ExportService.cs | 119 +++--- .../Meddle.Plugin/Services/InteropService.cs | 80 ---- Meddle/Meddle.Plugin/Services/ParseService.cs | 106 ++---- Meddle/Meddle.Plugin/Services/PbdHooks.cs | 56 +-- .../Services/PluginLoggerProvider.cs | 90 ++--- Meddle/Meddle.Plugin/Services/PluginState.cs | 6 - Meddle/Meddle.Plugin/Services/TextureCache.cs | 18 +- .../Meddle.Plugin/Services/WindowManager.cs | 16 +- .../Meddle.Plugin/Skeleton/SkeletonUtils.cs | 349 ------------------ Meddle/Meddle.Plugin/Skeleton/Skeletons.cs | 132 ------- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 222 +++++------ Meddle/Meddle.Plugin/UI/CharacterTab.cs | 87 +++-- Meddle/Meddle.Plugin/UI/DebugTab.cs | 98 +++-- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 325 ++++++++-------- .../Meddle.Plugin/UI/MaterialParameterTab.cs | 15 +- Meddle/Meddle.Plugin/UI/OptionsTab.cs | 17 +- Meddle/Meddle.Plugin/UI/WorldTab.cs | 16 +- Meddle/Meddle.Plugin/Utils/DXHelper.cs | 6 +- Meddle/Meddle.Plugin/Utils/EventLogger.cs | 25 +- Meddle/Meddle.Plugin/Utils/ObjectUtil.cs | 5 +- Meddle/Meddle.Plugin/Utils/PoseUtil.cs | 4 +- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 135 +++---- Meddle/Meddle.Plugin/packages.lock.json | 16 - Meddle/Meddle.UI/Windows/Views/PbdView.cs | 4 +- Meddle/Meddle.Utils/Files/PbdFile.cs | 2 - .../Files/Structs/Material/ColorTableRow.cs | 5 +- Meddle/Meddle.Utils/Meddle.Utils.csproj | 15 +- repo.json | 2 +- 49 files changed, 1419 insertions(+), 1331 deletions(-) delete mode 160000 FFXIVClientStructs create mode 100644 Meddle/Meddle.Plugin/Models/Skeletons/HkSkeleton.cs rename Meddle/Meddle.Plugin/{Skeleton/Attach.cs => Models/Skeletons/ParsedAttach.cs} (77%) create mode 100644 Meddle/Meddle.Plugin/Models/Skeletons/ParsedPartialSkeleton.cs create mode 100644 Meddle/Meddle.Plugin/Models/Skeletons/ParsedSkeleton.cs create mode 100644 Meddle/Meddle.Plugin/Models/Skeletons/SkeletonPose.cs create mode 100644 Meddle/Meddle.Plugin/Models/Skeletons/SkeletonUtils.cs rename Meddle/Meddle.Plugin/{Skeleton => Models/Skeletons}/Transform.cs (100%) create mode 100644 Meddle/Meddle.Plugin/Models/StructExtensions.cs create mode 100644 Meddle/Meddle.Plugin/Models/Structs/Attach.cs rename Meddle/Meddle.Plugin/Models/{ => Structs}/CustomizeParameter.cs (97%) create mode 100644 Meddle/Meddle.Plugin/Models/Structs/Deformer.cs create mode 100644 Meddle/Meddle.Plugin/Models/Structs/ModelResourceHandle.cs create mode 100644 Meddle/Meddle.Plugin/Models/Structs/Skeleton.cs delete mode 100644 Meddle/Meddle.Plugin/Services/InteropService.cs delete mode 100644 Meddle/Meddle.Plugin/Services/PluginState.cs delete mode 100644 Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs delete mode 100644 Meddle/Meddle.Plugin/Skeleton/Skeletons.cs diff --git a/.gitmodules b/.gitmodules index e1ce1a2..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "FFXIVClientStructs"] - path = FFXIVClientStructs - url = https://github.com/aers/FFXIVClientStructs.git diff --git a/FFXIVClientStructs b/FFXIVClientStructs deleted file mode 160000 index dbe63f0..0000000 --- a/FFXIVClientStructs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dbe63f040b3d4dc1d5dc949af13b0a7a7a6bbda8 diff --git a/Meddle.sln b/Meddle.sln index 3f26d53..a4f545b 100644 --- a/Meddle.sln +++ b/Meddle.sln @@ -5,20 +5,10 @@ VisualStudioVersion = 17.8.34309.116 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Meddle.Plugin", "Meddle\Meddle.Plugin\Meddle.Plugin.csproj", "{ABA6105F-84F8-421E-9365-4D19416FC850}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{13C16D26-657B-4EC2-90F2-365125403E2D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meddle.Utils", "Meddle\Meddle.Utils\Meddle.Utils.csproj", "{AF561495-196F-4076-AA2B-0415E7F85B8E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meddle.UI.InteropPlugin", "Meddle\Meddle.UI.InteropPlugin\Meddle.UI.InteropPlugin.csproj", "{F056F9E7-B1D1-4220-8B51-D180855D1EDA}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meddle.UI", "Meddle\Meddle.UI\Meddle.UI.csproj", "{853D0E4A-A38F-42DF-A2A1-10A1240773DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFXIVClientStructs.Generators", "FFXIVClientStructs\FFXIVClientStructs.Generators\FFXIVClientStructs.Generators.csproj", "{F428CB69-6B3C-4C30-AA47-2F69B07FBD5F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "FFXIVClientStructs\InteropGenerator\InteropGenerator.csproj", "{965F3CEF-32C7-41EA-AFC9-7AC55FA4BE21}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{AFB41DF8-3B49-4B35-8704-6810454971A0}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,34 +19,14 @@ Global {ABA6105F-84F8-421E-9365-4D19416FC850}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABA6105F-84F8-421E-9365-4D19416FC850}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABA6105F-84F8-421E-9365-4D19416FC850}.Release|Any CPU.Build.0 = Release|Any CPU - {13C16D26-657B-4EC2-90F2-365125403E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13C16D26-657B-4EC2-90F2-365125403E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13C16D26-657B-4EC2-90F2-365125403E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13C16D26-657B-4EC2-90F2-365125403E2D}.Release|Any CPU.Build.0 = Release|Any CPU {AF561495-196F-4076-AA2B-0415E7F85B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AF561495-196F-4076-AA2B-0415E7F85B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF561495-196F-4076-AA2B-0415E7F85B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF561495-196F-4076-AA2B-0415E7F85B8E}.Release|Any CPU.Build.0 = Release|Any CPU - {F056F9E7-B1D1-4220-8B51-D180855D1EDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F056F9E7-B1D1-4220-8B51-D180855D1EDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F056F9E7-B1D1-4220-8B51-D180855D1EDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F056F9E7-B1D1-4220-8B51-D180855D1EDA}.Release|Any CPU.Build.0 = Release|Any CPU {853D0E4A-A38F-42DF-A2A1-10A1240773DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {853D0E4A-A38F-42DF-A2A1-10A1240773DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {853D0E4A-A38F-42DF-A2A1-10A1240773DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {853D0E4A-A38F-42DF-A2A1-10A1240773DC}.Release|Any CPU.Build.0 = Release|Any CPU - {F428CB69-6B3C-4C30-AA47-2F69B07FBD5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F428CB69-6B3C-4C30-AA47-2F69B07FBD5F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F428CB69-6B3C-4C30-AA47-2F69B07FBD5F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F428CB69-6B3C-4C30-AA47-2F69B07FBD5F}.Release|Any CPU.Build.0 = Release|Any CPU - {965F3CEF-32C7-41EA-AFC9-7AC55FA4BE21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {965F3CEF-32C7-41EA-AFC9-7AC55FA4BE21}.Debug|Any CPU.Build.0 = Debug|Any CPU - {965F3CEF-32C7-41EA-AFC9-7AC55FA4BE21}.Release|Any CPU.ActiveCfg = Release|Any CPU - {965F3CEF-32C7-41EA-AFC9-7AC55FA4BE21}.Release|Any CPU.Build.0 = Release|Any CPU - {AFB41DF8-3B49-4B35-8704-6810454971A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AFB41DF8-3B49-4B35-8704-6810454971A0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AFB41DF8-3B49-4B35-8704-6810454971A0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AFB41DF8-3B49-4B35-8704-6810454971A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj index 5d401c9..03c5a93 100644 --- a/Meddle/Meddle.Plugin/Meddle.Plugin.csproj +++ b/Meddle/Meddle.Plugin/Meddle.Plugin.csproj @@ -48,6 +48,9 @@ ..\Meddle.Utils\Lib\OtterTex.dll + + $(DalamudLibPath)FFXIVClientStructs.dll + diff --git a/Meddle/Meddle.Plugin/Models/AttachSet.cs b/Meddle/Meddle.Plugin/Models/AttachSet.cs index 69ae224..6d171da 100644 --- a/Meddle/Meddle.Plugin/Models/AttachSet.cs +++ b/Meddle/Meddle.Plugin/Models/AttachSet.cs @@ -1,12 +1,12 @@ -using Meddle.Plugin.Skeleton; +using Meddle.Plugin.Models.Skeletons; using SharpGLTF.Transforms; namespace Meddle.Plugin.Models; - public class AttachSet { - public AttachSet(string id, Attach attach, Skeleton.Skeleton ownerSkeleton, AffineTransform transform, string? ownerId) + public AttachSet( + string id, ParsedAttach attach, ParsedSkeleton ownerSkeleton, AffineTransform transform, string? ownerId) { Id = id; Attach = attach; @@ -14,10 +14,10 @@ public AttachSet(string id, Attach attach, Skeleton.Skeleton ownerSkeleton, Affi Transform = transform; OwnerId = ownerId; } - + public string Id { get; set; } public string? OwnerId { get; set; } - public Attach Attach { get; set; } - public Skeleton.Skeleton OwnerSkeleton { get; set; } + public ParsedAttach Attach { get; set; } + public ParsedSkeleton OwnerSkeleton { get; set; } public AffineTransform Transform { get; set; } } diff --git a/Meddle/Meddle.Plugin/Models/Groups.cs b/Meddle/Meddle.Plugin/Models/Groups.cs index b3fe589..19136ce 100644 --- a/Meddle/Meddle.Plugin/Models/Groups.cs +++ b/Meddle/Meddle.Plugin/Models/Groups.cs @@ -1,22 +1,40 @@ using System.Numerics; -using Meddle.Plugin.Skeleton; +using Meddle.Plugin.Models.Skeletons; using Meddle.Utils.Export; using Meddle.Utils.Files; -using SharpGLTF.Transforms; namespace Meddle.Plugin.Models; + public record CharacterGroup( - Meddle.Utils.Export.CustomizeParameter CustomizeParams, + CustomizeParameter CustomizeParams, CustomizeData CustomizeData, GenderRace GenderRace, MdlFileGroup[] MdlGroups, - Skeleton.Skeleton Skeleton, + ParsedSkeleton Skeleton, AttachedModelGroup[] AttachedModelGroups); -public record AttachedModelGroup(Attach Attach, MdlFileGroup[] MdlGroups, Skeleton.Skeleton Skeleton); -public record MdlFileGroup(string CharacterPath, string Path, DeformerGroup? DeformerGroup, MdlFile MdlFile, MtrlFileGroup[] MtrlFiles, Model.ShapeAttributeGroup? ShapeAttributeGroup); -public record MtrlFileGroup(string MdlPath, string Path, MtrlFile MtrlFile, string ShpkPath, ShpkFile ShpkFile, TexResourceGroup[] TexFiles); +public record AttachedModelGroup(ParsedAttach Attach, MdlFileGroup[] MdlGroups, ParsedSkeleton Skeleton); + +public record MdlFileGroup( + string CharacterPath, + string Path, + DeformerGroup? DeformerGroup, + MdlFile MdlFile, + MtrlFileGroup[] MtrlFiles, + Model.ShapeAttributeGroup? ShapeAttributeGroup); + +public record MtrlFileGroup( + string MdlPath, + string Path, + MtrlFile MtrlFile, + string ShpkPath, + ShpkFile ShpkFile, + TexResourceGroup[] TexFiles); + public record TexResourceGroup(string MtrlPath, string Path, TextureResource Resource); + public record SklbFileGroup(string Path, SklbFile File); + public record Resource(string MdlPath, Vector3 Position, Quaternion Rotation, Vector3 Scale); + public record DeformerGroup(string Path, ushort RaceSexId, ushort DeformerId); diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/HkSkeleton.cs b/Meddle/Meddle.Plugin/Models/Skeletons/HkSkeleton.cs new file mode 100644 index 0000000..1aca8f8 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Skeletons/HkSkeleton.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +using FFXIVClientStructs.Havok.Animation.Rig; +using FFXIVClientStructs.Interop; +using Meddle.Utils.Skeletons; + +namespace Meddle.Plugin.Models.Skeletons; + +public class ParsedHkaSkeleton +{ + public unsafe ParsedHkaSkeleton(Pointer skeleton) : this(skeleton.Value) { } + + public unsafe ParsedHkaSkeleton(hkaSkeleton* skeleton) + { + var boneNames = new List(); + var boneParents = new List(); + var referencePose = new List(); + + for (var i = 0; i < skeleton->Bones.Length; ++i) + { + boneNames.Add(skeleton->Bones[i].Name.String); + boneParents.Add(skeleton->ParentIndices[i]); + referencePose.Add(new Transform(skeleton->ReferencePose[i])); + } + + BoneNames = boneNames; + BoneParents = boneParents; + ReferencePose = referencePose; + } + + public IReadOnlyList BoneNames { get; } + public IReadOnlyList BoneParents { get; } + + [JsonIgnore] + public IReadOnlyList ReferencePose { get; } +} diff --git a/Meddle/Meddle.Plugin/Skeleton/Attach.cs b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedAttach.cs similarity index 77% rename from Meddle/Meddle.Plugin/Skeleton/Attach.cs rename to Meddle/Meddle.Plugin/Models/Skeletons/ParsedAttach.cs index 8249e44..eb32639 100644 --- a/Meddle/Meddle.Plugin/Skeleton/Attach.cs +++ b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedAttach.cs @@ -1,13 +1,11 @@ -using FFXIVClientStructs.Interop; +using Meddle.Plugin.Models.Structs; using Meddle.Utils.Skeletons; -using CSAttach = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Attach; -using CSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton; -namespace Meddle.Plugin.Skeleton; +namespace Meddle.Plugin.Models.Skeletons; -public unsafe class Attach +public unsafe class ParsedAttach { - public Attach(CSAttach attach) + public ParsedAttach(Attach attach) { // 0 => Root // 3 => Fashion Accessories @@ -22,8 +20,8 @@ public Attach(CSAttach attach) case 3: { if (attach.OwnerCharacter->Skeleton != null) - OwnerSkeleton = new Skeleton(attach.OwnerCharacter->Skeleton); - TargetSkeleton = new Skeleton(attach.TargetSkeleton); + OwnerSkeleton = new ParsedSkeleton(attach.OwnerCharacter->Skeleton); + TargetSkeleton = new ParsedSkeleton(attach.TargetSkeleton); AttachmentCount = attach.AttachmentCount; if (attach.AttachmentCount != 0) { @@ -31,9 +29,9 @@ public Attach(CSAttach attach) OffsetTransform = new Transform(transform.ChildTransform); PartialSkeletonIdx = transform.BoneIndexMask.SkeletonIdx; - var ownerSkeleton = attach.OwnerCharacter->Skeleton; + var ownerSkeleton = (Skeleton*)attach.OwnerCharacter->Skeleton; - CSSkeleton.Bone? foundBone = null; + Skeleton.Bone? foundBone = null; var foundBoneIdx = 0; for (var i = 0; i < ownerSkeleton->AttachBoneCount; i++) { @@ -57,12 +55,13 @@ public Attach(CSAttach attach) PartialSkeletonIdx = boneMask.SkeletonIdx; BoneIdx = boneMask.BoneIdx; } + break; } case 4: { - OwnerSkeleton = new Skeleton(attach.OwnerSkeleton); - TargetSkeleton = new Skeleton(attach.TargetSkeleton); + OwnerSkeleton = new ParsedSkeleton(attach.OwnerSkeleton); + TargetSkeleton = new ParsedSkeleton(attach.TargetSkeleton); AttachmentCount = attach.AttachmentCount; if (attach.AttachmentCount != 0) { @@ -72,6 +71,7 @@ public Attach(CSAttach attach) PartialSkeletonIdx = att.BoneIndexMask.SkeletonIdx; BoneIdx = att.BoneIndexMask.BoneIdx; } + break; } default: @@ -83,8 +83,8 @@ public Attach(CSAttach attach) public int AttachmentCount { get; } public int ExecuteType { get; } - public Skeleton? TargetSkeleton { get; } - public Skeleton? OwnerSkeleton { get; } + public ParsedSkeleton? TargetSkeleton { get; } + public ParsedSkeleton? OwnerSkeleton { get; } public Transform? OffsetTransform { get; } public byte PartialSkeletonIdx { get; } public uint BoneIdx { get; } diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/ParsedPartialSkeleton.cs b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedPartialSkeleton.cs new file mode 100644 index 0000000..4822a93 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedPartialSkeleton.cs @@ -0,0 +1,46 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.Interop; + +namespace Meddle.Plugin.Models.Skeletons; + +public class ParsedPartialSkeleton +{ + public unsafe ParsedPartialSkeleton(Pointer partialSkeleton) : + this(partialSkeleton.Value) { } + + public unsafe ParsedPartialSkeleton(PartialSkeleton* partialSkeleton) + { + if (partialSkeleton->SkeletonResourceHandle != null) + { + HkSkeleton = new ParsedHkaSkeleton(partialSkeleton->SkeletonResourceHandle->HavokSkeleton); + HandlePath = partialSkeleton->SkeletonResourceHandle->FileName.ToString(); + } + + BoneCount = StructExtensions.GetBoneCount(partialSkeleton); + ConnectedBoneIndex = partialSkeleton->ConnectedBoneIndex; + + var poses = new List(); + for (var i = 0; i < partialSkeleton->HavokPoses.Length; ++i) + { + var pose = partialSkeleton->GetHavokPose(i); + if (pose != null) + { + if (pose->Skeleton != partialSkeleton->SkeletonResourceHandle->HavokSkeleton) + { + throw new ArgumentException( + $"Pose is not the same as the skeleton {(nint)pose->Skeleton:X16} != {(nint)partialSkeleton->SkeletonResourceHandle->HavokSkeleton:X16}"); + } + + poses.Add(new ParsedHkaPose(pose)); + } + } + + Poses = poses; + } + + public string? HandlePath { get; } + public ParsedHkaSkeleton? HkSkeleton { get; } + public IReadOnlyList Poses { get; } + public int ConnectedBoneIndex { get; } + public uint BoneCount { get; } +} diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/ParsedSkeleton.cs b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedSkeleton.cs new file mode 100644 index 0000000..9eb2f33 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Skeletons/ParsedSkeleton.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.Interop; +using Meddle.Utils.Skeletons; + +namespace Meddle.Plugin.Models.Skeletons; + +public class ParsedSkeleton +{ + public unsafe ParsedSkeleton(Pointer skeleton) : this(skeleton.Value) { } + + public unsafe ParsedSkeleton(Skeleton* skeleton) + { + Transform = new Transform(skeleton->Transform); + var partialSkeletons = new List(); + for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) + { + try + { + partialSkeletons.Add(new ParsedPartialSkeleton(&skeleton->PartialSkeletons[i])); + } + catch (Exception e) + { + throw new Exception($"Failed to load partial skeleton {i}/{skeleton->PartialSkeletonCount}", e); + } + } + + PartialSkeletons = partialSkeletons; + } + + public Transform Transform { get; } + public IReadOnlyList PartialSkeletons { get; } +} diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonPose.cs b/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonPose.cs new file mode 100644 index 0000000..fd3d441 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonPose.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; +using FFXIVClientStructs.Havok.Animation.Rig; +using FFXIVClientStructs.Interop; +using Meddle.Plugin.Utils; +using Meddle.Utils.Skeletons; + +namespace Meddle.Plugin.Models.Skeletons; + +public class ParsedHkaPose +{ + public unsafe ParsedHkaPose(Pointer pose) : this(pose.Value) { } + + public unsafe ParsedHkaPose(hkaPose* pose) + { + var transforms = new List(); + + var boneCount = pose->LocalPose.Length; + for (var i = 0; i < boneCount; ++i) + { + var localSpace = PoseUtil.AccessBoneLocalSpace(pose, i); + if (localSpace == null) + { + throw new Exception("Failed to access bone local space"); + } + + transforms.Add(new Transform(*localSpace)); + } + + Pose = transforms; + } + + [JsonIgnore] + public IReadOnlyList Pose { get; } +} diff --git a/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonUtils.cs new file mode 100644 index 0000000..e97fc2e --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Skeletons/SkeletonUtils.cs @@ -0,0 +1,191 @@ +using System.Numerics; +using Meddle.Utils; +using SharpGLTF.Scenes; + +namespace Meddle.Plugin.Models.Skeletons; + +public static class SkeletonUtils +{ + public static List GetBoneMap( + IReadOnlyList partialSkeletons, bool includePose, out BoneNodeBuilder? root) + { + List boneMap = new(); + root = null; + + for (var partialIdx = 0; partialIdx < partialSkeletons.Count; partialIdx++) + { + var partial = partialSkeletons[partialIdx]; + var hkSkeleton = partial.HkSkeleton; + if (hkSkeleton == null) + continue; + + var pose = partial.Poses.FirstOrDefault(); + + var skeleBones = new BoneNodeBuilder[hkSkeleton.BoneNames.Count]; + for (var i = 0; i < hkSkeleton.BoneNames.Count; i++) + { + var name = hkSkeleton.BoneNames[i]; + if (string.IsNullOrEmpty(name)) + continue; + + if (boneMap.FirstOrDefault(b => b.BoneName.Equals(name, StringComparison.OrdinalIgnoreCase)) is + { } dupeBone) + { + skeleBones[i] = dupeBone; + continue; + } + + if (partial.ConnectedBoneIndex == i) + { + throw new InvalidOperationException( + $"Bone {name} on {i} is connected to a skeleton that should've already been declared"); + } + + var bone = new BoneNodeBuilder(name) + { + BoneIndex = i, + PartialSkeletonHandle = partial.HandlePath ?? + throw new InvalidOperationException( + $"No handle path for {name} [{partialIdx},{i}]"), + PartialSkeletonIndex = partialIdx + }; + if (pose != null && includePose) + { + var transform = pose.Pose[i]; + bone.UseScale().UseTrackBuilder("pose").WithPoint(0, transform.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(0, transform.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(0, transform.Translation); + } + + bone.SetLocalTransform(hkSkeleton.ReferencePose[i].AffineTransform, false); + + var parentIdx = hkSkeleton.BoneParents[i]; + if (parentIdx != -1) + skeleBones[parentIdx].AddNode(bone); + else + { + if (root != null) + throw new InvalidOperationException("Multiple root bones found"); + root = bone; + } + + skeleBones[i] = bone; + boneMap.Add(bone); + } + } + + if (!NodeBuilder.IsValidArmature(boneMap)) + { + throw new InvalidOperationException( + $"Joints are not valid, {string.Join(", ", boneMap.Select(x => x.Name))}"); + } + + return boneMap; + } + + public static List GetBoneMap(ParsedSkeleton skeleton, bool includePose, out BoneNodeBuilder? root) + { + return GetBoneMap(skeleton.PartialSkeletons, includePose, out root); + } + + public static + Dictionary Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> + Timeline)> GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) + { + var attachDict = + new Dictionary Bones, BoneNodeBuilder? Root, + List<(DateTime Time, AttachSet Attach)> Timeline)>(); + var attachTimelines = new Dictionary>(); + foreach (var frame in frames) + { + foreach (var attach in frame.Attaches) + { + if (!attachTimelines.TryGetValue(attach.Id, out var timeline)) + { + timeline = new List<(DateTime Time, AttachSet Attach)>(); + attachTimelines.Add(attach.Id, timeline); + } + + timeline.Add((frame.Time, attach)); + } + } + + var allTimes = frames.Select(x => x.Time).ToArray(); + + var startTime = frames.Min(x => x.Time); + foreach (var (attachId, timeline) in attachTimelines) + { + var firstAttach = timeline.First().Attach; + if (!attachDict.TryGetValue(attachId, out var attachBoneMap)) + { + attachBoneMap = ([], null, timeline); + attachDict.Add(attachId, attachBoneMap); + } + + foreach (var time in allTimes) + { + var frame = timeline.FirstOrDefault(x => x.Time == time); + var frameTime = TotalSeconds(time, startTime); + if (frame != default) + { + var newMap = GetBoneMap(frame.Attach.OwnerSkeleton, false, out var attachRoot); + if (attachRoot == null) + continue; + + attachBoneMap.Root ??= attachRoot; + + foreach (var attachBone in newMap) + { + var bone = attachBoneMap.Bones.FirstOrDefault( + x => x.BoneName.Equals(attachBone.BoneName, StringComparison.OrdinalIgnoreCase)); + if (bone == null) + { + attachBoneMap.Bones.Add(attachBone); + bone = attachBone; + } + + var partial = frame.Attach.OwnerSkeleton.PartialSkeletons[attachBone.PartialSkeletonIndex]; + if (partial.Poses.Count == 0) + continue; + + var transform = partial.Poses[0].Pose[bone.BoneIndex]; + bone.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, transform.Scale); + bone.UseRotation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Rotation); + bone.UseTranslation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Translation); + } + + var firstTranslation = firstAttach.Transform.Translation; + attachRoot.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Scale); + attachRoot.UseRotation().UseTrackBuilder("pose") + .WithPoint(frameTime, frame.Attach.Transform.Rotation); + attachRoot.UseTranslation().UseTrackBuilder("pose") + .WithPoint(frameTime, frame.Attach.Transform.Translation - firstTranslation); + + attachDict[attachId] = attachBoneMap; + } + } + + foreach (var time in allTimes) + { + var frame = timeline.FirstOrDefault(x => x.Time == time); + if (frame != default) continue; + // set scaling to 0 when not present + foreach (var bone in attachBoneMap.Bones) + { + bone.UseScale().UseTrackBuilder("pose").WithPoint(TotalSeconds(time, startTime), Vector3.Zero); + } + } + } + + return attachDict; + } + + public static float TotalSeconds(DateTime time, DateTime startTime) + { + var value = (float)(time - startTime).TotalSeconds; + // handle really close to 0 values + if (value < 0.0001f) + return 0; + return value; + } +} diff --git a/Meddle/Meddle.Plugin/Skeleton/Transform.cs b/Meddle/Meddle.Plugin/Models/Skeletons/Transform.cs similarity index 100% rename from Meddle/Meddle.Plugin/Skeleton/Transform.cs rename to Meddle/Meddle.Plugin/Models/Skeletons/Transform.cs diff --git a/Meddle/Meddle.Plugin/Models/StructExtensions.cs b/Meddle/Meddle.Plugin/Models/StructExtensions.cs new file mode 100644 index 0000000..6cd897f --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/StructExtensions.cs @@ -0,0 +1,108 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Interop; +using Meddle.Plugin.Models.Skeletons; +using Meddle.Plugin.Models.Structs; +using PartialSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton; +using Skeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton; + +namespace Meddle.Plugin.Models; + +public static class StructExtensions +{ + public const int CharacterBaseAttachOffset = 0xD0; // CharacterBase + 0xD0 -> Attach + + public const int ModelEnabledAttributeIndexMaskOffset = 0xAC; // Model + 0xAC -> EnabledAttributeIndexMask + public const int ModelEnabledShapeKeyIndexMaskOffset = 0xC8; // Model + 0xC8 -> EnabledShapeKeyIndexMask + + public const int PartialSkeletonFlagsOffset = 0x8; // PartialSkeleton + 0x8 -> Flags + + public static unsafe Attach GetAttach(this Pointer character) + { + if (character == null) throw new ArgumentNullException(nameof(character)); + if (character.Value == null) throw new ArgumentNullException(nameof(character)); + + var characterBase = character.Value; // + 0xD0 gives Attach data + var offset = (nint)characterBase + CharacterBaseAttachOffset; + return *(Attach*)offset; + } + + public static ParsedAttach GetParsedAttach(this Pointer character) + { + var attach = GetAttach(character); + return new ParsedAttach(attach); + } + + public static unsafe uint GetFlags(this Pointer partialSkeleton) + { + if (partialSkeleton == null) throw new ArgumentNullException(nameof(partialSkeleton)); + if (partialSkeleton.Value == null) throw new ArgumentNullException(nameof(partialSkeleton)); + + var flags = *(uint*)((nint)partialSkeleton.Value + PartialSkeletonFlagsOffset); + return flags; + } + + public static uint GetBoneCount(this Pointer partialSkeleton) + { + var flags = partialSkeleton.GetFlags(); + return (flags >> 5) & 0xFFFu; + } + + public static unsafe (uint EnabledAttributeIndexMask, uint EnabledShapeKeyIndexMask) GetModelMasks( + this Pointer model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + if (model.Value == null) throw new ArgumentNullException(nameof(model)); + + var modelBase = model.Value; + var enabledAttributeIndexMask = *(uint*)((nint)modelBase + ModelEnabledAttributeIndexMaskOffset); + var enabledShapeKeyIndexMask = *(uint*)((nint)modelBase + ModelEnabledShapeKeyIndexMaskOffset); + return (enabledAttributeIndexMask, enabledShapeKeyIndexMask); + } + + public static unsafe ParsedSkeleton GetParsedSkeleton(this Pointer character) + { + if (character == null) throw new ArgumentNullException(nameof(character)); + if (character.Value == null) throw new ArgumentNullException(nameof(character)); + return GetParsedSkeleton(character.Value->Skeleton); + } + + public static unsafe ParsedSkeleton GetParsedSkeleton(this Pointer model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + if (model.Value == null) throw new ArgumentNullException(nameof(model)); + return GetParsedSkeleton(model.Value->Skeleton); + } + + private static unsafe ParsedSkeleton GetParsedSkeleton(this Pointer skeleton) + { + if (skeleton == null) throw new ArgumentNullException(nameof(skeleton)); + return new ParsedSkeleton(skeleton.Value); + } + + public static unsafe Meddle.Utils.Export.Model.ShapeAttributeGroup ParseModelShapeAttributes( + Pointer modelPointer) + { + if (modelPointer == null) throw new ArgumentNullException(nameof(modelPointer)); + if (modelPointer.Value == null) throw new ArgumentNullException(nameof(modelPointer)); + var model = modelPointer.Value; + var (enabledAttributeIndexMask, enabledShapeKeyIndexMask) = modelPointer.GetModelMasks(); + var shapes = new List<(string, short)>(); + foreach (var shape in model->ModelResourceHandle->Shapes) + { + shapes.Add((MemoryHelper.ReadStringNullTerminated((nint)shape.Item1.Value), shape.Item2)); + } + + var attributes = new List<(string, short)>(); + foreach (var attribute in model->ModelResourceHandle->Attributes) + { + attributes.Add((MemoryHelper.ReadStringNullTerminated((nint)attribute.Item1.Value), attribute.Item2)); + } + + var shapeAttributeGroup = new Meddle.Utils.Export.Model.ShapeAttributeGroup( + enabledShapeKeyIndexMask, enabledAttributeIndexMask, shapes.ToArray(), attributes.ToArray()); + + return shapeAttributeGroup; + } +} diff --git a/Meddle/Meddle.Plugin/Models/Structs/Attach.cs b/Meddle/Meddle.Plugin/Models/Structs/Attach.cs new file mode 100644 index 0000000..5504783 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Structs/Attach.cs @@ -0,0 +1,60 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok.Common.Base.Math.QsTransform; +using FFXIVClientStructs.Interop; + +namespace Meddle.Plugin.Models.Structs; + +[StructLayout(LayoutKind.Explicit, Size = 0x78)] +public unsafe struct Attach +{ + //[FieldOffset(0x00)] public PostBoneDeformerBase Base; + //[FieldOffset(0x38)] public Attach* LinkedListPrevious; + //[FieldOffset(0x40)] public Attach* LinkedListNext; + + [FieldOffset(0x50)] + public int ExecuteType; + + [FieldOffset(0x54)] + public int UnkValue; + + // type 0: - root object + // - mount + // - unmounted player + // type 1: - ? + // type 2: - ? + // type 3: + // - ornament + // - mounted player + // type 4: + // - weapon + // type 5: - + + [FieldOffset(0x58)] + public FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* TargetSkeleton; // ExecuteType 3/4 + + [FieldOffset(0x60)] + public FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* OwnerSkeleton; // ExecuteType 4 + + [FieldOffset(0x60)] + public CharacterBase* OwnerCharacter; // ExecuteType 3 + + [FieldOffset(0x68)] + public int AttachmentCount; + + [FieldOffset(0x70)] + public Attachment* SkeletonBoneAttachments; + + public Span> SkeletonBoneAttachmentSpan => new(SkeletonBoneAttachments, AttachmentCount); + + + [StructLayout(LayoutKind.Explicit, Size = 0x58)] + public struct Attachment + { + [FieldOffset(0x02)] + public Skeleton.BoneIndexMask BoneIndexMask; + + [FieldOffset(0x10)] + public hkQsTransformf ChildTransform; + } +} diff --git a/Meddle/Meddle.Plugin/Models/CustomizeParameter.cs b/Meddle/Meddle.Plugin/Models/Structs/CustomizeParameter.cs similarity index 97% rename from Meddle/Meddle.Plugin/Models/CustomizeParameter.cs rename to Meddle/Meddle.Plugin/Models/Structs/CustomizeParameter.cs index 433d767..b9c20e0 100644 --- a/Meddle/Meddle.Plugin/Models/CustomizeParameter.cs +++ b/Meddle/Meddle.Plugin/Models/Structs/CustomizeParameter.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Common.Math; -namespace Meddle.Plugin.Models; +namespace Meddle.Plugin.Models.Structs; [StructLayout(LayoutKind.Explicit, Size = 0x90)] public struct CustomizeParameter diff --git a/Meddle/Meddle.Plugin/Models/Structs/Deformer.cs b/Meddle/Meddle.Plugin/Models/Structs/Deformer.cs new file mode 100644 index 0000000..aae9b02 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Structs/Deformer.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Meddle.Plugin.Models.Structs; + +public struct DeformerCachedStruct +{ + public ushort RaceSexId; + public ushort DeformerId; + public string PbdPath; +} + +[StructLayout(LayoutKind.Explicit, Size = 0x20)] +public struct DeformerStruct +{ + [FieldOffset(0x10)] + public unsafe ResourceHandle* PbdPointer; + + [FieldOffset(0x18)] + public ushort RaceSexId; + + [FieldOffset(0x1A)] + public ushort DeformerId; +} diff --git a/Meddle/Meddle.Plugin/Models/Structs/ModelResourceHandle.cs b/Meddle/Meddle.Plugin/Models/Structs/ModelResourceHandle.cs new file mode 100644 index 0000000..87a4bb1 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Structs/ModelResourceHandle.cs @@ -0,0 +1,76 @@ +using System.Runtime.InteropServices; +using System.Text; +using FFXIVClientStructs.Interop; +using FFXIVClientStructs.STD; + +namespace Meddle.Plugin.Models.Structs; + +// Client::System::Resource::Handle::ModelResourceHandle +// Client::System::Resource::Handle::ResourceHandle +// Client::System::Common::NonCopyable +[StructLayout(LayoutKind.Explicit, Size = 0x260)] +public unsafe struct ModelResourceHandle +{ + [StructLayout(LayoutKind.Sequential, Size = 56)] + public struct ModelHeader + { + public float Radius; + public ushort MeshCount; + public ushort AttributeCount; + public ushort SubmeshCount; + public ushort MaterialCount; + public ushort BoneCount; + public ushort BoneTableCount; + public ushort ShapeCount; + public ushort ShapeMeshCount; + public ushort ShapeValueCount; + public byte LodCount; + public byte Flags1; + public ushort ElementIdCount; + public byte TerrainShadowMeshCount; + public byte Flags2; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public ushort Unknown4; + public ushort TerrainShadowSubmeshCount; + public byte Unknown5; + public byte BGChangeMaterialIndex; + public byte BGCrestChangeMaterialIndex; + public byte Unknown6; + public ushort Unknown7; + public ushort Unknown8; + public ushort Unknown9; + } + + public readonly ModelHeader* Header => (ModelHeader*)(StringTable + (((uint*)StringTable)[1] + 8)); + + // uint* StringTable[0] = StringCount + // uint* StringTable[1] = StringTableSize + [FieldOffset(0xC8)] + public byte* StringTable; + + [FieldOffset(0xF8)] + public uint* AttributeNameOffsets; + + [FieldOffset(0x110)] + public uint* MaterialNameOffsets; + + [FieldOffset(0x118)] + public uint* BoneNameOffsets; + + [FieldOffset(0x208)] + public StdMap, short> Attributes; + + [FieldOffset(0x228)] + public StdMap, short> Shapes; + + public string GetMaterialFileName(uint idx) + { + if (idx >= Header->MaterialCount) + throw new ArgumentOutOfRangeException(nameof(idx)); + + var offset = MaterialNameOffsets[idx]; + var dataSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(StringTable + offset + 8); + return Encoding.UTF8.GetString(dataSpan); + } +} diff --git a/Meddle/Meddle.Plugin/Models/Structs/Skeleton.cs b/Meddle/Meddle.Plugin/Models/Structs/Skeleton.cs new file mode 100644 index 0000000..4911c05 --- /dev/null +++ b/Meddle/Meddle.Plugin/Models/Structs/Skeleton.cs @@ -0,0 +1,50 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.STD; + +namespace Meddle.Plugin.Models.Structs; + +// Client::Graphics::Render::Skeleton +// Client::Graphics::ReferencedClassBase +[StructLayout(LayoutKind.Explicit, Size = 0x100)] +public unsafe struct Skeleton +{ + // Used by attach execute type 3 + // 1. OwnerCharacter->Skeleton->AttachBonesSpan; find bone by BoneIndex matching Attach.BoneIdx + // 2. Use the found bone's index to get the BoneIndexMask from OwnerCharacter->Skeleton->BoneMasksSpan + // 3. Use the BoneIndexMask to get the Skeleton index and Bone index in the owner's skeleton + [FieldOffset(0x88)] + public Bone* AttachBones; + + [FieldOffset(0xA0)] + public uint AttachBoneCount; + + [FieldOffset(0x98)] + public BoneIndexMask* AttachBoneMasks; + + [FieldOffset(0xB8)] + public CharacterBase* Owner; + + [StructLayout(LayoutKind.Explicit, Size = 0x40)] + public struct Bone + { + [FieldOffset(0x0)] + public StdString BoneName; + + [FieldOffset(0x20)] + public uint BoneIndex; + } + + [StructLayout(LayoutKind.Explicit, Size = 0x2)] + public struct BoneIndexMask + { + [FieldOffset(0x0)] + public ushort SkeletonIdxBoneIdx; + + public readonly byte SkeletonIdx => (byte)((SkeletonIdxBoneIdx >> 12) & 0xF); + public readonly ushort BoneIdx => (ushort)(SkeletonIdxBoneIdx & 0xFFF); + } + + public Span AttachBonesSpan => new(AttachBones, (int)AttachBoneCount); + public Span BoneMasksSpan => new(AttachBoneMasks, (int)AttachBoneCount); +} diff --git a/Meddle/Meddle.Plugin/Plugin.cs b/Meddle/Meddle.Plugin/Plugin.cs index baf6078..be09da6 100644 --- a/Meddle/Meddle.Plugin/Plugin.cs +++ b/Meddle/Meddle.Plugin/Plugin.cs @@ -1,9 +1,7 @@ using Dalamud.Configuration; -using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.Plugin; using Meddle.Plugin.Services; -using Meddle.Plugin.UI; using Meddle.Plugin.Utils; using Meddle.Utils.Files.SqPack; using Microsoft.Extensions.DependencyInjection; @@ -54,9 +52,7 @@ public Plugin(IDalamudPluginInterface pluginInterface) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(new SqPack(Environment.CurrentDirectory)) - .AddSingleton() - .AddHostedService(); + .AddSingleton(new SqPack(Environment.CurrentDirectory)); #if DEBUG services.AddOpenTelemetry() @@ -101,8 +97,6 @@ public class Configuration : IPluginConfiguration [JsonIgnore] private IDalamudPluginInterface PluginInterface { get; set; } = null!; - public event Action? OnConfigurationSaved; - public bool ShowDebug { get; set; } public bool ShowTesting { get; set; } public LogLevel MinimumNotificationLogLevel { get; set; } = LogLevel.Warning; @@ -115,6 +109,8 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = 1; + public event Action? OnConfigurationSaved; + public void Save() { PluginInterface.SavePluginConfig(this); diff --git a/Meddle/Meddle.Plugin/Service.cs b/Meddle/Meddle.Plugin/Service.cs index 12bf507..086b2d3 100644 --- a/Meddle/Meddle.Plugin/Service.cs +++ b/Meddle/Meddle.Plugin/Service.cs @@ -38,7 +38,7 @@ public class Service [PluginService] private IDataManager DataManager { get; set; } = null!; - + [PluginService] private INotificationManager NotificationManager { get; set; } = null!; diff --git a/Meddle/Meddle.Plugin/Services/ExportService.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs index 7cb6ffe..c7a5b0e 100644 --- a/Meddle/Meddle.Plugin/Services/ExportService.cs +++ b/Meddle/Meddle.Plugin/Services/ExportService.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Numerics; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; @@ -8,7 +9,6 @@ using Meddle.Utils.Files.SqPack; using Meddle.Utils.Materials; using Meddle.Utils.Models; -using Meddle.Utils.Skeletons; using Microsoft.Extensions.Logging; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; @@ -24,13 +24,12 @@ public class ExportService : IDisposable { private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ExportUtil"); private readonly TexFile catchlightTex; - private readonly TexFile tileNormTex; - private readonly TexFile tileOrbTex; private readonly EventLogger logger; - public event Action? OnLogEvent; private readonly SqPack pack; private readonly PbdFile pbdFile; - + private readonly TexFile tileNormTex; + private readonly TexFile tileOrbTex; + public ExportService(SqPack pack, ILogger logger) { this.pack = pack; @@ -44,17 +43,25 @@ public ExportService(SqPack pack, ILogger logger) var catchlight = pack.GetFile("chara/common/texture/sphere_d_array.tex"); if (catchlight == null) throw new InvalidOperationException("Failed to get catchlight texture"); - + var tileNorm = pack.GetFile("chara/common/texture/tile_norm_array.tex"); if (tileNorm == null) throw new InvalidOperationException("Failed to get tile norm texture"); - + var tileOrb = pack.GetFile("chara/common/texture/tile_orb_array.tex"); if (tileOrb == null) throw new InvalidOperationException("Failed to get tile orb texture"); tileNormTex = new TexFile(tileNorm.Value.file.RawData); tileOrbTex = new TexFile(tileOrb.Value.file.RawData); catchlightTex = new TexFile(catchlight.Value.file.RawData); } - + + public void Dispose() + { + logger.LogDebug("Disposing ExportUtil"); + OnLogEvent -= OnLog; + } + + public event Action? OnLogEvent; + private void OnLog(LogLevel logLevel, string message) { OnLogEvent?.Invoke(logLevel, message); @@ -102,7 +109,8 @@ public void ExportRawTextures(CharacterGroup characterGroup, CancellationToken t foreach (var texGroup in mtrlGroup.TexFiles) { if (token.IsCancellationRequested) return; - var outputPath = Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(texGroup.MtrlPath)}.png"); + var outputPath = + Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(texGroup.MtrlPath)}.png"); var texture = new Texture(texGroup.Resource, texGroup.MtrlPath, null, null, null); var str = new SKDynamicMemoryWStream(); texture.ToTexture().Bitmap.Encode(str, SKEncodedImageFormat.Png, 100); @@ -122,7 +130,8 @@ public void ExportRawTextures(CharacterGroup characterGroup, CancellationToken t } } - public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePositionalData, CancellationToken token = default) + public void ExportAnimation( + List<(DateTime, AttachSet[])> frames, bool includePositionalData, CancellationToken token = default) { try { @@ -152,14 +161,14 @@ public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePo root.UseScale().UseTrackBuilder("pose").WithPoint(time, scale); } } - + scene.AddNode(boneSet.Root); scene.AddSkinnedMesh(GetDummyMesh(id), Matrix4x4.Identity, boneSet.Bones.Cast().ToArray()); var sceneGraph = scene.ToGltf2(); var outputPath = Path.Combine(folder, $"motion_{id}.gltf"); sceneGraph.SaveGLTF(outputPath); } - + Process.Start("explorer.exe", folder); logger.LogInformation("Export complete"); } @@ -169,29 +178,30 @@ public void ExportAnimation(List<(DateTime, AttachSet[])> frames, bool includePo throw; } } - + // https://github.com/0ceal0t/Dalamud-VFXEditor/blob/be00131b93b3c6dd4014a4f27c2661093daf3a85/VFXEditor/Utils/Gltf/GltfSkeleton.cs#L132 - public static MeshBuilder GetDummyMesh(string name = "DUMMY_MESH") { - var dummyMesh = new MeshBuilder( name ); - var material = new MaterialBuilder( "material" ); + public static MeshBuilder GetDummyMesh(string name = "DUMMY_MESH") + { + var dummyMesh = new MeshBuilder(name); + var material = new MaterialBuilder("material"); var p1 = new VertexPosition { - Position = new Vector3( 0.000001f, 0, 0 ) + Position = new Vector3(0.000001f, 0, 0) }; var p2 = new VertexPosition { - Position = new Vector3( 0, 0.000001f, 0 ) + Position = new Vector3(0, 0.000001f, 0) }; var p3 = new VertexPosition { - Position = new Vector3( 0, 0, 0.000001f ) + Position = new Vector3(0, 0, 0.000001f) }; - dummyMesh.UsePrimitive( material ).AddTriangle( - (p1, new VertexEmpty(), new VertexJoints4( 0 )), - (p2, new VertexEmpty(), new VertexJoints4( 0 )), - (p3, new VertexEmpty(), new VertexJoints4( 0 )) + dummyMesh.UsePrimitive(material).AddTriangle( + (p1, new VertexEmpty(), new VertexJoints4(0)), + (p2, new VertexEmpty(), new VertexJoints4(0)), + (p3, new VertexEmpty(), new VertexJoints4(0)) ); return dummyMesh; @@ -307,7 +317,7 @@ public void Export(CharacterGroup characterGroup, string? outputFolder = null, C { Directory.CreateDirectory(outputFolder); } - + var folder = outputFolder ?? GetPathForOutput(); var outputPath = Path.Combine(folder, "character.gltf"); sceneGraph.SaveGLTF(outputPath); @@ -380,20 +390,22 @@ private MaterialBuilder HandleMaterial(CharacterGroup characterGroup, Material m } private List<(Model model, ModelBuilder.MeshExport mesh)> HandleModel( - CharacterGroup characterGroup, MdlFileGroup mdlGroup, ref List bones, BoneNodeBuilder? root, CancellationToken token) + CharacterGroup characterGroup, MdlFileGroup mdlGroup, ref List bones, BoneNodeBuilder? root, + CancellationToken token) { using var activity = ActivitySource.StartActivity(); activity?.SetTag("characterPath", mdlGroup.CharacterPath); activity?.SetTag("path", mdlGroup.Path); logger.LogInformation("Exporting {CharacterPath} => {Path}", mdlGroup.CharacterPath, mdlGroup.Path); - var model = new Model(mdlGroup.CharacterPath, mdlGroup.MdlFile, + var model = new Model(mdlGroup.CharacterPath, mdlGroup.MdlFile, mdlGroup.MtrlFiles.Select(x => ( - x.MdlPath, - x.MtrlFile, - x.TexFiles.ToDictionary(tf => tf.MtrlPath, tf => tf.Resource), - x.ShpkFile)).ToArray(), + x.MdlPath, + x.MtrlFile, + x.TexFiles.ToDictionary( + tf => tf.MtrlPath, tf => tf.Resource), + x.ShpkFile)).ToArray(), mdlGroup.ShapeAttributeGroup); - + foreach (var mesh in model.Meshes) { if (mesh.BoneTable == null) continue; @@ -413,13 +425,13 @@ private MaterialBuilder HandleMaterial(CharacterGroup characterGroup, Material m } } } - - + + var materials = new List(); var meshOutput = new List<(Model, ModelBuilder.MeshExport)>(); for (var i = 0; i < model.Materials.Count; i++) { - if (token.IsCancellationRequested) return meshOutput; + if (token.IsCancellationRequested) return meshOutput; var material = model.Materials[i]; var materialGroup = mdlGroup.MtrlFiles[i]; if (material == null) throw new InvalidOperationException("Material is null"); @@ -433,8 +445,10 @@ private MaterialBuilder HandleMaterial(CharacterGroup characterGroup, Material m if (mdlGroup.DeformerGroup != null) { var pbdFileData = pack.GetFileOrReadFromDisk(mdlGroup.DeformerGroup.Path); - if (pbdFileData == null) throw new InvalidOperationException($"Failed to get deformer pbd {mdlGroup.DeformerGroup.Path}"); - raceDeformerValue = ((GenderRace)mdlGroup.DeformerGroup.RaceSexId, new RaceDeformer(new PbdFile(pbdFileData), bones)); + if (pbdFileData == null) + throw new InvalidOperationException($"Failed to get deformer pbd {mdlGroup.DeformerGroup.Path}"); + raceDeformerValue = ((GenderRace)mdlGroup.DeformerGroup.RaceSexId, + new RaceDeformer(new PbdFile(pbdFileData), bones)); model.RaceCode = (GenderRace)mdlGroup.DeformerGroup.DeformerId; logger.LogDebug("Using deformer pbd {Path}", mdlGroup.DeformerGroup.Path); } @@ -474,7 +488,8 @@ public void ExportResource(Resource[] resources, Vector3 rootPosition) foreach (var resource in resources) { var mdlFileData = pack.GetFile(resource.MdlPath); - if (mdlFileData == null) throw new InvalidOperationException($"Failed to get resource {resource.MdlPath}"); + if (mdlFileData == null) + throw new InvalidOperationException($"Failed to get resource {resource.MdlPath}"); var data = mdlFileData.Value.file.RawData; var mdlFile = new MdlFile(data); var mtrlGroups = new List(); @@ -483,47 +498,53 @@ public void ExportResource(Resource[] resources, Vector3 rootPosition) if (mtrlPath.StartsWith('/')) throw new InvalidOperationException($"Relative path found on material {mtrlPath}"); var mtrlResource = pack.GetFile(mtrlPath); - if (mtrlResource == null) throw new InvalidOperationException($"Failed to get mtrl resource {mtrlPath}"); + if (mtrlResource == null) + throw new InvalidOperationException($"Failed to get mtrl resource {mtrlPath}"); var mtrlData = mtrlResource.Value.file.RawData; var mtrlFile = new MtrlFile(mtrlData); var shpkPath = mtrlFile.GetShaderPackageName(); var shpkResource = pack.GetFile($"shader/sm5/shpk/{shpkPath}"); - if (shpkResource == null) throw new InvalidOperationException($"Failed to get shpk resource {shpkPath}"); + if (shpkResource == null) + throw new InvalidOperationException($"Failed to get shpk resource {shpkPath}"); var shpkFile = new ShpkFile(shpkResource.Value.file.RawData); var texGroups = new List(); foreach (var (_, texPath) in mtrlFile.GetTexturePaths()) { var texResource = pack.GetFile(texPath); - if (texResource == null) throw new InvalidOperationException($"Failed to get tex resource {texPath}"); + if (texResource == null) + throw new InvalidOperationException($"Failed to get tex resource {texPath}"); var texData = texResource.Value.file.RawData; var texFile = new TexFile(texData); texGroups.Add(new TexResourceGroup(texPath, texPath, Texture.GetResource(texFile))); } - mtrlGroups.Add(new MtrlFileGroup(mtrlPath, mtrlPath, mtrlFile, shpkPath, shpkFile, texGroups.ToArray())); + mtrlGroups.Add(new MtrlFileGroup(mtrlPath, mtrlPath, mtrlFile, shpkPath, shpkFile, + texGroups.ToArray())); } var model = new Model(resource.MdlPath, mdlFile, mtrlGroups.Select(x => ( - x.Path, + x.Path, x.MtrlFile, - x.TexFiles.ToDictionary(tf => tf.MtrlPath, tf => tf.Resource), + x.TexFiles.ToDictionary( + tf => tf.MtrlPath, tf => tf.Resource), x.ShpkFile) - ) + ) .ToArray(), null); var materials = new List(); foreach (var material in model.Materials) { if (material == null) throw new InvalidOperationException("Material is null"); - + if (materialCache.TryGetValue(material.HandlePath, out var builder)) { materials.Add(builder); continue; } + var name = $"{Path.GetFileNameWithoutExtension(material.HandlePath)}_{Path.GetFileNameWithoutExtension(material.ShaderPackageName)}"; builder = material.ShaderPackageName switch @@ -562,7 +583,7 @@ public void ExportResource(Resource[] resources, Vector3 rootPosition) throw; } } - + private MaterialBuilder BuildAndLogFallbackMaterial(Material material, string name) { logger.LogWarning("Using fallback material for {Path}", material.HandlePath); @@ -577,10 +598,4 @@ private void ExportTextureFromPath(string path) var texture = new Texture(Texture.GetResource(texFile), path, null, null, null); ExportTexture(texture.ToTexture().Bitmap, path); } - - public void Dispose() - { - logger.LogDebug("Disposing ExportUtil"); - OnLogEvent -= OnLog; - } } diff --git a/Meddle/Meddle.Plugin/Services/InteropService.cs b/Meddle/Meddle.Plugin/Services/InteropService.cs deleted file mode 100644 index 85ea711..0000000 --- a/Meddle/Meddle.Plugin/Services/InteropService.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Dalamud.Game; -using Dalamud.Hooking; -using Dalamud.Plugin; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.Interop.Generated; -using InteropGenerator.Runtime; -using Meddle.Plugin.Utils; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Meddle.Plugin.Services; - -public class InteropService : IHostedService, IDisposable -{ - private readonly ILogger log; - private readonly IDalamudPluginInterface pluginInterface; - private readonly ISigScanner sigScanner; - private readonly PbdHooks pbdHooks; - private readonly PluginState state; - - private bool disposed; - - // Client::System::Framework::Framework_Tick - [Signature("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24", DetourName = nameof(PostTickDetour))] - private readonly Hook postTickHook = null!; - - public InteropService( - ISigScanner sigScanner, PbdHooks pbdHooks, ILogger log, IDalamudPluginInterface pluginInterface, PluginState state) - { - this.sigScanner = sigScanner; - this.pbdHooks = pbdHooks; - this.log = log; - this.pluginInterface = pluginInterface; - this.state = state; - } - - public void Dispose() - { - if (!disposed) - { - log.LogDebug("Disposing InteropService"); - postTickHook?.Dispose(); - disposed = true; - } - } - - public Task StartAsync(CancellationToken cancellationToken) - { - if (state.InteropResolved) - return Task.CompletedTask; - log.LogDebug("Resolving ClientStructs"); - Addresses.Register(); - - var cacheFile = - new FileInfo(Path.Combine(pluginInterface.ConfigDirectory.FullName, "Meddle.ClientStructs.cache")); - - Resolver.GetInstance.Setup(sigScanner.SearchBase, cacheFile: cacheFile); - Resolver.GetInstance.Resolve(); - state.InteropResolved = true; - log.LogDebug("Resolved ClientStructs"); - - pbdHooks.Setup(); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - Dispose(); - return Task.CompletedTask; - } - - private bool PostTickDetour(nint a1) - { - var ret = postTickHook.Original(a1); - return ret; - } - - private delegate bool PostTickDelegate(nint a1); -} diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index 4e9b7f7..b94c3f9 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using System.Runtime.InteropServices; -using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.Interop; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; @@ -13,7 +13,7 @@ using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Models; using Microsoft.Extensions.Logging; -using Attach = Meddle.Plugin.Skeleton.Attach; +using Material = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; namespace Meddle.Plugin.Services; @@ -22,15 +22,15 @@ public class ParseService : IDisposable { private static readonly ActivitySource ActivitySource = new("Meddle.Plugin.Utils.ParseUtil"); private readonly DXHelper dxHelper; - private readonly PbdHooks pbdHooks; - private readonly EventLogger logger; private readonly IFramework framework; + private readonly EventLogger logger; private readonly SqPack pack; - public event Action? OnLogEvent; + private readonly PbdHooks pbdHooks; private readonly Dictionary shpkCache = new(); - public ParseService(SqPack pack, IFramework framework, DXHelper dxHelper, PbdHooks pbdHooks, ILogger logger) + public ParseService( + SqPack pack, IFramework framework, DXHelper dxHelper, PbdHooks pbdHooks, ILogger logger) { this.pack = pack; this.framework = framework; @@ -39,12 +39,20 @@ public ParseService(SqPack pack, IFramework framework, DXHelper dxHelper, PbdHoo this.logger = new EventLogger(logger); this.logger.OnLogEvent += OnLog; } - + + public void Dispose() + { + logger.LogDebug("Disposing ParseUtil"); + logger.OnLogEvent -= OnLog; + } + + public event Action? OnLogEvent; + private void OnLog(LogLevel logLevel, string message) { OnLogEvent?.Invoke(logLevel, message); } - + public unsafe Dictionary ParseColorTableTextures(CharacterBase* characterBase) { using var activity = ActivitySource.StartActivity(); @@ -84,12 +92,12 @@ public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) { // legacy table var stridedData = ImageUtils.AdjustStride(stride, (int)colorTableTexture->Width * 8, - (int)colorTableTexture->Height, colorTableRes.Data); + (int)colorTableTexture->Height, colorTableRes.Data); var reader = new SpanBinaryReader(stridedData); var tableData = reader.Read(16); return tableData.ToArray().Select(x => x.ToNew()).ToArray(); } - + if (colorTableTexture->Width == 8 && colorTableTexture->Height == 32) { // new table @@ -99,7 +107,7 @@ public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) var tableData = reader.Read(32); return tableData.ToArray(); } - + throw new ArgumentException( $"Color table is not 4x16 or 8x32 ({colorTableTexture->Width}x{colorTableTexture->Height})"); } @@ -108,12 +116,12 @@ public unsafe CharacterGroup HandleCharacterGroup( CharacterBase* characterBase, Dictionary colorTableTextures, Dictionary, Dictionary> attachDict, - Meddle.Utils.Export.CustomizeParameter customizeParams, + CustomizeParameter customizeParams, CustomizeData customizeData, GenderRace genderRace) { using var activity = ActivitySource.StartActivity(); - var skeleton = new Skeleton.Skeleton(characterBase->Skeleton); + var skeleton = new ParsedSkeleton(characterBase->Skeleton); var mdlGroups = new List(); for (var i = 0; i < characterBase->SlotCount; i++) { @@ -140,28 +148,6 @@ public unsafe CharacterGroup HandleCharacterGroup( attachGroups.ToArray()); } - public unsafe Model.ShapeAttributeGroup ParseModelShapeAttributes( - FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* model) - { - var shapesMask = model->EnabledShapeKeyIndexMask; - var shapes = new List<(string, short)>(); - foreach (var shape in model->ModelResourceHandle->Shapes) - { - shapes.Add((MemoryHelper.ReadStringNullTerminated((nint)shape.Item1.Value), shape.Item2)); - } - - var attributeMask = model->EnabledAttributeIndexMask; - var attributes = new List<(string, short)>(); - foreach (var attribute in model->ModelResourceHandle->Attributes) - { - attributes.Add((MemoryHelper.ReadStringNullTerminated((nint)attribute.Item1.Value), attribute.Item2)); - } - - var shapeAttributeGroup = new Model.ShapeAttributeGroup(shapesMask, attributeMask, shapes.ToArray(), attributes.ToArray()); - - return shapeAttributeGroup; - } - public unsafe MdlFileGroup? HandleModelPtr( CharacterBase* characterBase, int slotIdx, Dictionary colorTables) { @@ -172,8 +158,9 @@ public unsafe Model.ShapeAttributeGroup ParseModelShapeAttributes( //logger.LogWarning("Model Ptr {ModelIndex} is null", modelIdx); return null; } + var model = modelPtr.Value; - + var mdlFileName = model->ModelResourceHandle->ResourceHandle.FileName.ToString(); var mdlFileActorName = characterBase->ResolveMdlPath((uint)slotIdx); activity?.SetTag("mdl", mdlFileName); @@ -183,8 +170,8 @@ public unsafe Model.ShapeAttributeGroup ParseModelShapeAttributes( logger.LogWarning("Model file {MdlFileName} not found", mdlFileName); return null; } - - var shapeAttributeGroup = ParseModelShapeAttributes(model); + + var shapeAttributeGroup = StructExtensions.ParseModelShapeAttributes(model); var mdlFile = new MdlFile(mdlFileResource); var mtrlFileNames = mdlFile.GetMaterialNames().Select(x => x.Value).ToArray(); var mtrlGroups = new List(); @@ -205,15 +192,17 @@ public unsafe Model.ShapeAttributeGroup ParseModelShapeAttributes( mtrlGroups.Add(mtrlGroup); } } - + var deformerData = pbdHooks.TryGetDeformer((nint)characterBase, (uint)slotIdx); DeformerGroup? deformerGroup = null; if (deformerData != null) { - deformerGroup = new DeformerGroup(deformerData.Value.PbdPath, deformerData.Value.RaceSexId, deformerData.Value.DeformerId); + deformerGroup = new DeformerGroup(deformerData.Value.PbdPath, deformerData.Value.RaceSexId, + deformerData.Value.DeformerId); } - return new MdlFileGroup(mdlFileActorName, mdlFileName, deformerGroup, mdlFile, mtrlGroups.ToArray(), shapeAttributeGroup); + return new MdlFileGroup(mdlFileActorName, mdlFileName, deformerGroup, mdlFile, mtrlGroups.ToArray(), + shapeAttributeGroup); } private ShpkFile HandleShpk(string shader) @@ -238,7 +227,7 @@ private ShpkFile HandleShpk(string shader) private unsafe MtrlFileGroup? HandleMtrl( string mdlPath, - FFXIVClientStructs.FFXIV.Client.Graphics.Render.Material* material, int modelIdx, int j, + Material* material, int modelIdx, int j, Dictionary colorTables) { using var activity = ActivitySource.StartActivity(); @@ -274,7 +263,7 @@ private ShpkFile HandleShpk(string shader) var shpkFile = HandleShpk(shader); var texGroups = new List(); - for (int i = 0; i < material->MaterialResourceHandle->TextureCount; i++) + for (var i = 0; i < material->MaterialResourceHandle->TextureCount; i++) { var textureEntry = material->MaterialResourceHandle->TexturesSpan[i]; if (textureEntry.TextureResourceHandle == null) @@ -282,38 +271,25 @@ private ShpkFile HandleShpk(string shader) logger.LogWarning("Texture handle is null on {MtrlFileName}", mtrlFileName); continue; } - + var texturePath = material->MaterialResourceHandle->TexturePathString(i); var resourcePath = textureEntry.TextureResourceHandle->ResourceHandle.FileName.ToString(); var data = dxHelper.ExportTextureResource(textureEntry.TextureResourceHandle->Texture); var texResourceGroup = new TexResourceGroup(texturePath, resourcePath, data.Resource); texGroups.Add(texResourceGroup); } + return new MtrlFileGroup(mdlPath, mtrlFileName, mtrlFile, shader, shpkFile, texGroups.ToArray()); } - /*private Meddle.Utils.Export.Texture.TexGroup? HandleTexture(string textureName) - { - using var activity = ActivitySource.StartActivity(); - var texFileResource = pack.GetFileOrReadFromDisk(textureName); - if (texFileResource == null) - { - logger.LogWarning("Texture file {TextureName} not found", textureName); - return null; - } - - var texFile = new TexFile(texFileResource); - return new Meddle.Utils.Export.Texture.TexGroup(textureName, texFile); - }*/ - public unsafe AttachedModelGroup HandleAttachGroup( - CharacterBase* attachBase, Dictionary colorTables) + Pointer attachBase, Dictionary colorTables) { using var activity = ActivitySource.StartActivity(); - var attach = new Attach(attachBase->Attach); + var attach = new ParsedAttach(attachBase.GetAttach()); var models = new List(); - var skeleton = new Skeleton.Skeleton(attachBase->Skeleton); - for (var i = 0; i < attachBase->ModelsSpan.Length; i++) + var skeleton = new ParsedSkeleton(attachBase.Value->Skeleton); + for (var i = 0; i < attachBase.Value->ModelsSpan.Length; i++) { var mdlGroup = HandleModelPtr(attachBase, i, colorTables); if (mdlGroup != null) @@ -325,10 +301,4 @@ public unsafe AttachedModelGroup HandleAttachGroup( var attachGroup = new AttachedModelGroup(attach, models.ToArray(), skeleton); return attachGroup; } - - public void Dispose() - { - logger.LogDebug("Disposing ParseUtil"); - logger.OnLogEvent -= OnLog; - } } diff --git a/Meddle/Meddle.Plugin/Services/PbdHooks.cs b/Meddle/Meddle.Plugin/Services/PbdHooks.cs index c2543e4..faea8fc 100644 --- a/Meddle/Meddle.Plugin/Services/PbdHooks.cs +++ b/Meddle/Meddle.Plugin/Services/PbdHooks.cs @@ -1,27 +1,34 @@ -using System.Runtime.InteropServices; -using Dalamud.Game; +using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Meddle.Plugin.Models.Structs; using Microsoft.Extensions.Logging; namespace Meddle.Plugin.Services; public class PbdHooks : IDisposable { - private readonly ISigScanner sigScanner; + public const string HumanCreateDeformerSig = "40 53 48 83 EC 20 4C 8B C1 83 FA 0D"; + private readonly Dictionary> deformerCache = new(); + public IReadOnlyDictionary> DeformerCache => deformerCache; private readonly IGameInteropProvider gameInterop; private readonly ILogger logger; - public const string HumanCreateDeformerSig = "40 53 48 83 EC 20 4C 8B C1 83 FA 0D"; - private delegate nint HumanCreateDeformerDelegate(nint humanPtr, uint slot); + private readonly ISigScanner sigScanner; private Hook? humanCreateDeformerHook; - private readonly Dictionary> deformerCache = new(); - + public PbdHooks(ISigScanner sigScanner, IGameInteropProvider gameInterop, ILogger logger) { this.sigScanner = sigScanner; this.gameInterop = gameInterop; this.logger = logger; + Setup(); + } + + public void Dispose() + { + logger.LogDebug("Disposing PbdHooks"); + humanCreateDeformerHook?.Dispose(); + deformerCache.Clear(); } public void Setup() @@ -29,7 +36,9 @@ public void Setup() if (sigScanner.TryScanText(HumanCreateDeformerSig, out var humanCreateDeformerPtr)) { logger.LogDebug("Found Human::CreateDeformer at {ptr:X}", humanCreateDeformerPtr); - humanCreateDeformerHook = gameInterop.HookFromAddress(humanCreateDeformerPtr, Human_CreateDeformerDetour); + humanCreateDeformerHook = + gameInterop.HookFromAddress( + humanCreateDeformerPtr, Human_CreateDeformerDetour); humanCreateDeformerHook.Enable(); } else @@ -46,7 +55,7 @@ public void Setup() return null; return deformer; } - + private unsafe nint Human_CreateDeformerDetour(nint humanPtr, uint slot) { var result = humanCreateDeformerHook!.Original(humanPtr, slot); @@ -80,30 +89,5 @@ private unsafe nint Human_CreateDeformerDetour(nint humanPtr, uint slot) return result; } - public struct DeformerCachedStruct - { - public ushort RaceSexId; - public ushort DeformerId; - public string PbdPath; - } - - [StructLayout(LayoutKind.Explicit, Size = 0x20)] - public struct DeformerStruct - { - [FieldOffset(0x10)] - public unsafe ResourceHandle* PbdPointer; - - [FieldOffset(0x18)] - public ushort RaceSexId; - - [FieldOffset(0x1A)] - public ushort DeformerId; - } - - public void Dispose() - { - logger.LogDebug("Disposing PbdHooks"); - humanCreateDeformerHook?.Dispose(); - deformerCache.Clear(); - } + private delegate nint HumanCreateDeformerDelegate(nint humanPtr, uint slot); } diff --git a/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs b/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs index cf4d0f2..99eb543 100644 --- a/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs +++ b/Meddle/Meddle.Plugin/Services/PluginLoggerProvider.cs @@ -9,16 +9,17 @@ public class PluginLoggerProvider : ILoggerProvider { private readonly Configuration config; + public PluginLoggerProvider(Configuration config) + { + this.config = config; + } + [PluginService] private IPluginLog PluginLog { get; set; } = null!; - + [PluginService] private INotificationManager NotificationManager { get; set; } = null!; - public PluginLoggerProvider(Configuration config) - { - this.config = config; - } public ILogger CreateLogger(string categoryName) { return new PluginLogger(PluginLog, NotificationManager, config, categoryName); @@ -33,11 +34,14 @@ public void Dispose() public class PluginLogger : ILogger { private readonly string categoryName; + private readonly Configuration config; private readonly IPluginLog log; private readonly INotificationManager notificationManager; - private readonly Configuration config; - public PluginLogger(IPluginLog log, INotificationManager notificationManager, Configuration config, string categoryName) + private readonly List notifications = new(); + + public PluginLogger( + IPluginLog log, INotificationManager notificationManager, Configuration config, string categoryName) { this.log = log; this.notificationManager = notificationManager; @@ -55,43 +59,6 @@ public bool IsEnabled(LogLevel logLevel) return null; } - private readonly List notifications = new(); - - private void LogNotification(string message, LogLevel level) - { - if (level < config.MinimumNotificationLogLevel) return; - - var type = level switch - { - LogLevel.Trace => NotificationType.Info, - LogLevel.Debug => NotificationType.Info, - LogLevel.Information => NotificationType.Info, - LogLevel.Warning => NotificationType.Warning, - LogLevel.Error => NotificationType.Error, - LogLevel.Critical => NotificationType.Error, - LogLevel.None => NotificationType.None, - _ => NotificationType.None - }; - - var notification = new Notification - { - Title = categoryName, - Content = message, - Type = type, - InitialDuration = TimeSpan.FromSeconds(2) - }; - - var notif = notificationManager.AddNotification(notification); - notifications.Add(notif); - - if (notifications.Count > 5) - { - var toRemove = notifications[0]; - notifications.RemoveAt(0); - toRemove.DismissNow(); - } - } - public void Log( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -128,4 +95,39 @@ public void Log( throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); } } + + private void LogNotification(string message, LogLevel level) + { + if (level < config.MinimumNotificationLogLevel) return; + + var type = level switch + { + LogLevel.Trace => NotificationType.Info, + LogLevel.Debug => NotificationType.Info, + LogLevel.Information => NotificationType.Info, + LogLevel.Warning => NotificationType.Warning, + LogLevel.Error => NotificationType.Error, + LogLevel.Critical => NotificationType.Error, + LogLevel.None => NotificationType.None, + _ => NotificationType.None + }; + + var notification = new Notification + { + Title = categoryName, + Content = message, + Type = type, + InitialDuration = TimeSpan.FromSeconds(2) + }; + + var notif = notificationManager.AddNotification(notification); + notifications.Add(notif); + + if (notifications.Count > 5) + { + var toRemove = notifications[0]; + notifications.RemoveAt(0); + toRemove.DismissNow(); + } + } } diff --git a/Meddle/Meddle.Plugin/Services/PluginState.cs b/Meddle/Meddle.Plugin/Services/PluginState.cs deleted file mode 100644 index 72d5564..0000000 --- a/Meddle/Meddle.Plugin/Services/PluginState.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Meddle.Plugin.Services; - -public class PluginState -{ - public bool InteropResolved { get; set; } -} diff --git a/Meddle/Meddle.Plugin/Services/TextureCache.cs b/Meddle/Meddle.Plugin/Services/TextureCache.cs index e2fc035..5ee463b 100644 --- a/Meddle/Meddle.Plugin/Services/TextureCache.cs +++ b/Meddle/Meddle.Plugin/Services/TextureCache.cs @@ -13,7 +13,7 @@ public CachedTexture(IDalamudTextureWrap wrap) public IDalamudTextureWrap Wrap { get; set; } public DateTime LastAccessTime { get; set; } - + public void Dispose() { Wrap.Dispose(); @@ -23,17 +23,23 @@ public void Dispose() public sealed class TextureCache : IDisposable { private readonly Dictionary cache = new(); + private readonly Timer cleanupTimer; private readonly TimeSpan expirationTime; private readonly ILogger logger; - private readonly Timer cleanupTimer; public TextureCache(ILogger logger) { - this.expirationTime = TimeSpan.FromSeconds(10); + expirationTime = TimeSpan.FromSeconds(10); this.logger = logger; cleanupTimer = new Timer(CleanupExpiredTextures, null, expirationTime, expirationTime); } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + public IDalamudTextureWrap GetOrAdd(string key, Func createWrap) { if (cache.TryGetValue(key, out var cachedTexture)) @@ -85,12 +91,6 @@ private void Dispose(bool disposing) } } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - ~TextureCache() { Dispose(false); diff --git a/Meddle/Meddle.Plugin/Services/WindowManager.cs b/Meddle/Meddle.Plugin/Services/WindowManager.cs index 75764a9..520ff29 100644 --- a/Meddle/Meddle.Plugin/Services/WindowManager.cs +++ b/Meddle/Meddle.Plugin/Services/WindowManager.cs @@ -35,14 +35,6 @@ public WindowManager( config.OnConfigurationSaved += OnSave; } - private void OnSave() - { - pluginInterface.UiBuilder.DisableGposeUiHide = config.DisableGposeUiHide; - pluginInterface.UiBuilder.DisableCutsceneUiHide = config.DisableCutsceneUiHide; - pluginInterface.UiBuilder.DisableAutomaticUiHide = config.DisableAutomaticUiHide; - pluginInterface.UiBuilder.DisableUserUiHide = config.DisableUserUiHide; - } - public WindowSystem WindowSystem { get; set; } public MainWindow MainWindow { get; set; } @@ -94,6 +86,14 @@ public Task StopAsync(CancellationToken cancellationToken) return Task.CompletedTask; } + private void OnSave() + { + pluginInterface.UiBuilder.DisableGposeUiHide = config.DisableGposeUiHide; + pluginInterface.UiBuilder.DisableCutsceneUiHide = config.DisableCutsceneUiHide; + pluginInterface.UiBuilder.DisableAutomaticUiHide = config.DisableAutomaticUiHide; + pluginInterface.UiBuilder.DisableUserUiHide = config.DisableUserUiHide; + } + public void OpenUi() { MainWindow.IsOpen = true; diff --git a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs b/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs deleted file mode 100644 index 19fbf4c..0000000 --- a/Meddle/Meddle.Plugin/Skeleton/SkeletonUtils.cs +++ /dev/null @@ -1,349 +0,0 @@ -using System.Numerics; -using Meddle.Plugin.Models; -using Meddle.Plugin.Skeleton; -using SharpGLTF.Scenes; -using SharpGLTF.Transforms; - -namespace Meddle.Utils.Skeletons; - -public static class SkeletonUtils -{ - public static List GetBoneMap( - IReadOnlyList partialSkeletons, bool includePose, out BoneNodeBuilder? root) - { - List boneMap = new(); - root = null; - - for (var partialIdx = 0; partialIdx < partialSkeletons.Count; partialIdx++) - { - var partial = partialSkeletons[partialIdx]; - var hkSkeleton = partial.HkSkeleton; - if (hkSkeleton == null) - continue; - - var pose = partial.Poses.FirstOrDefault(); - - var skeleBones = new BoneNodeBuilder[hkSkeleton.BoneNames.Count]; - for (var i = 0; i < hkSkeleton.BoneNames.Count; i++) - { - var name = hkSkeleton.BoneNames[i]; - if (string.IsNullOrEmpty(name)) - continue; - - if (boneMap.FirstOrDefault(b => b.BoneName.Equals(name, StringComparison.OrdinalIgnoreCase)) is - { } dupeBone) - { - skeleBones[i] = dupeBone; - continue; - } - - if (partial.ConnectedBoneIndex == i) - { - throw new InvalidOperationException( - $"Bone {name} on {i} is connected to a skeleton that should've already been declared"); - } - - var bone = new BoneNodeBuilder(name) - { - BoneIndex = i, - PartialSkeletonHandle = partial.HandlePath ?? throw new InvalidOperationException($"No handle path for {name} [{partialIdx},{i}]"), - PartialSkeletonIndex = partialIdx - }; - if (pose != null && includePose) - { - var transform = pose.Pose[i]; - bone.UseScale().UseTrackBuilder("pose").WithPoint(0, transform.Scale); - bone.UseRotation().UseTrackBuilder("pose").WithPoint(0, transform.Rotation); - bone.UseTranslation().UseTrackBuilder("pose").WithPoint(0, transform.Translation); - } - - bone.SetLocalTransform(hkSkeleton.ReferencePose[i].AffineTransform, false); - - var parentIdx = hkSkeleton.BoneParents[i]; - if (parentIdx != -1) - skeleBones[parentIdx].AddNode(bone); - else - { - if (root != null) - throw new InvalidOperationException("Multiple root bones found"); - root = bone; - } - - skeleBones[i] = bone; - boneMap.Add(bone); - } - } - - if (!NodeBuilder.IsValidArmature(boneMap)) - { - throw new InvalidOperationException( - $"Joints are not valid, {string.Join(", ", boneMap.Select(x => x.Name))}"); - } - - return boneMap; - } - - public static List GetBoneMap(Skeleton skeleton, bool includePose, out BoneNodeBuilder? root) - { - return GetBoneMap(skeleton.PartialSkeletons, includePose, out root); - } - - /*public static List GetAnimatedBoneMap( - List<(DateTime, FrameData)> animation, bool includePositionalData, out BoneNodeBuilder? root) - { - root = null; - - var firstFrame = animation.FirstOrDefault(); - if (firstFrame == null) - throw new InvalidOperationException("No skeleton found in animation"); - - var partialSkeletons = animation.SelectMany(x => x.Skeleton.PartialSkeletons) - .Where(x => x.HandlePath != null) - .DistinctBy(x => x.HandlePath) - .ToArray(); - - - var boneMap = GetBoneMap(partialSkeletons, false, out root); - - var bonePoseMap = GetBonePoseMap(boneMap, animation); - - var startTime = bonePoseMap.Keys.Min(); - foreach (var (time, boneTransforms) in bonePoseMap) - { - var totalSeconds = (float)(time - startTime).TotalSeconds; - foreach (var (bone, transform) in boneTransforms) - { - bone.UseScale().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Scale); - bone.UseRotation().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Rotation); - bone.UseTranslation().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Translation); - } - } - - if (root != null && includePositionalData) - { - var firstTranslation = firstFrame.Skeleton.Transform.Translation; - foreach (var frame in animation) - { - var totalSeconds = (float)(frame.Time - startTime).TotalSeconds; - var position = frame.Transform.Translation - firstTranslation; - var rotation = frame.Transform.Rotation; - var scale = frame.Transform.Scale; - root.UseScale().UseTrackBuilder("pose").WithPoint(totalSeconds, scale); - root.UseRotation().UseTrackBuilder("pose").WithPoint(totalSeconds, rotation); - root.UseTranslation().UseTrackBuilder("pose").WithPoint(totalSeconds, position); - } - } - - // handle attach skeletons - var distinctAttaches = new List<(AnimationFrameData AttachFrame, AttachedSkeleton DistinctAttach)>(); - foreach (var frame in animation) - { - // add first occurrence of each attach id - foreach (var attach in frame.Attachments) - { - if (distinctAttaches.Any(x => x.Item2.AttachId == attach.AttachId)) - continue; - distinctAttaches.Add((frame, attach)); - } - } - - //foreach (var distinctAttach in distinctAttaches) - for (int i = 0; i < distinctAttaches.Count; i++) - { - var da = distinctAttaches[i]; - var attachBoneMap = GetBoneMap(da.DistinctAttach.Skeleton.PartialSkeletons, false, out var attachRoot); - if (attachRoot == null) - continue; - - var attachName = da.AttachFrame.Skeleton.PartialSkeletons[da.DistinctAttach.Attach.PartialSkeletonIdx] - .HkSkeleton!.BoneNames[(int)da.DistinctAttach.Attach.BoneIdx]; - var attachPointBone = boneMap.FirstOrDefault(x => x.BoneName.Equals(attachName, StringComparison.OrdinalIgnoreCase)); - if (attachPointBone == null) - continue; - - attachPointBone.AddNode(attachRoot); - - attachRoot.SetSuffixRecursively(i); - if (da.DistinctAttach.Attach.OffsetTransform is { } ct) - { - attachRoot.WithLocalScale(ct.Scale); - attachRoot.WithLocalRotation(ct.Rotation); - attachRoot.WithLocalTranslation(ct.Translation); - } - - var attachBonePoseMap = GetAttachBonePoseMap(da.DistinctAttach, attachBoneMap, animation); - foreach (var (time, boneTransforms) in attachBonePoseMap) - { - var totalSeconds = (float)(time - startTime).TotalSeconds; - foreach (var (bone, transform) in boneTransforms) - { - bone.UseScale().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Scale); - bone.UseRotation().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Rotation); - bone.UseTranslation().UseTrackBuilder("pose").WithPoint(totalSeconds, transform.Translation); - } - } - } - - if (!NodeBuilder.IsValidArmature(boneMap)) - { - throw new InvalidOperationException( - $"Joints are not valid, {string.Join(", ", boneMap.Select(x => x.Name))}"); - } - - return boneMap; - } - - private static Dictionary> GetAttachBonePoseMap( - AttachedSkeleton distinctAttach, List attachBoneMap, List animation) - { - var attachBonePoseMap = new Dictionary>(); - foreach (var bone in attachBoneMap) - { - if (bone.PartialSkeletonIndex == null || bone.BoneIndex == null) - continue; - - foreach (var frame in animation) - { - var attach = frame.Attachments.FirstOrDefault(x => x.AttachId == distinctAttach.AttachId); - var pose = attach?.Skeleton.PartialSkeletons[bone.PartialSkeletonIndex.Value].Poses.FirstOrDefault(); - if (pose == null) - continue; - - var transform = pose.Pose[bone.BoneIndex.Value]; - if (!attachBonePoseMap.TryGetValue(frame.Time, out var boneTransforms)) - { - boneTransforms = new Dictionary(); - attachBonePoseMap.Add(frame.Time, boneTransforms); - } - - boneTransforms.TryAdd(bone, transform); - } - } - - return attachBonePoseMap; - } - - private static Dictionary> GetBonePoseMap(List boneMap, List<(DateTime time, FrameData data)> animation) - { - var bonePoseMap = new Dictionary>(); - - foreach (var bone in boneMap) - { - if (bone.PartialSkeletonIndex == null || bone.BoneIndex == null) - continue; - - foreach (var frame in animation) - { - var pose = frame.data.Skeleton.PartialSkeletons[bone.PartialSkeletonIndex.Value].Poses.FirstOrDefault(); - if (pose == null) - continue; - - var transform = pose.Pose[bone.BoneIndex.Value]; - if (!bonePoseMap.TryGetValue(frame.time, out var boneTransforms)) - { - boneTransforms = new Dictionary(); - bonePoseMap.Add(frame.time, boneTransforms); - } - - boneTransforms.TryAdd(bone, transform); - } - } - - return bonePoseMap; - }*/ - - public static Dictionary Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline)> GetAnimatedBoneMap((DateTime Time, AttachSet[] Attaches)[] frames) - { - var attachDict = new Dictionary Bones, BoneNodeBuilder? Root, List<(DateTime Time, AttachSet Attach)> Timeline)>(); - var attachTimelines = new Dictionary>(); - foreach (var frame in frames) - { - foreach (var attach in frame.Attaches) - { - if (!attachTimelines.TryGetValue(attach.Id, out var timeline)) - { - timeline = new List<(DateTime Time, AttachSet Attach)>(); - attachTimelines.Add(attach.Id, timeline); - } - - timeline.Add((frame.Time, attach)); - } - } - - var allTimes = frames.Select(x => x.Time).ToArray(); - - var startTime = frames.Min(x => x.Time); - foreach (var (attachId, timeline) in attachTimelines) - { - var firstAttach = timeline.First().Attach; - if (!attachDict.TryGetValue(attachId, out var attachBoneMap)) - { - attachBoneMap = ([], null, timeline); - attachDict.Add(attachId, attachBoneMap); - } - - foreach (var time in allTimes) - { - var frame = timeline.FirstOrDefault(x => x.Time == time); - var frameTime = TotalSeconds(time, startTime); - if (frame != default) - { - var newMap = GetBoneMap(frame.Attach.OwnerSkeleton, false, out var attachRoot); - if (attachRoot == null) - continue; - - attachBoneMap.Root ??= attachRoot; - - foreach (var attachBone in newMap) - { - var bone = attachBoneMap.Bones.FirstOrDefault( - x => x.BoneName.Equals(attachBone.BoneName, StringComparison.OrdinalIgnoreCase)); - if (bone == null) - { - attachBoneMap.Bones.Add(attachBone); - bone = attachBone; - } - - var partial = frame.Attach.OwnerSkeleton.PartialSkeletons[attachBone.PartialSkeletonIndex]; - if (partial.Poses.Count == 0) - continue; - - var transform = partial.Poses[0].Pose[bone.BoneIndex]; - bone.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, transform.Scale); - bone.UseRotation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Rotation); - bone.UseTranslation().UseTrackBuilder("pose").WithPoint(frameTime, transform.Translation); - } - - var firstTranslation = firstAttach.Transform.Translation; - attachRoot.UseScale().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Scale); - attachRoot.UseRotation().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Rotation); - attachRoot.UseTranslation().UseTrackBuilder("pose").WithPoint(frameTime, frame.Attach.Transform.Translation - firstTranslation); - - attachDict[attachId] = attachBoneMap; - } - } - - foreach (var time in allTimes) - { - var frame = timeline.FirstOrDefault(x => x.Time == time); - if (frame != default) continue; - // set scaling to 0 when not present - foreach (var bone in attachBoneMap.Bones) - { - bone.UseScale().UseTrackBuilder("pose").WithPoint(TotalSeconds(time, startTime), Vector3.Zero); - } - } - } - - return attachDict; - } - - public static float TotalSeconds(DateTime time, DateTime startTime) - { - var value = (float)(time - startTime).TotalSeconds; - // handle really close to 0 values - if (value < 0.0001f) - return 0; - return value; - } -} diff --git a/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs b/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs deleted file mode 100644 index 047cd83..0000000 --- a/Meddle/Meddle.Plugin/Skeleton/Skeletons.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Text.Json.Serialization; -using FFXIVClientStructs.Havok.Animation.Rig; -using FFXIVClientStructs.Interop; -using Meddle.Plugin.Utils; -using Meddle.Utils.Skeletons; -using PartialCSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.PartialSkeleton; -using CSSkeleton = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton; - -namespace Meddle.Plugin.Skeleton; - -public class Skeleton -{ - public unsafe Skeleton(Pointer skeleton) : this(skeleton.Value) { } - - public unsafe Skeleton(CSSkeleton* skeleton) - { - Transform = new Transform(skeleton->Transform); - var partialSkeletons = new List(); - for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) - { - try - { - partialSkeletons.Add(new PartialSkeleton(&skeleton->PartialSkeletons[i])); - } - catch (Exception e) - { - throw new Exception($"Failed to load partial skeleton {i}/{skeleton->PartialSkeletonCount}", e); - } - } - - PartialSkeletons = partialSkeletons; - } - - public Transform Transform { get; } - public IReadOnlyList PartialSkeletons { get; } -} - -public class PartialSkeleton -{ - public unsafe PartialSkeleton(Pointer partialSkeleton) : - this(partialSkeleton.Value) { } - - public unsafe PartialSkeleton(PartialCSSkeleton* partialSkeleton) - { - if (partialSkeleton->SkeletonResourceHandle != null) - { - HkSkeleton = new HkSkeleton(partialSkeleton->SkeletonResourceHandle->HavokSkeleton); - HandlePath = partialSkeleton->SkeletonResourceHandle->FileName.ToString(); - } - - BoneCount = partialSkeleton->BoneCount; - ConnectedBoneIndex = partialSkeleton->ConnectedBoneIndex; - - var poses = new List(); - for (var i = 0; i < partialSkeleton->HavokPoses.Length; ++i) - { - var pose = partialSkeleton->GetHavokPose(i); - if (pose != null) - { - if (pose->Skeleton != partialSkeleton->SkeletonResourceHandle->HavokSkeleton) - { - throw new ArgumentException("Pose is not the same as the skeleton"); - } - - poses.Add(new SkeletonPose(pose)); - } - } - - Poses = poses; - } - - public string? HandlePath { get; } - public HkSkeleton? HkSkeleton { get; } - public IReadOnlyList Poses { get; } - public int ConnectedBoneIndex { get; } - public uint BoneCount { get; } -} - -public class HkSkeleton -{ - public unsafe HkSkeleton(Pointer skeleton) : this(skeleton.Value) { } - - public unsafe HkSkeleton(hkaSkeleton* skeleton) - { - var boneNames = new List(); - var boneParents = new List(); - var referencePose = new List(); - - for (var i = 0; i < skeleton->Bones.Length; ++i) - { - boneNames.Add(skeleton->Bones[i].Name.String); - boneParents.Add(skeleton->ParentIndices[i]); - referencePose.Add(new Transform(skeleton->ReferencePose[i])); - } - - BoneNames = boneNames; - BoneParents = boneParents; - ReferencePose = referencePose; - } - - public IReadOnlyList BoneNames { get; } - public IReadOnlyList BoneParents { get; } - - [JsonIgnore] - public IReadOnlyList ReferencePose { get; } -} - -public class SkeletonPose -{ - public unsafe SkeletonPose(Pointer pose) : this(pose.Value) { } - - public unsafe SkeletonPose(hkaPose* pose) - { - var transforms = new List(); - - var boneCount = pose->LocalPose.Length; - for (var i = 0; i < boneCount; ++i) - { - var localSpace = PoseUtil.AccessBoneLocalSpace(pose, i); - if (localSpace == null) - { - throw new Exception("Failed to access bone local space"); - } - transforms.Add(new Transform(*localSpace)); - } - - Pose = transforms; - } - - [JsonIgnore] - public IReadOnlyList Pose { get; } -} diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index 13b1103..a71ec40 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -3,72 +3,56 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.Havok.Common.Base.Math.QsTransform; +using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Services; using Meddle.Plugin.Utils; -using Meddle.Utils.Skeletons; using Microsoft.Extensions.Logging; using SharpGLTF.Transforms; -using Attach = Meddle.Plugin.Skeleton.Attach; namespace Meddle.Plugin.UI; public class AnimationTab : ITab { + private readonly IClientState clientState; + private readonly Configuration config; + private readonly ExportService exportService; + private readonly List<(DateTime Time, AttachSet[])> frames = []; private readonly IFramework framework; private readonly ILogger logger; - private readonly IClientState clientState; private readonly IObjectTable objectTable; - private readonly ExportService exportService; - private readonly PluginState pluginState; - private readonly Configuration config; - public string Name => "Animation"; - public int Order => 2; - public bool DisplayTab => true; private bool captureAnimation; - private ICharacter? selectedCharacter; - private readonly List<(DateTime Time, AttachSet[])> frames = []; private bool includePositionalData; - - public AnimationTab(IFramework framework, ILogger logger, - IClientState clientState, - IObjectTable objectTable, - ExportService exportService, - PluginState pluginState, - Configuration config) + private ICharacter? selectedCharacter; + + public AnimationTab( + IFramework framework, ILogger logger, + IClientState clientState, + IObjectTable objectTable, + ExportService exportService, + Configuration config) { this.framework = framework; this.logger = logger; this.clientState = clientState; this.objectTable = objectTable; this.exportService = exportService; - this.pluginState = pluginState; this.config = config; this.framework.Update += OnFrameworkUpdate; } - private void OnFrameworkUpdate(IFramework framework1) - { - if (!pluginState.InteropResolved) - { - return; - } - Capture(); - } + public string Name => "Animation"; + public int Order => 2; + public bool DisplayTab => true; - public unsafe void Draw() + public void Draw() { - if (!pluginState.InteropResolved) - { - ImGui.Text("Waiting for Interop to resolve..."); - return; - } - // Warning text: - ImGui.TextWrapped("NOTE: Animation exports are experimental, held weapons, mounts and other attached objects may not work as expected."); - + ImGui.TextWrapped( + "NOTE: Animation exports are experimental, held weapons, mounts and other attached objects may not work as expected."); + ICharacter[] objects; if (clientState.LocalPlayer != null) { @@ -87,9 +71,11 @@ public unsafe void Draw() } selectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; - + ImGui.Text("Select Character"); - var preview = selectedCharacter != null ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) : "None"; + var preview = selectedCharacter != null + ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) + : "None"; using (var combo = ImRaii.Combo("##Character", preview)) { if (combo) @@ -103,7 +89,7 @@ public unsafe void Draw() } } } - + if (selectedCharacter == null) return; if (ImGui.Checkbox("Capture Animation", ref captureAnimation)) { @@ -116,40 +102,50 @@ public unsafe void Draw() logger.LogInformation("Stopped capturing animation"); } } - + var frameCount = frames.Count; ImGui.Text($"Frames: {frameCount}"); if (ImGui.Button("Export")) { exportService.ExportAnimation(frames, includePositionalData); } - + ImGui.SameLine(); ImGui.Checkbox("Include Positional Data", ref includePositionalData); - + if (ImGui.Button("Clear")) { frames.Clear(); } - + ImGui.Separator(); - + if (ImGui.CollapsingHeader("Skeleton")) { DrawSelectedCharacter(); } } + public void Dispose() + { + framework.Update -= OnFrameworkUpdate; + } + + private void OnFrameworkUpdate(IFramework framework1) + { + Capture(); + } + private unsafe void Capture() { if (!captureAnimation) return; if (selectedCharacter == null) return; - + if (frames.Count > 0 && DateTime.UtcNow - frames[^1].Time < TimeSpan.FromMilliseconds(100)) { return; } - + var charPtr = (Character*)selectedCharacter.Address; if (charPtr == null) { @@ -157,6 +153,7 @@ private unsafe void Capture() captureAnimation = false; return; } + var root = (CharacterBase*)charPtr->GameObject.DrawObject; if (root == null) { @@ -166,23 +163,28 @@ private unsafe void Capture() } var attachCollection = new List(); - var rootSkeleton = new Skeleton.Skeleton(root->Skeleton); + var rootSkeleton = StructExtensions.GetParsedSkeleton(root); string rootName; - if (root->Attach.ExecuteType == 3) + var attach = StructExtensions.GetAttach(root); + if (attach.ExecuteType == 3) { - var owner = root->Attach.OwnerCharacter; - var rootAttach = new Attach(root->Attach); - var ownerSkeleton = new Skeleton.Skeleton(owner->Skeleton); - var attachBoneName = ownerSkeleton.PartialSkeletons[rootAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)rootAttach.BoneIdx] ?? "Bone"; + var owner = attach.OwnerCharacter; + var rootAttach = StructExtensions.GetParsedAttach(root); + var ownerSkeleton = StructExtensions.GetParsedSkeleton(owner); + var attachBoneName = ownerSkeleton.PartialSkeletons[rootAttach.PartialSkeletonIdx].HkSkeleton + ?.BoneNames[(int)rootAttach.BoneIdx] ?? "Bone"; rootName = $"{(nint)root:X8}_{attachBoneName}"; - var rootAttachSet = new AttachSet(rootName, rootAttach, rootSkeleton, GetTransform(root), $"{(nint)owner:X8}"); + var rootAttachSet = + new AttachSet(rootName, rootAttach, rootSkeleton, GetTransform(root), $"{(nint)owner:X8}"); attachCollection.Add(rootAttachSet); - attachCollection.Add(new AttachSet($"{(nint)owner:X8}", new Attach(owner->Attach), ownerSkeleton, GetTransform(owner), null)); - } + attachCollection.Add(new AttachSet($"{(nint)owner:X8}", StructExtensions.GetParsedAttach(owner), + ownerSkeleton, GetTransform(owner), null)); + } else { rootName = $"{(nint)root:X8}"; - var rootAttach = new AttachSet(rootName, new Attach(root->Attach), rootSkeleton, GetTransform(root), null); + var rootAttach = new AttachSet(rootName, StructExtensions.GetParsedAttach(root), rootSkeleton, + GetTransform(root), null); attachCollection.Add(rootAttach); } @@ -193,21 +195,25 @@ private unsafe void Capture() { continue; } + attachCollection.Add(characterAttach); } - + frames.Add((DateTime.UtcNow, attachCollection.ToArray())); } - - public static unsafe AffineTransform GetTransform(CharacterBase* character) + + public static unsafe AffineTransform GetTransform(Pointer characterPointer) { + if (characterPointer == null || characterPointer.Value == null) + throw new ArgumentNullException(nameof(characterPointer)); + var character = characterPointer.Value; var position = character->Position; var rotation = character->Rotation; var scale = character->Scale; return new AffineTransform(scale, rotation, position); } - - private static unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Skeleton ownerSkeleton, string ownerId) + + private static unsafe AttachSet[] GetAttachData(Character* charPtr, ParsedSkeleton ownerSkeleton, string ownerId) { var attachments = new List(); var ornament = charPtr->OrnamentData.OrnamentObject; @@ -215,31 +221,40 @@ private static unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Ske var mount = charPtr->Mount.MountObject; var weaponData = charPtr->DrawData.WeaponData; - if (ornament != null && ornament->DrawObject != null && ornament->DrawObject->GetObjectType() == ObjectType.CharacterBase) + if (ornament != null && ornament->DrawObject != null && + ornament->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var ornamentBase = (CharacterBase*)ornament->DrawObject; - var ornamentAttach = new Attach(ornamentBase->Attach); - var attachBoneName = ownerSkeleton.PartialSkeletons[ornamentAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)ornamentAttach.BoneIdx] ?? "Bone"; - attachments.Add(new ($"{(nint)ornamentBase:X8}_{attachBoneName}", ornamentAttach, - new Skeleton.Skeleton(ornamentBase->Skeleton), GetTransform(ornamentBase), ownerId)); + var ornamentAttach = StructExtensions.GetParsedAttach(ornamentBase); + var attachBoneName = ownerSkeleton.PartialSkeletons[ornamentAttach.PartialSkeletonIdx].HkSkeleton + ?.BoneNames[(int)ornamentAttach.BoneIdx] ?? "Bone"; + attachments.Add(new AttachSet($"{(nint)ornamentBase:X8}_{attachBoneName}", ornamentAttach, + StructExtensions.GetParsedSkeleton(ornamentBase), GetTransform(ornamentBase), + ownerId)); } - if (companion != null && companion->DrawObject != null && companion->DrawObject->GetObjectType() == ObjectType.CharacterBase) + if (companion != null && companion->DrawObject != null && + companion->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var companionBase = (CharacterBase*)companion->DrawObject; - var companionAttach = new Attach(companionBase->Attach); - var attachBoneName = ownerSkeleton.PartialSkeletons[companionAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)companionAttach.BoneIdx] ?? "Bone"; - attachments.Add(new ($"{(nint)companionBase:X8}_{attachBoneName}", companionAttach, - new Skeleton.Skeleton(companionBase->Skeleton), GetTransform(companionBase), ownerId)); + var companionAttach = StructExtensions.GetParsedAttach(companionBase); + var attachBoneName = ownerSkeleton.PartialSkeletons[companionAttach.PartialSkeletonIdx].HkSkeleton + ?.BoneNames[(int)companionAttach.BoneIdx] ?? "Bone"; + attachments.Add(new AttachSet($"{(nint)companionBase:X8}_{attachBoneName}", companionAttach, + StructExtensions.GetParsedSkeleton(companionBase), + GetTransform(companionBase), ownerId)); } - if (mount != null && mount->DrawObject != null && mount->DrawObject->GetObjectType() == ObjectType.CharacterBase) + if (mount != null && mount->DrawObject != null && + mount->DrawObject->GetObjectType() == ObjectType.CharacterBase) { var mountBase = (CharacterBase*)mount->DrawObject; - var mountAttach = new Attach(mountBase->Attach); - var attachBoneName = ownerSkeleton.PartialSkeletons[mountAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)mountAttach.BoneIdx] ?? "Bone"; - attachments.Add(new ($"{(nint)mountBase:X8}_{attachBoneName}", mountAttach, - new Skeleton.Skeleton(mountBase->Skeleton), GetTransform(mountBase), ownerId)); + var mountAttach = StructExtensions.GetParsedAttach(mountBase); + var attachBoneName = ownerSkeleton.PartialSkeletons[mountAttach.PartialSkeletonIdx].HkSkeleton + ?.BoneNames[(int)mountAttach.BoneIdx] ?? "Bone"; + attachments.Add(new AttachSet($"{(nint)mountBase:X8}_{attachBoneName}", mountAttach, + StructExtensions.GetParsedSkeleton(mountBase), GetTransform(mountBase), + ownerId)); } if (weaponData != null) @@ -250,14 +265,16 @@ private static unsafe AttachSet[] GetAttachData(Character* charPtr, Skeleton.Ske if (weapon.DrawObject != null && weapon.DrawObject->GetObjectType() == ObjectType.CharacterBase) { var weaponBase = (CharacterBase*)weapon.DrawObject; - var weaponAttach = new Attach(weaponBase->Attach); - var attachBoneName = ownerSkeleton.PartialSkeletons[weaponAttach.PartialSkeletonIdx].HkSkeleton?.BoneNames[(int)weaponAttach.BoneIdx] ?? "Bone"; - attachments.Add(new ($"{(nint)weaponBase:X8}_{attachBoneName}", weaponAttach, - new Skeleton.Skeleton(weaponBase->Skeleton), GetTransform(weaponBase), ownerId)); + var weaponAttach = StructExtensions.GetParsedAttach(weaponBase); + var attachBoneName = ownerSkeleton.PartialSkeletons[weaponAttach.PartialSkeletonIdx].HkSkeleton + ?.BoneNames[(int)weaponAttach.BoneIdx] ?? "Bone"; + attachments.Add(new AttachSet($"{(nint)weaponBase:X8}_{attachBoneName}", weaponAttach, + StructExtensions.GetParsedSkeleton(weaponBase), + GetTransform(weaponBase), ownerId)); } } } - + return attachments.ToArray(); } @@ -266,46 +283,7 @@ private unsafe void DrawSelectedCharacter() if (selectedCharacter == null) return; var charPtr = (Character*)selectedCharacter.Address; if (charPtr == null) return; - var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; - if (cBase == null) return; - - UiUtil.DrawCharacterAttaches(charPtr); - } - - private static unsafe Dictionary? GetPose(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* skeleton) - { - if (skeleton == null) - return null; - - var ret = new Dictionary(); - - for(var i = 0; i < skeleton->PartialSkeletonCount; ++i) - { - var partial = skeleton->PartialSkeletons[i]; - var pose = partial.GetHavokPose(0); - if (pose == null) - continue; - - var partialSkele = pose->Skeleton; - - for (var j = 0; j < partialSkele->Bones.Length; ++j) - { - if (j == partial.ConnectedBoneIndex) - continue; - - var boneName = pose->Skeleton->Bones[j].Name.String; - if (string.IsNullOrEmpty(boneName)) - continue; - ret[boneName] = pose->LocalPose[j]; - } - } - - return ret; - } - - public void Dispose() - { - framework.Update -= OnFrameworkUpdate; + UiUtil.DrawCharacterAttaches(charPtr); } } diff --git a/Meddle/Meddle.Plugin/UI/CharacterTab.cs b/Meddle/Meddle.Plugin/UI/CharacterTab.cs index 94d1b0c..046d987 100644 --- a/Meddle/Meddle.Plugin/UI/CharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/CharacterTab.cs @@ -13,13 +13,13 @@ using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; -using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Materials; using Meddle.Utils.Models; using Microsoft.Extensions.Logging; using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; +using Model = Meddle.Utils.Export.Model; namespace Meddle.Plugin.UI; @@ -28,12 +28,11 @@ public unsafe class CharacterTab : ITab private readonly Dictionary channelCache = new(); private readonly IClientState clientState; + private readonly Configuration config; private readonly ExportService exportService; private readonly ILogger log; private readonly IObjectTable objectTable; private readonly ParseService parseService; - private readonly Configuration config; - private readonly PluginState pluginState; private readonly Dictionary textureCache = new(); private readonly ITextureProvider textureProvider; @@ -46,14 +45,12 @@ public CharacterTab( IObjectTable objectTable, IClientState clientState, ILogger log, - PluginState pluginState, ExportService exportService, ITextureProvider textureProvider, ParseService parseService, Configuration config) { this.log = log; - this.pluginState = pluginState; this.exportService = exportService; this.parseService = parseService; this.config = config; @@ -61,27 +58,22 @@ public CharacterTab( this.clientState = clientState; this.textureProvider = textureProvider; } - + private ICharacter? SelectedCharacter { get; set; } private bool ExportTaskIncomplete => exportTask?.IsCompleted == false; + + + private bool IsDisposed { get; set; } public string Name => "Character"; public int Order => 0; public bool DisplayTab => true; public void Draw() { - if (!pluginState.InteropResolved) - { - ImGui.Text("Waiting for game data..."); - return; - } - DrawObjectPicker(); } - - private bool IsDisposed { get; set; } public void Dispose() { if (!IsDisposed) @@ -91,6 +83,7 @@ public void Dispose() { textureImage.Wrap.Dispose(); } + textureCache.Clear(); IsDisposed = true; } @@ -101,7 +94,7 @@ private void DrawObjectPicker() // Warning text: ImGui.TextWrapped("NOTE: Exported models use a rudimentary approximation of the games pixel shaders, " + "they will likely not match 1:1 to the in-game appearance."); - + ICharacter[] objects; if (clientState.LocalPlayer != null) { @@ -122,7 +115,9 @@ private void DrawObjectPicker() SelectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; ImGui.Text("Select Character"); - var preview = SelectedCharacter != null ? clientState.GetCharacterDisplayText(SelectedCharacter, config.PlayerNameOverride) : "None"; + var preview = SelectedCharacter != null + ? clientState.GetCharacterDisplayText(SelectedCharacter, config.PlayerNameOverride) + : "None"; using (var combo = ImRaii.Combo("##Character", preview)) { if (combo) @@ -163,7 +158,7 @@ private void DrawObjectPicker() ImGui.Text("Parse a character to view data"); return; } - + DrawCharacterGroup(); } @@ -195,7 +190,7 @@ private void DrawCharacterGroup() { ImGui.Columns(1); } - + ImGui.Separator(); DrawExportOptions(); @@ -219,7 +214,10 @@ private void DrawCharacterGroup() { try { - exportService.Export(characterGroup with {MdlGroups = [mdlGroup], AttachedModelGroups = []}); + exportService.Export(characterGroup with + { + MdlGroups = [mdlGroup], AttachedModelGroups = [] + }); } catch (Exception e) { @@ -264,11 +262,11 @@ private void DrawCharacterGroup() ImGui.PopID(); } - + ImGui.EndTable(); } - - + + if (characterGroup.AttachedModelGroups.Length > 0) { if (ImGui.BeginTable("AttachedCharacterTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) @@ -290,7 +288,10 @@ private void DrawCharacterGroup() { try { - exportService.Export(characterGroup with {AttachedModelGroups = [attachedModelGroup], MdlGroups = []}); + exportService.Export(characterGroup with + { + AttachedModelGroups = [attachedModelGroup], MdlGroups = [] + }); } catch (Exception e) { @@ -312,14 +313,16 @@ private void DrawCharacterGroup() { selectedSetGroup = selectedSetGroup with { - AttachedModelGroups = selectedSetGroup.AttachedModelGroups.Append(attachedModelGroup).ToArray() + AttachedModelGroups = selectedSetGroup.AttachedModelGroups + .Append(attachedModelGroup).ToArray() }; } else { selectedSetGroup = selectedSetGroup with { - AttachedModelGroups = selectedSetGroup.AttachedModelGroups.Where(m => m != attachedModelGroup).ToArray() + AttachedModelGroups = selectedSetGroup.AttachedModelGroups + .Where(m => m != attachedModelGroup).ToArray() }; } } @@ -338,14 +341,14 @@ private void DrawCharacterGroup() ImGui.PopID(); } - + ImGui.PopID(); } - + ImGui.EndTable(); } } - + ImGui.PopID(); } @@ -354,7 +357,7 @@ private void DrawExportOptions() { if (characterGroup == null) return; var availWidth = ImGui.GetContentRegionAvail().X; - + ImGui.BeginDisabled(ExportTaskIncomplete); if (ImGui.Button("Export All")) { @@ -362,7 +365,7 @@ private void DrawExportOptions() { try { - exportService.Export(characterGroup, default); + exportService.Export(characterGroup); } catch (Exception e) { @@ -371,9 +374,10 @@ private void DrawExportOptions() } }); } - + ImGui.SameLine(); - var selectedCount = (selectedSetGroup?.MdlGroups.Length ?? 0) + (selectedSetGroup?.AttachedModelGroups.Length ?? 0); + var selectedCount = (selectedSetGroup?.MdlGroups.Length ?? 0) + + (selectedSetGroup?.AttachedModelGroups.Length ?? 0); if (ImGui.Button($"Export {selectedCount} Selected##ExportSelected")) { if (selectedSetGroup == null) @@ -381,6 +385,7 @@ private void DrawExportOptions() log.LogWarning("No selected set group"); return; } + exportTask = Task.Run(() => { try @@ -441,7 +446,7 @@ private Task ParseCharacter(ICharacter character) if (modelType == CharacterBase.ModelType.Human) { var human = (Human*)drawObject; - var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; + var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; customizeParams = new CustomizeParameter { SkinColor = customizeCBuf.SkinColor, @@ -522,7 +527,7 @@ private Task ParseCharacter(ICharacter character) try { characterGroup = parseService.HandleCharacterGroup(characterBase, colorTableTextures, attachDict, - customizeParams, customizeData, genderRace); + customizeParams, customizeData, genderRace); selectedSetGroup = characterGroup; } catch (Exception e) @@ -551,9 +556,9 @@ private void DrawMdlGroup(MdlFileGroup mdlGroup) ImGui.Text("No Deformer Group"); } - var shouldShowShapeAttributeMenu = + var shouldShowShapeAttributeMenu = mdlGroup.ShapeAttributeGroup is {ShapeMasks.Length: > 0} or {AttributeMasks.Length: > 0}; - + if (shouldShowShapeAttributeMenu && ImGui.CollapsingHeader("Shape/Attribute Masks")) { var enabledShapes = Model.GetEnabledValues(mdlGroup.ShapeAttributeGroup!.EnabledShapeMask, @@ -566,7 +571,7 @@ private void DrawMdlGroup(MdlFileGroup mdlGroup) ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableSetupColumn("Enabled", ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - + foreach (var shape in mdlGroup.ShapeAttributeGroup.ShapeMasks) { ImGui.TableNextRow(); @@ -575,7 +580,7 @@ private void DrawMdlGroup(MdlFileGroup mdlGroup) ImGui.TableSetColumnIndex(1); ImGui.Text(enabledShapes.Contains(shape.name) ? "Yes" : "No"); } - + foreach (var attribute in mdlGroup.ShapeAttributeGroup.AttributeMasks) { ImGui.TableNextRow(); @@ -584,7 +589,7 @@ private void DrawMdlGroup(MdlFileGroup mdlGroup) ImGui.TableSetColumnIndex(1); ImGui.Text(enabledAttributes.Contains(attribute.name) ? "Yes" : "No"); } - + ImGui.EndTable(); } } @@ -659,7 +664,7 @@ private void DrawMtrlGroup(MtrlFileGroup mtrlGroup) ImGui.TableSetColumnIndex(3); ImGui.Text(string.Join(", ", floats.ToArray())); } - + ImGui.EndTable(); } } @@ -722,7 +727,7 @@ private void DrawTexGroup(TexResourceGroup texGroup) { ImGui.Text($"Mtrl Path: {texGroup.MtrlPath}"); ImGui.Text($"Path: {texGroup.Path}"); - + ImGui.PushID(texGroup.GetHashCode()); try { diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs index 9485125..29ad2e0 100644 --- a/Meddle/Meddle.Plugin/UI/DebugTab.cs +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -4,6 +4,8 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; +using Meddle.Plugin.Models; +using Meddle.Plugin.Services; using Meddle.Plugin.Utils; using Meddle.Utils.Skeletons; @@ -11,26 +13,34 @@ namespace Meddle.Plugin.UI; public class DebugTab : ITab { - private readonly Configuration config; private readonly IClientState clientState; + private readonly Configuration config; private readonly IObjectTable objectTable; + private readonly PbdHooks pbdHooks; + private int selectedBoneIndex; + + private ICharacter? selectedCharacter; - public DebugTab(Configuration config, IClientState clientState, IObjectTable objectTable) + private int selectedPartialSkeletonIndex; + + public DebugTab(Configuration config, IClientState clientState, IObjectTable objectTable, PbdHooks pbdHooks) { this.config = config; this.clientState = clientState; this.objectTable = objectTable; + this.pbdHooks = pbdHooks; } - + public void Dispose() { // TODO release managed resources here } - public string Name => "Debug"; + public string Name => "Debug"; public int Order => int.MaxValue; public bool DisplayTab => config.ShowDebug; - public void Draw() + + private void DrawCharacterSelect() { ICharacter[] objects; if (clientState.LocalPlayer != null) @@ -50,9 +60,11 @@ public void Draw() } selectedCharacter ??= objects.FirstOrDefault() ?? clientState.LocalPlayer; - + ImGui.Text("Select Character"); - var preview = selectedCharacter != null ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) : "None"; + var preview = selectedCharacter != null + ? clientState.GetCharacterDisplayText(selectedCharacter, config.PlayerNameOverride) + : "None"; using (var combo = ImRaii.Combo("##Character", preview)) { if (combo) @@ -66,37 +78,73 @@ public void Draw() } } } - - DrawDebugMenu(); + } + + public void Draw() + { + if (ImGui.CollapsingHeader("Selected Character")) + { + DrawSelectedCharacter(); + } + + if (ImGui.CollapsingHeader("PBD Info")) + { + using var table = ImRaii.Table("##PbdInfo", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Human", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.WidthFixed, 50); + ImGui.TableSetupColumn("DeformerId", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("RaceSexId", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("PbdPath"); + ImGui.TableHeadersRow(); + foreach (var cachedDeformer in pbdHooks.DeformerCache) + { + foreach (var deformer in cachedDeformer.Value) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{cachedDeformer.Key:X8}"); + ImGui.TableNextColumn(); + ImGui.Text($"{deformer.Key}"); + ImGui.TableNextColumn(); + ImGui.Text($"{deformer.Value.DeformerId}"); + ImGui.TableNextColumn(); + ImGui.Text($"{deformer.Value.RaceSexId}"); + ImGui.TableNextColumn(); + ImGui.Text($"{deformer.Value.PbdPath}"); + } + } + } } - private unsafe void DrawDebugMenu() + private unsafe void DrawSelectedCharacter() { + DrawCharacterSelect(); if (selectedCharacter == null) { ImGui.Text("No characters found"); return; } - + // player address ImGui.Text($"Address: {selectedCharacter.Address:X8}"); if (ImGui.IsItemHovered()) { ImGui.SetTooltip("Click to copy"); } + if (ImGui.IsItemClicked()) { ImGui.SetClipboardText($"{selectedCharacter.Address:X8}"); } - - + + var character = (Character*)selectedCharacter.Address; if (character == null) { ImGui.Text("Character is null"); return; } - + ImGui.Text($"Character Name: {character->NameString}"); var drawObject = character->DrawObject; @@ -105,7 +153,7 @@ private unsafe void DrawDebugMenu() ImGui.Text("DrawObject is null"); return; } - + ImGui.Text($"DrawObject Address: {(nint)drawObject:X8}"); var objectType = drawObject->GetObjectType(); @@ -114,7 +162,7 @@ private unsafe void DrawDebugMenu() { return; } - + var cBase = (CharacterBase*)drawObject; var skeleton = cBase->Skeleton; if (skeleton == null) @@ -122,7 +170,7 @@ private unsafe void DrawDebugMenu() ImGui.Text("Skeleton is null"); return; } - + // imgui select partial skeleton by index ImGui.Text($"Partial Skeleton Count: {skeleton->PartialSkeletonCount}"); if (ImGui.InputInt("##PartialSkeletonIndex", ref selectedPartialSkeletonIndex)) @@ -136,19 +184,19 @@ private unsafe void DrawDebugMenu() selectedPartialSkeletonIndex = skeleton->PartialSkeletonCount - 1; } } - + var partialSkeleton = skeleton->PartialSkeletons[selectedPartialSkeletonIndex]; - - ImGui.Text($"Partial Skeleton Bone Count: {partialSkeleton.BoneCount}"); + var boneCount = StructExtensions.GetBoneCount(&partialSkeleton); + ImGui.Text($"Partial Skeleton Bone Count: {boneCount}"); if (ImGui.InputInt("##BoneIndex", ref selectedBoneIndex)) { if (selectedBoneIndex < 0) { selectedBoneIndex = 0; } - else if (selectedBoneIndex >= partialSkeleton.BoneCount) + else if (selectedBoneIndex >= boneCount) { - selectedBoneIndex = (int)partialSkeleton.BoneCount - 1; + selectedBoneIndex = (int)boneCount - 1; } } @@ -168,12 +216,8 @@ private unsafe void DrawDebugMenu() ImGui.Text("Pose Bone is null"); return; } + var boneTransform = new Transform(*poseBone); ImGui.Text($"Bone Transform: {boneTransform}"); } - - private int selectedPartialSkeletonIndex; - private int selectedBoneIndex; - - private ICharacter? selectedCharacter; } diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 8af92e7..8685dcd 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -9,11 +9,11 @@ using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Structs; using Meddle.Plugin.Services; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; -using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; using Microsoft.Extensions.Logging; using SkiaSharp; @@ -29,30 +29,32 @@ namespace Meddle.Plugin.UI; public unsafe class LiveCharacterTab : ITab { private readonly IClientState clientState; - private readonly ExportService exportService; - private readonly ITextureProvider textureProvider; - private readonly ILogger log; - private readonly IObjectTable objectTable; - private readonly ParseService parseService; - private readonly DXHelper dxHelper; - private readonly TextureCache textureCache; - private readonly SqPack pack; - private readonly PbdHooks pbd; private readonly Configuration config; - private readonly PluginState pluginState; - private ICharacter? selectedCharacter; - private readonly Dictionary, bool> selectedModels = new(); + private readonly DXHelper dxHelper; + private readonly ExportService exportService; private readonly FileDialogManager fileDialog = new() { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + private readonly ILogger log; + private readonly IObjectTable objectTable; + private readonly SqPack pack; + private readonly ParseService parseService; + private readonly PbdHooks pbd; + private readonly Dictionary selectedModels = new(); + private readonly TextureCache textureCache; + private readonly ITextureProvider textureProvider; + private bool cacheHumanCustomizeData; + + private readonly Dictionary, (CustomizeData, CustomizeParameter)> humanCustomizeData = new(); + private ICharacter? selectedCharacter; + public LiveCharacterTab( IObjectTable objectTable, IClientState clientState, ILogger log, - PluginState pluginState, ExportService exportService, ITextureProvider textureProvider, ParseService parseService, @@ -63,7 +65,6 @@ public LiveCharacterTab( Configuration config) { this.log = log; - this.pluginState = pluginState; this.exportService = exportService; this.textureProvider = textureProvider; this.parseService = parseService; @@ -76,24 +77,20 @@ public LiveCharacterTab( this.clientState = clientState; } + + private bool IsDisposed { get; set; } + public string Name => "CharacterAlt"; public int Order => 1; public bool DisplayTab => true; public void Draw() { - if (!pluginState.InteropResolved) - { - ImGui.Text("Waiting for game data..."); - return; - } - DrawObjectPicker(); + DrawSelectedCharacter(); + fileDialog.Draw(); } - - private bool IsDisposed { get; set; } - public void Dispose() { if (!IsDisposed) @@ -147,9 +144,6 @@ private void DrawObjectPicker() } } } - - DrawSelectedCharacter(); - fileDialog.Draw(); } private void DrawSelectedCharacter() @@ -163,7 +157,7 @@ private void DrawSelectedCharacter() var charPtr = (CSCharacter*)selectedCharacter.Address; DrawCharacter(charPtr); } - + private void DrawCharacter(CSCharacter* character) { if (character == null) @@ -181,14 +175,14 @@ private void DrawCharacter(CSCharacter* character) ImGui.Text("Mount"); DrawCharacter(character->Mount.MountObject); } - + if (character->CompanionData.CompanionObject != null) { ImGui.Separator(); ImGui.Text("Companion"); DrawCharacter(&character->CompanionData.CompanionObject->Character); } - + if (character->OrnamentData.OrnamentObject != null) { ImGui.Separator(); @@ -207,7 +201,7 @@ private void DrawCharacter(CSCharacter* character) } } } - + private void DrawDrawObject(DrawObject* drawObject) { if (drawObject == null) @@ -215,21 +209,21 @@ private void DrawDrawObject(DrawObject* drawObject) ImGui.Text("Draw object is null"); return; } - + var objectType = drawObject->Object.GetObjectType(); if (objectType != ObjectType.CharacterBase) { ImGui.Text($"Draw object is not a character base ({objectType})"); return; } - + using var drawObjectId = ImRaii.PushId($"{(nint)drawObject}"); var cBase = (CSCharacterBase*)drawObject; var modelType = cBase->GetModelType(); CustomizeParameter? customizeParams = null; CustomizeData? customizeData = null; - GenderRace genderRace = GenderRace.Unknown; - if (modelType == CSCharacterBase.ModelType.Human) + var genderRace = GenderRace.Unknown; + if (modelType == CharacterBase.ModelType.Human) { DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); } @@ -248,25 +242,25 @@ private void DrawDrawObject(DrawObject* drawObject) models.Add(modelData); } - var skeleton = new Skeleton.Skeleton(cBase->Skeleton); + var skeleton = StructExtensions.GetParsedSkeleton(cBase); var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, models.ToArray(), skeleton, []); fileDialog.SaveFolderDialog("Save Model", "Character", - (result, path) => - { - if (!result) return; - - Task.Run(() => { exportService.Export(cGroup, path); }); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + + Task.Run(() => { exportService.Export(cGroup, path); }); + }, Plugin.TempDirectory); } ImGui.SameLine(); var selectedModelCount = cBase->ModelsSpan.ToArray().Count(modelPtr => { if (modelPtr == null) return false; - return selectedModels.ContainsKey(modelPtr.Value) && selectedModels[modelPtr.Value]; + return selectedModels.ContainsKey((nint)modelPtr.Value) && selectedModels[(nint)modelPtr.Value]; }); using (var disable = ImRaii.Disabled(selectedModelCount == 0)) { @@ -277,7 +271,7 @@ private void DrawDrawObject(DrawObject* drawObject) foreach (var modelPtr in cBase->ModelsSpan) { if (modelPtr == null) continue; - if (!selectedModels.TryGetValue(modelPtr, out var isSelected) || !isSelected) continue; + if (!selectedModels.TryGetValue((nint)modelPtr.Value, out var isSelected) || !isSelected) continue; var model = modelPtr.Value; if (model == null) continue; var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); @@ -285,16 +279,18 @@ private void DrawDrawObject(DrawObject* drawObject) models.Add(modelData); } - var skeleton = new Skeleton.Skeleton(cBase->Skeleton); - var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, models.ToArray(), skeleton, []); + var skeleton = StructExtensions.GetParsedSkeleton(cBase); + var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), + customizeData ?? new CustomizeData(), genderRace, models.ToArray(), + skeleton, []); fileDialog.SaveFolderDialog("Save Model", "Character", - (result, path) => - { - if (!result) return; - - Task.Run(() => { exportService.Export(cGroup, path); }); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + + Task.Run(() => { exportService.Export(cGroup, path); }); + }, Plugin.TempDirectory); } } @@ -314,18 +310,22 @@ private void DrawDrawObject(DrawObject* drawObject) } } - private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + private void DrawModel( + Pointer cPtr, Pointer mPtr, CustomizeParameter? customizeParams, + CustomizeData? customizeData, GenderRace genderRace) { - if (cBase == null) + if (cPtr == null || cPtr.Value == null) { return; } - if (model == null || model->ModelResourceHandle == null) + if (mPtr == null || mPtr.Value == null || mPtr.Value->ModelResourceHandle == null) { return; } + var cBase = cPtr.Value; + var model = mPtr.Value; using var modelId = ImRaii.PushId($"{(nint)model}"); ImGui.TableNextRow(); var fileName = model->ModelResourceHandle->FileName.ToString(); @@ -338,18 +338,18 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete { ImGui.OpenPopup("ExportModelPopup"); } - + ImGui.SameLine(); - var selected = selectedModels.ContainsKey(model); + var selected = selectedModels.ContainsKey((nint)model); if (ImGui.Checkbox("##Selected", ref selected)) { if (selected) { - selectedModels[model] = true; + selectedModels[(nint)model] = true; } else { - selectedModels.Remove(model); + selectedModels.Remove((nint)model); } } } @@ -361,41 +361,48 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete { var defaultFileName = Path.GetFileName(fileName); fileDialog.SaveFileDialog("Save Model", "Model File{.mdl}", defaultFileName, ".mdl", - (result, path) => - { - if (!result) return; - var data = pack.GetFileOrReadFromDisk(fileName); - if (data == null) - { - log.LogError("Failed to get model data from pack or disk for {FileName}", fileName); - return; - } - - File.WriteAllBytes(path, data); - }); + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(fileName); + if (data == null) + { + log.LogError( + "Failed to get model data from pack or disk for {FileName}", + fileName); + return; + } + + File.WriteAllBytes(path, data); + }); } - + if (ImGui.MenuItem("Export as glTF")) { var folderName = Path.GetFileNameWithoutExtension(fileName); fileDialog.SaveFolderDialog("Save Model", folderName, - (result, path) => - { - if (!result) return; - var colorTableTextures = parseService.ParseColorTableTextures(cBase); - var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); - if (modelData == null) - { - log.LogError("Failed to get model data for {FileName}", fileName); - return; - } - - var skeleton = new Skeleton.Skeleton(model->Skeleton); - var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), customizeData ?? new CustomizeData(), genderRace, [modelData], skeleton, []); - - - Task.Run(() => { exportService.Export(cGroup, path); }); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + var colorTableTextures = parseService.ParseColorTableTextures(cBase); + var modelData = + parseService.HandleModelPtr( + cBase, (int)model->SlotIndex, colorTableTextures); + if (modelData == null) + { + log.LogError("Failed to get model data for {FileName}", fileName); + return; + } + + var skeleton = StructExtensions.GetParsedSkeleton(model); + var cGroup = new CharacterGroup( + customizeParams ?? new CustomizeParameter(), + customizeData ?? new CustomizeData(), genderRace, [modelData], + skeleton, []); + + + Task.Run(() => { exportService.Export(cGroup, path); }); + }, Plugin.TempDirectory); } ImGui.EndPopup(); @@ -412,7 +419,8 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete var deformerInfo = pbd.TryGetDeformer((nint)cBase, model->SlotIndex); if (deformerInfo != null) { - ImGui.Text($"Deformer Id: {(GenderRace)deformerInfo.Value.DeformerId} ({deformerInfo.Value.DeformerId})"); + ImGui.Text( + $"Deformer Id: {(GenderRace)deformerInfo.Value.DeformerId} ({deformerInfo.Value.DeformerId})"); ImGui.Text($"RaceSex Id: {(GenderRace)deformerInfo.Value.RaceSexId} ({deformerInfo.Value.RaceSexId})"); ImGui.Text($"Pbd Path: {deformerInfo.Value.PbdPath}"); } @@ -420,8 +428,8 @@ private void DrawModel(CSCharacterBase* cBase, CSModel* model, CustomizeParamete { ImGui.Text("No deformer info found"); } - - var modelShapeAttributes = parseService.ParseModelShapeAttributes(model); + + var modelShapeAttributes = StructExtensions.ParseModelShapeAttributes(model); DrawShapeAttributeTable(modelShapeAttributes); for (var materialIdx = 0; materialIdx < model->MaterialsSpan.Length; materialIdx++) @@ -482,26 +490,33 @@ private void DrawShapeAttributeTable(Model.ShapeAttributeGroup shapeAttributeGro } } - private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* material, int materialIdx) + private void DrawMaterial( + Pointer cPtr, Pointer mPtr, Pointer mtPtr, int materialIdx) { - if (cBase == null) + if (cPtr == null || cPtr.Value == null) { return; } - if (model == null) + + if (mPtr == null || mPtr.Value == null || mPtr.Value->ModelResourceHandle == null) { return; } - if (material == null || material->MaterialResourceHandle == null) + if (mtPtr == null || mtPtr.Value == null || mtPtr.Value->MaterialResourceHandle == null) { return; } + + var cBase = cPtr.Value; + var model = mPtr.Value; + var material = mtPtr.Value; + using var materialId = ImRaii.PushId($"{(nint)material}"); var materialFileName = material->MaterialResourceHandle->FileName.ToString(); - var materialName = model->ModelResourceHandle->GetMaterialFileName((uint)materialIdx); + var materialName = ((ModelResourceHandle*)model->ModelResourceHandle)->GetMaterialFileName((uint)materialIdx); // in same row as model export button, draw button for export material ImGui.TableNextRow(); @@ -521,25 +536,26 @@ private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* ma { var defaultFileName = Path.GetFileName(materialName); fileDialog.SaveFileDialog("Save Material", "Material File{.mtrl}", defaultFileName, ".mtrl", - (result, path) => - { - if (!result) return; - var data = pack.GetFileOrReadFromDisk(materialFileName); - if (data == null) - { - log.LogError("Failed to get material data from pack or disk for {MaterialFileName}", - materialFileName); - return; - } - - File.WriteAllBytes(path, data); - }); + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(materialFileName); + if (data == null) + { + log.LogError( + "Failed to get material data from pack or disk for {MaterialFileName}", + materialFileName); + return; + } + + File.WriteAllBytes(path, data); + }); } - + if (ImGui.MenuItem("Export raw textures as pngs")) { var textureBuffer = new Dictionary(); - for (int i = 0; i < material->TexturesSpan.Length; i++) + for (var i = 0; i < material->TexturesSpan.Length; i++) { var textureEntry = material->TexturesSpan[i]; if (textureEntry.Texture == null) @@ -558,21 +574,21 @@ private void DrawMaterial(CSCharacterBase* cBase, CSModel* model, CSMaterial* ma var materialNameNoExt = Path.GetFileNameWithoutExtension(materialFileName); fileDialog.SaveFolderDialog("Save Textures", materialNameNoExt, - (result, path) => - { - if (!result) return; - Directory.CreateDirectory(path); - - foreach (var (name, texture) in textureBuffer) - { - var fileName = Path.GetFileNameWithoutExtension(name); - var filePath = Path.Combine(path, $"{fileName}.png"); - using var str = new SKDynamicMemoryWStream(); - texture.Encode(str, SKEncodedImageFormat.Png, 100); - var imageData = str.DetachAsData().AsSpan(); - File.WriteAllBytes(filePath, imageData.ToArray()); - } - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + Directory.CreateDirectory(path); + + foreach (var (name, texture) in textureBuffer) + { + var fileName = Path.GetFileNameWithoutExtension(name); + var filePath = Path.Combine(path, $"{fileName}.png"); + using var str = new SKDynamicMemoryWStream(); + texture.Encode(str, SKEncodedImageFormat.Png, 100); + var imageData = str.DetachAsData().AsSpan(); + File.WriteAllBytes(filePath, imageData.ToArray()); + } + }, Plugin.TempDirectory); } @@ -640,33 +656,34 @@ private void DrawTexture(CSMaterial* material, CSMaterial.TextureEntry textureEn var textureData = gpuTex.Resource.ToBitmap(); fileDialog.SaveFileDialog("Save Texture", "PNG Image{.png}", defaultFileName, ".png", - (result, path) => - { - if (!result) return; - using var str = new SKDynamicMemoryWStream(); - textureData.Encode(str, SKEncodedImageFormat.Png, 100); - var imageData = str.DetachAsData().AsSpan(); - File.WriteAllBytes(path, imageData.ToArray()); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + using var str = new SKDynamicMemoryWStream(); + textureData.Encode(str, SKEncodedImageFormat.Png, 100); + var imageData = str.DetachAsData().AsSpan(); + File.WriteAllBytes(path, imageData.ToArray()); + }, Plugin.TempDirectory); } if (ImGui.MenuItem("Export as tex")) { var defaultFileName = Path.GetFileName(textureFileName); fileDialog.SaveFileDialog("Save Texture", "TEX File{.tex}", defaultFileName, ".tex", - (result, path) => - { - if (!result) return; - var data = pack.GetFileOrReadFromDisk(textureFileName); - if (data == null) - { - log.LogError("Failed to get texture data from pack or disk for {TextureFileName}", - textureFileName); - return; - } - - File.WriteAllBytes(path, data); - }, Plugin.TempDirectory); + (result, path) => + { + if (!result) return; + var data = pack.GetFileOrReadFromDisk(textureFileName); + if (data == null) + { + log.LogError( + "Failed to get texture data from pack or disk for {TextureFileName}", + textureFileName); + return; + } + + File.WriteAllBytes(path, data); + }, Plugin.TempDirectory); } ImGui.EndPopup(); @@ -702,10 +719,10 @@ private void DrawTexture(CSMaterial* material, CSMaterial.TextureEntry textureEn ImGui.Image(wrap.ImGuiHandle, new Vector2(displayWidth, displayHeight)); } } - - private Dictionary, (CustomizeData, CustomizeParameter)> humanCustomizeData = new(); - private bool cacheHumanCustomizeData; - private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, out CustomizeParameter customizeParams, out GenderRace genderRace) + + private void DrawHumanCharacter( + CSHuman* cBase, out CustomizeData customizeData, out CustomizeParameter customizeParams, + out GenderRace genderRace) { if (cacheHumanCustomizeData && humanCustomizeData.TryGetValue(cBase, out var data)) { @@ -715,7 +732,7 @@ private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, } else { - var customizeCBuf = cBase->CustomizeParameterCBuffer->TryGetBuffer()[0]; + var customizeCBuf = cBase->CustomizeParameterCBuffer->TryGetBuffer()[0]; customizeParams = new CustomizeParameter { SkinColor = customizeCBuf.SkinColor, @@ -753,7 +770,7 @@ private void DrawHumanCharacter(CSHuman* cBase, out CustomizeData customizeData, ImGui.TableSetupColumn("Params", ImGuiTableColumnFlags.WidthFixed, width * 0.75f); ImGui.TableSetupColumn("Data"); ImGui.TableHeadersRow(); - + ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); UiUtil.DrawCustomizeParams(ref customizeParams); diff --git a/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs b/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs index 800466b..7aa6688 100644 --- a/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs +++ b/Meddle/Meddle.Plugin/UI/MaterialParameterTab.cs @@ -19,19 +19,19 @@ public class MaterialParameterTab : ITab { private readonly IClientState clientState; private readonly Configuration config; + private readonly Dictionary> materialCache = new(); + private readonly Dictionary mtrlCache = new(); + private readonly Dictionary mtrlConstantCache = new(); private readonly IObjectTable objectTable; private readonly SqPack pack; + + private readonly Dictionary shpkCache = new(); private Vector4[]? CustomizeParameters; private Pointer lastHuman; - private readonly Dictionary> materialCache = new(); - private readonly Dictionary mtrlCache = new(); - private readonly Dictionary mtrlConstantCache = new(); // only show values that are different from the shader default private bool onlyShowChanged; - private readonly Dictionary shpkCache = new(); - public MaterialParameterTab(IObjectTable objectTable, IClientState clientState, SqPack pack, Configuration config) { this.objectTable = objectTable; @@ -55,8 +55,9 @@ public void Dispose() public void Draw() { - ImGui.TextColored(new Vector4(1, 0, 0, 1), "Warning: This tab is for advanced users only. Modifying material parameters may cause crashes or other issues."); - + ImGui.TextColored(new Vector4(1, 0, 0, 1), + "Warning: This tab is for advanced users only. Modifying material parameters may cause crashes or other issues."); + ICharacter[] objects; if (clientState.LocalPlayer != null) { diff --git a/Meddle/Meddle.Plugin/UI/OptionsTab.cs b/Meddle/Meddle.Plugin/UI/OptionsTab.cs index d1a82ae..1bf29fa 100644 --- a/Meddle/Meddle.Plugin/UI/OptionsTab.cs +++ b/Meddle/Meddle.Plugin/UI/OptionsTab.cs @@ -33,18 +33,18 @@ public void Draw() config.ShowDebug = debug; config.Save(); } - + var test = config.ShowTesting; if (ImGui.Checkbox("Show Testing Menus", ref test)) { config.ShowTesting = test; config.Save(); } - + var minimumNotificationLogLevel = config.MinimumNotificationLogLevel; if (ImGui.BeginCombo("Minimum Notification Log Level", minimumNotificationLogLevel.ToString())) { - foreach (LogLevel level in (LogLevel[])Enum.GetValues(typeof(LogLevel))) + foreach (var level in (LogLevel[])Enum.GetValues(typeof(LogLevel))) { if (ImGui.Selectable(level.ToString(), level == minimumNotificationLogLevel)) { @@ -52,6 +52,7 @@ public void Draw() config.Save(); } } + ImGui.EndCombo(); } @@ -61,35 +62,35 @@ public void Draw() config.OpenOnLoad = openOnLoad; config.Save(); } - + var disableUserUiHide = config.DisableUserUiHide; if (ImGui.Checkbox("Disable User UI Hide", ref disableUserUiHide)) { config.DisableUserUiHide = disableUserUiHide; config.Save(); } - + var disableAutomaticUiHide = config.DisableAutomaticUiHide; if (ImGui.Checkbox("Disable Automatic UI Hide", ref disableAutomaticUiHide)) { config.DisableAutomaticUiHide = disableAutomaticUiHide; config.Save(); } - + var disableCutsceneUiHide = config.DisableCutsceneUiHide; if (ImGui.Checkbox("Disable Cutscene UI Hide", ref disableCutsceneUiHide)) { config.DisableCutsceneUiHide = disableCutsceneUiHide; config.Save(); } - + var disableGposeUiHide = config.DisableGposeUiHide; if (ImGui.Checkbox("Disable Gpose UI Hide", ref disableGposeUiHide)) { config.DisableGposeUiHide = disableGposeUiHide; config.Save(); } - + var playerNameOverride = config.PlayerNameOverride; if (ImGui.InputText("Player Name Override", ref playerNameOverride, 64)) { diff --git a/Meddle/Meddle.Plugin/UI/WorldTab.cs b/Meddle/Meddle.Plugin/UI/WorldTab.cs index 7d4a37e..8818eea 100644 --- a/Meddle/Meddle.Plugin/UI/WorldTab.cs +++ b/Meddle/Meddle.Plugin/UI/WorldTab.cs @@ -1,13 +1,11 @@ using System.Numerics; using System.Runtime.InteropServices; -using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using ImGuiNET; using Meddle.Plugin.Models; using Meddle.Plugin.Services; -using Meddle.Plugin.Utils; using Microsoft.Extensions.Logging; namespace Meddle.Plugin.UI; @@ -20,15 +18,13 @@ public class WorldTab : ITab private readonly ILogger log; private readonly List objects = new(); - private readonly PluginState pluginState; private readonly List selectedObjects = new(); private Task exportTask = Task.CompletedTask; public WorldTab( - PluginState pluginState, IClientState clientState, ExportService exportService, ILogger log, + IClientState clientState, ExportService exportService, ILogger log, Configuration config) { - this.pluginState = pluginState; this.clientState = clientState; this.exportService = exportService; this.log = log; @@ -48,10 +44,9 @@ public void Dispose() public void Draw() { ImGui.Text("This is a testing menu, functionality may not work as expected."); - - if (!pluginState.InteropResolved) return; + var position = clientState.LocalPlayer?.Position ?? Vector3.Zero; - + if (ImGui.Button("Parse world objects")) { ParseWorld(); @@ -153,7 +148,7 @@ public unsafe void ParseWorld() log.LogWarning("World is null, unable to parse objects"); return; } - + foreach (var childObject in world->ChildObjects) { if (childObject == null) continue; @@ -176,11 +171,10 @@ public unsafe void ParseWorld() } } } - + private record ObjectData(ObjectType Type, string Path, Vector3 Position, Quaternion Rotation, Vector3 Scale); } - [StructLayout(LayoutKind.Explicit, Size = 0xD0)] public struct BgObject { diff --git a/Meddle/Meddle.Plugin/Utils/DXHelper.cs b/Meddle/Meddle.Plugin/Utils/DXHelper.cs index f8ef583..52307ab 100644 --- a/Meddle/Meddle.Plugin/Utils/DXHelper.cs +++ b/Meddle/Meddle.Plugin/Utils/DXHelper.cs @@ -36,7 +36,7 @@ public DXHelper(ILogger log) return ret; } - + /*public byte[] ExportVertexBuffer(VertexBuffer* buffer) { if (buffer->DxPtr1 == nint.Zero) @@ -75,14 +75,18 @@ private ID3D11Buffer CloneBuffer(ID3D11Buffer r) { var blockHeight = Math.Max(1, (desc.Height + 3) / 4); if (map.RowPitch * blockHeight != map.DepthPitch) + { throw new InvalidDataException( $"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; Block Height = {blockHeight}; DepthPitch = {map.DepthPitch}"); + } } else { if (map.RowPitch * desc.Height != map.DepthPitch) + { throw new InvalidDataException( $"Invalid/unknown texture size for {desc.Format}: RowPitch = {map.RowPitch}; Height = {desc.Height}; DepthPitch = {map.DepthPitch}"); + } } var buf = new byte[map.DepthPitch]; diff --git a/Meddle/Meddle.Plugin/Utils/EventLogger.cs b/Meddle/Meddle.Plugin/Utils/EventLogger.cs index 67b61dd..b97c38f 100644 --- a/Meddle/Meddle.Plugin/Utils/EventLogger.cs +++ b/Meddle/Meddle.Plugin/Utils/EventLogger.cs @@ -4,22 +4,31 @@ namespace Meddle.Plugin.Utils; public class EventLogger : ILogger { - public ILogger Logger { get; } - public event Action? OnLogEvent; - public EventLogger(ILogger logger) { Logger = logger; } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + public ILogger Logger { get; } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) { var message = formatter(state, exception); Logger.Log(logLevel, eventId, state, exception, formatter); OnLogEvent?.Invoke(logLevel, message); } - - public bool IsEnabled(LogLevel logLevel) => Logger.IsEnabled(logLevel); - - public IDisposable? BeginScope(TState state) where TState : notnull => Logger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) + { + return Logger.IsEnabled(logLevel); + } + + public IDisposable? BeginScope(TState state) where TState : notnull + { + return Logger.BeginScope(state); + } + + public event Action? OnLogEvent; } diff --git a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs index c2e4368..4b7aca5 100644 --- a/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ObjectUtil.cs @@ -51,7 +51,8 @@ public static Vector3 GetDistanceToLocalPlayer(this IClientState clientState, IG return new Vector3(obj.YalmDistanceX, 0, obj.YalmDistanceZ); } - public static unsafe string GetCharacterDisplayText(this IClientState clientState, IGameObject obj, string overrideName = "") + public static unsafe string GetCharacterDisplayText( + this IClientState clientState, IGameObject obj, string overrideName = "") { var drawObject = ((GameObject*)obj.Address)->DrawObject; if (drawObject == null) @@ -63,7 +64,7 @@ public static unsafe string GetCharacterDisplayText(this IClientState clientStat var modelType = ((CharacterBase*)drawObject)->GetModelType(); var name = obj.Name.TextValue; - if (obj.ObjectKind == ObjectKind.Player && !string.IsNullOrWhiteSpace(overrideName)) + if (obj.ObjectKind == ObjectKind.Player && !string.IsNullOrWhiteSpace(overrideName)) name = overrideName; return $"[{obj.Address:X8}:{obj.GameObjectId:X}][{obj.ObjectKind}][{modelType}] - " + diff --git a/Meddle/Meddle.Plugin/Utils/PoseUtil.cs b/Meddle/Meddle.Plugin/Utils/PoseUtil.cs index 9779ce8..65980da 100644 --- a/Meddle/Meddle.Plugin/Utils/PoseUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/PoseUtil.cs @@ -7,12 +7,12 @@ namespace Meddle.Plugin.Utils; public static class PoseUtil { public static ISigScanner? SigScanner { get; set; } = null!; - + public static unsafe hkQsTransformf* AccessBoneLocalSpace(hkaPose* pose, int boneIdx) { if (SigScanner == null) throw new Exception("SigScanner not set"); - + if (SigScanner.TryScanText("4C 8B DC 53 55 56 57 41 54 41 56 48 81 EC", out var accessBoneLocalSpacePtr)) { var accessBoneLocalSpace = (delegate* unmanaged)accessBoneLocalSpacePtr; diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 43959e0..18862d1 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -2,14 +2,16 @@ using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Interop; using ImGuiNET; -using Meddle.Utils.Export; +using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Skeletons; using Meddle.Utils.Files; using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Skeletons; using SharpGLTF.Transforms; -using Attach = Meddle.Plugin.Skeleton.Attach; using CustomizeData = Meddle.Utils.Export.CustomizeData; +using CustomizeParameter = Meddle.Utils.Export.CustomizeParameter; namespace Meddle.Plugin.Utils; @@ -27,7 +29,7 @@ public static void Text(string text, string? copyValue) } } } - + public static void DrawCustomizeParams(ref CustomizeParameter customize) { ImGui.ColorEdit3("Skin Color", ref customize.SkinColor); @@ -159,32 +161,52 @@ private static void DrawRow(int i, ref ColorTableRow row, ColorDyeTable? dyeTabl ImGui.TableSetColumnIndex(8); ImGui.Text($"{row.TileIndex}"); } - - public static unsafe void DrawCharacterAttaches(Character* charPtr) + + public static unsafe void DrawCharacterAttaches(Pointer characterPointer) { - var cBase = (CharacterBase*)charPtr->GameObject.DrawObject; + if (characterPointer == null || characterPointer.Value == null) + { + ImGui.Text("Character is null"); + return; + } + + var drawObject = characterPointer.Value->GameObject.GetDrawObject(); + if (drawObject == null) + { + ImGui.Text("DrawObject is null"); + return; + } + + var objectType = drawObject->GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + ImGui.Text($"Character is not a CharacterBase ({objectType})"); + return; + } + + var cBase = (CharacterBase*)characterPointer.Value->GameObject.DrawObject; DrawCharacterBase(cBase, "Main"); - DrawOrnamentContainer(charPtr->OrnamentData); - DrawCompanionContainer(charPtr->CompanionData); - DrawMountContainer(charPtr->Mount); - DrawDrawDataContainer(charPtr->DrawData); + DrawOrnamentContainer(characterPointer.Value->OrnamentData); + DrawCompanionContainer(characterPointer.Value->CompanionData); + DrawMountContainer(characterPointer.Value->Mount); + DrawDrawDataContainer(characterPointer.Value->DrawData); } - + private static unsafe void DrawDrawDataContainer(DrawDataContainer drawDataContainer) { if (drawDataContainer.OwnerObject == null) { - ImGui.Text($"[DrawDataContainer] Owner is null"); + ImGui.Text("[DrawDataContainer] Owner is null"); return; } - + var ownerObject = drawDataContainer.OwnerObject; if (ownerObject == null) { - ImGui.Text($"[DrawDataContainer] Owner is null"); + ImGui.Text("[DrawDataContainer] Owner is null"); return; } - + var weaponData = drawDataContainer.WeaponData; foreach (var weapon in weaponData) { @@ -193,7 +215,7 @@ private static unsafe void DrawDrawDataContainer(DrawDataContainer drawDataConta { continue; } - + var objectType = weaponDrawObject->GetObjectType(); if (objectType != ObjectType.CharacterBase) { @@ -204,7 +226,7 @@ private static unsafe void DrawDrawDataContainer(DrawDataContainer drawDataConta DrawCharacterBase((CharacterBase*)weaponDrawObject, "Weapon"); } } - + private static unsafe void DrawCompanionContainer(CompanionContainer companionContainer) { var owner = companionContainer.OwnerObject; @@ -213,6 +235,7 @@ private static unsafe void DrawCompanionContainer(CompanionContainer companionCo ImGui.Text($"[Companion:{companionContainer.CompanionId}] Owner is null"); return; } + var companion = companionContainer.CompanionObject; if (companion == null) { @@ -228,7 +251,7 @@ private static unsafe void DrawCompanionContainer(CompanionContainer companionCo DrawCharacterBase((CharacterBase*)companion->DrawObject, "Companion"); } - + private static unsafe void DrawMountContainer(MountContainer mountContainer) { var owner = mountContainer.OwnerObject; @@ -237,19 +260,20 @@ private static unsafe void DrawMountContainer(MountContainer mountContainer) ImGui.Text($"[Mount:{mountContainer.MountId}] Owner is null"); return; } + var mount = mountContainer.MountObject; if (mount == null) { return; } - + var drawObject = mount->DrawObject; if (drawObject == null) { ImGui.Text($"[Mount:{mountContainer.MountId}] DrawObject is null"); return; } - + var objectType = drawObject->GetObjectType(); if (objectType != ObjectType.CharacterBase) { @@ -268,6 +292,7 @@ private static unsafe void DrawOrnamentContainer(OrnamentContainer ornamentConta ImGui.Text($"[Ornament:{ornamentContainer.OrnamentId}] Owner is null"); return; } + var ornament = ornamentContainer.OrnamentObject; if (ornament == null) { @@ -277,18 +302,20 @@ private static unsafe void DrawOrnamentContainer(OrnamentContainer ornamentConta DrawCharacterBase((CharacterBase*)ornament->DrawObject, "Ornament"); } - private static unsafe void DrawCharacterBase(CharacterBase* character, string name) + private static unsafe void DrawCharacterBase(Pointer characterPointer, string name) { - if (character == null) + if (characterPointer == null || characterPointer.Value == null) return; + var character = characterPointer.Value; var skeleton = character->Skeleton; if (skeleton == null) return; - Attach attachPoint; + var attach = characterPointer.GetAttach(); + ParsedAttach attachPoint; try { - attachPoint = new Attach(character->Attach); + attachPoint = new ParsedAttach(attach); } catch (Exception e) { @@ -298,16 +325,17 @@ private static unsafe void DrawCharacterBase(CharacterBase* character, string na var modelType = character->GetModelType(); var attachHeader = $"[{modelType}]{name} Attach Pose ({attachPoint.ExecuteType},{attachPoint.AttachmentCount})"; - if (character->Attach.ExecuteType >= 3) + if (attachPoint.ExecuteType >= 3) { var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[(int)attachPoint.BoneIdx]; attachHeader += $" at {boneName}"; } - else if (character->Attach.ExecuteType == 3) + else if (attachPoint.ExecuteType == 3) { var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; - if (attachedPartialSkeleton.HkSkeleton != null && attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) + if (attachedPartialSkeleton.HkSkeleton != null && + attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) { var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[(int)attachPoint.BoneIdx]; attachHeader += $" at {boneName}"; @@ -323,13 +351,15 @@ private static unsafe void DrawCharacterBase(CharacterBase* character, string na using var attachId = ImRaii.PushId($"{(nint)character:X8}_Attach"); DrawAttachInfo(character, attachPoint); using var attachIndent = ImRaii.PushIndent(); - if (attachPoint.TargetSkeleton != null && ImGui.CollapsingHeader($"Target Skeleton {(nint)character->Attach.TargetSkeleton:X8}")) + if (attachPoint.TargetSkeleton != null && + ImGui.CollapsingHeader($"Target Skeleton {(nint)attach.TargetSkeleton:X8}")) { using var id = ImRaii.PushId($"{(nint)character:X8}_Target"); DrawSkeleton(attachPoint.TargetSkeleton); } - if (attachPoint.OwnerSkeleton != null && ImGui.CollapsingHeader($"Owner Skeleton {(nint)character->Attach.OwnerSkeleton:X8}")) + if (attachPoint.OwnerSkeleton != null && + ImGui.CollapsingHeader($"Owner Skeleton {(nint)attach.OwnerSkeleton:X8}")) { using var id = ImRaii.PushId($"{(nint)character:X8}_Owner"); DrawSkeleton(attachPoint.OwnerSkeleton); @@ -337,8 +367,11 @@ private static unsafe void DrawCharacterBase(CharacterBase* character, string na } } - private static unsafe void DrawAttachInfo(CharacterBase* character, Attach attachPoint) + private static unsafe void DrawAttachInfo(Pointer characterPointer, ParsedAttach attachPoint) { + if (characterPointer == null || characterPointer.Value == null) + return; + var character = characterPointer.Value; var position = character->Position; var rotation = character->Rotation; var scale = character->Scale; @@ -356,24 +389,24 @@ private static unsafe void DrawAttachInfo(CharacterBase* character, Attach attac } else { - var characterSkeleton = new Skeleton.Skeleton(character->Skeleton); + var characterSkeleton = characterPointer.GetParsedSkeleton(); DrawSkeleton(characterSkeleton); } - //DrawModels(character); } - - public static void DrawSkeleton(Skeleton.Skeleton skeleton) + + public static void DrawSkeleton(ParsedSkeleton skeleton) { using var skeletonIndent = ImRaii.PushIndent(); ImGui.Text($"Partial Skeletons: {skeleton.PartialSkeletons.Count}"); ImGui.Text($"Transform: {skeleton.Transform}"); - for (int i = 0; i < skeleton.PartialSkeletons.Count; i++) + for (var i = 0; i < skeleton.PartialSkeletons.Count; i++) { var partial = skeleton.PartialSkeletons[i]; if (partial.HandlePath == null) { continue; } + using var partialIndent = ImRaii.PushIndent(); using var partialId = ImRaii.PushId(i); if (ImGui.CollapsingHeader($"[{i}]Partial: {partial.HandlePath}")) @@ -381,7 +414,7 @@ public static void DrawSkeleton(Skeleton.Skeleton skeleton) ImGui.Text($"Connected Bone Index: {partial.ConnectedBoneIndex}"); var poseData = partial.Poses.FirstOrDefault(); if (poseData == null) continue; - for (int j = 0; j < poseData.Pose.Count; j++) + for (var j = 0; j < poseData.Pose.Count; j++) { var transform = poseData.Pose[j]; var boneName = partial.HkSkeleton?.BoneNames[j] ?? "Bone"; @@ -390,34 +423,4 @@ public static void DrawSkeleton(Skeleton.Skeleton skeleton) } } } - - private static unsafe void DrawSkeleton(FFXIVClientStructs.FFXIV.Client.Graphics.Render.Skeleton* sk, string context) - { - using var skeletonIndent = ImRaii.PushIndent(); - using var skeletonId = ImRaii.PushId($"{(nint)sk:X8}"); - ImGui.Text($"Partial Skeletons: {sk->PartialSkeletonCount}"); - ImGui.Text($"Transform: {new Transform(sk->Transform)}"); - for (var i = 0; i < sk->PartialSkeletonCount; ++i) - { - using var partialId = ImRaii.PushId($"PartialSkeleton_{i}"); - var handle = sk->PartialSkeletons[i].SkeletonResourceHandle; - if (handle == null) - { - continue; - } - - if (ImGui.CollapsingHeader($"Partial {i}: {handle->FileName.ToString()}")) - { - var p = sk->PartialSkeletons[i].GetHavokPose(0); - if (p != null && p->Skeleton != null) - { - for (var j = 0; j < p->Skeleton->Bones.Length; ++j) - { - var boneName = p->Skeleton->Bones[j].Name.String ?? $"Bone {j}"; - ImGui.TextUnformatted($"[{i}, {j}] => {boneName}"); - } - } - } - } - } } diff --git a/Meddle/Meddle.Plugin/packages.lock.json b/Meddle/Meddle.Plugin/packages.lock.json index 0dce9a7..845c1d2 100644 --- a/Meddle/Meddle.Plugin/packages.lock.json +++ b/Meddle/Meddle.Plugin/packages.lock.json @@ -127,11 +127,6 @@ "Grpc.Core.Api": "2.52.0" } }, - "JetBrains.Annotations": { - "type": "Transitive", - "resolved": "2021.2.0", - "contentHash": "kKSyoVfndMriKHLfYGmr0uzQuI4jcc3TKGyww7buJFCYeHb/X0kodYBPL7n9454q7v6ASiRmDgpPGaDGerg/Hg==" - }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "8.0.0", @@ -514,20 +509,9 @@ "resolved": "1.7.8", "contentHash": "hpVlfej54yC6Qaq9AYXWgTgF8SDz2dtKbDNZGO8msjn3NjdkUBnFcIzYfOWyvwvpr3aqZPKh4WSSnJ78GxyIWg==" }, - "ffxivclientstructs": { - "type": "Project", - "dependencies": { - "InteropGenerator.Runtime": "[1.0.0, )", - "JetBrains.Annotations": "[2021.2.0, )" - } - }, - "interopgenerator.runtime": { - "type": "Project" - }, "meddle.utils": { "type": "Project", "dependencies": { - "FFXIVClientStructs": "[1.0.0, )", "SharpGLTF.Core": "[1.0.0-alpha0031, )", "SharpGLTF.Toolkit": "[1.0.0-alpha0031, )", "SkiaSharp": "[2.88.8, )", diff --git a/Meddle/Meddle.UI/Windows/Views/PbdView.cs b/Meddle/Meddle.UI/Windows/Views/PbdView.cs index 64f01a8..2535464 100644 --- a/Meddle/Meddle.UI/Windows/Views/PbdView.cs +++ b/Meddle/Meddle.UI/Windows/Views/PbdView.cs @@ -66,13 +66,13 @@ public void Draw() for (var i = 0; i < deformer.Value.BoneCount; i++) { ImGui.Text($"Bone Name: {deformer.Value.BoneNames[i]}"); - var deformMatrix = deformer.Value.DeformMatrices[i]; + /*var deformMatrix = deformer.Value.DeformMatrices[i]; if (deformMatrix != null) { ImGui.Text($"Translation: {deformMatrix.Value.Translation}"); ImGui.Text($"Rotation: {deformMatrix.Value.Rotation}"); ImGui.Text($"Scale: {deformMatrix.Value.Scale}"); - } + }*/ } } } diff --git a/Meddle/Meddle.Utils/Files/PbdFile.cs b/Meddle/Meddle.Utils/Files/PbdFile.cs index e5cf8ae..97c86e8 100644 --- a/Meddle/Meddle.Utils/Files/PbdFile.cs +++ b/Meddle/Meddle.Utils/Files/PbdFile.cs @@ -1,8 +1,6 @@ using System.Numerics; using System.Runtime.InteropServices; using FFXIVClientStructs.Havok.Common.Base.Math.QsTransform; -using FFXIVClientStructs.Havok.Common.Base.Math.Quaternion; -using FFXIVClientStructs.Havok.Common.Base.Math.Vector; namespace Meddle.Utils.Files; diff --git a/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableRow.cs b/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableRow.cs index d13cde9..7769112 100644 --- a/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableRow.cs +++ b/Meddle/Meddle.Utils/Files/Structs/Material/ColorTableRow.cs @@ -1,6 +1,5 @@ -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Common.Math; - +using System.Numerics; +using System.Runtime.InteropServices; namespace Meddle.Utils.Files.Structs.Material; // https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MaterialStructs/ColorTable.cs diff --git a/Meddle/Meddle.Utils/Meddle.Utils.csproj b/Meddle/Meddle.Utils/Meddle.Utils.csproj index 6eb620e..b84d8af 100644 --- a/Meddle/Meddle.Utils/Meddle.Utils.csproj +++ b/Meddle/Meddle.Utils/Meddle.Utils.csproj @@ -20,14 +20,21 @@ + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + + + + $(DALAMUD_HOME)/ + + - - - - + + $(DalamudLibPath)FFXIVClientStructs.dll + diff --git a/repo.json b/repo.json index bbcc89b..5e880b8 100644 --- a/repo.json +++ b/repo.json @@ -8,7 +8,7 @@ "TestingAssemblyVersion": "0.1.9", "RepoUrl": "https://github.com/PassiveModding/Meddle", "IconUrl": "https://github.com/PassiveModding/Meddle/raw/main/icon.png", - "ApplicableVersion": "2024.08.02.0000.0000", + "ApplicableVersion": "any", "DalamudApiLevel": 10, "Tags": [ "Xande", From 47db802f50f692aeb009d1200548e61335ac9f94 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:29:38 +1000 Subject: [PATCH 17/19] Animation tab ui cleanup --- Meddle/Meddle.Plugin/UI/AnimationTab.cs | 37 +++++++++--------- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 52 +++++++++++++++---------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/Meddle/Meddle.Plugin/UI/AnimationTab.cs b/Meddle/Meddle.Plugin/UI/AnimationTab.cs index a71ec40..2829ba1 100644 --- a/Meddle/Meddle.Plugin/UI/AnimationTab.cs +++ b/Meddle/Meddle.Plugin/UI/AnimationTab.cs @@ -91,20 +91,30 @@ public void Draw() } if (selectedCharacter == null) return; - if (ImGui.Checkbox("Capture Animation", ref captureAnimation)) + + switch (captureAnimation) { - if (captureAnimation) - { - logger.LogInformation("Capturing animation"); - } - else - { + case true when ImGui.Button("Stop Capture"): + captureAnimation = false; logger.LogInformation("Stopped capturing animation"); - } + break; + case false when ImGui.Button("Start Capture"): + captureAnimation = true; + logger.LogInformation("Capturing animation"); + break; + } + + ImGui.SameLine(); + if (ImGui.Button("Clear")) + { + frames.Clear(); } + ImGui.SameLine(); var frameCount = frames.Count; ImGui.Text($"Frames: {frameCount}"); + + if (ImGui.Button("Export")) { exportService.ExportAnimation(frames, includePositionalData); @@ -112,18 +122,9 @@ public void Draw() ImGui.SameLine(); ImGui.Checkbox("Include Positional Data", ref includePositionalData); - - if (ImGui.Button("Clear")) - { - frames.Clear(); - } - ImGui.Separator(); - if (ImGui.CollapsingHeader("Skeleton")) - { - DrawSelectedCharacter(); - } + DrawSelectedCharacter(); } public void Dispose() diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 18862d1..c7410fc 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -184,6 +184,12 @@ public static unsafe void DrawCharacterAttaches(Pointer characterPoin return; } + using var table = ImRaii.Table("AttachTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Model Type", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Skeleton"); + ImGui.TableHeadersRow(); + var cBase = (CharacterBase*)characterPointer.Value->GameObject.DrawObject; DrawCharacterBase(cBase, "Main"); DrawOrnamentContainer(characterPointer.Value->OrnamentData); @@ -324,33 +330,37 @@ private static unsafe void DrawCharacterBase(Pointer characterPoi } var modelType = character->GetModelType(); - var attachHeader = $"[{modelType}]{name} Attach Pose ({attachPoint.ExecuteType},{attachPoint.AttachmentCount})"; - if (attachPoint.ExecuteType >= 3) + var attachType = attachPoint.ExecuteType switch + { + 0 => "Root", + 3 => "Owner Attach", + 4 => "Skeleton Attach", + _ => "Unknown" + }; + + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + ImGui.Text(name); + ImGui.TableSetColumnIndex(1); + ImGui.Text(modelType.ToString()); + ImGui.TableSetColumnIndex(2); + + string attachHeader; + if (attachPoint.ExecuteType != 0) { var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; var boneName = attachedPartialSkeleton.HkSkeleton!.BoneNames[(int)attachPoint.BoneIdx]; - attachHeader += $" at {boneName}"; + attachHeader = $"[{attachPoint.ExecuteType}]{attachType} at {boneName}"; } - else if (attachPoint.ExecuteType == 3) + else { - var attachedPartialSkeleton = attachPoint.OwnerSkeleton!.PartialSkeletons[attachPoint.PartialSkeletonIdx]; - if (attachedPartialSkeleton.HkSkeleton != null && - attachPoint.BoneIdx < attachedPartialSkeleton.HkSkeleton.BoneNames.Count) - { - var boneName = attachedPartialSkeleton.HkSkeleton.BoneNames[(int)attachPoint.BoneIdx]; - attachHeader += $" at {boneName}"; - } - else - { - attachHeader += $" at {attachPoint.BoneIdx} > {attachedPartialSkeleton.HandlePath}"; - } + attachHeader = $"[{attachPoint.ExecuteType}]{attachType}"; } - if (ImGui.CollapsingHeader(attachHeader)) { using var attachId = ImRaii.PushId($"{(nint)character:X8}_Attach"); DrawAttachInfo(character, attachPoint); - using var attachIndent = ImRaii.PushIndent(); if (attachPoint.TargetSkeleton != null && ImGui.CollapsingHeader($"Target Skeleton {(nint)attach.TargetSkeleton:X8}")) { @@ -385,20 +395,21 @@ private static unsafe void DrawAttachInfo(Pointer characterPointe ImGui.Text($"Root: {attachPoint.OffsetTransform?.ToString() ?? "None"}"); if (attachPoint.TargetSkeleton != null) { + ImGui.Text($"Skeleton Transform: {attachPoint.TargetSkeleton.Transform}"); + ImGui.Text($"Partial Skeletons: {attachPoint.TargetSkeleton.PartialSkeletons.Count}"); DrawSkeleton(attachPoint.TargetSkeleton); } else { var characterSkeleton = characterPointer.GetParsedSkeleton(); + ImGui.Text($"Skeleton Transform: {characterSkeleton.Transform}"); + ImGui.Text($"Partial Skeletons: {characterSkeleton.PartialSkeletons.Count}"); DrawSkeleton(characterSkeleton); } } public static void DrawSkeleton(ParsedSkeleton skeleton) { - using var skeletonIndent = ImRaii.PushIndent(); - ImGui.Text($"Partial Skeletons: {skeleton.PartialSkeletons.Count}"); - ImGui.Text($"Transform: {skeleton.Transform}"); for (var i = 0; i < skeleton.PartialSkeletons.Count; i++) { var partial = skeleton.PartialSkeletons[i]; @@ -407,7 +418,6 @@ public static void DrawSkeleton(ParsedSkeleton skeleton) continue; } - using var partialIndent = ImRaii.PushIndent(); using var partialId = ImRaii.PushId(i); if (ImGui.CollapsingHeader($"[{i}]Partial: {partial.HandlePath}")) { From 062b40f8809e40fe76008cad110138f25f04a295 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:21:05 +1000 Subject: [PATCH 18/19] Restructure unused UI + name changes for character tab --- .../Meddle.Plugin/Models/StructExtensions.cs | 24 +-- .../Meddle.Plugin/Services/ExportService.cs | 91 ++++----- Meddle/Meddle.Plugin/Services/ParseService.cs | 48 ++++- .../UI/{ => Archive}/CharacterTab.cs | 9 +- .../UI/{ => Archive}/WorldTab.cs | 56 +++--- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 177 +++++++++++++----- 6 files changed, 262 insertions(+), 143 deletions(-) rename Meddle/Meddle.Plugin/UI/{ => Archive}/CharacterTab.cs (99%) rename Meddle/Meddle.Plugin/UI/{ => Archive}/WorldTab.cs (79%) diff --git a/Meddle/Meddle.Plugin/Models/StructExtensions.cs b/Meddle/Meddle.Plugin/Models/StructExtensions.cs index 6cd897f..56a2306 100644 --- a/Meddle/Meddle.Plugin/Models/StructExtensions.cs +++ b/Meddle/Meddle.Plugin/Models/StructExtensions.cs @@ -49,18 +49,6 @@ public static uint GetBoneCount(this Pointer partialSkeleton) return (flags >> 5) & 0xFFFu; } - public static unsafe (uint EnabledAttributeIndexMask, uint EnabledShapeKeyIndexMask) GetModelMasks( - this Pointer model) - { - if (model == null) throw new ArgumentNullException(nameof(model)); - if (model.Value == null) throw new ArgumentNullException(nameof(model)); - - var modelBase = model.Value; - var enabledAttributeIndexMask = *(uint*)((nint)modelBase + ModelEnabledAttributeIndexMaskOffset); - var enabledShapeKeyIndexMask = *(uint*)((nint)modelBase + ModelEnabledShapeKeyIndexMaskOffset); - return (enabledAttributeIndexMask, enabledShapeKeyIndexMask); - } - public static unsafe ParsedSkeleton GetParsedSkeleton(this Pointer character) { if (character == null) throw new ArgumentNullException(nameof(character)); @@ -81,6 +69,18 @@ private static unsafe ParsedSkeleton GetParsedSkeleton(this Pointer sk return new ParsedSkeleton(skeleton.Value); } + public static unsafe (uint EnabledAttributeIndexMask, uint EnabledShapeKeyIndexMask) GetModelMasks( + this Pointer model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + if (model.Value == null) throw new ArgumentNullException(nameof(model)); + + var modelBase = model.Value; + var enabledAttributeIndexMask = *(uint*)((nint)modelBase + ModelEnabledAttributeIndexMaskOffset); + var enabledShapeKeyIndexMask = *(uint*)((nint)modelBase + ModelEnabledShapeKeyIndexMaskOffset); + return (enabledAttributeIndexMask, enabledShapeKeyIndexMask); + } + public static unsafe Meddle.Utils.Export.Model.ShapeAttributeGroup ParseModelShapeAttributes( Pointer modelPointer) { diff --git a/Meddle/Meddle.Plugin/Services/ExportService.cs b/Meddle/Meddle.Plugin/Services/ExportService.cs index c7a5b0e..420213f 100644 --- a/Meddle/Meddle.Plugin/Services/ExportService.cs +++ b/Meddle/Meddle.Plugin/Services/ExportService.cs @@ -94,42 +94,6 @@ public static void ExportTexture(SKBitmap bitmap, string path) Process.Start("explorer.exe", folder); } - public void ExportRawTextures(CharacterGroup characterGroup, CancellationToken token = default) - { - try - { - var folder = GetPathForOutput(); - using var activity = ActivitySource.StartActivity(); - activity?.SetTag("folder", folder); - - foreach (var mdlGroup in characterGroup.MdlGroups) - { - foreach (var mtrlGroup in mdlGroup.MtrlFiles) - { - foreach (var texGroup in mtrlGroup.TexFiles) - { - if (token.IsCancellationRequested) return; - var outputPath = - Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(texGroup.MtrlPath)}.png"); - var texture = new Texture(texGroup.Resource, texGroup.MtrlPath, null, null, null); - var str = new SKDynamicMemoryWStream(); - texture.ToTexture().Bitmap.Encode(str, SKEncodedImageFormat.Png, 100); - - var data = str.DetachAsData().AsSpan(); - File.WriteAllBytes(outputPath, data.ToArray()); - } - } - } - - Process.Start("explorer.exe", folder); - } - catch (Exception e) - { - logger.LogError(e, "Failed to export textures"); - throw; - } - } - public void ExportAnimation( List<(DateTime, AttachSet[])> frames, bool includePositionalData, CancellationToken token = default) { @@ -250,6 +214,7 @@ public void Export(CharacterGroup characterGroup, string? outputFolder = null, C var attachName = characterGroup.Skeleton.PartialSkeletons[attachedModelGroup.Attach.PartialSkeletonIdx] .HkSkeleton!.BoneNames[(int)attachedModelGroup.Attach.BoneIdx]; var attachBones = SkeletonUtils.GetBoneMap(attachedModelGroup.Skeleton, true, out var attachRoot); + logger.LogDebug("Adding attach {AttachName} to {ParentBone}", attachName, attachRoot?.BoneName); if (attachRoot == null) { throw new InvalidOperationException("Failed to get attach root"); @@ -383,11 +348,17 @@ private MaterialBuilder HandleMaterial(CharacterGroup characterGroup, Material m characterGroup.CustomizeData, (tileNormTex, tileOrbTex)), "iris.shpk" => MaterialUtility.BuildIris(material, name, catchlightTex, characterGroup.CustomizeParams, characterGroup.CustomizeData), - _ => MaterialUtility.BuildFallback(material, name) + _ => BuildAndLogFallbackMaterial(material, name) }; return builder; } + + private MaterialBuilder BuildAndLogFallbackMaterial(Material material, string name) + { + logger.LogWarning("[{Shpk}] Using fallback material for {Path}", material.ShaderPackageName, material.HandlePath); + return MaterialUtility.BuildFallback(material, name); + } private List<(Model model, ModelBuilder.MeshExport mesh)> HandleModel( CharacterGroup characterGroup, MdlFileGroup mdlGroup, ref List bones, BoneNodeBuilder? root, @@ -479,7 +450,7 @@ private static void ApplyMeshShapes(InstanceBuilder builder, Model model, IReadO builder.Content.UseMorphing().SetValue(shapes.Select(x => x.Item2 ? 1f : 0).ToArray()); } - public void ExportResource(Resource[] resources, Vector3 rootPosition) + /*public void ExportResource(Resource[] resources, Vector3 rootPosition) { try { @@ -582,14 +553,44 @@ public void ExportResource(Resource[] resources, Vector3 rootPosition) logger.LogError(e, "Failed to export resource"); throw; } - } - - private MaterialBuilder BuildAndLogFallbackMaterial(Material material, string name) + }*/ + + /*public void ExportRawTextures(CharacterGroup characterGroup, CancellationToken token = default) { - logger.LogWarning("Using fallback material for {Path}", material.HandlePath); - return MaterialUtility.BuildFallback(material, name); - } + try + { + var folder = GetPathForOutput(); + using var activity = ActivitySource.StartActivity(); + activity?.SetTag("folder", folder); + + foreach (var mdlGroup in characterGroup.MdlGroups) + { + foreach (var mtrlGroup in mdlGroup.MtrlFiles) + { + foreach (var texGroup in mtrlGroup.TexFiles) + { + if (token.IsCancellationRequested) return; + var outputPath = + Path.Combine(folder, $"{Path.GetFileNameWithoutExtension(texGroup.MtrlPath)}.png"); + var texture = new Texture(texGroup.Resource, texGroup.MtrlPath, null, null, null); + var str = new SKDynamicMemoryWStream(); + texture.ToTexture().Bitmap.Encode(str, SKEncodedImageFormat.Png, 100); + + var data = str.DetachAsData().AsSpan(); + File.WriteAllBytes(outputPath, data.ToArray()); + } + } + } + Process.Start("explorer.exe", folder); + } + catch (Exception e) + { + logger.LogError(e, "Failed to export textures"); + throw; + } + } + private void ExportTextureFromPath(string path) { var data = pack.GetFileOrReadFromDisk(path); @@ -597,5 +598,5 @@ private void ExportTextureFromPath(string path) var texFile = new TexFile(data); var texture = new Texture(Texture.GetResource(texFile), path, null, null, null); ExportTexture(texture.ToTexture().Bitmap, path); - } + }*/ } diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index b94c3f9..c619a43 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -77,6 +77,17 @@ public unsafe Dictionary ParseColorTableTextures(CharacterBase* return colorTableTextures; } + public unsafe AttachedModelGroup? ParseDrawObjectAsAttach(DrawObject* drawObject) + { + if (drawObject == null) return null; + if (drawObject->GetObjectType() != ObjectType.CharacterBase) return null; + var drawCharacterBase = (CharacterBase*)drawObject; + var attachGroup = ParseCharacterBase(drawCharacterBase); + var attach = StructExtensions.GetParsedAttach(drawCharacterBase); + return new AttachedModelGroup(attach, attachGroup.MdlGroups, attachGroup.Skeleton); + + } + // Only call from main thread or you will probably crash public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) { @@ -112,7 +123,29 @@ public unsafe ColorTableRow[] ParseColorTableTexture(Texture* colorTableTexture) $"Color table is not 4x16 or 8x32 ({colorTableTexture->Width}x{colorTableTexture->Height})"); } - public unsafe CharacterGroup HandleCharacterGroup( + /// + /// Parse a character base into a character group excluding attach data and customize data + /// + /// + /// + public unsafe CharacterGroup ParseCharacterBase(CharacterBase* characterBase) + { + var colorTableTextures = ParseColorTableTextures(characterBase); + var models = new List(); + foreach (var modelPtr in characterBase->ModelsSpan) + { + if (modelPtr == null) continue; + var model = modelPtr.Value; + if (model == null) continue; + var modelData = HandleModelPtr(characterBase, (int)model->SlotIndex, colorTableTextures); + if (modelData == null) continue; + models.Add(modelData); + } + var skeleton = StructExtensions.GetParsedSkeleton(characterBase); + return new CharacterGroup(new CustomizeParameter(), new CustomizeData(), GenderRace.Unknown, models.ToArray(), skeleton, []); + } + + /*public unsafe CharacterGroup HandleCharacterGroup( CharacterBase* characterBase, Dictionary colorTableTextures, Dictionary, Dictionary> attachDict, @@ -146,10 +179,9 @@ public unsafe CharacterGroup HandleCharacterGroup( mdlGroups.ToArray(), skeleton, attachGroups.ToArray()); - } + }*/ - public unsafe MdlFileGroup? HandleModelPtr( - CharacterBase* characterBase, int slotIdx, Dictionary colorTables) + public unsafe MdlFileGroup? HandleModelPtr(CharacterBase* characterBase, int slotIdx, Dictionary colorTables) { using var activity = ActivitySource.StartActivity(); var modelPtr = characterBase->ModelsSpan[slotIdx]; @@ -186,7 +218,7 @@ public unsafe CharacterGroup HandleCharacterGroup( } var mdlMtrlFileName = mtrlFileNames[j]; - var mtrlGroup = HandleMtrl(mdlMtrlFileName, material, slotIdx, j, colorTables); + var mtrlGroup = ParseMtrl(mdlMtrlFileName, material, slotIdx, j, colorTables); if (mtrlGroup != null) { mtrlGroups.Add(mtrlGroup); @@ -225,7 +257,7 @@ private ShpkFile HandleShpk(string shader) return shpkFile; } - private unsafe MtrlFileGroup? HandleMtrl( + private unsafe MtrlFileGroup? ParseMtrl( string mdlPath, Material* material, int modelIdx, int j, Dictionary colorTables) @@ -282,7 +314,7 @@ private ShpkFile HandleShpk(string shader) return new MtrlFileGroup(mdlPath, mtrlFileName, mtrlFile, shader, shpkFile, texGroups.ToArray()); } - public unsafe AttachedModelGroup HandleAttachGroup( + /*public unsafe AttachedModelGroup HandleAttachGroup( Pointer attachBase, Dictionary colorTables) { using var activity = ActivitySource.StartActivity(); @@ -300,5 +332,5 @@ public unsafe AttachedModelGroup HandleAttachGroup( var attachGroup = new AttachedModelGroup(attach, models.ToArray(), skeleton); return attachGroup; - } + }*/ } diff --git a/Meddle/Meddle.Plugin/UI/CharacterTab.cs b/Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs similarity index 99% rename from Meddle/Meddle.Plugin/UI/CharacterTab.cs rename to Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs index 046d987..9b9b482 100644 --- a/Meddle/Meddle.Plugin/UI/CharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/Archive/CharacterTab.cs @@ -1,4 +1,4 @@ -using System.Numerics; +/*using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Textures; @@ -65,9 +65,9 @@ public CharacterTab( private bool IsDisposed { get; set; } - public string Name => "Character"; - public int Order => 0; - public bool DisplayTab => true; + public string Name => "(Old)Character"; + public int Order => 999; + public bool DisplayTab => config.ShowTesting; public void Draw() { @@ -881,3 +881,4 @@ private enum Channel private record TextureImage(SKTexture Bitmap, IDalamudTextureWrap Wrap); } +*/ diff --git a/Meddle/Meddle.Plugin/UI/WorldTab.cs b/Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs similarity index 79% rename from Meddle/Meddle.Plugin/UI/WorldTab.cs rename to Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs index 8818eea..882c491 100644 --- a/Meddle/Meddle.Plugin/UI/WorldTab.cs +++ b/Meddle/Meddle.Plugin/UI/Archive/WorldTab.cs @@ -1,4 +1,4 @@ -using System.Numerics; +/*using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -68,18 +68,18 @@ public void Draw() } ImGui.EndChild(); - /*ImGui.BeginChild("TeraTable", new Vector2(0, availHeight / 4), true); - foreach (var obj in objects.Where(x => x.Path.EndsWith(".tera")).OrderBy(o => Vector3.Distance(o.Position, position))) - { - var distance = Vector3.Distance(obj.Position, position); - if (ImGui.Selectable($"[{obj.Type}][{distance:F1}y] {obj.Path}")) - { - selectedObjects.Add(obj); - // set clipboard - ImGui.SetClipboardText(obj.Path); - } - } - ImGui.EndChild();*/ + // ImGui.BeginChild("TeraTable", new Vector2(0, availHeight / 4), true); + // foreach (var obj in objects.Where(x => x.Path.EndsWith(".tera")).OrderBy(o => Vector3.Distance(o.Position, position))) + // { + // var distance = Vector3.Distance(obj.Position, position); + // if (ImGui.Selectable($"[{obj.Type}][{distance:F1}y] {obj.Path}")) + // { + // selectedObjects.Add(obj); + // // set clipboard + // ImGui.SetClipboardText(obj.Path); + // } + // } + // ImGui.EndChild(); if (selectedObjects.Count > 0) { @@ -106,21 +106,20 @@ public void Draw() } else if (obj.Path.EndsWith(".tera")) { - /* - var fileData = pack.GetFile(obj.Path); - if (fileData != null) - { - var teraFile = new TeraFile(fileData.Value.file.RawData); - // bg/ffxiv/..../bgplate/terrain.tera - // need to get bg.lgb file - // bg/ffxiv/..../level/bg.lgb - var bgLgbPath = obj.Path.Replace("bgplate/terrain.tera", "level/bg.lgb"); - var bgLgbData = pack.GetFile(bgLgbPath); - if (bgLgbData != null) - { - var lgbFile = new LgbFile(bgLgbData.Value.file.RawData); - } - }*/ + // var fileData = pack.GetFile(obj.Path); + // if (fileData != null) + // { + // var teraFile = new TeraFile(fileData.Value.file.RawData); + // // bg/ffxiv/..../bgplate/terrain.tera + // // need to get bg.lgb file + // // bg/ffxiv/..../level/bg.lgb + // var bgLgbPath = obj.Path.Replace("bgplate/terrain.tera", "level/bg.lgb"); + // var bgLgbData = pack.GetFile(bgLgbPath); + // if (bgLgbData != null) + // { + // var lgbFile = new LgbFile(bgLgbData.Value.file.RawData); + // } + // } } } @@ -188,3 +187,4 @@ public struct Terrain [FieldOffset(0x90)] public unsafe ResourceHandle* ResourceHandle; } +*/ diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 8685dcd..9e860ff 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -80,7 +80,7 @@ public LiveCharacterTab( private bool IsDisposed { get; set; } - public string Name => "CharacterAlt"; + public string Name => "Character"; public int Order => 1; public bool DisplayTab => true; @@ -140,6 +140,7 @@ private void DrawObjectPicker() if (ImGui.Selectable(clientState.GetCharacterDisplayText(character, config.PlayerNameOverride))) { selectedCharacter = character; + } } } @@ -155,10 +156,10 @@ private void DrawSelectedCharacter() } var charPtr = (CSCharacter*)selectedCharacter.Address; - DrawCharacter(charPtr); + DrawCharacter(charPtr, "Character"); } - private void DrawCharacter(CSCharacter* character) + private void DrawCharacter(CSCharacter* character, string name) { if (character == null) { @@ -167,27 +168,50 @@ private void DrawCharacter(CSCharacter* character) } var drawObject = character->GameObject.DrawObject; - DrawDrawObject(drawObject); - + ImGui.Text(name); + if (drawObject->GetObjectType() != ObjectType.CharacterBase) + { + ImGui.Text("Draw object is not a character base"); + return; + } + var cBase = (CSCharacterBase*)drawObject; + var modelType = cBase->GetModelType(); + CustomizeData? customizeData; + CustomizeParameter? customizeParams; + GenderRace genderRace; + if (modelType == CSCharacterBase.ModelType.Human) + { + DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); + if (ImGui.Button("Export All Models With Attaches")) + { + ExportAllModelsWithAttaches(character, customizeParams, customizeData, genderRace); + } + } + else + { + customizeData = null; + customizeParams = null; + genderRace = GenderRace.Unknown; + } + + DrawDrawObject(drawObject, customizeData, customizeParams, genderRace); + if (character->Mount.MountObject != null) { ImGui.Separator(); - ImGui.Text("Mount"); - DrawCharacter(character->Mount.MountObject); + DrawCharacter(character->Mount.MountObject, "Mount"); } if (character->CompanionData.CompanionObject != null) { ImGui.Separator(); - ImGui.Text("Companion"); - DrawCharacter(&character->CompanionData.CompanionObject->Character); + DrawCharacter(&character->CompanionData.CompanionObject->Character, "Companion"); } if (character->OrnamentData.OrnamentObject != null) { ImGui.Separator(); - ImGui.Text("Ornament"); - DrawCharacter(&character->OrnamentData.OrnamentObject->Character); + DrawCharacter(&character->OrnamentData.OrnamentObject->Character, "Ornament"); } for (var weaponIdx = 0; weaponIdx < character->DrawData.WeaponData.Length; weaponIdx++) @@ -197,12 +221,12 @@ private void DrawCharacter(CSCharacter* character) { ImGui.Separator(); ImGui.Text($"Weapon {weaponIdx}"); - DrawDrawObject(weaponData.DrawObject); + DrawDrawObject(weaponData.DrawObject, null, null, GenderRace.Unknown); } } } - private void DrawDrawObject(DrawObject* drawObject) + private void DrawDrawObject(DrawObject* drawObject, CustomizeData? customizeData, CustomizeParameter? customizeParams, GenderRace genderRace) { if (drawObject == null) { @@ -219,41 +243,9 @@ private void DrawDrawObject(DrawObject* drawObject) using var drawObjectId = ImRaii.PushId($"{(nint)drawObject}"); var cBase = (CSCharacterBase*)drawObject; - var modelType = cBase->GetModelType(); - CustomizeParameter? customizeParams = null; - CustomizeData? customizeData = null; - var genderRace = GenderRace.Unknown; - if (modelType == CharacterBase.ModelType.Human) - { - DrawHumanCharacter((CSHuman*)cBase, out customizeData, out customizeParams, out genderRace); - } - if (ImGui.Button("Export All Models")) { - var colorTableTextures = parseService.ParseColorTableTextures(cBase); - var models = new List(); - foreach (var modelPtr in cBase->ModelsSpan) - { - if (modelPtr == null) continue; - var model = modelPtr.Value; - if (model == null) continue; - var modelData = parseService.HandleModelPtr(cBase, (int)model->SlotIndex, colorTableTextures); - if (modelData == null) continue; - models.Add(modelData); - } - - var skeleton = StructExtensions.GetParsedSkeleton(cBase); - var cGroup = new CharacterGroup(customizeParams ?? new CustomizeParameter(), - customizeData ?? new CustomizeData(), - genderRace, models.ToArray(), - skeleton, []); - fileDialog.SaveFolderDialog("Save Model", "Character", - (result, path) => - { - if (!result) return; - - Task.Run(() => { exportService.Export(cGroup, path); }); - }, Plugin.TempDirectory); + ExportAllModels(cBase, customizeParams, customizeData, genderRace); } ImGui.SameLine(); @@ -310,6 +302,99 @@ private void DrawDrawObject(DrawObject* drawObject) } } + private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + { + var drawObject = character->GameObject.DrawObject; + if (drawObject == null) + { + log.LogError("Draw object is null"); + return; + } + + var cBase = (CSCharacterBase*)drawObject; + var group = parseService.ParseCharacterBase(cBase) with + { + CustomizeParams = customizeParams ?? new CustomizeParameter(), + CustomizeData = customizeData ?? new CustomizeData(), + GenderRace = genderRace + }; + + var attaches = new List(); + if (character->OrnamentData.OrnamentObject != null) + { + var draw = character->OrnamentData.OrnamentObject->GetDrawObject(); + var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + if (attachGroup != null) + { + attaches.Add(attachGroup); + } + } + + if (character->Mount.MountObject != null) + { + var draw = character->Mount.MountObject->GetDrawObject(); + var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + if (attachGroup != null) + { + attaches.Add(attachGroup); + } + } + + if (character->CompanionData.CompanionObject != null) + { + var draw = character->CompanionData.CompanionObject->GetDrawObject(); + var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + if (attachGroup != null) + { + attaches.Add(attachGroup); + } + } + + foreach (var weaponData in character->DrawData.WeaponData) + { + if (weaponData.DrawObject == null) continue; + var draw = weaponData.DrawObject; + var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + if (attachGroup != null) + { + attaches.Add(attachGroup); + } + } + + group = group with { AttachedModelGroups = attaches.ToArray() }; + fileDialog.SaveFolderDialog("Save Model", "Character", + (result, path) => + { + if (!result) return; + + Task.Run(() => + { + exportService.Export(group, path); + }); + }, Plugin.TempDirectory); + } + + private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) + { + var group = parseService.ParseCharacterBase(cBase) with + { + CustomizeParams = customizeParams ?? new CustomizeParameter(), + CustomizeData = customizeData ?? new CustomizeData(), + GenderRace = genderRace + }; + + fileDialog.SaveFolderDialog("Save Model", "Character", + (result, path) => + { + if (!result) return; + + Task.Run(() => + { + exportService.Export(group, path); + }); + }, Plugin.TempDirectory); + } + private void DrawModel( Pointer cPtr, Pointer mPtr, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) From 7349dfbf8b3bba8339b706497a32e47e839557ef Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 9 Aug 2024 21:14:18 +1000 Subject: [PATCH 19/19] Update LiveCharacterTab.cs --- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 49 ++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index 9e860ff..3800b55 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -9,12 +9,14 @@ using FFXIVClientStructs.Interop; using ImGuiNET; using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Skeletons; using Meddle.Plugin.Models.Structs; using Meddle.Plugin.Services; using Meddle.Plugin.Utils; using Meddle.Utils; using Meddle.Utils.Export; using Meddle.Utils.Files.SqPack; +using Meddle.Utils.Skeletons; using Microsoft.Extensions.Logging; using SkiaSharp; using CSCharacter = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -159,8 +161,14 @@ private void DrawSelectedCharacter() DrawCharacter(charPtr, "Character"); } - private void DrawCharacter(CSCharacter* character, string name) + private void DrawCharacter(CSCharacter* character, string name, int depth = 0) { + if (depth > 3) + { + ImGui.Text("Bad things happened, too deep"); + return; + } + if (character == null) { ImGui.Text("Character is null"); @@ -169,6 +177,11 @@ private void DrawCharacter(CSCharacter* character, string name) var drawObject = character->GameObject.DrawObject; ImGui.Text(name); + if (drawObject == null) + { + ImGui.Text("Draw object is null"); + return; + } if (drawObject->GetObjectType() != ObjectType.CharacterBase) { ImGui.Text("Draw object is not a character base"); @@ -199,19 +212,19 @@ private void DrawCharacter(CSCharacter* character, string name) if (character->Mount.MountObject != null) { ImGui.Separator(); - DrawCharacter(character->Mount.MountObject, "Mount"); + DrawCharacter(character->Mount.MountObject, "Mount", depth + 1); } if (character->CompanionData.CompanionObject != null) { ImGui.Separator(); - DrawCharacter(&character->CompanionData.CompanionObject->Character, "Companion"); + DrawCharacter(&character->CompanionData.CompanionObject->Character, "Companion", depth + 1); } if (character->OrnamentData.OrnamentObject != null) { ImGui.Separator(); - DrawCharacter(&character->OrnamentData.OrnamentObject->Character, "Ornament"); + DrawCharacter(&character->OrnamentData.OrnamentObject->Character, "Ornament", depth + 1); } for (var weaponIdx = 0; weaponIdx < character->DrawData.WeaponData.Length; weaponIdx++) @@ -333,9 +346,33 @@ private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParame if (character->Mount.MountObject != null) { var draw = character->Mount.MountObject->GetDrawObject(); - var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + var attachGroup = parseService.ParseDrawObjectAsAttach(draw); + if (attachGroup != null) { + // hacky workaround since mount is actually a "root" and the character is attached to them + // TODO: transform needs to be adjusted to be relative to the mount + /*var playerAttach = StructExtensions.GetParsedAttach(cBase); + var attachPointName = + playerAttach.OwnerSkeleton!.PartialSkeletons[playerAttach.PartialSkeletonIdx].HkSkeleton!.BoneNames[ + (int)playerAttach.BoneIdx]; + + attachGroup.Attach.OwnerSkeleton = playerAttach.TargetSkeleton; + attachGroup.Attach.TargetSkeleton = attachGroup.Skeleton; + for (int i = 0; i < attachGroup.Skeleton.PartialSkeletons.Count; i++) + { + var partial = attachGroup.Skeleton.PartialSkeletons[i]; + for (int j = 0; j < partial.HkSkeleton!.BoneNames.Count; j++) + { + if (partial.HkSkeleton.BoneNames[j] == attachPointName) + { + attachGroup.Attach.BoneIdx = (uint)j; + attachGroup.Attach.PartialSkeletonIdx = (byte)i; + break; + } + } + }*/ + attaches.Add(attachGroup); } } @@ -373,7 +410,7 @@ private void ExportAllModelsWithAttaches(CSCharacter* character, CustomizeParame }); }, Plugin.TempDirectory); } - + private void ExportAllModels(CSCharacterBase* cBase, CustomizeParameter? customizeParams, CustomizeData? customizeData, GenderRace genderRace) { var group = parseService.ParseCharacterBase(cBase) with