Attempt fix on crash.

This commit is contained in:
cake
2026-01-06 13:50:58 +01:00
parent 032201ed9e
commit 5161c6bad3
2 changed files with 116 additions and 79 deletions

View File

@@ -566,7 +566,8 @@ public class PlayerDataFactory
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false); await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
try try
{ {
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) papIndices = await _dalamudUtil
.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(hash, persistToConfig: false))
.ConfigureAwait(false); .ConfigureAwait(false);
} }
finally finally
@@ -577,9 +578,6 @@ public class PlayerDataFactory
if (papIndices == null || papIndices.Count == 0) if (papIndices == null || papIndices.Count == 0)
continue; continue;
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
continue;
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
var papBuckets = papIndices var papBuckets = papIndices
@@ -658,8 +656,8 @@ public class PlayerDataFactory
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly(); return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
} }
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray(); var forwardPathsLower = forwardPaths.Length == 0 ? [] : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray(); var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal); Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player) if (handler.ObjectKind != ObjectKind.Player)

View File

@@ -2,6 +2,7 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.Havok.Animation; using FFXIVClientStructs.Havok.Animation;
using FFXIVClientStructs.Havok.Common.Base.Types; using FFXIVClientStructs.Havok.Common.Base.Types;
using FFXIVClientStructs.Havok.Common.Serialize.Resource;
using FFXIVClientStructs.Havok.Common.Serialize.Util; using FFXIVClientStructs.Havok.Common.Serialize.Util;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.GameModel; using LightlessSync.Interop.GameModel;
@@ -9,6 +10,7 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OtterGui.Text.EndObjects;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -172,13 +174,18 @@ public sealed partial class XivDataAnalyzer
reader.BaseStream.Position = havokPosition; reader.BaseStream.Position = havokPosition;
var havokData = reader.ReadBytes(havokDataSize); var havokData = reader.ReadBytes(havokDataSize);
if (havokData.Length <= 8) if (havokData.Length != havokDataSize)
return null; 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<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase); var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx"); var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx");
IntPtr tempHavokDataPathAnsi = IntPtr.Zero; var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
try try
{ {
@@ -190,63 +197,63 @@ public sealed partial class XivDataAnalyzer
return null; return null;
} }
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); var pathBytes = System.Text.Encoding.ASCII.GetBytes(tempHavokDataPath + "\0");
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; hkSerializeUtil.LoadOptions loadOptions = default;
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); loadOptions.TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); loadOptions.ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int> loadOptions.Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
{ {
Storage = (int)hkSerializeUtil.LoadOptionBits.Default Storage = (int)hkSerializeUtil.LoadOptionBits.Default
}; };
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); fixed (byte* pPath = pathBytes)
if (resource == null)
{ {
_logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath); var resource = hkSerializeUtil.LoadFromFile(pPath, errorResult: null, &loadOptions);
return null; if (resource == null)
}
var rootLevelName = @"hkRootLevelContainer"u8;
fixed (byte* n1 = rootLevelName)
{
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
if (container == null)
return null; return null;
var animationName = @"hkaAnimationContainer"u8; var rootLevelName = @"hkRootLevelContainer"u8;
fixed (byte* n2 = animationName) fixed (byte* n1 = rootLevelName)
{ {
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
if (animContainer == null) if (container == null)
return 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; var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
if (binding == null) if (animContainer == null)
continue; return null;
var rawSkel = binding->OriginalSkeletonName.String; for (int i = 0; i < animContainer->Bindings.Length; i++)
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))
{ {
set = []; var binding = animContainer->Bindings[i].ptr;
tempSets[skeletonKey] = set; if (binding == null)
} continue;
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) var rawSkel = binding->OriginalSkeletonName.String;
{ var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
var v = boneTransform[boneIdx]; if (string.IsNullOrEmpty(skeletonKey) || string.Equals(skeletonKey, "skeleton", StringComparison.OrdinalIgnoreCase))
if (v < 0) continue; skeletonKey = "__any__";
set.Add((ushort)v);
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; return output;
} }
public static string CanonicalizeSkeletonKey(string? raw) public static string CanonicalizeSkeletonKey(string? raw)
{ {
if (string.IsNullOrWhiteSpace(raw)) if (string.IsNullOrWhiteSpace(raw))
@@ -375,41 +381,56 @@ public sealed partial class XivDataAnalyzer
if (mode == AnimationValidationMode.Unsafe) if (mode == AnimationValidationMode.Unsafe)
return true; return true;
var papBuckets = papBoneIndices.Keys var papByBucket = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
.Select(CanonicalizeSkeletonKey)
.Where(k => !string.IsNullOrEmpty(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
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"; reason = "No skeleton bucket bindings found in the PAP";
return false; return false;
} }
if (mode == AnimationValidationMode.Safe) static bool AllIndicesOk(
HashSet<ushort> available,
List<ushort> indices,
bool papLikelyOneBased,
bool allowOneBasedShift,
bool allowNeighborTolerance,
out ushort missing)
{ {
if (papBuckets.Any(b => localBoneSets.ContainsKey(b))) foreach (var idx in indices)
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))
{ {
reason = $"Missing skeleton bucket '{bucket}' on local actor."; if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
return false; {
missing = idx;
return false;
}
} }
var indices = papBoneIndices missing = 0;
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase)) return true;
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>()) }
.Distinct()
.ToList();
foreach (var (bucket, indices) in papByBucket)
{
if (indices.Count == 0) if (indices.Count == 0)
continue; continue;
@@ -423,14 +444,32 @@ public sealed partial class XivDataAnalyzer
} }
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0; 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}."; if (AllIndicesOk(ls, indices, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance, out _))
return false; 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; return true;