Added more loose matching options, fixed some race issues

This commit is contained in:
cake
2026-01-03 15:59:10 +01:00
parent e41a7149c5
commit 02d091eefa
5 changed files with 122 additions and 134 deletions

View File

@@ -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();