diff --git a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs index 5e1d99e..7a21ac9 100644 --- a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs +++ b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs @@ -566,7 +566,8 @@ public class PlayerDataFactory await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false); try { - papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) + papIndices = await _dalamudUtil + .RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(hash, persistToConfig: false)) .ConfigureAwait(false); } finally @@ -577,9 +578,6 @@ public class PlayerDataFactory if (papIndices == null || papIndices.Count == 0) continue; - if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) - continue; - if (_logger.IsEnabled(LogLevel.Debug)) { var papBuckets = papIndices @@ -658,8 +656,8 @@ public class PlayerDataFactory return new Dictionary(StringComparer.OrdinalIgnoreCase).AsReadOnly(); } - var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray(); - var reversePathsLower = reversePaths.Length == 0 ? Array.Empty() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray(); + var forwardPathsLower = forwardPaths.Length == 0 ? [] : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray(); + var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray(); Dictionary> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal); if (handler.ObjectKind != ObjectKind.Player) diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index 997df16..f6cf933 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.Havok.Animation; using FFXIVClientStructs.Havok.Common.Base.Types; +using FFXIVClientStructs.Havok.Common.Serialize.Resource; using FFXIVClientStructs.Havok.Common.Serialize.Util; using LightlessSync.FileCache; using LightlessSync.Interop.GameModel; @@ -9,6 +10,7 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Handlers; using Microsoft.Extensions.Logging; +using OtterGui.Text.EndObjects; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -172,13 +174,18 @@ public sealed partial class XivDataAnalyzer reader.BaseStream.Position = havokPosition; var havokData = reader.ReadBytes(havokDataSize); - if (havokData.Length <= 8) + if (havokData.Length != havokDataSize) return null; + if (havokPosition < 0 || footerPosition < 0) return null; + if (havokPosition >= fs.Length) return null; + if (footerPosition > fs.Length) return null; + if (havokPosition + havokDataSizeLong > fs.Length) return null; + var tempSets = new Dictionary>(StringComparer.OrdinalIgnoreCase); var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx"); - IntPtr tempHavokDataPathAnsi = IntPtr.Zero; + var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); try { @@ -190,63 +197,63 @@ public sealed partial class XivDataAnalyzer return null; } - tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); + var pathBytes = System.Text.Encoding.ASCII.GetBytes(tempHavokDataPath + "\0"); - var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; - loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); - loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); - loadoptions->Flags = new hkFlags + hkSerializeUtil.LoadOptions loadOptions = default; + loadOptions.TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); + loadOptions.ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); + loadOptions.Flags = new hkFlags { Storage = (int)hkSerializeUtil.LoadOptionBits.Default }; - var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); - if (resource == null) + fixed (byte* pPath = pathBytes) { - _logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath); - return null; - } - - var rootLevelName = @"hkRootLevelContainer"u8; - fixed (byte* n1 = rootLevelName) - { - var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); - if (container == null) + var resource = hkSerializeUtil.LoadFromFile(pPath, errorResult: null, &loadOptions); + if (resource == null) return null; - var animationName = @"hkaAnimationContainer"u8; - fixed (byte* n2 = animationName) + var rootLevelName = @"hkRootLevelContainer"u8; + fixed (byte* n1 = rootLevelName) { - var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); - if (animContainer == null) + var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); + if (container == null) return null; - for (int i = 0; i < animContainer->Bindings.Length; i++) + var animationName = @"hkaAnimationContainer"u8; + fixed (byte* n2 = animationName) { - var binding = animContainer->Bindings[i].ptr; - if (binding == null) - continue; + var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); + if (animContainer == null) + return null; - var rawSkel = binding->OriginalSkeletonName.String; - var skeletonKey = CanonicalizeSkeletonKey(rawSkel); - if (string.IsNullOrEmpty(skeletonKey)) - continue; - - var boneTransform = binding->TransformTrackToBoneIndices; - if (boneTransform.Length <= 0) - continue; - - if (!tempSets.TryGetValue(skeletonKey, out var set)) + for (int i = 0; i < animContainer->Bindings.Length; i++) { - set = []; - tempSets[skeletonKey] = set; - } + var binding = animContainer->Bindings[i].ptr; + if (binding == null) + continue; - for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) - { - var v = boneTransform[boneIdx]; - if (v < 0) continue; - set.Add((ushort)v); + var rawSkel = binding->OriginalSkeletonName.String; + var skeletonKey = CanonicalizeSkeletonKey(rawSkel); + if (string.IsNullOrEmpty(skeletonKey) || string.Equals(skeletonKey, "skeleton", StringComparison.OrdinalIgnoreCase)) + skeletonKey = "__any__"; + + var boneTransform = binding->TransformTrackToBoneIndices; + if (boneTransform.Length <= 0) + continue; + + if (!tempSets.TryGetValue(skeletonKey, out var set)) + { + set = []; + tempSets[skeletonKey] = set; + } + + for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) + { + var v = boneTransform[boneIdx]; + if (v < 0) continue; + set.Add((ushort)v); + } } } } @@ -297,7 +304,6 @@ public sealed partial class XivDataAnalyzer return output; } - public static string CanonicalizeSkeletonKey(string? raw) { if (string.IsNullOrWhiteSpace(raw)) @@ -375,41 +381,56 @@ public sealed partial class XivDataAnalyzer if (mode == AnimationValidationMode.Unsafe) return true; - var papBuckets = papBoneIndices.Keys - .Select(CanonicalizeSkeletonKey) - .Where(k => !string.IsNullOrEmpty(k)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); + var papByBucket = new Dictionary>(StringComparer.OrdinalIgnoreCase); - if (papBuckets.Count == 0) + foreach (var (rawKey, list) in papBoneIndices) + { + var key = CanonicalizeSkeletonKey(rawKey); + if (string.IsNullOrEmpty(key)) + continue; + + if (string.Equals(key, "skeleton", StringComparison.OrdinalIgnoreCase)) + key = "__any__"; + + if (!papByBucket.TryGetValue(key, out var acc)) + papByBucket[key] = acc = []; + + if (list is { Count: > 0 }) + acc.AddRange(list); + } + + foreach (var k in papByBucket.Keys.ToList()) + papByBucket[k] = papByBucket[k].Distinct().ToList(); + + if (papByBucket.Count == 0) { reason = "No skeleton bucket bindings found in the PAP"; return false; } - if (mode == AnimationValidationMode.Safe) + static bool AllIndicesOk( + HashSet available, + List indices, + bool papLikelyOneBased, + bool allowOneBasedShift, + bool allowNeighborTolerance, + out ushort missing) { - if (papBuckets.Any(b => localBoneSets.ContainsKey(b))) - return true; - - reason = $"No matching skeleton bucket between PAP [{string.Join(", ", papBuckets)}] and local [{string.Join(", ", localBoneSets.Keys.Order())}]."; - return false; - } - - foreach (var bucket in papBuckets) - { - if (!localBoneSets.TryGetValue(bucket, out var available)) + foreach (var idx in indices) { - reason = $"Missing skeleton bucket '{bucket}' on local actor."; - return false; + if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance)) + { + missing = idx; + return false; + } } - var indices = papBoneIndices - .Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase)) - .SelectMany(kvp => kvp.Value ?? Enumerable.Empty()) - .Distinct() - .ToList(); + missing = 0; + return true; + } + foreach (var (bucket, indices) in papByBucket) + { if (indices.Count == 0) continue; @@ -423,14 +444,32 @@ public sealed partial class XivDataAnalyzer } bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0; - foreach (var idx in indices) + if (string.Equals(bucket, "__any__", StringComparison.OrdinalIgnoreCase)) { - if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance)) + foreach (var (lk, ls) in localBoneSets) { - reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}."; - return false; + if (AllIndicesOk(ls, indices, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance, out _)) + goto nextBucket; } + + reason = $"No compatible local skeleton bucket for generic PAP skeleton '{bucket}'. Local buckets: {string.Join(", ", localBoneSets.Keys)}"; + return false; } + + if (!localBoneSets.TryGetValue(bucket, out var available)) + { + reason = $"Missing skeleton bucket '{bucket}' on local actor."; + return false; + } + + if (!AllIndicesOk(available, indices, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance, out var missing)) + { + reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {missing}."; + return false; + } + + nextBucket: + ; } return true;