Refactored many parts, added settings for detection
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Data;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
@@ -20,6 +21,7 @@ public class PlayerDataFactory
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILogger<PlayerDataFactory> _logger;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly TransientResourceManager _transientResourceManager;
|
||||
@@ -45,7 +47,8 @@ public class PlayerDataFactory
|
||||
FileCacheManager fileReplacementFactory,
|
||||
PerformanceCollectorService performanceCollector,
|
||||
XivDataAnalyzer modelAnalyzer,
|
||||
LightlessMediator lightlessMediator)
|
||||
LightlessMediator lightlessMediator,
|
||||
LightlessConfigService configService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
@@ -55,6 +58,7 @@ public class PlayerDataFactory
|
||||
_performanceCollector = performanceCollector;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_configService = configService;
|
||||
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
||||
}
|
||||
private sealed record CacheEntry(CharacterDataFragment Fragment, DateTime CreatedUtc);
|
||||
@@ -338,7 +342,7 @@ public class PlayerDataFactory
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
||||
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
||||
!f.IsFileSwap && f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (hasPapFiles)
|
||||
{
|
||||
@@ -353,6 +357,7 @@ public class PlayerDataFactory
|
||||
if (hasPapFiles && boneIndices != null)
|
||||
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
|
||||
#endif
|
||||
|
||||
if (hasPapFiles)
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (CharacterDataFragmentPlayer)fragment, ct)
|
||||
@@ -458,77 +463,72 @@ public class PlayerDataFactory
|
||||
return (resolved, clearedReplacements);
|
||||
}
|
||||
|
||||
|
||||
private async Task VerifyPlayerAnimationBones(
|
||||
Dictionary<string, List<ushort>>? playerBoneIndices,
|
||||
CharacterDataFragmentPlayer fragment,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var mode = _configService.Current.AnimationValidationMode;
|
||||
|
||||
if (mode == AnimationValidationMode.Unsafe)
|
||||
return;
|
||||
|
||||
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
|
||||
return;
|
||||
|
||||
var playerBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (rawLocalKey, indices) in playerBoneIndices)
|
||||
{
|
||||
if (indices == null || indices.Count == 0)
|
||||
if (indices is not { Count: > 0 })
|
||||
continue;
|
||||
|
||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
|
||||
if (string.IsNullOrEmpty(key))
|
||||
continue;
|
||||
|
||||
if (!playerBoneSets.TryGetValue(key, out var set))
|
||||
playerBoneSets[key] = set = [];
|
||||
if (!localBoneSets.TryGetValue(key, out var set))
|
||||
localBoneSets[key] = set = [];
|
||||
|
||||
foreach (var idx in indices)
|
||||
set.Add(idx);
|
||||
}
|
||||
|
||||
if (playerBoneSets.Count == 0)
|
||||
if (localBoneSets.Count == 0)
|
||||
return;
|
||||
|
||||
var papFiles = fragment.FileReplacements
|
||||
.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("SEND local buckets: {b}",
|
||||
string.Join(", ", localBoneSets.Keys.Order(StringComparer.Ordinal)));
|
||||
|
||||
if (papFiles.Count == 0)
|
||||
return;
|
||||
foreach (var kvp in localBoneSets.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var min = kvp.Value.Count > 0 ? kvp.Value.Min() : 0;
|
||||
var max = kvp.Value.Count > 0 ? kvp.Value.Max() : 0;
|
||||
_logger.LogDebug("Local bucket {bucket}: count={count} min={min} max={max}",
|
||||
kvp.Key, kvp.Value.Count, min, max);
|
||||
}
|
||||
}
|
||||
|
||||
var papGroupsByHash = papFiles
|
||||
.Where(f => !string.IsNullOrEmpty(f.Hash))
|
||||
.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase)
|
||||
var papGroups = fragment.FileReplacements
|
||||
.Where(f => !f.IsFileSwap
|
||||
&& !string.IsNullOrEmpty(f.Hash)
|
||||
&& f.GamePaths is { Count: > 0 }
|
||||
&& f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.GroupBy(f => f.Hash!, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
int noValidationFailed = 0;
|
||||
|
||||
static ushort MaxIndex(List<ushort> list)
|
||||
{
|
||||
if (list == null || list.Count == 0) return 0;
|
||||
ushort max = 0;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
if (list[i] > max) max = list[i];
|
||||
return max;
|
||||
}
|
||||
|
||||
static bool ShouldIgnorePap(Dictionary<string, List<ushort>> pap)
|
||||
{
|
||||
foreach (var kv in pap)
|
||||
{
|
||||
if (kv.Value == null || kv.Value.Count == 0)
|
||||
continue;
|
||||
|
||||
if (MaxIndex(kv.Value) > 105)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var group in papGroupsByHash)
|
||||
foreach (var g in papGroups)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var hash = group.Key;
|
||||
Dictionary<string, List<ushort>>? papSkeletonIndices;
|
||||
var hash = g.Key;
|
||||
|
||||
Dictionary<string, List<ushort>>? papSkeletonIndices = null;
|
||||
|
||||
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
@@ -544,41 +544,36 @@ public class PlayerDataFactory
|
||||
if (papSkeletonIndices == null || papSkeletonIndices.Count == 0)
|
||||
continue;
|
||||
|
||||
if (ShouldIgnorePap(papSkeletonIndices))
|
||||
if (papSkeletonIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
continue;
|
||||
|
||||
bool invalid = false;
|
||||
string? reason = null;
|
||||
|
||||
foreach (var (rawPapName, usedIndices) in papSkeletonIndices)
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var papKey = XivDataAnalyzer.CanonicalizeSkeletonKey(rawPapName);
|
||||
if (string.IsNullOrEmpty(papKey))
|
||||
continue;
|
||||
|
||||
if (!playerBoneSets.TryGetValue(papKey, out var available))
|
||||
{
|
||||
invalid = true;
|
||||
reason = $"Missing skeleton bucket '{papKey}' (raw '{rawPapName}') on local player.";
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < usedIndices.Count; i++)
|
||||
{
|
||||
var idx = usedIndices[i];
|
||||
if (!available.Contains(idx))
|
||||
var papBuckets = papSkeletonIndices
|
||||
.Select(kvp => new
|
||||
{
|
||||
invalid = true;
|
||||
reason = $"Skeleton '{papKey}' missing bone index {idx} (raw '{rawPapName}').";
|
||||
break;
|
||||
}
|
||||
}
|
||||
Raw = kvp.Key,
|
||||
Key = XivDataAnalyzer.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)
|
||||
.Select(grp =>
|
||||
{
|
||||
var all = grp.SelectMany(v => v.Indices).ToList();
|
||||
var min = all.Count > 0 ? all.Min() : 0;
|
||||
var max = all.Count > 0 ? all.Max() : 0;
|
||||
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (invalid)
|
||||
break;
|
||||
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
|
||||
hash,
|
||||
string.Join(" | ", papBuckets));
|
||||
}
|
||||
|
||||
if (!invalid)
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papSkeletonIndices, mode, out var reason))
|
||||
continue;
|
||||
|
||||
noValidationFailed++;
|
||||
@@ -588,27 +583,36 @@ public class PlayerDataFactory
|
||||
hash,
|
||||
reason);
|
||||
|
||||
foreach (var file in group.ToList())
|
||||
{
|
||||
fragment.FileReplacements.Remove(file);
|
||||
var removedGamePaths = fragment.FileReplacements
|
||||
.Where(fr => !fr.IsFileSwap
|
||||
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.SelectMany(fr => fr.GamePaths.Where(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
foreach (var gamePath in file.GamePaths)
|
||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
||||
}
|
||||
fragment.FileReplacements.RemoveWhere(fr =>
|
||||
!fr.IsFileSwap
|
||||
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
foreach (var gp in removedGamePaths)
|
||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gp);
|
||||
}
|
||||
|
||||
if (noValidationFailed > 0)
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Invalid Skeleton Setup",
|
||||
$"Your client is attempting to send {noValidationFailed} animation file groups with bone indices not present on your current skeleton. " +
|
||||
"Those animation files have been removed from your sent data. Verify that you are using the correct skeleton for those animations " +
|
||||
"(Check /xllog for more information).",
|
||||
$"Your client is attempting to send {noValidationFailed} animation files that don't match your current skeleton validation mode ({mode}). " +
|
||||
"Please adjust your skeleton/mods or change the validation mode if this is unexpected. " +
|
||||
"Those animation files have been removed from your sent (player) data. (Check /xllog for details).",
|
||||
NotificationType.Warning,
|
||||
TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
||||
GameObjectHandler handler,
|
||||
HashSet<string> forwardResolve,
|
||||
@@ -681,7 +685,7 @@ public class PlayerDataFactory
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||
else
|
||||
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
||||
resolvedPaths[filePath] = [.. reverse[i].Select(c => c.ToLowerInvariant()).ToList()];
|
||||
}
|
||||
|
||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||
|
||||
Reference in New Issue
Block a user