Attempt fix on crash.
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user