Files
LightlessClient/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs

1752 lines
72 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
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;
namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </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);
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
{
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
private readonly DalamudUtilService _dalamudUtil;
private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager;
private readonly IHostApplicationLifetime _lifetime;
private readonly PlayerPerformanceService _playerPerformanceService;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigManager;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache;
private readonly PairManager _pairManager;
private CancellationTokenSource? _applicationCancellationTokenSource = new();
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 bool _forceApplyMods = false;
private bool _forceFullReapply;
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
private bool _needsCollectionRebuild;
private bool _isVisible;
private Guid _penumbraCollection;
private readonly object _collectionGate = new();
private bool _redrawOnNextApplication = false;
private readonly object _initializationGate = new();
private readonly object _pauseLock = new();
private Task _pauseTransitionTask = Task.CompletedTask;
private bool _pauseRequested;
public string Ident { get; }
public bool Initialized { get; private set; }
public bool ScheduledForDeletion { get; set; }
public bool IsVisible
{
get => _isVisible;
private set
{
if (_isVisible != value)
{
_isVisible = value;
if (!_isVisible)
{
DisableSync();
ResetPenumbraCollection(reason: "VisibilityLost");
}
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
{
_ = EnsurePenumbraCollection();
}
var user = GetPrimaryUserData();
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
Mediator.Publish(new RefreshUiMessage());
Mediator.Publish(new VisibilityChange());
}
}
}
public long LastAppliedDataBytes { get; private set; }
public long LastAppliedDataTris { get; set; } = -1;
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
public CharacterData? LastReceivedCharacterData { get; private set; }
public PairHandlerAdapter(
ILogger<PairHandlerAdapter> logger,
LightlessMediator mediator,
PairManager pairManager,
string ident,
GameObjectHandlerFactory gameObjectHandlerFactory,
IpcManager ipcManager,
FileDownloadManager transferManager,
PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil,
IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager,
PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache) : base(logger, mediator)
{
_pairManager = pairManager;
Ident = ident;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_ipcManager = ipcManager;
_downloadManager = transferManager;
_pluginWarningNotificationManager = pluginWarningNotificationManager;
_dalamudUtil = dalamudUtil;
_lifetime = lifetime;
_fileDbManager = fileDbManager;
_playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService;
_pairStateCache = pairStateCache;
LastAppliedDataBytes = -1;
}
public void Initialize()
{
EnsureInitialized();
}
private void EnsureInitialized()
{
if (Initialized)
{
return;
}
lock (_initializationGate)
{
if (Initialized)
{
return;
}
var user = GetPrimaryUserData();
if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{
_forceApplyMods = true;
}
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
{
_downloadCancellationTokenSource?.CancelDispose();
_charaHandler?.Invalidate();
IsVisible = false;
});
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ =>
{
ResetPenumbraCollection(releaseFromPenumbra: false, reason: "PenumbraInitialized");
if (!IsVisible && _charaHandler is not null)
{
PlayerName = string.Empty;
_charaHandler.Dispose();
_charaHandler = null;
}
EnableSync();
});
Mediator.Subscribe<PenumbraDisposedMessage>(this, _ => ResetPenumbraCollection(releaseFromPenumbra: false, reason: "PenumbraDisposed"));
Mediator.Subscribe<ClassJobChangedMessage>(this, msg =>
{
if (msg.GameObjectHandler == _charaHandler)
{
_redrawOnNextApplication = true;
}
});
Mediator.Subscribe<CombatEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<CombatStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<PerformanceEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<PerformanceStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<InstanceOrDutyStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<InstanceOrDutyEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<CutsceneStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
{
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
{
return;
}
TryApplyQueuedData();
});
Initialized = true;
}
}
private IReadOnlyList<PairConnection> GetCurrentPairs()
{
return _pairManager.GetPairsByIdent(Ident);
}
private PairConnection? GetPrimaryPair()
{
var pairs = GetCurrentPairs();
var direct = pairs.FirstOrDefault(p => p.IsDirectlyPaired);
if (direct is not null)
{
return direct;
}
var online = pairs.FirstOrDefault(p => p.IsOnline);
if (online is not null)
{
return online;
}
return pairs.FirstOrDefault();
}
private UserData GetPrimaryUserData()
{
return GetPrimaryPair()?.User ?? new UserData(Ident);
}
private string GetPrimaryAliasOrUid()
{
var pair = GetPrimaryPair();
if (pair?.User is null)
{
return Ident;
}
return string.IsNullOrEmpty(pair.User.AliasOrUID) ? Ident : pair.User.AliasOrUID;
}
private string GetPrimaryAliasOrUidSafe()
{
try
{
return GetPrimaryAliasOrUid();
}
catch
{
return Ident;
}
}
private UserData GetPrimaryUserDataSafe()
{
try
{
return GetPrimaryUserData();
}
catch
{
return new UserData(Ident);
}
}
private string GetLogIdentifier()
{
var alias = GetPrimaryAliasOrUidSafe();
return string.Equals(alias, Ident, StringComparison.Ordinal) ? alias : $"{alias} ({Ident})";
}
private Guid EnsurePenumbraCollection()
{
if (!IsVisible)
{
return Guid.Empty;
}
if (_penumbraCollection != Guid.Empty)
{
return _penumbraCollection;
}
lock (_collectionGate)
{
if (_penumbraCollection != Guid.Empty)
{
return _penumbraCollection;
}
var cached = _pairStateCache.TryGetTemporaryCollection(Ident);
if (cached.HasValue && cached.Value != Guid.Empty)
{
_penumbraCollection = cached.Value;
return _penumbraCollection;
}
if (!_ipcManager.Penumbra.APIAvailable)
{
return Guid.Empty;
}
var user = GetPrimaryUserDataSafe();
var uid = !string.IsNullOrEmpty(user.UID) ? user.UID : Ident;
var created = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, uid)
.ConfigureAwait(false).GetAwaiter().GetResult();
if (created != Guid.Empty)
{
_penumbraCollection = created;
_pairStateCache.StoreTemporaryCollection(Ident, created);
}
return _penumbraCollection;
}
}
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
{
Guid toRelease = Guid.Empty;
bool hadCollection = false;
lock (_collectionGate)
{
if (_penumbraCollection != Guid.Empty)
{
toRelease = _penumbraCollection;
_penumbraCollection = Guid.Empty;
hadCollection = true;
}
}
var cached = _pairStateCache.ClearTemporaryCollection(Ident);
if (cached.HasValue && cached.Value != Guid.Empty)
{
toRelease = cached.Value;
hadCollection = true;
}
if (hadCollection)
{
_needsCollectionRebuild = true;
_forceFullReapply = true;
}
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
{
return;
}
try
{
var applicationId = Guid.NewGuid();
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
}
}
private bool AnyPair(Func<PairConnection, bool> predicate)
{
return GetCurrentPairs().Any(predicate);
}
private bool ShouldSkipDownscale()
{
return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky());
}
private bool IsPaused()
{
var pairs = GetCurrentPairs();
return pairs.Count > 0 && pairs.Any(p => p.IsPaused);
}
bool IPairPerformanceSubject.IsPaused => IsPaused();
bool IPairPerformanceSubject.IsDirectlyPaired => AnyPair(p => p.IsDirectlyPaired);
bool IPairPerformanceSubject.HasStickyPermissions => AnyPair(p => p.SelfToOtherPermissions.HasFlag(UserPermissions.Sticky));
UserData IPairPerformanceSubject.UserData => GetPrimaryUserData();
string IPairPerformanceSubject.PlayerName => PlayerName ?? GetPrimaryAliasOrUidSafe();
private UserPermissions GetCombinedPermissions()
{
var pairs = GetCurrentPairs();
if (pairs.Count == 0)
{
return UserPermissions.NoneSet;
}
var combined = pairs[0].SelfToOtherPermissions | pairs[0].OtherToSelfPermissions;
for (int i = 1; i < pairs.Count; i++)
{
var perms = pairs[i].SelfToOtherPermissions | pairs[i].OtherToSelfPermissions;
combined &= perms;
}
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 string? PlayerName { get; private set; }
public string PlayerNameHash => Ident;
public void ApplyData(CharacterData data)
{
EnsureInitialized();
LastReceivedCharacterData = data;
ApplyLastReceivedData();
}
public void LoadCachedCharacterData(CharacterData data)
{
if (data is null)
{
return;
}
LastReceivedCharacterData = data;
_cachedData = null;
_forceApplyMods = true;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
public void ApplyLastReceivedData(bool forced = false)
{
EnsureInitialized();
if (LastReceivedCharacterData is null)
{
Logger.LogTrace("No cached data to apply for {Ident}", Ident);
return;
}
var shouldForce = forced || HasMissingCachedFiles(LastReceivedCharacterData);
if (IsPaused())
{
Logger.LogTrace("Permissions paused for {Ident}, skipping reapply", Ident);
return;
}
if (shouldForce)
{
_forceApplyMods = true;
_cachedData = null;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
var sanitized = RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone());
if (sanitized is null)
{
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
return;
}
_pairStateCache.Store(Ident, sanitized);
if (!IsVisible)
{
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
_cachedData = sanitized;
_forceFullReapply = true;
return;
}
ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce);
}
private bool HasMissingCachedFiles(CharacterData characterData)
{
try
{
HashSet<string> inspectedHashes = new(StringComparer.OrdinalIgnoreCase);
foreach (var replacements in characterData.FileReplacements.Values)
{
foreach (var replacement in replacements)
{
if (!string.IsNullOrEmpty(replacement.FileSwapPath))
{
if (!File.Exists(replacement.FileSwapPath))
{
Logger.LogTrace("Missing file swap path {Path} detected for {Handler}", replacement.FileSwapPath, GetLogIdentifier());
return true;
}
continue;
}
if (string.IsNullOrEmpty(replacement.Hash) || !inspectedHashes.Add(replacement.Hash))
{
continue;
}
var cacheEntry = _fileDbManager.GetFileCacheByHash(replacement.Hash);
if (cacheEntry is null)
{
Logger.LogTrace("Missing cached file {Hash} detected for {Handler}", replacement.Hash, GetLogIdentifier());
return true;
}
if (!File.Exists(cacheEntry.ResolvedFilepath))
{
Logger.LogTrace("Cached file {Hash} missing on disk for {Handler}, removing cache entry", replacement.Hash, GetLogIdentifier());
_fileDbManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
return true;
}
}
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to determine cache availability for {Handler}", GetLogIdentifier());
}
return false;
}
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
{
Logger.LogTrace("Removing not synced files for {Ident}", Ident);
if (data is null)
{
return null;
}
var permissions = GetCombinedPermissions();
bool disableAnimations = permissions.IsDisableAnimations();
bool disableVfx = permissions.IsDisableVFX();
bool disableSounds = permissions.IsDisableSounds();
if (!(disableAnimations || disableVfx || disableSounds))
{
return data;
}
foreach (var objectKind in data.FileReplacements.Keys.ToList())
{
var replacements = data.FileReplacements[objectKind];
if (disableSounds)
{
replacements = replacements
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
.ToList();
}
if (disableAnimations)
{
replacements = replacements
.Where(f => !f.GamePaths.Any(p =>
p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) ||
p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
.ToList();
}
if (disableVfx)
{
replacements = replacements
.Where(f => !f.GamePaths.Any(p =>
p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) ||
p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase)))
.ToList();
}
data.FileReplacements[objectKind] = replacements;
}
return data;
}
private bool HasValidCachedModdedPaths()
{
if (_lastAppliedModdedPaths is null || _lastAppliedModdedPaths.Count == 0)
{
return false;
}
foreach (var entry in _lastAppliedModdedPaths)
{
if (string.IsNullOrEmpty(entry.Value) || !File.Exists(entry.Value))
{
Logger.LogDebug("Cached file path {path} missing for {handler}, forcing recalculation", entry.Value ?? "empty", GetLogIdentifier());
return false;
}
}
return true;
}
private bool CanApplyNow()
{
return !_dalamudUtil.IsInCombat
&& !_dalamudUtil.IsPerforming
&& !_dalamudUtil.IsInInstance
&& !_dalamudUtil.IsInCutscene
&& !_dalamudUtil.IsInGpose
&& _ipcManager.Penumbra.APIAvailable
&& _ipcManager.Glamourer.APIAvailable;
}
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
{
if (characterData is null)
{
Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier());
SetUploading(isUploading: false);
return;
}
var user = GetPrimaryUserData();
if (_dalamudUtil.IsInCombat)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
if (_dalamudUtil.IsPerforming)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
if (_dalamudUtil.IsInInstance)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
if (_dalamudUtil.IsInCutscene)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
if (_dalamudUtil.IsInGpose)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"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);
return;
}
var handlerReady = _charaHandler is not null && PlayerCharacter != IntPtr.Zero;
if (!handlerReady)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
this, forceApplyCustomization, forceApplyMods: false)
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
_forceApplyMods = hasDiffMods || _forceApplyMods || _cachedData == null;
_cachedData = characterData;
_forceFullReapply = true;
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
}
SetUploading(isUploading: 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");
if (handlerReady
&& string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal)
&& !forceApplyCustomization && !_forceApplyMods)
{
return;
}
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
"Applying Character Data")));
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
if (handlerReady && _forceApplyMods)
{
_forceApplyMods = false;
}
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
{
player.Add(PlayerChanges.ForcedRedraw);
_redrawOnNextApplication = false;
}
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
{
_pluginWarningNotificationManager.NotifyForMissingPlugins(user, PlayerName!, playerChanges);
}
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe());
var forceFullReapply = _forceFullReapply || forceApplyCustomization
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0;
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply);
}
public override string ToString()
{
var alias = GetPrimaryAliasOrUidSafe();
return $"{alias}:{PlayerName ?? string.Empty}:{(PlayerCharacter != nint.Zero ? "HasChar" : "NoChar")}";
}
public void SetUploading(bool isUploading = true)
{
Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), isUploading);
if (_charaHandler != null)
{
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
}
}
public void SetPaused(bool paused)
{
lock (_pauseLock)
{
if (_pauseRequested == paused)
{
return;
}
_pauseRequested = paused;
_pauseTransitionTask = _pauseTransitionTask
.ContinueWith(_ => paused ? PauseInternalAsync() : ResumeInternalAsync(), TaskScheduler.Default)
.Unwrap();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
SetUploading(isUploading: false);
var name = PlayerName;
var user = GetPrimaryUserDataSafe();
var alias = GetPrimaryAliasOrUidSafe();
Logger.LogDebug("Disposing {name} ({user})", name, alias);
try
{
Guid applicationId = Guid.NewGuid();
_applicationCancellationTokenSource?.CancelDispose();
_applicationCancellationTokenSource = null;
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
_downloadManager.Dispose();
_charaHandler?.Dispose();
_charaHandler = null;
if (!string.IsNullOrEmpty(name))
{
Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), EventSeverity.Informational, "Disposing User")));
}
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
{
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
ResetPenumbraCollection(reason: nameof(Dispose));
if (!IsVisible)
{
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
}
else
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(60));
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
if (effectiveCachedData is not null)
{
_cachedData = effectiveCachedData;
}
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}",
applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
{
try
{
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
}
catch (InvalidOperationException ex)
{
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
break;
}
}
}
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error on disposal of {name}", name);
}
finally
{
PlayerName = null;
_cachedData = null;
_lastAppliedModdedPaths = null;
_needsCollectionRebuild = false;
Logger.LogDebug("Disposing {name} complete", name);
}
}
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
{
if (PlayerCharacter == nint.Zero) return;
var ptr = PlayerCharacter;
var handler = changes.Key switch
{
ObjectKind.Player => _charaHandler!,
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false),
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMountPtr(ptr), isWatched: false).ConfigureAwait(false),
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false),
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
};
try
{
if (handler.Address == nint.Zero)
{
return;
}
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
foreach (var change in changes.Value.OrderBy(p => (int)p))
{
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
switch (change)
{
case PlayerChanges.Customize:
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
{
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
}
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
{
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
_customizeIds.Remove(changes.Key);
}
break;
case PlayerChanges.Heels:
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
break;
case PlayerChanges.Honorific:
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
break;
case PlayerChanges.Glamourer:
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
{
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
}
break;
case PlayerChanges.Moodles:
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
break;
case PlayerChanges.PetNames:
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
break;
case PlayerChanges.ForcedRedraw:
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
break;
default:
break;
}
token.ThrowIfCancellationRequested();
}
}
finally
{
if (handler != _charaHandler) handler.Dispose();
}
}
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
{
var result = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
foreach (var objectKind in Enum.GetValues<ObjectKind>())
{
var changes = new HashSet<PlayerChanges>();
if (characterData.FileReplacements.TryGetValue(objectKind, out var replacements) && replacements.Count > 0)
{
changes.Add(PlayerChanges.ModFiles);
if (objectKind == ObjectKind.Player)
{
changes.Add(PlayerChanges.ForcedRedraw);
}
}
if (characterData.GlamourerData.TryGetValue(objectKind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
{
changes.Add(PlayerChanges.Glamourer);
}
if (characterData.CustomizePlusData.TryGetValue(objectKind, out var customize) && !string.IsNullOrEmpty(customize))
{
changes.Add(PlayerChanges.Customize);
}
if (objectKind == ObjectKind.Player)
{
if (!string.IsNullOrEmpty(characterData.ManipulationData))
{
changes.Add(PlayerChanges.ModManip);
changes.Add(PlayerChanges.ForcedRedraw);
}
if (!string.IsNullOrEmpty(characterData.HeelsData))
{
changes.Add(PlayerChanges.Heels);
}
if (!string.IsNullOrEmpty(characterData.HonorificData))
{
changes.Add(PlayerChanges.Honorific);
}
if (!string.IsNullOrEmpty(characterData.MoodlesData))
{
changes.Add(PlayerChanges.Moodles);
}
if (!string.IsNullOrEmpty(characterData.PetNamesData))
{
changes.Add(PlayerChanges.PetNames);
}
}
if (changes.Count > 0)
{
result[objectKind] = changes;
}
}
return result;
}
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool forceFullReapply)
{
if (!updatedData.Any())
{
if (forceFullReapply)
{
updatedData = BuildFullChangeSet(charaData);
}
if (!updatedData.Any())
{
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, GetLogIdentifier());
_forceFullReapply = false;
return;
}
}
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip));
var needsCollectionRebuild = _needsCollectionRebuild;
var reuseCachedModdedPaths = !updateModdedPaths && needsCollectionRebuild && _lastAppliedModdedPaths is not null;
updateModdedPaths = updateModdedPaths || needsCollectionRebuild;
updateManip = updateManip || needsCollectionRebuild;
Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths = null;
if (reuseCachedModdedPaths)
{
if (HasValidCachedModdedPaths())
{
cachedModdedPaths = _lastAppliedModdedPaths;
}
else
{
Logger.LogDebug("{handler}: Cached files missing, recalculating mappings", GetLogIdentifier());
_lastAppliedModdedPaths = null;
}
}
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token;
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken).ConfigureAwait(false);
}
private Task? _pairDownloadTask;
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)
{
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
{
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);
}
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
{
try
{
_applicationId = Guid.NewGuid();
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {handler}: {appId}", applicationBase, GetLogIdentifier(), _applicationId);
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
Guid penumbraCollection = Guid.Empty;
if (updateModdedPaths || updateManip)
{
penumbraCollection = EnsurePenumbraCollection();
if (penumbraCollection == Guid.Empty)
{
Logger.LogTrace("[BASE-{applicationId}] Penumbra collection unavailable for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
return;
}
}
if (updateModdedPaths)
{
// ensure collection is set
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObject = handlerForApply.GetGameObject();
return gameObject?.ObjectIndex;
}).ConfigureAwait(false);
if (!objIndex.HasValue)
{
Logger.LogDebug("[BASE-{applicationId}] GameObject not available for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
return;
}
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection,
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer);
LastAppliedDataBytes = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
{
if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0;
LastAppliedDataBytes += path.Length;
}
}
if (updateManip)
{
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, penumbraCollection, charaData.ManipulationData).ConfigureAwait(false);
}
token.ThrowIfCancellationRequested();
foreach (var kind in updatedData)
{
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
}
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = false;
_needsCollectionRebuild = false;
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
}
if (LastAppliedDataTris < 0)
{
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
}
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;
}
catch (Exception ex)
{
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;
}
}
}
private void FrameworkUpdate()
{
if (string.IsNullOrEmpty(PlayerName))
{
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
if (pc == default((string, nint))) return;
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
Initialize(pc.Name);
Logger.LogDebug("One-Time Initialized {handler}", GetLogIdentifier());
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
$"Initializing User For Character {pc.Name}")));
}
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
{
Guid appData = Guid.NewGuid();
IsVisible = true;
if (_cachedData is not null)
{
var cachedData = _cachedData;
Logger.LogTrace("[BASE-{appBase}] {handler} visibility changed, now: {visi}, cached data exists", appData, GetLogIdentifier(), IsVisible);
_ = Task.Run(() =>
{
try
{
ApplyCharacterData(appData, cachedData!, forceApplyCustomization: true);
}
catch (Exception ex)
{
Logger.LogError(ex, "[BASE-{appBase}] Failed to apply cached character data for {handler}", appData, GetLogIdentifier());
}
});
}
else if (LastReceivedCharacterData is not null)
{
Logger.LogTrace("[BASE-{appBase}] {handler} visibility changed, now: {visi}, last received data exists", appData, GetLogIdentifier(), IsVisible);
_ = Task.Run(() =>
{
try
{
ApplyLastReceivedData(forced: true);
}
catch (Exception ex)
{
Logger.LogError(ex, "[BASE-{appBase}] Failed to reapply last received data for {handler}", appData, GetLogIdentifier());
}
});
}
else
{
Logger.LogTrace("{handler} visibility changed, now: {visi}, no cached or received data exists", GetLogIdentifier(), IsVisible);
}
}
else if (_charaHandler?.Address == nint.Zero && IsVisible)
{
IsVisible = false;
_charaHandler.Invalidate();
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
}
TryApplyQueuedData();
}
private void Initialize(string name)
{
PlayerName = name;
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult();
var user = GetPrimaryUserData();
if (!string.IsNullOrEmpty(user.UID))
{
_serverConfigManager.AutoPopulateNoteForUid(user.UID, name);
}
Mediator.Subscribe<HonorificReadyMessage>(this, async (_) =>
{
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier());
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false);
});
Mediator.Subscribe<PetNamesReadyMessage>(this, async (_) =>
{
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier());
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
});
}
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
{
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident);
if (address == nint.Zero) return;
var alias = GetPrimaryAliasOrUid();
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, alias, name, objectKind);
if (_customizeIds.TryGetValue(objectKind, out var customizeId))
{
_customizeIds.Remove(objectKind);
}
if (objectKind == ObjectKind.Player)
{
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false);
tempHandler.CompareNameAndThrow(name);
Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, alias, name);
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
tempHandler.CompareNameAndThrow(name);
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, alias, name);
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
tempHandler.CompareNameAndThrow(name);
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, alias, name);
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
tempHandler.CompareNameAndThrow(name);
Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, alias, name);
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, alias, name);
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, alias, name);
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
}
else if (objectKind == ObjectKind.MinionOrMount)
{
var minionOrMount = await _dalamudUtil.GetMinionOrMountAsync(address).ConfigureAwait(false);
if (minionOrMount != nint.Zero)
{
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false);
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
}
}
else if (objectKind == ObjectKind.Pet)
{
var pet = await _dalamudUtil.GetPetAsync(address).ConfigureAwait(false);
if (pet != nint.Zero)
{
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false);
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
}
}
else if (objectKind == ObjectKind.Companion)
{
var companion = await _dalamudUtil.GetCompanionAsync(address).ConfigureAwait(false);
if (companion != nint.Zero)
{
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false);
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
}
}
}
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
{
Stopwatch st = Stopwatch.StartNew();
ConcurrentBag<FileReplacementData> missingFiles = [];
moddedDictionary = [];
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
bool hasMigrationChanges = false;
bool skipDownscaleForPair = ShouldSkipDownscale();
try
{
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
Parallel.ForEach(replacementList, new ParallelOptions()
{
CancellationToken = token,
MaxDegreeOfParallelism = 4
},
(item) =>
{
token.ThrowIfCancellationRequested();
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
if (fileCache != null)
{
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;
}
}
if (fileCache != null)
{
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
{
hasMigrationChanges = true;
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
}
foreach (var gamePath in item.GamePaths)
{
var preferredPath = skipDownscaleForPair
? fileCache.ResolvedFilepath
: _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath);
outputDict[(gamePath, item.Hash)] = preferredPath;
}
}
else
{
Logger.LogTrace("Missing file: {hash}", item.Hash);
missingFiles.Add(item);
}
});
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
{
foreach (var gamePath in item.GamePaths)
{
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
}
}
}
catch (OperationCanceledException)
{
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
}
if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv();
st.Stop();
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
return [.. missingFiles];
}
private async Task PauseInternalAsync()
{
try
{
Logger.LogDebug("Pausing handler {handler}", GetLogIdentifier());
DisableSync();
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
{
IsVisible = false;
return;
}
var applicationId = Guid.NewGuid();
await RevertToRestoredAsync(applicationId).ConfigureAwait(false);
IsVisible = false;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to pause handler {handler}", GetLogIdentifier());
}
}
private async Task ResumeInternalAsync()
{
try
{
Logger.LogDebug("Resuming handler {handler}", GetLogIdentifier());
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
{
return;
}
if (!IsVisible)
{
IsVisible = true;
}
if (LastReceivedCharacterData is not null)
{
ApplyLastReceivedData(forced: true);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to resume handler {handler}", GetLogIdentifier());
}
}
private async Task RevertToRestoredAsync(Guid applicationId)
{
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
{
return;
}
try
{
var gameObject = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler.GetGameObject()).ConfigureAwait(false);
if (gameObject is not Dalamud.Game.ClientState.Objects.Types.ICharacter character)
{
return;
}
if (_ipcManager.Penumbra.APIAvailable)
{
var penumbraCollection = EnsurePenumbraCollection();
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.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, string.Empty).ConfigureAwait(false);
}
}
var kinds = new HashSet<ObjectKind>(_customizeIds.Keys);
if (_cachedData is not null)
{
foreach (var kind in _cachedData.FileReplacements.Keys)
{
kinds.Add(kind);
}
}
kinds.Add(ObjectKind.Player);
var characterName = character.Name.TextValue;
if (string.IsNullOrEmpty(characterName))
{
characterName = character.Name.ToString();
}
if (string.IsNullOrEmpty(characterName))
{
Logger.LogWarning("[{applicationId}] Failed to determine character name for {handler} while reverting", applicationId, GetLogIdentifier());
return;
}
foreach (var kind in kinds)
{
await RevertCustomizationDataAsync(kind, characterName, applicationId, CancellationToken.None).ConfigureAwait(false);
}
_cachedData = null;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to revert handler {handler} during pause", GetLogIdentifier());
}
}
private void DisableSync()
{
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
}
private void EnableSync()
{
TryApplyQueuedData();
}
private void TryApplyQueuedData()
{
var pending = _dataReceivedInDowntime;
if (pending is null || !IsVisible)
{
return;
}
if (!CanApplyNow())
{
return;
}
_dataReceivedInDowntime = null;
ApplyCharacterData(pending.ApplicationId,
pending.CharacterData, pending.Forced);
}
}
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;
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)
{
_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;
}
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);
}
}