Added more loose matching options, fixed some race issues
This commit is contained in:
@@ -158,4 +158,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
|
||||
public bool AnimationAllowOneBasedShift { get; set; } = true;
|
||||
|
||||
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -470,6 +470,8 @@ public class PlayerDataFactory
|
||||
CancellationToken ct)
|
||||
{
|
||||
var mode = _configService.Current.AnimationValidationMode;
|
||||
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
||||
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
||||
|
||||
if (mode == AnimationValidationMode.Unsafe)
|
||||
return;
|
||||
@@ -528,12 +530,12 @@ public class PlayerDataFactory
|
||||
|
||||
var hash = g.Key;
|
||||
|
||||
Dictionary<string, List<ushort>>? papSkeletonIndices = null;
|
||||
Dictionary<string, List<ushort>>? papIndices = null;
|
||||
|
||||
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
papSkeletonIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
|
||||
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
@@ -541,15 +543,15 @@ public class PlayerDataFactory
|
||||
_papParseLimiter.Release();
|
||||
}
|
||||
|
||||
if (papSkeletonIndices == null || papSkeletonIndices.Count == 0)
|
||||
if (papIndices == null || papIndices.Count == 0)
|
||||
continue;
|
||||
|
||||
if (papSkeletonIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
continue;
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var papBuckets = papSkeletonIndices
|
||||
var papBuckets = papIndices
|
||||
.Select(kvp => new
|
||||
{
|
||||
Raw = kvp.Key,
|
||||
@@ -573,7 +575,7 @@ public class PlayerDataFactory
|
||||
string.Join(" | ", papBuckets));
|
||||
}
|
||||
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papSkeletonIndices, mode, out var reason))
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
|
||||
continue;
|
||||
|
||||
noValidationFailed++;
|
||||
|
||||
@@ -93,7 +93,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
".avfx",
|
||||
".scd"
|
||||
};
|
||||
|
||||
private readonly ConcurrentDictionary<string, byte> _blockedPapHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, byte> _dumpedRemoteSkeletonForHash = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private DateTime? _invisibleSinceUtc;
|
||||
private DateTime? _visibilityEvictionDueAtUtc;
|
||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||
@@ -2389,6 +2392,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
CancellationToken token)
|
||||
{
|
||||
var mode = _configService.Current.AnimationValidationMode;
|
||||
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
||||
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
||||
|
||||
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
|
||||
return 0;
|
||||
|
||||
@@ -2432,7 +2438,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
continue;
|
||||
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, out var reason))
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
|
||||
continue;
|
||||
|
||||
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3114,7 +3114,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
var tooltips = new[]
|
||||
{
|
||||
"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).",
|
||||
};
|
||||
|
||||
@@ -3154,6 +3154,27 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
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();
|
||||
animationTree.MarkContentEnd();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user