diff --git a/MareSynchronos/Interop/Structs.cs b/MareSynchronos/Interop/Structs.cs new file mode 100644 index 0000000..8f54df6 --- /dev/null +++ b/MareSynchronos/Interop/Structs.cs @@ -0,0 +1,49 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace MareSynchronos.Interop; + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct RenderModel +{ + [FieldOffset(0x18)] + public RenderModel* PreviousModel; + + [FieldOffset(0x20)] + public RenderModel* NextModel; + + [FieldOffset(0x30)] + public ResourceHandle* ResourceHandle; + + [FieldOffset(0x40)] + public Skeleton* Skeleton; + + [FieldOffset(0x58)] + public void** BoneList; + + [FieldOffset(0x60)] + public int BoneListCount; + + + [FieldOffset(0x98)] + public void** Materials; + + [FieldOffset(0xA0)] + public int MaterialCount; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct Weapon +{ + [FieldOffset(0x18)] public IntPtr Parent; + [FieldOffset(0x20)] public IntPtr NextSibling; + [FieldOffset(0x28)] public IntPtr PreviousSibling; + [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct WeaponDrawObject +{ + [FieldOffset(0x00)] public RenderModel* RenderModel; +} diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 737e539..63b045d 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -1,13 +1,19 @@ using System.Diagnostics; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.API.Data.Enum; using MareSynchronos.Interop; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using MareSynchronos.FileCache; using Microsoft.Extensions.Logging; +using System.Globalization; using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using CharacterData = MareSynchronos.PlayerData.Data.CharacterData; +using Weapon = MareSynchronos.Interop.Weapon; namespace MareSynchronos.PlayerData.Factories; @@ -100,6 +106,190 @@ public class PlayerDataFactory previousData.CustomizePlusScale = previousCustomize; } + private unsafe void AddPlayerSpecificReplacements(Human* human, HashSet forwardResolve, HashSet reverseResolve) + { + var weaponObject = (Weapon*)((Object*)human)->ChildObject; + + if ((IntPtr)weaponObject != IntPtr.Zero) + { + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel((Model*)mainHandWeapon, forwardResolve, reverseResolve); + + foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) + { + _logger.LogTrace("Found transient weapon resource: {item}", item); + forwardResolve.Add(item); + } + + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel((Model*)offHandWeapon, forwardResolve, reverseResolve); + + foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) + { + _logger.LogTrace("Found transient offhand weapon resource: {item}", item); + forwardResolve.Add(item); + } + } + } + + AddReplacementSkeleton((human)->RaceSexId, forwardResolve); + try + { + AddReplacementsFromTexture((human)->Decal->ResourceHandle.FileName.ToString(), forwardResolve, reverseResolve, doNotReverseResolve: false); + } + catch + { + _logger.LogWarning("Could not get Decal data"); + } + try + { + AddReplacementsFromTexture((human)->LegacyBodyDecal->ResourceHandle.FileName.ToString(), forwardResolve, reverseResolve, doNotReverseResolve: false); + } + catch + { + _logger.LogWarning("Could not get Legacy Body Decal Data"); + } + } + + private unsafe void AddReplacementsFromMaterial(Material* mtrl, HashSet forwardResolve, HashSet reverseResolve) + { + string fileName; + try + { + fileName = mtrl->MaterialResourceHandle->ResourceHandle.FileName.ToString(); + } + catch + { + _logger.LogWarning("Could not get material data"); + return; + } + + _logger.LogTrace("Checking File Replacement for Material {file}", fileName); + var mtrlPath = fileName.Split("|")[2]; + + reverseResolve.Add(mtrlPath); + + var mtrlResourceHandle = mtrl->MaterialResourceHandle; + for (var resIdx = 0; resIdx < mtrlResourceHandle->TextureCount; resIdx++) + { + string? texPath = null; + try + { + texPath = mtrlResourceHandle->TexturePathString(resIdx); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not get Texture data for Material {file}", fileName); + } + + if (string.IsNullOrEmpty(texPath)) continue; + + AddReplacementsFromTexture(texPath, forwardResolve, reverseResolve); + } + + try + { + var shpkPath = "shader/sm5/shpk/" + mtrlResourceHandle->ShpkNameString; + _logger.LogTrace("Checking File Replacement for Shader {path}", shpkPath); + forwardResolve.Add(shpkPath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not find shpk for Material {path}", fileName); + } + } + + private unsafe void AddReplacementsFromRenderModel(Model* mdl, HashSet forwardResolve, HashSet reverseResolve) + { + if (mdl == null || mdl->ModelResourceHandle == null || (ResourceCategory)mdl->ModelResourceHandle->ResourceHandle.Type.Value != ResourceCategory.Chara) + { + return; + } + + string mdlPath; + try + { + mdlPath = mdl->ModelResourceHandle->ResourceHandle.FileName.ToString(); + } + catch + { + _logger.LogWarning("Could not get model data"); + return; + } + _logger.LogTrace("Checking File Replacement for Model {path}", mdlPath); + + reverseResolve.Add(mdlPath); + + for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) + { + var mtrl = mdl->Materials[mtrlIdx]; + if (mtrl == null) continue; + + AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve); + } + } + + private void AddReplacementsFromTexture(string texPath, HashSet forwardResolve, HashSet reverseResolve, bool doNotReverseResolve = true) + { + if (string.IsNullOrEmpty(texPath)) return; + + _logger.LogTrace("Checking File Replacement for Texture {path}", texPath); + + if (doNotReverseResolve) + forwardResolve.Add(texPath); + else + reverseResolve.Add(texPath); + + if (texPath.Contains("/--", StringComparison.Ordinal)) return; + + var dx11Path = texPath.Insert(texPath.LastIndexOf('/') + 1, "--"); + if (doNotReverseResolve) + forwardResolve.Add(dx11Path); + else + reverseResolve.Add(dx11Path); + } + + private void AddReplacementSkeleton(ushort raceSexId, HashSet forwardResolve) + { + string raceSexIdString = raceSexId.ToString("0000", CultureInfo.InvariantCulture); + + string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; + + _logger.LogTrace("Checking skeleton {path}", skeletonPath); + + forwardResolve.Add(skeletonPath); + } + + private unsafe (HashSet forwardResolve, HashSet reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token) + { + HashSet forwardResolve = new(StringComparer.Ordinal); + HashSet reverseResolve = new(StringComparer.Ordinal); + var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); + for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) + { + var mdl = human->CharacterBase.Models[mdlIdx]; + if (mdl == null || mdl->ModelResourceHandle == null || (ResourceCategory)mdl->ModelResourceHandle->ResourceHandle.Type.Value != ResourceCategory.Chara) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve); + } + + if (objectKind == ObjectKind.Player) + { + AddPlayerSpecificReplacements(human, forwardResolve, reverseResolve); + } + + return (forwardResolve, reverseResolve); + } + private async Task CheckForNullDrawObject(IntPtr playerPointer) { return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false); @@ -143,12 +333,15 @@ public class PlayerDataFactory Stopwatch st = Stopwatch.StartNew(); - // gather static replacements from render model - var data = (await _ipcManager.PenumbraGetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false))![0]; - if (data == null) throw new InvalidOperationException("Penumbra returned null data"); + // penumbra call, it's currently broken + // var data = (await _ipcManager.PenumbraGetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false))![0]; + // if (data == null) throw new InvalidOperationException("Penumbra returned null data"); + // gather static replacements from render model + var (forwardResolve, reverseResolve) = await _dalamudUtil.RunOnFrameworkThread(() => BuildDataFromModel(objectKind, charaPointer, token)).ConfigureAwait(false); + Dictionary> resolvedPaths = await GetFileReplacementsFromPaths(forwardResolve, reverseResolve).ConfigureAwait(false); previousData.FileReplacements[objectKind] = - new HashSet(data.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance) + new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value.ToArray(), c.Key, _fileCacheManager)), FileReplacementComparer.Instance) .Where(p => p.HasFileReplacement).ToHashSet(); previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !AllowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));