|
|
|
|
@@ -16,43 +16,17 @@ using LightlessSync.Services.TextureCompression;
|
|
|
|
|
using LightlessSync.Utils;
|
|
|
|
|
using LightlessSync.WebAPI.Files;
|
|
|
|
|
using LightlessSync.WebAPI.Files.Models;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
|
|
|
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
|
|
|
|
|
|
|
|
|
namespace LightlessSync.PlayerData.Pairs;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// orchestrates the lifecycle of a paired character
|
|
|
|
|
/// handles lifecycle, visibility, queued data, character data for a paired user
|
|
|
|
|
/// </summary>
|
|
|
|
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
|
|
|
|
{
|
|
|
|
|
string Ident { get; }
|
|
|
|
|
bool Initialized { get; }
|
|
|
|
|
bool IsVisible { get; }
|
|
|
|
|
bool ScheduledForDeletion { get; set; }
|
|
|
|
|
CharacterData? LastReceivedCharacterData { get; }
|
|
|
|
|
long LastAppliedDataBytes { get; }
|
|
|
|
|
string? PlayerName { get; }
|
|
|
|
|
string PlayerNameHash { get; }
|
|
|
|
|
uint PlayerCharacterId { get; }
|
|
|
|
|
|
|
|
|
|
void Initialize();
|
|
|
|
|
void ApplyData(CharacterData data);
|
|
|
|
|
void ApplyLastReceivedData(bool forced = false);
|
|
|
|
|
bool FetchPerformanceMetricsFromCache();
|
|
|
|
|
void LoadCachedCharacterData(CharacterData data);
|
|
|
|
|
void SetUploading(bool uploading);
|
|
|
|
|
void SetPaused(bool paused);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public interface IPairHandlerAdapterFactory
|
|
|
|
|
{
|
|
|
|
|
IPairHandlerAdapter Create(string ident);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPairHandlerAdapter, IPairPerformanceSubject
|
|
|
|
|
internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPairHandlerAdapter
|
|
|
|
|
{
|
|
|
|
|
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
|
|
|
|
|
|
|
|
|
@@ -70,14 +44,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private readonly PairStateCache _pairStateCache;
|
|
|
|
|
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
|
|
|
|
private readonly PairManager _pairManager;
|
|
|
|
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
|
|
|
|
private CancellationTokenSource? _applicationCancellationTokenSource;
|
|
|
|
|
private Guid _applicationId;
|
|
|
|
|
private Task? _applicationTask;
|
|
|
|
|
private CharacterData? _cachedData = null;
|
|
|
|
|
private GameObjectHandler? _charaHandler;
|
|
|
|
|
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
|
|
|
|
private CombatData? _dataReceivedInDowntime;
|
|
|
|
|
private CancellationTokenSource? _downloadCancellationTokenSource = new();
|
|
|
|
|
private CancellationTokenSource? _downloadCancellationTokenSource;
|
|
|
|
|
private bool _forceApplyMods = false;
|
|
|
|
|
private bool _forceFullReapply;
|
|
|
|
|
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
|
|
|
|
@@ -86,6 +60,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private Guid _penumbraCollection;
|
|
|
|
|
private readonly object _collectionGate = new();
|
|
|
|
|
private bool _redrawOnNextApplication = false;
|
|
|
|
|
private bool _explicitRedrawQueued;
|
|
|
|
|
private readonly object _initializationGate = new();
|
|
|
|
|
private readonly object _pauseLock = new();
|
|
|
|
|
private Task _pauseTransitionTask = Task.CompletedTask;
|
|
|
|
|
@@ -183,7 +158,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user = GetPrimaryUserData();
|
|
|
|
|
if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0
|
|
|
|
|
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
|
|
|
|
{
|
|
|
|
|
@@ -441,9 +415,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return combined;
|
|
|
|
|
}
|
|
|
|
|
public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero;
|
|
|
|
|
public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero
|
|
|
|
|
? uint.MaxValue
|
|
|
|
|
: ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId;
|
|
|
|
|
public uint PlayerCharacterId => _charaHandler?.EntityId ?? uint.MaxValue;
|
|
|
|
|
public string? PlayerName { get; private set; }
|
|
|
|
|
public string PlayerNameHash => Ident;
|
|
|
|
|
|
|
|
|
|
@@ -490,14 +462,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (shouldForce)
|
|
|
|
|
{
|
|
|
|
|
_forceApplyMods = true;
|
|
|
|
|
_cachedData = null;
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
LastAppliedDataBytes = -1;
|
|
|
|
|
LastAppliedDataTris = -1;
|
|
|
|
|
LastAppliedApproximateVRAMBytes = -1;
|
|
|
|
|
LastAppliedApproximateEffectiveVRAMBytes = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
|
|
|
|
|
var sanitized = CloneAndSanitizeLastReceived(out _);
|
|
|
|
|
if (sanitized is null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
|
|
|
|
|
@@ -746,7 +718,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (characterData is null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier());
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -757,7 +729,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: you are in combat, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -767,7 +739,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: you are performing music, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -777,7 +749,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: you are in an instance, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -787,7 +759,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: you are in a cutscene, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -797,7 +769,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: you are in GPose, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -807,7 +779,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: Penumbra or Glamourer is not available, deferring application")));
|
|
|
|
|
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -828,7 +800,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), forceApplyCustomization, _forceApplyMods);
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
|
|
|
|
@@ -850,10 +822,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_forceApplyMods = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_explicitRedrawQueued = false;
|
|
|
|
|
|
|
|
|
|
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
|
|
|
|
{
|
|
|
|
|
player.Add(PlayerChanges.ForcedRedraw);
|
|
|
|
|
_redrawOnNextApplication = false;
|
|
|
|
|
_explicitRedrawQueued = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
|
|
|
|
@@ -863,7 +838,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe());
|
|
|
|
|
|
|
|
|
|
var forceFullReapply = _forceFullReapply || forceApplyCustomization
|
|
|
|
|
var forceFullReapply = _forceFullReapply
|
|
|
|
|
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0;
|
|
|
|
|
|
|
|
|
|
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply);
|
|
|
|
|
@@ -875,12 +850,12 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return $"{alias}:{PlayerName ?? string.Empty}:{(PlayerCharacter != nint.Zero ? "HasChar" : "NoChar")}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetUploading(bool isUploading = true)
|
|
|
|
|
public void SetUploading(bool uploading)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), isUploading);
|
|
|
|
|
Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), uploading);
|
|
|
|
|
if (_charaHandler != null)
|
|
|
|
|
{
|
|
|
|
|
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
|
|
|
|
|
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, uploading));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -904,7 +879,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
base.Dispose(disposing);
|
|
|
|
|
|
|
|
|
|
SetUploading(isUploading: false);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
var name = PlayerName;
|
|
|
|
|
var user = GetPrimaryUserDataSafe();
|
|
|
|
|
var alias = GetPrimaryAliasOrUidSafe();
|
|
|
|
|
@@ -1046,6 +1021,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.ForcedRedraw:
|
|
|
|
|
if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
@@ -1061,6 +1041,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection<PlayerChanges> changeSet, CharacterData newData)
|
|
|
|
|
{
|
|
|
|
|
if (objectKind != ObjectKind.Player)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles);
|
|
|
|
|
var hasManip = changeSet.Contains(PlayerChanges.ModManip);
|
|
|
|
|
var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData);
|
|
|
|
|
var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
|
|
|
|
|
|
|
|
|
if (modsChanged)
|
|
|
|
|
{
|
|
|
|
|
_explicitRedrawQueued = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (manipChanged)
|
|
|
|
|
{
|
|
|
|
|
_explicitRedrawQueued = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_explicitRedrawQueued)
|
|
|
|
|
{
|
|
|
|
|
_explicitRedrawQueued = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild))
|
|
|
|
|
{
|
|
|
|
|
_explicitRedrawQueued = false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
|
|
|
|
{
|
|
|
|
|
var result = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
|
|
|
|
@@ -1126,6 +1145,39 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool PlayerModFilesChanged(CharacterData newData, CharacterData? previousData)
|
|
|
|
|
{
|
|
|
|
|
return !FileReplacementListsEqual(
|
|
|
|
|
TryGetFileReplacementList(newData, ObjectKind.Player),
|
|
|
|
|
TryGetFileReplacementList(previousData, ObjectKind.Player));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static IReadOnlyCollection<FileReplacementData>? TryGetFileReplacementList(CharacterData? data, ObjectKind objectKind)
|
|
|
|
|
{
|
|
|
|
|
if (data is null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data.FileReplacements.TryGetValue(objectKind, out var list) ? list : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool FileReplacementListsEqual(IReadOnlyCollection<FileReplacementData>? left, IReadOnlyCollection<FileReplacementData>? right)
|
|
|
|
|
{
|
|
|
|
|
if (left is null || left.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return right is null || right.Count == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (right is null || right.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var comparer = FileReplacementDataComparer.Instance;
|
|
|
|
|
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool forceFullReapply)
|
|
|
|
|
{
|
|
|
|
|
if (!updatedData.Any())
|
|
|
|
|
@@ -1165,7 +1217,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
|
|
|
|
var downloadToken = _downloadCancellationTokenSource.Token;
|
|
|
|
|
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken).ConfigureAwait(false);
|
|
|
|
|
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task? _pairDownloadTask;
|
|
|
|
|
@@ -1173,107 +1226,114 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
|
|
|
|
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
|
|
|
|
{
|
|
|
|
|
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
|
|
|
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
|
|
|
|
var user = GetPrimaryUserData();
|
|
|
|
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
|
|
|
|
|
|
|
|
|
if (updateModdedPaths)
|
|
|
|
|
var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (cachedModdedPaths is not null)
|
|
|
|
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
|
|
|
|
var user = GetPrimaryUserData();
|
|
|
|
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
|
|
|
|
|
|
|
|
|
if (updateModdedPaths)
|
|
|
|
|
{
|
|
|
|
|
moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer);
|
|
|
|
|
if (cachedModdedPaths is not null)
|
|
|
|
|
{
|
|
|
|
|
moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
int attempts = 0;
|
|
|
|
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
|
|
|
|
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
|
|
|
|
await _pairDownloadTask.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
|
|
|
|
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
|
|
|
|
$"Starting download for {toDownloadReplacements.Count} files")));
|
|
|
|
|
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
|
|
|
|
{
|
|
|
|
|
_downloadManager.ClearDownload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var handlerForDownload = _charaHandler;
|
|
|
|
|
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
|
|
|
|
|
|
|
|
|
|
await _pairDownloadTask.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
|
|
|
|
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
int attempts = 0;
|
|
|
|
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
|
|
|
|
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
|
|
|
|
await _pairDownloadTask.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
|
|
|
|
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
|
|
|
|
$"Starting download for {toDownloadReplacements.Count} files")));
|
|
|
|
|
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
|
|
|
|
{
|
|
|
|
|
_downloadManager.ClearDownload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var handlerForDownload = _charaHandler;
|
|
|
|
|
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
|
|
|
|
|
|
|
|
|
|
await _pairDownloadTask.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
|
|
|
|
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
moddedPaths = cachedModdedPaths is not null
|
|
|
|
|
? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer)
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
var handlerForApply = _charaHandler;
|
|
|
|
|
if (handlerForApply is null || handlerForApply.Address == nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier());
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var appToken = _applicationCancellationTokenSource?.Token;
|
|
|
|
|
while ((!_applicationTask?.IsCompleted ?? false)
|
|
|
|
|
&& !downloadToken.IsCancellationRequested
|
|
|
|
|
&& (!appToken?.IsCancellationRequested ?? false))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
|
|
|
|
await Task.Delay(250).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
|
|
|
|
{
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
|
|
|
|
var token = _applicationCancellationTokenSource.Token;
|
|
|
|
|
|
|
|
|
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
moddedPaths = cachedModdedPaths is not null
|
|
|
|
|
? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer)
|
|
|
|
|
: [];
|
|
|
|
|
await concurrencyLease.DisposeAsync().ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
var handlerForApply = _charaHandler;
|
|
|
|
|
if (handlerForApply is null || handlerForApply.Address == nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier());
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var appToken = _applicationCancellationTokenSource?.Token;
|
|
|
|
|
while ((!_applicationTask?.IsCompleted ?? false)
|
|
|
|
|
&& !downloadToken.IsCancellationRequested
|
|
|
|
|
&& (!appToken?.IsCancellationRequested ?? false))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
|
|
|
|
await Task.Delay(250).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
|
|
|
|
{
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
|
|
|
|
var token = _applicationCancellationTokenSource.Token;
|
|
|
|
|
|
|
|
|
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
|
|
|
|
@@ -1416,6 +1476,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
ApplyCharacterData(appData, cachedData!, forceApplyCustomization: true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
@@ -1432,6 +1493,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
ApplyLastReceivedData(forced: true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
@@ -1468,21 +1530,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_serverConfigManager.AutoPopulateNoteForUid(user.UID, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Mediator.Subscribe<HonorificReadyMessage>(this, async (_) =>
|
|
|
|
|
Mediator.Subscribe<HonorificReadyMessage>(this, _message =>
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
|
|
|
|
Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier());
|
|
|
|
|
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false);
|
|
|
|
|
var honorificData = _cachedData?.HonorificData;
|
|
|
|
|
if (string.IsNullOrEmpty(honorificData))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_ = ReapplyHonorificAsync(honorificData!);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Mediator.Subscribe<PetNamesReadyMessage>(this, async (_) =>
|
|
|
|
|
Mediator.Subscribe<PetNamesReadyMessage>(this, _message =>
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
|
|
|
|
Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier());
|
|
|
|
|
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
|
|
|
|
|
var petNamesData = _cachedData?.PetNamesData;
|
|
|
|
|
if (string.IsNullOrEmpty(petNamesData))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_ = ReapplyPetNamesAsync(petNamesData!);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ReapplyHonorificAsync(string honorificData)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier());
|
|
|
|
|
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, honorificData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ReapplyPetNamesAsync(string petNamesData)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier());
|
|
|
|
|
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, petNamesData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
|
|
|
|
|
{
|
|
|
|
|
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident);
|
|
|
|
|
@@ -1572,14 +1650,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
|
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
|
|
|
|
if (fileCache != null)
|
|
|
|
|
if (fileCache is not null && !File.Exists(fileCache.ResolvedFilepath))
|
|
|
|
|
{
|
|
|
|
|
if (!File.Exists(fileCache.ResolvedFilepath))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Cached path {Path} missing on disk for hash {Hash}, removing cache entry", applicationBase, fileCache.ResolvedFilepath, item.Hash);
|
|
|
|
|
_fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
|
|
|
|
fileCache = null;
|
|
|
|
|
}
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Cached path {Path} missing on disk for hash {Hash}, removing cache entry", applicationBase, fileCache.ResolvedFilepath, item.Hash);
|
|
|
|
|
_fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
|
|
|
|
fileCache = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fileCache != null)
|
|
|
|
|
@@ -1701,7 +1776,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (penumbraCollection != Guid.Empty)
|
|
|
|
|
{
|
|
|
|
|
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, character.ObjectIndex).ConfigureAwait(false);
|
|
|
|
|
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, new Dictionary<string, string>()).ConfigureAwait(false);
|
|
|
|
|
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, new Dictionary<string, string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
|
|
|
|
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, string.Empty).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1775,83 +1850,3 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|
|
|
|
{
|
|
|
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
|
|
|
private readonly LightlessMediator _mediator;
|
|
|
|
|
private readonly PairManager _pairManager;
|
|
|
|
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
|
|
|
|
private readonly IpcManager _ipcManager;
|
|
|
|
|
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
|
|
|
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
|
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
private readonly IHostApplicationLifetime _lifetime;
|
|
|
|
|
private readonly FileCacheManager _fileCacheManager;
|
|
|
|
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
|
|
|
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
|
|
|
|
private readonly ServerConfigurationManager _serverConfigManager;
|
|
|
|
|
private readonly TextureDownscaleService _textureDownscaleService;
|
|
|
|
|
private readonly PairStateCache _pairStateCache;
|
|
|
|
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
|
|
|
|
|
|
|
|
|
public PairHandlerAdapterFactory(
|
|
|
|
|
ILoggerFactory loggerFactory,
|
|
|
|
|
LightlessMediator mediator,
|
|
|
|
|
PairManager pairManager,
|
|
|
|
|
GameObjectHandlerFactory gameObjectHandlerFactory,
|
|
|
|
|
IpcManager ipcManager,
|
|
|
|
|
FileDownloadManagerFactory fileDownloadManagerFactory,
|
|
|
|
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
|
|
|
|
IServiceProvider serviceProvider,
|
|
|
|
|
IHostApplicationLifetime lifetime,
|
|
|
|
|
FileCacheManager fileCacheManager,
|
|
|
|
|
PlayerPerformanceService playerPerformanceService,
|
|
|
|
|
PairProcessingLimiter pairProcessingLimiter,
|
|
|
|
|
ServerConfigurationManager serverConfigManager,
|
|
|
|
|
TextureDownscaleService textureDownscaleService,
|
|
|
|
|
PairStateCache pairStateCache,
|
|
|
|
|
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
|
|
|
|
{
|
|
|
|
|
_loggerFactory = loggerFactory;
|
|
|
|
|
_mediator = mediator;
|
|
|
|
|
_pairManager = pairManager;
|
|
|
|
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
|
|
|
|
_ipcManager = ipcManager;
|
|
|
|
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
|
|
|
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
|
|
|
|
_serviceProvider = serviceProvider;
|
|
|
|
|
_lifetime = lifetime;
|
|
|
|
|
_fileCacheManager = fileCacheManager;
|
|
|
|
|
_playerPerformanceService = playerPerformanceService;
|
|
|
|
|
_pairProcessingLimiter = pairProcessingLimiter;
|
|
|
|
|
_serverConfigManager = serverConfigManager;
|
|
|
|
|
_textureDownscaleService = textureDownscaleService;
|
|
|
|
|
_pairStateCache = pairStateCache;
|
|
|
|
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IPairHandlerAdapter Create(string ident)
|
|
|
|
|
{
|
|
|
|
|
var downloadManager = _fileDownloadManagerFactory.Create();
|
|
|
|
|
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
|
|
|
|
return new PairHandlerAdapter(
|
|
|
|
|
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
|
|
|
|
_mediator,
|
|
|
|
|
_pairManager,
|
|
|
|
|
ident,
|
|
|
|
|
_gameObjectHandlerFactory,
|
|
|
|
|
_ipcManager,
|
|
|
|
|
downloadManager,
|
|
|
|
|
_pluginWarningNotificationManager,
|
|
|
|
|
dalamudUtilService,
|
|
|
|
|
_lifetime,
|
|
|
|
|
_fileCacheManager,
|
|
|
|
|
_playerPerformanceService,
|
|
|
|
|
_pairProcessingLimiter,
|
|
|
|
|
_serverConfigManager,
|
|
|
|
|
_textureDownscaleService,
|
|
|
|
|
_pairStateCache,
|
|
|
|
|
_pairPerformanceMetricsCache);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|