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

@@ -158,4 +158,7 @@ public class LightlessConfig : ILightlessConfiguration
public string LastSeenVersion { get; set; } = string.Empty; public string LastSeenVersion { get; set; } = string.Empty;
public HashSet<Guid> OrphanableTempCollections { get; set; } = []; public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe; public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
public bool AnimationAllowOneBasedShift { get; set; } = true;
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
} }

View File

@@ -470,6 +470,8 @@ public class PlayerDataFactory
CancellationToken ct) CancellationToken ct)
{ {
var mode = _configService.Current.AnimationValidationMode; var mode = _configService.Current.AnimationValidationMode;
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
if (mode == AnimationValidationMode.Unsafe) if (mode == AnimationValidationMode.Unsafe)
return; return;
@@ -528,12 +530,12 @@ public class PlayerDataFactory
var hash = g.Key; var hash = g.Key;
Dictionary<string, List<ushort>>? papSkeletonIndices = null; Dictionary<string, List<ushort>>? papIndices = null;
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false); await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
try try
{ {
papSkeletonIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
finally finally
@@ -541,15 +543,15 @@ public class PlayerDataFactory
_papParseLimiter.Release(); _papParseLimiter.Release();
} }
if (papSkeletonIndices == null || papSkeletonIndices.Count == 0) if (papIndices == null || papIndices.Count == 0)
continue; continue;
if (papSkeletonIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
continue; continue;
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
var papBuckets = papSkeletonIndices var papBuckets = papIndices
.Select(kvp => new .Select(kvp => new
{ {
Raw = kvp.Key, Raw = kvp.Key,
@@ -573,7 +575,7 @@ public class PlayerDataFactory
string.Join(" | ", papBuckets)); string.Join(" | ", papBuckets));
} }
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papSkeletonIndices, mode, out var reason)) if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
continue; continue;
noValidationFailed++; noValidationFailed++;

View File

@@ -93,7 +93,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
".avfx", ".avfx",
".scd" ".scd"
}; };
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, byte> _dumpedRemoteSkeletonForHash = new(StringComparer.OrdinalIgnoreCase);
private DateTime? _invisibleSinceUtc; private DateTime? _invisibleSinceUtc;
private DateTime? _visibilityEvictionDueAtUtc; private DateTime? _visibilityEvictionDueAtUtc;
private DateTime _nextActorLookupUtc = DateTime.MinValue; private DateTime _nextActorLookupUtc = DateTime.MinValue;
@@ -2389,6 +2392,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
CancellationToken token) CancellationToken token)
{ {
var mode = _configService.Current.AnimationValidationMode; var mode = _configService.Current.AnimationValidationMode;
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0) if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
return 0; return 0;
@@ -2432,7 +2438,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
continue; continue;
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, out var reason)) if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
continue; continue;
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList(); var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();

View File

@@ -236,7 +236,7 @@ public sealed partial class XivDataAnalyzer
if (!tempSets.TryGetValue(skeletonKey, out var set)) if (!tempSets.TryGetValue(skeletonKey, out var set))
{ {
set = new HashSet<ushort>(); set = [];
tempSets[skeletonKey] = set; tempSets[skeletonKey] = set;
} }
@@ -325,13 +325,37 @@ public sealed partial class XivDataAnalyzer
return string.Empty; 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)) Span<ushort> candidates = stackalloc ushort[2];
return true; int count = 0;
if (papLikelyOneBased && idx > 0 && available.Contains((ushort)(idx - 1))) candidates[count++] = idx;
return true;
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; return false;
} }
@@ -340,141 +364,73 @@ public sealed partial class XivDataAnalyzer
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets, IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
IReadOnlyDictionary<string, List<ushort>> papBoneIndices, IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
AnimationValidationMode mode, AnimationValidationMode mode,
bool allowOneBasedShift,
bool allowNeighborTolerance,
out string reason) out string reason)
{ {
if (mode == AnimationValidationMode.Unsafe) reason = string.Empty;
{
reason = string.Empty;
return true;
}
// Group PAP bindings by canonical skeleton key (with raw as fallback) if (mode == AnimationValidationMode.Unsafe)
var groups = papBoneIndices return true;
.Select(kvp => new
{ var papBuckets = papBoneIndices.Keys
Raw = kvp.Key, .Select(CanonicalizeSkeletonKey)
Key = CanonicalizeSkeletonKey(kvp.Key), .Where(k => !string.IsNullOrEmpty(k))
Indices = kvp.Value .Distinct(StringComparer.OrdinalIgnoreCase)
})
.Where(x => x.Indices is { Count: > 0 })
.GroupBy(
x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key,
StringComparer.OrdinalIgnoreCase)
.ToList(); .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; return false;
} }
// Determine relevant groups based on mode if (mode == AnimationValidationMode.Safe)
var relevantGroups = groups.AsEnumerable();
if (mode == AnimationValidationMode.Safest)
{ {
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)); reason = $"Missing skeleton bucket '{bucket}' on local actor.";
var localKeys = string.Join(", ", localBoneSets.Keys.Order(StringComparer.OrdinalIgnoreCase));
reason = $"No matching skeleton bucket between PAP [{papKeys}] and local [{localKeys}].";
return false; 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; return true;
} }

View File

@@ -3114,7 +3114,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var tooltips = new[] var tooltips = new[]
{ {
"No validation. Fastest, but may allow incompatible animations (riskier).", "No validation. Fastest, but may allow incompatible animations (riskier).",
"Validates bone indices against your current skeleton (recommended).", "Validates skeleton race (recommended).",
"Requires matching skeleton race + bone compatibility (strictest).", "Requires matching skeleton race + bone compatibility (strictest).",
}; };
@@ -3154,6 +3154,27 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
var cfg = _configService.Current;
bool oneBased = cfg.AnimationAllowOneBasedShift;
if (ImGui.Checkbox("Treat 1-based PAP indices as compatible", ref oneBased))
{
cfg.AnimationAllowOneBasedShift = oneBased;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Fixes off-by-one PAPs (one bone differance in bones and PAP). Can also increase crashing, toggle off if alot of crashing is happening");
bool neighbor = cfg.AnimationAllowNeighborIndexTolerance;
if (ImGui.Checkbox("Allow 1+- bone index tolerance", ref neighbor))
{
cfg.AnimationAllowNeighborIndexTolerance = neighbor;
_configService.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Looser matching on bone matching. Can reduce false blocks happening but also reduces safety and more prone to crashing.");
ImGui.TreePop(); ImGui.TreePop();
animationTree.MarkContentEnd(); animationTree.MarkContentEnd();
} }