|
|
|
|
@@ -8,6 +8,7 @@ using LightlessSync.Interop.Ipc;
|
|
|
|
|
using LightlessSync.PlayerData.Factories;
|
|
|
|
|
using LightlessSync.PlayerData.Handlers;
|
|
|
|
|
using LightlessSync.Services;
|
|
|
|
|
using LightlessSync.Services.ActorTracking;
|
|
|
|
|
using LightlessSync.Services.Events;
|
|
|
|
|
using LightlessSync.Services.Mediator;
|
|
|
|
|
using LightlessSync.Services.PairProcessing;
|
|
|
|
|
@@ -18,6 +19,7 @@ using LightlessSync.WebAPI.Files;
|
|
|
|
|
using LightlessSync.WebAPI.Files.Models;
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|
|
|
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
|
|
|
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
|
|
|
|
|
|
|
|
|
@@ -31,6 +33,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
|
|
|
|
|
|
|
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
|
|
|
private readonly ActorObjectService _actorObjectService;
|
|
|
|
|
private readonly FileDownloadManager _downloadManager;
|
|
|
|
|
private readonly FileCacheManager _fileDbManager;
|
|
|
|
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
|
|
|
|
@@ -56,11 +59,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private bool _forceFullReapply;
|
|
|
|
|
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
|
|
|
|
private bool _needsCollectionRebuild;
|
|
|
|
|
private bool _pendingModReapply;
|
|
|
|
|
private bool _lastModApplyDeferred;
|
|
|
|
|
private int _lastMissingCriticalMods;
|
|
|
|
|
private int _lastMissingNonCriticalMods;
|
|
|
|
|
private int _lastMissingForbiddenMods;
|
|
|
|
|
private bool _isVisible;
|
|
|
|
|
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;
|
|
|
|
|
@@ -73,8 +80,23 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private readonly object _visibilityGraceGate = new();
|
|
|
|
|
private CancellationTokenSource? _visibilityGraceCts;
|
|
|
|
|
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
|
|
|
|
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
{
|
|
|
|
|
".tmb",
|
|
|
|
|
".pap",
|
|
|
|
|
".atex",
|
|
|
|
|
".avfx",
|
|
|
|
|
".scd"
|
|
|
|
|
};
|
|
|
|
|
private DateTime? _invisibleSinceUtc;
|
|
|
|
|
private DateTime? _visibilityEvictionDueAtUtc;
|
|
|
|
|
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
|
|
|
|
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
|
|
|
|
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
|
|
|
|
private readonly object _actorInitializationGate = new();
|
|
|
|
|
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
|
|
|
|
private bool _actorInitializationInProgress;
|
|
|
|
|
private bool _frameworkUpdateSubscribed;
|
|
|
|
|
|
|
|
|
|
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
|
|
|
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
|
|
|
|
@@ -126,6 +148,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
|
|
|
|
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
|
|
|
|
public CharacterData? LastReceivedCharacterData { get; private set; }
|
|
|
|
|
public bool PendingModReapply => _pendingModReapply;
|
|
|
|
|
public bool ModApplyDeferred => _lastModApplyDeferred;
|
|
|
|
|
public int MissingCriticalMods => _lastMissingCriticalMods;
|
|
|
|
|
public int MissingNonCriticalMods => _lastMissingNonCriticalMods;
|
|
|
|
|
public int MissingForbiddenMods => _lastMissingForbiddenMods;
|
|
|
|
|
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
|
|
|
|
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
|
|
|
|
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
|
|
|
|
@@ -146,6 +173,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
FileDownloadManager transferManager,
|
|
|
|
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
|
|
|
|
DalamudUtilService dalamudUtil,
|
|
|
|
|
ActorObjectService actorObjectService,
|
|
|
|
|
IHostApplicationLifetime lifetime,
|
|
|
|
|
FileCacheManager fileDbManager,
|
|
|
|
|
PlayerPerformanceService playerPerformanceService,
|
|
|
|
|
@@ -162,6 +190,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_downloadManager = transferManager;
|
|
|
|
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
|
|
|
|
_dalamudUtil = dalamudUtil;
|
|
|
|
|
_actorObjectService = actorObjectService;
|
|
|
|
|
_lifetime = lifetime;
|
|
|
|
|
_fileDbManager = fileDbManager;
|
|
|
|
|
_playerPerformanceService = playerPerformanceService;
|
|
|
|
|
@@ -185,6 +214,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
|
|
|
|
lock (_initializationGate)
|
|
|
|
|
{
|
|
|
|
|
if (Initialized)
|
|
|
|
|
@@ -198,7 +228,12 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_forceApplyMods = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
|
|
|
|
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
|
|
|
|
if (useFrameworkUpdate)
|
|
|
|
|
{
|
|
|
|
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
|
|
|
|
_frameworkUpdateSubscribed = true;
|
|
|
|
|
}
|
|
|
|
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
|
|
|
|
{
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
@@ -234,17 +269,49 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
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))
|
|
|
|
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
|
|
|
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
|
|
|
|
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_pendingModReapply && IsVisible)
|
|
|
|
|
{
|
|
|
|
|
if (LastReceivedCharacterData is not null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data", GetLogIdentifier());
|
|
|
|
|
ApplyLastReceivedData(forced: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_cachedData is not null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data from cache", GetLogIdentifier());
|
|
|
|
|
ApplyCharacterData(Guid.NewGuid(), _cachedData, forceApplyCustomization: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryApplyQueuedData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!useFrameworkUpdate
|
|
|
|
|
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
|
|
|
|
|
&& descriptor.Address != nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
trackedDescriptor = descriptor;
|
|
|
|
|
}
|
|
|
|
|
TryApplyQueuedData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Initialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trackedDescriptor.HasValue)
|
|
|
|
|
{
|
|
|
|
|
HandleActorTracked(trackedDescriptor.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
|
|
|
|
@@ -737,6 +804,67 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsForbiddenHash(string hash)
|
|
|
|
|
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
|
|
|
|
|
|
|
|
|
|
private static bool IsNonPriorityModPath(string? gamePath)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(gamePath))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var extension = Path.GetExtension(gamePath);
|
|
|
|
|
return !string.IsNullOrEmpty(extension) && NonPriorityModExtensions.Contains(extension);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsCriticalModReplacement(FileReplacementData replacement)
|
|
|
|
|
{
|
|
|
|
|
foreach (var gamePath in replacement.GamePaths)
|
|
|
|
|
{
|
|
|
|
|
if (!IsNonPriorityModPath(gamePath))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CountMissingReplacements(IEnumerable<FileReplacementData> missing, out int critical, out int nonCritical, out int forbidden)
|
|
|
|
|
{
|
|
|
|
|
critical = 0;
|
|
|
|
|
nonCritical = 0;
|
|
|
|
|
forbidden = 0;
|
|
|
|
|
|
|
|
|
|
foreach (var replacement in missing)
|
|
|
|
|
{
|
|
|
|
|
if (IsForbiddenHash(replacement.Hash))
|
|
|
|
|
{
|
|
|
|
|
forbidden++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsCriticalModReplacement(replacement))
|
|
|
|
|
{
|
|
|
|
|
critical++;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
nonCritical++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void RemoveModApplyChanges(Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
|
|
|
|
{
|
|
|
|
|
foreach (var changes in updatedData.Values)
|
|
|
|
|
{
|
|
|
|
|
changes.Remove(PlayerChanges.ModFiles);
|
|
|
|
|
changes.Remove(PlayerChanges.ModManip);
|
|
|
|
|
changes.Remove(PlayerChanges.ForcedRedraw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool CanApplyNow()
|
|
|
|
|
{
|
|
|
|
|
return !_dalamudUtil.IsInCombat
|
|
|
|
|
@@ -760,6 +888,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_lastBlockingConditions = Array.Empty<string>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DeferApplication(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization, UserData user, string reason,
|
|
|
|
|
string failureKey, LogLevel logLevel, string logMessage, params object?[] logArgs)
|
|
|
|
|
{
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, reason)));
|
|
|
|
|
Logger.Log(logLevel, logMessage, logArgs);
|
|
|
|
|
RecordFailure(reason, failureKey);
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
|
|
|
|
{
|
|
|
|
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
|
|
|
|
@@ -777,72 +915,48 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (_dalamudUtil.IsInCombat)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Combat");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
|
|
|
|
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsPerforming)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Performance");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
|
|
|
|
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInInstance)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Instance");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
|
|
|
|
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInCutscene)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Cutscene");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
|
|
|
|
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInGpose)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
|
|
|
|
RecordFailure(reason, "GPose");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
|
|
|
|
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
|
|
|
|
RecordFailure(reason, "PluginUnavailable");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
|
|
|
|
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -885,13 +999,10 @@ 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))
|
|
|
|
|
@@ -1085,7 +1196,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
|
|
|
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
|
|
|
|
if (handler.Address != nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
|
var tasks = new List<Task>();
|
|
|
|
|
bool needsRedraw = false;
|
|
|
|
|
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
|
|
|
|
@@ -1094,45 +1212,39 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
case PlayerChanges.Customize:
|
|
|
|
|
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
|
|
|
|
{
|
|
|
|
|
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(ApplyCustomizeAsync(handler.Address, customizePlusData, changes.Key));
|
|
|
|
|
}
|
|
|
|
|
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
|
|
|
|
{
|
|
|
|
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
|
|
|
|
_customizeIds.Remove(changes.Key);
|
|
|
|
|
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key));
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.Heels:
|
|
|
|
|
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.Honorific:
|
|
|
|
|
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.Glamourer:
|
|
|
|
|
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
|
|
|
|
{
|
|
|
|
|
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(_ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token));
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.Moodles:
|
|
|
|
|
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case PlayerChanges.PetNames:
|
|
|
|
|
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
|
|
|
|
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
|
|
|
|
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);
|
|
|
|
|
needsRedraw = true;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
@@ -1140,6 +1252,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tasks.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (needsRedraw)
|
|
|
|
|
{
|
|
|
|
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
@@ -1147,44 +1269,6 @@ 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)
|
|
|
|
|
{
|
|
|
|
|
@@ -1339,6 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
|
|
|
|
var user = GetPrimaryUserData();
|
|
|
|
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
|
|
|
|
List<FileReplacementData> missingReplacements = [];
|
|
|
|
|
|
|
|
|
|
if (updateModdedPaths)
|
|
|
|
|
{
|
|
|
|
|
@@ -1350,6 +1435,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
int attempts = 0;
|
|
|
|
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
missingReplacements = toDownloadReplacements;
|
|
|
|
|
|
|
|
|
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
@@ -1399,6 +1485,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
missingReplacements = toDownloadReplacements;
|
|
|
|
|
|
|
|
|
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
|
|
|
|
{
|
|
|
|
|
@@ -1422,6 +1509,54 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
: [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var wantsModApply = updateModdedPaths || updateManip;
|
|
|
|
|
var pendingModReapply = false;
|
|
|
|
|
var deferModApply = false;
|
|
|
|
|
|
|
|
|
|
if (wantsModApply && missingReplacements.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
CountMissingReplacements(missingReplacements, out var missingCritical, out var missingNonCritical, out var missingForbidden);
|
|
|
|
|
_lastMissingCriticalMods = missingCritical;
|
|
|
|
|
_lastMissingNonCriticalMods = missingNonCritical;
|
|
|
|
|
_lastMissingForbiddenMods = missingForbidden;
|
|
|
|
|
|
|
|
|
|
var hasCriticalMissing = missingCritical > 0;
|
|
|
|
|
var hasNonCriticalMissing = missingNonCritical > 0;
|
|
|
|
|
var hasDownloadableMissing = missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash));
|
|
|
|
|
var hasDownloadableCriticalMissing = hasCriticalMissing
|
|
|
|
|
&& missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash) && IsCriticalModReplacement(replacement));
|
|
|
|
|
|
|
|
|
|
pendingModReapply = hasDownloadableMissing;
|
|
|
|
|
_lastModApplyDeferred = false;
|
|
|
|
|
|
|
|
|
|
if (hasDownloadableCriticalMissing)
|
|
|
|
|
{
|
|
|
|
|
deferModApply = true;
|
|
|
|
|
_lastModApplyDeferred = true;
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Critical mod files missing for {handler}, deferring mod apply ({count} missing)",
|
|
|
|
|
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
|
|
|
|
}
|
|
|
|
|
else if (hasNonCriticalMissing && hasDownloadableMissing)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Non-critical mod files missing for {handler}, applying partial mods and reapplying after downloads ({count} missing)",
|
|
|
|
|
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_lastMissingCriticalMods = 0;
|
|
|
|
|
_lastMissingNonCriticalMods = 0;
|
|
|
|
|
_lastMissingForbiddenMods = 0;
|
|
|
|
|
_lastModApplyDeferred = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deferModApply)
|
|
|
|
|
{
|
|
|
|
|
updateModdedPaths = false;
|
|
|
|
|
updateManip = false;
|
|
|
|
|
RemoveModApplyChanges(updatedData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
var handlerForApply = _charaHandler;
|
|
|
|
|
@@ -1454,7 +1589,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
|
|
|
|
var token = _applicationCancellationTokenSource.Token;
|
|
|
|
|
|
|
|
|
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
|
|
|
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
@@ -1463,7 +1598,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths, bool wantsModApply, bool pendingModReapply, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
@@ -1472,6 +1607,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
|
|
|
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
|
|
|
|
if (handlerForApply.Address != nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
@@ -1538,7 +1677,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = false;
|
|
|
|
|
if (wantsModApply)
|
|
|
|
|
{
|
|
|
|
|
_pendingModReapply = pendingModReapply;
|
|
|
|
|
}
|
|
|
|
|
_forceFullReapply = _pendingModReapply;
|
|
|
|
|
_needsCollectionRebuild = false;
|
|
|
|
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
|
|
|
|
{
|
|
|
|
|
@@ -1584,8 +1727,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
private void FrameworkUpdate()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(PlayerName))
|
|
|
|
|
if (string.IsNullOrEmpty(PlayerName) && _charaHandler is null)
|
|
|
|
|
{
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
if (now < _nextActorLookupUtc)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_nextActorLookupUtc = now + ActorLookupInterval;
|
|
|
|
|
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
|
|
|
|
if (pc == default((string, nint))) return;
|
|
|
|
|
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
|
|
|
|
@@ -1595,6 +1745,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
$"Initializing User For Character {pc.Name}")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryHandleVisibilityUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryHandleVisibilityUpdate()
|
|
|
|
|
{
|
|
|
|
|
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
|
|
|
|
{
|
|
|
|
|
Guid appData = Guid.NewGuid();
|
|
|
|
|
@@ -1641,16 +1796,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
|
|
|
|
{
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
_charaHandler.Invalidate();
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_downloadCancellationTokenSource = null;
|
|
|
|
|
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
|
|
|
|
HandleVisibilityLoss(logChange: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryApplyQueuedData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleVisibilityLoss(bool logChange)
|
|
|
|
|
{
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
_charaHandler?.Invalidate();
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_downloadCancellationTokenSource = null;
|
|
|
|
|
if (logChange)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Initialize(string name)
|
|
|
|
|
{
|
|
|
|
|
PlayerName = name;
|
|
|
|
|
@@ -1977,7 +2140,164 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dataReceivedInDowntime = null;
|
|
|
|
|
ApplyCharacterData(pending.ApplicationId,
|
|
|
|
|
pending.CharacterData, pending.Forced);
|
|
|
|
|
_ = Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (descriptor.Address == nint.Zero)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
RefreshTrackedHandler(descriptor);
|
|
|
|
|
QueueActorInitialization(descriptor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
lock (_actorInitializationGate)
|
|
|
|
|
{
|
|
|
|
|
_pendingActorDescriptor = descriptor;
|
|
|
|
|
if (_actorInitializationInProgress)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_actorInitializationInProgress = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = Task.Run(InitializeFromTrackedAsync);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task InitializeFromTrackedAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await ActorInitializationLimiter.WaitAsync().ConfigureAwait(false);
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
ActorObjectService.ActorDescriptor? descriptor;
|
|
|
|
|
lock (_actorInitializationGate)
|
|
|
|
|
{
|
|
|
|
|
descriptor = _pendingActorDescriptor;
|
|
|
|
|
_pendingActorDescriptor = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!descriptor.HasValue)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_frameworkUpdateSubscribed && _actorObjectService.HooksActive)
|
|
|
|
|
{
|
|
|
|
|
Mediator.Unsubscribe<FrameworkUpdateMessage>(this);
|
|
|
|
|
_frameworkUpdateSubscribed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(PlayerName) || _charaHandler is null)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("Actor tracked for {handler}, initializing from hook", GetLogIdentifier());
|
|
|
|
|
Initialize(descriptor.Value.Name);
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
|
|
|
|
|
$"Initializing User For Character {descriptor.Value.Name}")));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RefreshTrackedHandler(descriptor.Value);
|
|
|
|
|
TryHandleVisibilityUpdate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
ActorInitializationLimiter.Release();
|
|
|
|
|
lock (_actorInitializationGate)
|
|
|
|
|
{
|
|
|
|
|
_actorInitializationInProgress = false;
|
|
|
|
|
if (_pendingActorDescriptor.HasValue)
|
|
|
|
|
{
|
|
|
|
|
_actorInitializationInProgress = true;
|
|
|
|
|
_ = Task.Run(InitializeFromTrackedAsync);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RefreshTrackedHandler(ActorObjectService.ActorDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
if (_charaHandler is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (descriptor.Address == nint.Zero)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (_charaHandler.Address == descriptor.Address)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
_charaHandler.Refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
|
|
|
|
{
|
|
|
|
|
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
|
|
|
|
{
|
|
|
|
|
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (descriptor.Address != _charaHandler.Address)
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (descriptor.Address != _charaHandler.Address)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
HandleVisibilityLoss(logChange: false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
|
|
|
|
{
|
|
|
|
|
hashedCid = descriptor.HashedContentId ?? string.Empty;
|
|
|
|
|
if (!string.IsNullOrEmpty(hashedCid))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
if (descriptor.ObjectKind != DalamudObjectKind.Player || descriptor.Address == nint.Zero)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
|
|
|
|
|
return !string.IsNullOrEmpty(hashedCid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
|
|
|
|
{
|
|
|
|
|
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind)
|
|
|
|
|
{
|
|
|
|
|
if (!customizeId.HasValue)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
|
|
|
|
_customizeIds.Remove(kind);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|