diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index f4cf37a..2b634b3 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.Interop; using MareSynchronos.Managers; @@ -11,232 +12,173 @@ using MareSynchronos.Models; using MareSynchronos.Utils; using Penumbra.GameData.ByteString; using Penumbra.Interop.Structs; -using Human = MareSynchronos.Interop.Human; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; -namespace MareSynchronos.Factories +namespace MareSynchronos.Factories; +public class CharacterDataFactory { - public class CharacterDataFactory + private readonly DalamudUtil _dalamudUtil; + private readonly IpcManager _ipcManager; + + public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager) { - private readonly DalamudUtil _dalamudUtil; - private readonly IpcManager _ipcManager; + Logger.Debug("Creating " + nameof(CharacterDataFactory)); - public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager) + _dalamudUtil = dalamudUtil; + _ipcManager = ipcManager; + } + + public CharacterData BuildCharacterData() + { + if (!_ipcManager.Initialized) { - Logger.Debug("Creating " + nameof(CharacterDataFactory)); - - _dalamudUtil = dalamudUtil; - _ipcManager = ipcManager; + throw new ArgumentException("Penumbra is not connected"); } - private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) - { - var fileReplacement = new FileReplacement(_ipcManager.PenumbraModDirectory()!); - if (!doNotReverseResolve) - { - fileReplacement.GamePaths = - _ipcManager.PenumbraReverseResolvePath(path, _dalamudUtil.PlayerName).ToList(); - fileReplacement.SetResolvedPath(path); - } - else - { - fileReplacement.GamePaths = new List { path }; - fileReplacement.SetResolvedPath(_ipcManager.PenumbraResolvePath(path, _dalamudUtil.PlayerName)!); - } + return CreateCharacterData(); + } - return fileReplacement; + private (string, string) GetIndentationForInheritanceLevel(int inheritanceLevel) + { + return (string.Join("", Enumerable.Repeat("\t", inheritanceLevel)), string.Join("", Enumerable.Repeat("\t", inheritanceLevel + 2))); + } + + private void DebugPrint(FileReplacement fileReplacement, string objectKind, string resourceType, int inheritanceLevel) + { + var indentation = GetIndentationForInheritanceLevel(inheritanceLevel); + objectKind += string.IsNullOrEmpty(objectKind) ? "" : " "; + + Logger.Debug(indentation.Item1 + objectKind + resourceType + " [" + string.Join(", ", fileReplacement.GamePaths) + "]"); + Logger.Debug(indentation.Item2 + "=> " + fileReplacement.ResolvedPath); + } + + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + { + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + return; } - public CharacterData BuildCharacterData() + var mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString(); + FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); + DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel); + + cache.AddFileReplacement(mdlFileReplacement); + + for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) { - if (!_ipcManager.Initialized) - { - throw new ArgumentException("Penumbra is not connected"); - } + var mtrl = (Material*)mdl->Materials[mtrlIdx]; + if (mtrl == null) continue; - return CreateCharacterData(); - } - - private unsafe CharacterData CreateCharacterData() - { - Stopwatch st = Stopwatch.StartNew(); - while (!_dalamudUtil.IsPlayerPresent) - { - Logger.Debug("Character is null but it shouldn't be, waiting"); - Thread.Sleep(50); - } - _dalamudUtil.WaitWhileCharacterIsDrawing(_dalamudUtil.PlayerPointer); - var cache = new CharacterData - { - JobId = _dalamudUtil.PlayerJobId, - GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(_dalamudUtil.PlayerCharacter), - ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_dalamudUtil.PlayerName) - }; - var human = (Human*)((Character*)_dalamudUtil.PlayerPointer)->GameObject.GetDrawObject(); - for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) - { - var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } - - var mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString(); - - FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); - Logger.Debug("Model " + string.Join(", ", mdlFileReplacement.GamePaths)); - Logger.Debug("\t\t=> " + mdlFileReplacement.ResolvedPath); - - cache.AddFileReplacement(mdlFileReplacement); - - for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mdl->Materials[mtrlIdx]; - if (mtrl == null) continue; - - var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; - - var mtrlFileReplacement = CreateFileReplacement(mtrlPath); - Logger.Debug("\tMaterial " + string.Join(", ", mtrlFileReplacement.GamePaths)); - Logger.Debug("\t\t\t=> " + mtrlFileReplacement.ResolvedPath); - - cache.AddFileReplacement(mtrlFileReplacement); - - var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; - for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) - { - var texPath = new Utf8String(mtrlResourceHandle->TexString(resIdx)).ToString(); - - if (string.IsNullOrEmpty(texPath)) continue; - - var texFileReplacement = CreateFileReplacement(texPath, true); - Logger.Debug("\t\tTexture " + string.Join(", ", texFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texFileReplacement.ResolvedPath); - - cache.AddFileReplacement(texFileReplacement); - - if (texPath.Contains("/--")) continue; - - var texDoubleMinusFileReplacement = - CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), true); - - Logger.Debug("\t\tTexture-- " + string.Join(", ", texDoubleMinusFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texDoubleMinusFileReplacement.ResolvedPath); - cache.AddFileReplacement(texDoubleMinusFileReplacement); - } - } - } - - var mainHandWeapon = (RenderModel*)human->Weapon->WeaponRenderModel->RenderModel; - - var mainHandWeaponPath = new Utf8String(mainHandWeapon->ResourceHandle->FileName()).ToString(); - FileReplacement weaponReplacement = CreateFileReplacement(mainHandWeaponPath); - cache.AddFileReplacement(weaponReplacement); - - Logger.Debug("MainHand Weapon " + string.Join(", ", weaponReplacement.GamePaths)); - Logger.Debug("\t\t=> " + weaponReplacement.ResolvedPath); - - for (var mtrlIdx = 0; mtrlIdx < mainHandWeapon->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mainHandWeapon->Materials[mtrlIdx]; - if (mtrl == null) continue; - - var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; - - var mtrlFileReplacement = CreateFileReplacement(mtrlPath); - Logger.Debug("\tMainHand Weapon Material " + string.Join(", ", mtrlFileReplacement.GamePaths)); - Logger.Debug("\t\t\t=> " + mtrlFileReplacement.ResolvedPath); - - cache.AddFileReplacement(mtrlFileReplacement); - - var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; - for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) - { - var texPath = new Utf8String(mtrlResourceHandle->TexString(resIdx)).ToString(); - - if (string.IsNullOrEmpty(texPath)) continue; - - var texFileReplacement = CreateFileReplacement(texPath, true); - Logger.Debug("\t\tMainHand tWeapon Texture " + string.Join(", ", texFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texFileReplacement.ResolvedPath); - - cache.AddFileReplacement(texFileReplacement); - - if (texPath.Contains("/--")) continue; - - var texDoubleMinusFileReplacement = - CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), true); - - Logger.Debug("\t\tMainHand Weapon Texture-- " + string.Join(", ", texDoubleMinusFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texDoubleMinusFileReplacement.ResolvedPath); - cache.AddFileReplacement(texDoubleMinusFileReplacement); - } - } - - if (human->Weapon->NextSibling != (IntPtr)human->Weapon) - { - var offHandWeapon = ((Weapon*)human->Weapon->NextSibling)->WeaponRenderModel->RenderModel; - - var offHandWeaponPath = new Utf8String(offHandWeapon->ResourceHandle->FileName()).ToString(); - FileReplacement offHandWeaponReplacement = CreateFileReplacement(offHandWeaponPath); - cache.AddFileReplacement(offHandWeaponReplacement); - - Logger.Debug("OffHand Weapon " + string.Join(", ", offHandWeaponReplacement.GamePaths)); - Logger.Debug("\t\t=> " + offHandWeaponReplacement.ResolvedPath); - - for (var mtrlIdx = 0; mtrlIdx < offHandWeapon->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)offHandWeapon->Materials[mtrlIdx]; - if (mtrl == null) continue; - - var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; - - var mtrlFileReplacement = CreateFileReplacement(mtrlPath); - Logger.Debug("\tOffHand Weapon Material " + string.Join(", ", mtrlFileReplacement.GamePaths)); - Logger.Debug("\t\t\t=> " + mtrlFileReplacement.ResolvedPath); - - cache.AddFileReplacement(mtrlFileReplacement); - - var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; - for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) - { - var texPath = new Utf8String(mtrlResourceHandle->TexString(resIdx)).ToString(); - - if (string.IsNullOrEmpty(texPath)) continue; - - var texFileReplacement = CreateFileReplacement(texPath, true); - Logger.Debug("\t\tOffHand tWeapon Texture " + string.Join(", ", texFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texFileReplacement.ResolvedPath); - - cache.AddFileReplacement(texFileReplacement); - - if (texPath.Contains("/--")) continue; - - var texDoubleMinusFileReplacement = - CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), true); - - Logger.Debug("\t\tOffHand Weapon Texture-- " + string.Join(", ", texDoubleMinusFileReplacement.GamePaths)); - Logger.Debug("\t\t\t\t=> " + texDoubleMinusFileReplacement.ResolvedPath); - cache.AddFileReplacement(texDoubleMinusFileReplacement); - } - } - } - - var tattooDecalFileReplacement = - CreateFileReplacement(new Utf8String(human->Decal->FileName()).ToString()); - cache.AddFileReplacement(tattooDecalFileReplacement); - Logger.Debug("Decal " + string.Join(", ", tattooDecalFileReplacement.GamePaths)); - Logger.Debug("\t\t=> " + tattooDecalFileReplacement.ResolvedPath); - - var legacyDecalFileReplacement = - CreateFileReplacement(new Utf8String(human->LegacyBodyDecal->FileName()).ToString()); - cache.AddFileReplacement(legacyDecalFileReplacement); - Logger.Debug("Legacy Decal " + string.Join(", ", legacyDecalFileReplacement.GamePaths)); - Logger.Debug("\t\t=> " + legacyDecalFileReplacement.ResolvedPath); - - st.Stop(); - Logger.Verbose("Building Character Data took " + st.Elapsed); - - return cache; + AddReplacementsFromMaterial(mtrl, cache, inheritanceLevel + 1, objectKind); } } + + private unsafe void AddReplacementsFromMaterial(Material* mtrl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + { + var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; + + var mtrlFileReplacement = CreateFileReplacement(mtrlPath); + DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel); + + cache.AddFileReplacement(mtrlFileReplacement); + + var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; + for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) + { + var texPath = new Utf8String(mtrlResourceHandle->TexString(resIdx)).ToString(); + + if (string.IsNullOrEmpty(texPath)) continue; + + AddReplacementsFromTexture(texPath, cache, inheritanceLevel + 1, objectKind); + } + } + + private void AddReplacementsFromTexture(string texPath, CharacterData cache, int inheritanceLevel = 0, string objectKind = "", bool doNotReverseResolve = true) + { + var texFileReplacement = CreateFileReplacement(texPath, doNotReverseResolve); + DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel); + + cache.AddFileReplacement(texFileReplacement); + + if (texPath.Contains("/--")) return; + + var texDx11Replacement = + CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), true); + + DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); + + cache.AddFileReplacement(texDx11Replacement); + } + + private unsafe CharacterData CreateCharacterData() + { + Stopwatch st = Stopwatch.StartNew(); + while (!_dalamudUtil.IsPlayerPresent) + { + Logger.Debug("Character is null but it shouldn't be, waiting"); + Thread.Sleep(50); + } + _dalamudUtil.WaitWhileCharacterIsDrawing(_dalamudUtil.PlayerPointer); + var cache = new CharacterData + { + JobId = _dalamudUtil.PlayerJobId, + GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(_dalamudUtil.PlayerCharacter), + ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_dalamudUtil.PlayerName) + }; + + var human = (Human*)((Character*)_dalamudUtil.PlayerPointer)->GameObject.GetDrawObject(); + for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) + { + var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + continue; + } + + AddReplacementsFromRenderModel(mdl, cache, 0, "Character"); + } + + var weaponObject = (Weapon*)((Object*)human)->ChildObject; + + if ((IntPtr)weaponObject != IntPtr.Zero) + { + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(mainHandWeapon, cache, 0, "Weapon"); + + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(offHandWeapon, cache, 1, "OffHand Weapon"); + } + } + + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), cache, 0, "Decal", false); + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), cache, 0, "Legacy Decal", false); + + st.Stop(); + Logger.Verbose("Building Character Data took " + st.Elapsed); + + return cache; + } + + private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) + { + var fileReplacement = new FileReplacement(_ipcManager.PenumbraModDirectory()!); + if (!doNotReverseResolve) + { + fileReplacement.GamePaths = + _ipcManager.PenumbraReverseResolvePath(path, _dalamudUtil.PlayerName).ToList(); + fileReplacement.SetResolvedPath(path); + } + else + { + fileReplacement.GamePaths = new List { path }; + fileReplacement.SetResolvedPath(_ipcManager.PenumbraResolvePath(path, _dalamudUtil.PlayerName)!); + } + + return fileReplacement; + } } diff --git a/MareSynchronos/Interop/Human.cs b/MareSynchronos/Interop/Human.cs deleted file mode 100644 index b4dd347..0000000 --- a/MareSynchronos/Interop/Human.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Penumbra.Interop.Structs; - -namespace MareSynchronos.Interop -{ - [StructLayout(LayoutKind.Explicit, Size = 0xA80)] - public unsafe struct Human - { - [FieldOffset(0x0)] public CharacterBase CharacterBase; - [FieldOffset(0x30)] public Weapon* Weapon; - [FieldOffset(0x8F0)] public fixed byte CustomizeData[0x1A]; - [FieldOffset(0x8F0)] public byte Race; - [FieldOffset(0x8F1)] public byte Sex; - [FieldOffset(0x8F2)] public byte BodyType; - [FieldOffset(0x8F4)] public byte Clan; - [FieldOffset(0x904)] public byte LipColorFurPattern; - [FieldOffset(0x90C)] public uint SlotNeedsUpdateBitfield; - [FieldOffset(0x910)] public fixed byte EquipSlotData[4 * 0xA]; - [FieldOffset(0x910)] public short HeadSetID; - [FieldOffset(0x912)] public byte HeadVariantID; - [FieldOffset(0x913)] public byte HeadDyeID; - [FieldOffset(0x914)] public short TopSetID; - [FieldOffset(0x916)] public byte TopVariantID; - [FieldOffset(0x917)] public byte TopDyeID; - [FieldOffset(0x918)] public short ArmsSetID; - [FieldOffset(0x91A)] public byte ArmsVariantID; - [FieldOffset(0x91B)] public byte ArmsDyeID; - [FieldOffset(0x91C)] public short LegsSetID; - [FieldOffset(0x91E)] public byte LegsVariantID; - [FieldOffset(0x91F)] public byte LegsDyeID; - [FieldOffset(0x920)] public short FeetSetID; - [FieldOffset(0x922)] public byte FeetVariantID; - [FieldOffset(0x923)] public byte FeetDyeID; - [FieldOffset(0x924)] public short EarSetID; - [FieldOffset(0x926)] public byte EarVariantID; - [FieldOffset(0x928)] public short NeckSetID; - [FieldOffset(0x92A)] public byte NeckVariantID; - [FieldOffset(0x92C)] public short WristSetID; - [FieldOffset(0x92E)] public byte WristVariantID; - [FieldOffset(0x930)] public short RFingerSetID; - [FieldOffset(0x932)] public byte RFingerVariantID; - [FieldOffset(0x934)] public short LFingerSetID; - [FieldOffset(0x936)] public byte LFingerVariantID; - [FieldOffset(0x938)] public ushort RaceSexId; // cXXXX ID (0101, 0201, etc) - [FieldOffset(0x93A)] public ushort HairId; // hXXXX - [FieldOffset(0x93C)] public ushort FaceId; // fXXXX ID - - [FieldOffset(0x93E)] public ushort TailEarId; // tXXXX/zXXXX(viera) - - [FieldOffset(0x9D6)] public ushort Unknown; // 80 3F in memory - [FieldOffset(0x9D8)] public IntPtr Buffer1; - [FieldOffset(0x9E0)] public IntPtr Buffer; - [FieldOffset(0x9E8)] public ResourceHandle* Decal; - [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; - [FieldOffset(0x9F8)] public IntPtr Unk2; - - // see Client::Graphics::Scene::Human_FlagSlotForUpdate(thisPtr, uint slot, EquipSlotData* slotBytes) -> 48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A - // array of 10*12 byte storage for changing equipment models - [FieldOffset(0xA38)] public byte* ChangedEquipData; - } -} diff --git a/MareSynchronos/Interop/Weapon.cs b/MareSynchronos/Interop/Weapon.cs index 1833abc..97ac51c 100644 --- a/MareSynchronos/Interop/Weapon.cs +++ b/MareSynchronos/Interop/Weapon.cs @@ -23,4 +23,12 @@ namespace MareSynchronos.Interop { [FieldOffset(0x00)] public RenderModel* RenderModel; } + + [StructLayout(LayoutKind.Explicit)] + public unsafe struct HumanExt + { + [FieldOffset(0x0)] public Human Human; + [FieldOffset(0x9E8)] public ResourceHandle* Decal; + [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; + } } diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs index 165018e..0e23e67 100644 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ b/MareSynchronos/WebAPI/ApiController.Connectivity.cs @@ -200,7 +200,6 @@ namespace MareSynchronos.WebAPI _dalamudUtil.LogOut -= DalamudUtilOnLogOut; Task.Run(async () => await StopAllConnections(_cts.Token)); - _cts?.Cancel(); } private HubConnection BuildHubConnection(string hubName)