Refactored many parts, added settings for detection

This commit is contained in:
cake
2026-01-03 14:58:54 +01:00
parent e16ddb0a1d
commit e41a7149c5
7 changed files with 481 additions and 280 deletions

View File

@@ -0,0 +1,9 @@
namespace LightlessSync.PlayerData.Factories
{
public enum AnimationValidationMode
{
Unsafe = 0,
Safe = 1,
Safest = 2,
}
}

View File

@@ -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();

View File

@@ -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)));
}
}

View File

@@ -1,5 +1,6 @@
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
@@ -32,6 +33,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer;
public PairHandlerAdapterFactory(
@@ -52,7 +54,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor,
XivDataAnalyzer modelAnalyzer)
XivDataAnalyzer modelAnalyzer,
LightlessConfigService configService)
{
_loggerFactory = loggerFactory;
_mediator = mediator;
@@ -72,6 +75,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer;
_configService = configService;
}
public IPairHandlerAdapter Create(string ident)
@@ -99,6 +103,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pairStateCache,
_pairPerformanceMetricsCache,
_tempCollectionJanitor,
_modelAnalyzer);
_modelAnalyzer,
_configService);
}
}