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 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +442,7 @@ public sealed partial class XivDataAnalyzer
|
|||||||
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
|
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = skels.Keys
|
var keys = skels.Keys
|
||||||
.Order(StringComparer.OrdinalIgnoreCase)
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user