Refactored many parts, added settings for detection
This commit is contained in:
@@ -22,7 +22,7 @@ using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
@@ -49,6 +49,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairManager _pairManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||
private Guid _applicationId;
|
||||
@@ -188,7 +189,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache performanceMetricsCache,
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||
XivDataAnalyzer modelAnalyzer) : base(logger, mediator)
|
||||
XivDataAnalyzer modelAnalyzer,
|
||||
LightlessConfigService configService) : base(logger, mediator)
|
||||
{
|
||||
_pairManager = pairManager;
|
||||
Ident = ident;
|
||||
@@ -208,6 +210,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_performanceMetricsCache = performanceMetricsCache;
|
||||
_tempCollectionJanitor = tempCollectionJanitor;
|
||||
_modelAnalyzer = modelAnalyzer;
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
@@ -1736,45 +1739,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_needsCollectionRebuild = false;
|
||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||
{
|
||||
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
|
||||
}
|
||||
if (LastAppliedDataTris < 0)
|
||||
{
|
||||
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
|
||||
}
|
||||
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
|
||||
}
|
||||
if (LastAppliedDataTris < 0)
|
||||
{
|
||||
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
StorePerformanceMetrics(charaData);
|
||||
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
||||
ClearFailureState();
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
_forceFullReapply = true;
|
||||
RecordFailure("Application cancelled", "Cancellation");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||
StorePerformanceMetrics(charaData);
|
||||
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
||||
ClearFailureState();
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
IsVisible = false;
|
||||
_forceApplyMods = true;
|
||||
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
_forceFullReapply = true;
|
||||
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||
RecordFailure("Application cancelled", "Cancellation");
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||
_forceFullReapply = true;
|
||||
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||
{
|
||||
IsVisible = false;
|
||||
_forceApplyMods = true;
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
_forceFullReapply = true;
|
||||
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||
_forceFullReapply = true;
|
||||
}
|
||||
RecordFailure($"Application failed: {ex.Message}", "Exception");
|
||||
}
|
||||
RecordFailure($"Application failed: {ex.Message}", "Exception");
|
||||
}
|
||||
}
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
@@ -2008,12 +2011,27 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
|
||||
{
|
||||
hasMigrationChanges = true;
|
||||
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
|
||||
var anyGamePath = item.GamePaths.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrEmpty(anyGamePath))
|
||||
{
|
||||
var ext = Path.GetExtension(anyGamePath);
|
||||
var extNoDot = ext.StartsWith('.') ? ext[1..] : ext;
|
||||
|
||||
if (!string.IsNullOrEmpty(extNoDot))
|
||||
{
|
||||
hasMigrationChanges = true;
|
||||
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, extNoDot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
if (gamePath.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
|
||||
var mode = _configService.Current.AnimationValidationMode;
|
||||
|
||||
if (mode != AnimationValidationMode.Unsafe
|
||||
&& gamePath.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(item.Hash)
|
||||
&& _blockedPapHashes.ContainsKey(item.Hash))
|
||||
{
|
||||
@@ -2346,100 +2364,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return !string.IsNullOrEmpty(hashedCid);
|
||||
}
|
||||
|
||||
private static bool ContainsIndexCompat(HashSet<ushort> available, ushort idx)
|
||||
{
|
||||
if (available.Contains(idx)) return true;
|
||||
|
||||
if (idx > 0 && available.Contains((ushort)(idx - 1))) return true;
|
||||
if (idx < ushort.MaxValue && available.Contains((ushort)(idx + 1))) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPapCompatible(
|
||||
IReadOnlyDictionary<string, HashSet<ushort>> targetBoneSets,
|
||||
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
|
||||
out string reason)
|
||||
{
|
||||
var groups = papBoneIndices
|
||||
.Select(kvp => new
|
||||
{
|
||||
Raw = kvp.Key,
|
||||
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
||||
Indices = kvp.Value
|
||||
})
|
||||
.Where(x => !string.IsNullOrEmpty(x.Key) && x.Indices is { Count: > 0 })
|
||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
reason = "No bindings found in the PAP";
|
||||
return false;
|
||||
}
|
||||
|
||||
var papKeys = string.Join(", ", groups.Select(g => g.Key).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
var targetKeys = string.Join(", ", targetBoneSets.Keys);
|
||||
|
||||
foreach (var g in groups)
|
||||
{
|
||||
var candidates = targetBoneSets
|
||||
.Where(kvp => string.Equals(kvp.Key, g.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
if (targetBoneSets.Count == 1)
|
||||
{
|
||||
candidates = targetBoneSets.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = $"No matching skeleton bucket between PAP [{papKeys}] and target [{targetKeys}].";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool groupOk = false;
|
||||
string? lastFail = null;
|
||||
|
||||
foreach (var (targetKey, available) in candidates)
|
||||
{
|
||||
foreach (var variant in g)
|
||||
{
|
||||
bool ok = true;
|
||||
foreach (var idx in variant.Indices)
|
||||
{
|
||||
if (!ContainsIndexCompat(available, idx))
|
||||
{
|
||||
ok = false;
|
||||
lastFail = $"Target bucket '{targetKey}' missing bone index {idx}. (pap raw '{variant.Raw}')";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok)
|
||||
{
|
||||
groupOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (groupOk) break;
|
||||
}
|
||||
|
||||
if (!groupOk)
|
||||
{
|
||||
reason = lastFail
|
||||
?? $"Target skeleton missing required bone indices for PAP bucket '{g.Key}'.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void SplitPapMappings(
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths,
|
||||
out Dictionary<(string GamePath, string? Hash), string> withoutPap,
|
||||
@@ -2464,7 +2388,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
Dictionary<(string GamePath, string? Hash), string> papOnly,
|
||||
CancellationToken token)
|
||||
{
|
||||
if (papOnly.Count == 0)
|
||||
var mode = _configService.Current.AnimationValidationMode;
|
||||
if (mode == AnimationValidationMode.Unsafe || papOnly.Count == 0)
|
||||
return 0;
|
||||
|
||||
var boneIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||
@@ -2472,15 +2397,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (boneIndices == null || boneIndices.Count == 0)
|
||||
return papOnly.Count;
|
||||
{
|
||||
var removedCount = papOnly.Count;
|
||||
papOnly.Clear();
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (rawKey, list) in boneIndices)
|
||||
{
|
||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawKey);
|
||||
if (string.IsNullOrEmpty(key) || list is null || list.Count == 0)
|
||||
continue;
|
||||
if (string.IsNullOrEmpty(key)) continue;
|
||||
|
||||
if (!localBoneSets.TryGetValue(key, out var set))
|
||||
localBoneSets[key] = set = [];
|
||||
@@ -2496,8 +2423,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
||||
() => _modelAnalyzer.GetBoneIndicesFromPap(hash!))
|
||||
.ConfigureAwait(false);
|
||||
() => _modelAnalyzer.GetBoneIndicesFromPap(hash!))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (papIndices == null || papIndices.Count == 0)
|
||||
continue;
|
||||
@@ -2505,26 +2432,22 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||
continue;
|
||||
|
||||
if (!IsPapCompatible(localBoneSets, papIndices, out var reason))
|
||||
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, out var reason))
|
||||
continue;
|
||||
|
||||
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
foreach (var k in keysToRemove)
|
||||
papOnly.Remove(k);
|
||||
|
||||
removed += keysToRemove.Count;
|
||||
|
||||
if (_blockedPapHashes.TryAdd(hash!, 0))
|
||||
Logger.LogWarning("Blocked remote object PAP (hash {hash}) for {handler}: {reason}", hash, GetLogIdentifier(), reason);
|
||||
|
||||
if (charaData.FileReplacements.TryGetValue(ObjectKind.Player, out var list))
|
||||
{
|
||||
var keysToRemove = papOnly.Keys.Where(k => string.Equals(k.Hash, hash, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
foreach (var k in keysToRemove)
|
||||
papOnly.Remove(k);
|
||||
|
||||
removed += keysToRemove.Count;
|
||||
if (hash == null)
|
||||
continue;
|
||||
|
||||
if (_blockedPapHashes.TryAdd(hash, 0))
|
||||
{
|
||||
Logger.LogWarning("Blocked remote object PAP (hash {hash}) for {handler}: {reason}", hash, GetLogIdentifier(), reason);
|
||||
}
|
||||
|
||||
if (charaData.FileReplacements.TryGetValue(ObjectKind.Player, out var list))
|
||||
{
|
||||
list.RemoveAll(r => string.Equals(r.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& r.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
list.RemoveAll(r => string.Equals(r.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& r.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user