Added more loose matching options, fixed some race issues
This commit is contained in:
@@ -236,7 +236,7 @@ public sealed partial class XivDataAnalyzer
|
||||
|
||||
if (!tempSets.TryGetValue(skeletonKey, out var set))
|
||||
{
|
||||
set = new HashSet<ushort>();
|
||||
set = [];
|
||||
tempSets[skeletonKey] = set;
|
||||
}
|
||||
|
||||
@@ -325,13 +325,37 @@ public sealed partial class XivDataAnalyzer
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static bool ContainsIndexCompat(HashSet<ushort> available, ushort idx, bool papLikelyOneBased)
|
||||
public static bool ContainsIndexCompat(
|
||||
HashSet<ushort> available,
|
||||
ushort idx,
|
||||
bool papLikelyOneBased,
|
||||
bool allowOneBasedShift,
|
||||
bool allowNeighborTolerance)
|
||||
{
|
||||
if (available.Contains(idx))
|
||||
return true;
|
||||
Span<ushort> candidates = stackalloc ushort[2];
|
||||
int count = 0;
|
||||
|
||||
if (papLikelyOneBased && idx > 0 && available.Contains((ushort)(idx - 1)))
|
||||
return true;
|
||||
candidates[count++] = idx;
|
||||
|
||||
if (allowOneBasedShift && papLikelyOneBased && idx > 0)
|
||||
candidates[count++] = (ushort)(idx - 1);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var c = candidates[i];
|
||||
|
||||
if (available.Contains(c))
|
||||
return true;
|
||||
|
||||
if (allowNeighborTolerance)
|
||||
{
|
||||
if (c > 0 && available.Contains((ushort)(c - 1)))
|
||||
return true;
|
||||
|
||||
if (c < ushort.MaxValue && available.Contains((ushort)(c + 1)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -340,141 +364,73 @@ public sealed partial class XivDataAnalyzer
|
||||
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
|
||||
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
|
||||
AnimationValidationMode mode,
|
||||
bool allowOneBasedShift,
|
||||
bool allowNeighborTolerance,
|
||||
out string reason)
|
||||
{
|
||||
if (mode == AnimationValidationMode.Unsafe)
|
||||
{
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
reason = string.Empty;
|
||||
|
||||
// Group PAP bindings by canonical skeleton key (with raw as fallback)
|
||||
var groups = papBoneIndices
|
||||
.Select(kvp => new
|
||||
{
|
||||
Raw = kvp.Key,
|
||||
Key = CanonicalizeSkeletonKey(kvp.Key),
|
||||
Indices = kvp.Value
|
||||
})
|
||||
.Where(x => x.Indices is { Count: > 0 })
|
||||
.GroupBy(
|
||||
x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key,
|
||||
StringComparer.OrdinalIgnoreCase)
|
||||
if (mode == AnimationValidationMode.Unsafe)
|
||||
return true;
|
||||
|
||||
var papBuckets = papBoneIndices.Keys
|
||||
.Select(CanonicalizeSkeletonKey)
|
||||
.Where(k => !string.IsNullOrEmpty(k))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (groups.Count == 0)
|
||||
if (papBuckets.Count == 0)
|
||||
{
|
||||
reason = "No bindings found in the PAP";
|
||||
reason = "No skeleton bucket bindings found in the PAP";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine relevant groups based on mode
|
||||
var relevantGroups = groups.AsEnumerable();
|
||||
|
||||
if (mode == AnimationValidationMode.Safest)
|
||||
if (mode == AnimationValidationMode.Safe)
|
||||
{
|
||||
relevantGroups = groups.Where(g => localBoneSets.ContainsKey(g.Key));
|
||||
if (papBuckets.Any(b => localBoneSets.ContainsKey(b)))
|
||||
return true;
|
||||
|
||||
if (!relevantGroups.Any())
|
||||
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))
|
||||
{
|
||||
var papKeys = string.Join(", ", groups.Select(g => g.Key).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
var localKeys = string.Join(", ", localBoneSets.Keys.Order(StringComparer.OrdinalIgnoreCase));
|
||||
reason = $"No matching skeleton bucket between PAP [{papKeys}] and local [{localKeys}].";
|
||||
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var indices = papBoneIndices
|
||||
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (indices.Count == 0)
|
||||
continue;
|
||||
|
||||
bool has0 = false, has1 = false;
|
||||
ushort min = ushort.MaxValue;
|
||||
foreach (var v in indices)
|
||||
{
|
||||
if (v == 0) has0 = true;
|
||||
if (v == 1) has1 = true;
|
||||
if (v < min) min = v;
|
||||
}
|
||||
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
|
||||
|
||||
foreach (var idx in indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
|
||||
{
|
||||
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var g in relevantGroups)
|
||||
{
|
||||
// Each group may have multiple variants (different raw names mapping to same canonical key)
|
||||
bool anyVariantOk = false;
|
||||
|
||||
foreach (var variant in g)
|
||||
{
|
||||
// Check this variant against local skeleton(s)
|
||||
var min = variant.Indices.Min();
|
||||
var papLikelyOneBased = min == 1 && !variant.Indices.Contains(0);
|
||||
|
||||
bool variantOk;
|
||||
|
||||
if (mode == AnimationValidationMode.Safest)
|
||||
{
|
||||
var available = localBoneSets[g.Key];
|
||||
|
||||
variantOk = true;
|
||||
foreach (var idx in variant.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx, papLikelyOneBased))
|
||||
{
|
||||
variantOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Safe mode: any local skeleton matching this canonical key
|
||||
variantOk = false;
|
||||
|
||||
foreach (var available in localBoneSets.Values)
|
||||
{
|
||||
bool ok = true;
|
||||
foreach (var idx in variant.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx, papLikelyOneBased))
|
||||
{
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
variantOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (variantOk)
|
||||
{
|
||||
anyVariantOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyVariantOk)
|
||||
{
|
||||
// No variant was compatible for this skeleton key
|
||||
var first = g.First();
|
||||
ushort? missing = null;
|
||||
|
||||
HashSet<ushort> best;
|
||||
if (mode == AnimationValidationMode.Safest && localBoneSets.TryGetValue(g.Key, out var exact))
|
||||
best = exact;
|
||||
else
|
||||
best = localBoneSets.Values.OrderByDescending(s => s.Count).First();
|
||||
|
||||
var min = first.Indices.Min();
|
||||
var papLikelyOneBased = min == 1 && !first.Indices.Contains(0);
|
||||
|
||||
foreach (var idx in first.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(best, idx, papLikelyOneBased))
|
||||
{
|
||||
missing = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reason = missing.HasValue
|
||||
? $"Skeleton '{g.Key}' missing bone index {missing.Value}. (raw '{first.Raw}')"
|
||||
: $"Skeleton '{g.Key}' missing required bone indices. (raw '{first.Raw}')";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -486,7 +442,7 @@ public sealed partial class XivDataAnalyzer
|
||||
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var keys = skels.Keys
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
Reference in New Issue
Block a user