"improving" pair handler clean up and some other stuff
This commit is contained in:
Submodule LightlessAPI updated: 56566003e0...4ecd5375e6
@@ -280,6 +280,26 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? CreateCacheEntryWithKnownHash(string path, string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
{
|
||||||
|
return CreateCacheEntry(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo fi = new(path);
|
||||||
|
if (!fi.Exists) return null;
|
||||||
|
_logger.LogTrace("Creating cache entry for {path} using provided hash", path);
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||||
|
if (!TryBuildPrefixedPath(fi.FullName, cacheFolder, CachePrefix, out var prefixedPath, out _))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath, hash);
|
||||||
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateFileEntry(string path)
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
{
|
{
|
||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace LightlessSync.Interop.Ipc;
|
|||||||
|
|
||||||
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
|
private bool _wasInitialized;
|
||||||
|
|
||||||
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
||||||
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||||
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
||||||
@@ -18,7 +20,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
PetNames = ipcCallerPetNames;
|
PetNames = ipcCallerPetNames;
|
||||||
Brio = ipcCallerBrio;
|
Brio = ipcCallerBrio;
|
||||||
|
|
||||||
if (Initialized)
|
_wasInitialized = Initialized;
|
||||||
|
if (_wasInitialized)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PenumbraInitializedMessage());
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
}
|
}
|
||||||
@@ -58,5 +61,13 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
Moodles.CheckAPI();
|
Moodles.CheckAPI();
|
||||||
PetNames.CheckAPI();
|
PetNames.CheckAPI();
|
||||||
Brio.CheckAPI();
|
Brio.CheckAPI();
|
||||||
|
|
||||||
|
var initialized = Initialized;
|
||||||
|
if (initialized && !_wasInitialized)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
_wasInitialized = initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly PairHandlerRegistry _pairHandlerRegistry;
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private IServiceScope? _runtimeServiceScope;
|
private IServiceScope? _runtimeServiceScope;
|
||||||
private Task? _launchTask = null;
|
private Task? _launchTask = null;
|
||||||
@@ -81,11 +82,13 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
|
PairHandlerRegistry pairHandlerRegistry,
|
||||||
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_pairHandlerRegistry = pairHandlerRegistry;
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,12 +111,20 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug("Halting LightlessPlugin");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pairHandlerRegistry.ResetAllHandlers();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to reset pair handlers on shutdown");
|
||||||
|
}
|
||||||
|
|
||||||
UnsubscribeAll();
|
UnsubscribeAll();
|
||||||
|
|
||||||
DalamudUtilOnLogOut();
|
DalamudUtilOnLogOut();
|
||||||
|
|
||||||
Logger.LogDebug("Halting LightlessPlugin");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ public sealed partial class PairCoordinator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
|
||||||
PublishPairDataChanged();
|
PublishPairDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
@@ -48,6 +49,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly IFramework _framework;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||||
private Guid _applicationId;
|
private Guid _applicationId;
|
||||||
private Task? _applicationTask;
|
private Task? _applicationTask;
|
||||||
@@ -66,6 +68,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private int _lastMissingNonCriticalMods;
|
private int _lastMissingNonCriticalMods;
|
||||||
private int _lastMissingForbiddenMods;
|
private int _lastMissingForbiddenMods;
|
||||||
private bool _lastMissingCachedFiles;
|
private bool _lastMissingCachedFiles;
|
||||||
|
private string? _lastSuccessfulDataHash;
|
||||||
private bool _isVisible;
|
private bool _isVisible;
|
||||||
private Guid _penumbraCollection;
|
private Guid _penumbraCollection;
|
||||||
private readonly object _collectionGate = new();
|
private readonly object _collectionGate = new();
|
||||||
@@ -82,6 +85,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly object _visibilityGraceGate = new();
|
private readonly object _visibilityGraceGate = new();
|
||||||
private CancellationTokenSource? _visibilityGraceCts;
|
private CancellationTokenSource? _visibilityGraceCts;
|
||||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||||
|
private readonly object _ownedRetryGate = new();
|
||||||
|
private readonly Dictionary<ObjectKind, HashSet<PlayerChanges>> _pendingOwnedChanges = new();
|
||||||
|
private CancellationTokenSource? _ownedRetryCts;
|
||||||
|
private Task _ownedRetryTask = Task.CompletedTask;
|
||||||
|
private static readonly TimeSpan OwnedRetryInitialDelay = TimeSpan.FromSeconds(1);
|
||||||
|
private static readonly TimeSpan OwnedRetryMaxDelay = TimeSpan.FromSeconds(10);
|
||||||
|
private static readonly TimeSpan OwnedRetryStaleDataGrace = TimeSpan.FromMinutes(5);
|
||||||
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
".tmb",
|
".tmb",
|
||||||
@@ -95,10 +105,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
||||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
||||||
|
private const int FullyLoadedTimeoutMsPlayer = 30000;
|
||||||
|
private const int FullyLoadedTimeoutMsOther = 5000;
|
||||||
private readonly object _actorInitializationGate = new();
|
private readonly object _actorInitializationGate = new();
|
||||||
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
||||||
private bool _actorInitializationInProgress;
|
private bool _actorInitializationInProgress;
|
||||||
private bool _frameworkUpdateSubscribed;
|
private bool _frameworkUpdateSubscribed;
|
||||||
|
private nint _lastKnownAddress = nint.Zero;
|
||||||
|
private ushort _lastKnownObjectIndex = ushort.MaxValue;
|
||||||
|
private string? _lastKnownName;
|
||||||
|
|
||||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||||
@@ -175,6 +190,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
FileDownloadManager transferManager,
|
FileDownloadManager transferManager,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
|
IFramework framework,
|
||||||
ActorObjectService actorObjectService,
|
ActorObjectService actorObjectService,
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager,
|
FileCacheManager fileDbManager,
|
||||||
@@ -193,6 +209,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadManager = transferManager;
|
_downloadManager = transferManager;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_framework = framework;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
@@ -432,7 +449,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
|
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null, bool awaitIpc = true)
|
||||||
{
|
{
|
||||||
Guid toRelease = Guid.Empty;
|
Guid toRelease = Guid.Empty;
|
||||||
bool hadCollection = false;
|
bool hadCollection = false;
|
||||||
@@ -466,16 +483,33 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var applicationId = Guid.NewGuid();
|
||||||
|
if (awaitIpc)
|
||||||
{
|
{
|
||||||
var applicationId = Guid.NewGuid();
|
try
|
||||||
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
|
{
|
||||||
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
|
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());
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
|
try
|
||||||
}
|
{
|
||||||
|
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
|
||||||
|
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AnyPair(Func<PairConnection, bool> predicate)
|
private bool AnyPair(Func<PairConnection, bool> predicate)
|
||||||
@@ -559,9 +593,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
var hasMissingCachedFiles = HasMissingCachedFiles(LastReceivedCharacterData);
|
var hasMissingCachedFiles = HasMissingCachedFiles(LastReceivedCharacterData);
|
||||||
|
var missingStarted = !_lastMissingCachedFiles && hasMissingCachedFiles;
|
||||||
var missingResolved = _lastMissingCachedFiles && !hasMissingCachedFiles;
|
var missingResolved = _lastMissingCachedFiles && !hasMissingCachedFiles;
|
||||||
_lastMissingCachedFiles = hasMissingCachedFiles;
|
_lastMissingCachedFiles = hasMissingCachedFiles;
|
||||||
var shouldForce = forced || missingResolved;
|
var shouldForce = forced || missingStarted || missingResolved;
|
||||||
|
var forceApplyCustomization = forced;
|
||||||
|
|
||||||
if (IsPaused())
|
if (IsPaused())
|
||||||
{
|
{
|
||||||
@@ -569,7 +605,22 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldForce)
|
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
|
||||||
|
if (sanitized is null)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dataApplied = !string.IsNullOrEmpty(dataHash)
|
||||||
|
&& string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal);
|
||||||
|
var needsApply = !dataApplied;
|
||||||
|
var hasModReplacements = sanitized.FileReplacements.Values.Any(list => list.Count > 0);
|
||||||
|
var needsModReapply = needsApply && hasModReplacements;
|
||||||
|
var shouldForceMods = shouldForce || needsModReapply;
|
||||||
|
forceApplyCustomization = forced || needsApply;
|
||||||
|
var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied;
|
||||||
|
|
||||||
|
if (shouldForceMods)
|
||||||
{
|
{
|
||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
_forceFullReapply = true;
|
_forceFullReapply = true;
|
||||||
@@ -579,15 +630,21 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
LastAppliedApproximateEffectiveVRAMBytes = -1;
|
LastAppliedApproximateEffectiveVRAMBytes = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sanitized = CloneAndSanitizeLastReceived(out _);
|
|
||||||
if (sanitized is null)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pairStateCache.Store(Ident, sanitized);
|
_pairStateCache.Store(Ident, sanitized);
|
||||||
|
|
||||||
|
if (!IsVisible && !_pauseRequested)
|
||||||
|
{
|
||||||
|
if (_charaHandler is not null && _charaHandler.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
_charaHandler.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlayerCharacter != nint.Zero)
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsVisible)
|
if (!IsVisible)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
|
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
|
||||||
@@ -596,7 +653,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce);
|
ApplyCharacterData(Guid.NewGuid(), sanitized, forceApplyCustomization, suppressForcedModRedraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool FetchPerformanceMetricsFromCache()
|
public bool FetchPerformanceMetricsFromCache()
|
||||||
@@ -906,7 +963,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
SetUploading(false);
|
SetUploading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false, bool suppressForcedModRedraw = false)
|
||||||
{
|
{
|
||||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||||
ClearFailureState();
|
ClearFailureState();
|
||||||
@@ -1000,7 +1057,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
||||||
"Applying Character Data")));
|
"Applying Character Data")));
|
||||||
|
|
||||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this,
|
||||||
|
forceApplyCustomization, _forceApplyMods, suppressForcedModRedraw);
|
||||||
|
|
||||||
if (handlerReady && _forceApplyMods)
|
if (handlerReady && _forceApplyMods)
|
||||||
{
|
{
|
||||||
@@ -1097,12 +1155,183 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ScheduleOwnedObjectRetry(ObjectKind kind, HashSet<PlayerChanges> changes)
|
||||||
|
{
|
||||||
|
if (kind == ObjectKind.Player || changes.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_ownedRetryGate)
|
||||||
|
{
|
||||||
|
_pendingOwnedChanges[kind] = new HashSet<PlayerChanges>(changes);
|
||||||
|
if (!_ownedRetryTask.IsCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ownedRetryCts = _ownedRetryCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
var token = _ownedRetryCts.Token;
|
||||||
|
_ownedRetryTask = Task.Run(() => OwnedObjectRetryLoopAsync(token), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearOwnedObjectRetry(ObjectKind kind)
|
||||||
|
{
|
||||||
|
lock (_ownedRetryGate)
|
||||||
|
{
|
||||||
|
if (!_pendingOwnedChanges.Remove(kind))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearAllOwnedObjectRetries()
|
||||||
|
{
|
||||||
|
lock (_ownedRetryGate)
|
||||||
|
{
|
||||||
|
_pendingOwnedChanges.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOwnedRetryDataStale()
|
||||||
|
{
|
||||||
|
if (!_lastDataReceivedAt.HasValue)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.UtcNow - _lastDataReceivedAt.Value > OwnedRetryStaleDataGrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OwnedObjectRetryLoopAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
var delay = OwnedRetryInitialDelay;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (IsOwnedRetryDataStale())
|
||||||
|
{
|
||||||
|
ClearAllOwnedObjectRetries();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<ObjectKind, HashSet<PlayerChanges>> pending;
|
||||||
|
lock (_ownedRetryGate)
|
||||||
|
{
|
||||||
|
if (_pendingOwnedChanges.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = _pendingOwnedChanges.ToDictionary(kvp => kvp.Key, kvp => new HashSet<PlayerChanges>(kvp.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsVisible || IsPaused() || !CanApplyNow() || PlayerCharacter == nint.Zero || _charaHandler is null)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
delay = IncreaseRetryDelay(delay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_applicationTask?.IsCompleted ?? true) == false || (_pairDownloadTask?.IsCompleted ?? true) == false)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
delay = IncreaseRetryDelay(delay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitized = CloneAndSanitizeLastReceived(out _);
|
||||||
|
if (sanitized is null)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
delay = IncreaseRetryDelay(delay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anyApplied = false;
|
||||||
|
foreach (var entry in pending)
|
||||||
|
{
|
||||||
|
if (!HasAppearanceDataForKind(sanitized, entry.Key))
|
||||||
|
{
|
||||||
|
ClearOwnedObjectRetry(entry.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var applied = await ApplyCustomizationDataAsync(Guid.NewGuid(), entry, sanitized, token).ConfigureAwait(false);
|
||||||
|
if (applied)
|
||||||
|
{
|
||||||
|
ClearOwnedObjectRetry(entry.Key);
|
||||||
|
anyApplied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyApplied)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
delay = IncreaseRetryDelay(delay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
delay = OwnedRetryInitialDelay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Owned object retry task failed for {handler}", GetLogIdentifier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan IncreaseRetryDelay(TimeSpan delay)
|
||||||
|
{
|
||||||
|
var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds);
|
||||||
|
return TimeSpan.FromMilliseconds(nextMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasAppearanceDataForKind(CharacterData data, ObjectKind kind)
|
||||||
|
{
|
||||||
|
if (data.FileReplacements.TryGetValue(kind, out var replacements) && replacements.Count > 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.GlamourerData.TryGetValue(kind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.CustomizePlusData.TryGetValue(kind, out var customize) && !string.IsNullOrEmpty(customize))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
SetUploading(false);
|
SetUploading(false);
|
||||||
var name = PlayerName;
|
var name = PlayerName;
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
_lastKnownName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentAddress = PlayerCharacter;
|
||||||
|
if (currentAddress != nint.Zero)
|
||||||
|
{
|
||||||
|
_lastKnownAddress = currentAddress;
|
||||||
|
}
|
||||||
|
|
||||||
var user = GetPrimaryUserDataSafe();
|
var user = GetPrimaryUserDataSafe();
|
||||||
var alias = GetPrimaryAliasOrUidSafe();
|
var alias = GetPrimaryAliasOrUidSafe();
|
||||||
Logger.LogDebug("Disposing {name} ({user})", name, alias);
|
Logger.LogDebug("Disposing {name} ({user})", name, alias);
|
||||||
@@ -1113,6 +1342,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_applicationCancellationTokenSource = null;
|
_applicationCancellationTokenSource = null;
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
_downloadCancellationTokenSource = null;
|
_downloadCancellationTokenSource = null;
|
||||||
|
ClearAllOwnedObjectRetries();
|
||||||
|
_ownedRetryCts?.CancelDispose();
|
||||||
|
_ownedRetryCts = null;
|
||||||
_downloadManager.Dispose();
|
_downloadManager.Dispose();
|
||||||
_charaHandler?.Dispose();
|
_charaHandler?.Dispose();
|
||||||
CancelVisibilityGraceTask();
|
CancelVisibilityGraceTask();
|
||||||
@@ -1125,43 +1357,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), EventSeverity.Informational, "Disposing User")));
|
Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), EventSeverity.Informational, "Disposing User")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
|
if (IsFrameworkUnloading())
|
||||||
|
|
||||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
|
||||||
{
|
{
|
||||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
|
Logger.LogWarning("Framework is unloading, skipping disposal for {name} ({user})", name, alias);
|
||||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
|
return;
|
||||||
ResetPenumbraCollection(reason: nameof(Dispose));
|
}
|
||||||
if (!IsVisible)
|
|
||||||
|
var isStopping = _lifetime.ApplicationStopping.IsCancellationRequested;
|
||||||
|
if (isStopping)
|
||||||
|
{
|
||||||
|
ResetPenumbraCollection(reason: "DisposeStopping", awaitIpc: false);
|
||||||
|
ScheduleSafeRevertOnDisposal(applicationId, name, alias);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canCleanup = !string.IsNullOrEmpty(name)
|
||||||
|
&& _dalamudUtil.IsLoggedIn
|
||||||
|
&& !_dalamudUtil.IsZoning
|
||||||
|
&& !_dalamudUtil.IsInCutscene;
|
||||||
|
|
||||||
|
if (!canCleanup)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
|
_cachedData = effectiveCachedData;
|
||||||
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
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 ?? [])
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource();
|
try
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
|
||||||
|
|
||||||
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
|
|
||||||
if (effectiveCachedData is not null)
|
|
||||||
{
|
{
|
||||||
_cachedData = effectiveCachedData;
|
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
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
|
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||||
{
|
break;
|
||||||
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1174,6 +1425,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
PlayerName = null;
|
PlayerName = null;
|
||||||
_cachedData = null;
|
_cachedData = null;
|
||||||
|
_lastSuccessfulDataHash = null;
|
||||||
_lastAppliedModdedPaths = null;
|
_lastAppliedModdedPaths = null;
|
||||||
_needsCollectionRebuild = false;
|
_needsCollectionRebuild = false;
|
||||||
_performanceMetricsCache.Clear(Ident);
|
_performanceMetricsCache.Clear(Ident);
|
||||||
@@ -1181,9 +1433,145 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
private bool IsFrameworkUnloading()
|
||||||
{
|
{
|
||||||
if (PlayerCharacter == nint.Zero) return;
|
try
|
||||||
|
{
|
||||||
|
var prop = _framework.GetType().GetProperty("IsFrameworkUnloading");
|
||||||
|
if (prop?.PropertyType == typeof(bool))
|
||||||
|
{
|
||||||
|
return (bool)prop.GetValue(_framework)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScheduleSafeRevertOnDisposal(Guid applicationId, string? name, string alias)
|
||||||
|
{
|
||||||
|
var cleanupName = !string.IsNullOrEmpty(name) ? name : _lastKnownName;
|
||||||
|
var cleanupAddress = _lastKnownAddress != nint.Zero
|
||||||
|
? _lastKnownAddress
|
||||||
|
: _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident);
|
||||||
|
var cleanupObjectIndex = _lastKnownObjectIndex;
|
||||||
|
var cleanupIdent = Ident;
|
||||||
|
var customizeIds = _customizeIds.Values.Where(id => id.HasValue)
|
||||||
|
.Select(id => id!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(cleanupName)
|
||||||
|
&& cleanupAddress == nint.Zero
|
||||||
|
&& cleanupObjectIndex == ushort.MaxValue
|
||||||
|
&& customizeIds.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(() => SafeRevertOnDisposalAsync(
|
||||||
|
applicationId,
|
||||||
|
cleanupName,
|
||||||
|
cleanupAddress,
|
||||||
|
cleanupObjectIndex,
|
||||||
|
cleanupIdent,
|
||||||
|
customizeIds,
|
||||||
|
alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SafeRevertOnDisposalAsync(
|
||||||
|
Guid applicationId,
|
||||||
|
string? cleanupName,
|
||||||
|
nint cleanupAddress,
|
||||||
|
ushort cleanupObjectIndex,
|
||||||
|
string cleanupIdent,
|
||||||
|
IReadOnlyList<Guid> customizeIds,
|
||||||
|
string alias)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsFrameworkUnloading())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(cleanupName) && _ipcManager.Glamourer.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, cleanupName, alias);
|
||||||
|
await _ipcManager.Glamourer.RevertByNameAsync(Logger, cleanupName, applicationId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.CustomizePlus.APIAvailable && customizeIds.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var customizeId in customizeIds)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = cleanupAddress;
|
||||||
|
if (address == nint.Zero && cleanupObjectIndex != ushort.MaxValue)
|
||||||
|
{
|
||||||
|
address = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(cleanupObjectIndex);
|
||||||
|
if (obj is not Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter player)
|
||||||
|
{
|
||||||
|
return nint.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DalamudUtilService.TryGetHashedCID(player, out var hash)
|
||||||
|
|| !string.Equals(hash, cleanupIdent, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return nint.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
return player.Address;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address == nint.Zero)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.CustomizePlus.APIAvailable)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertAsync(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.Heels.APIAvailable)
|
||||||
|
{
|
||||||
|
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.Honorific.APIAvailable)
|
||||||
|
{
|
||||||
|
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.Moodles.APIAvailable)
|
||||||
|
{
|
||||||
|
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ipcManager.PetNames.APIAvailable)
|
||||||
|
{
|
||||||
|
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Failed shutdown cleanup for {name}", cleanupName ?? cleanupIdent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (PlayerCharacter == nint.Zero) return false;
|
||||||
var ptr = PlayerCharacter;
|
var ptr = PlayerCharacter;
|
||||||
|
|
||||||
var handler = changes.Key switch
|
var handler = changes.Key switch
|
||||||
@@ -1199,14 +1587,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
if (handler.Address == nint.Zero)
|
if (handler.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
|
||||||
|
if (handler.ObjectKind != ObjectKind.Player
|
||||||
|
&& handler.CurrentDrawCondition == GameObjectHandler.DrawCondition.DrawObjectZero)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Skipping customization apply for {handler}, draw object not available", applicationId, handler);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
|
||||||
|
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? FullyLoadedTimeoutMsPlayer : FullyLoadedTimeoutMsOther;
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, drawTimeoutMs, token).ConfigureAwait(false);
|
||||||
if (handler.Address != nint.Zero)
|
if (handler.Address != nint.Zero)
|
||||||
{
|
{
|
||||||
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs).ConfigureAwait(false);
|
||||||
|
if (!fullyLoaded)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Timed out waiting for {handler} to fully load, skipping customization apply", applicationId, handler);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
@@ -1270,6 +1673,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1577,37 +1982,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
|
||||||
|
|
||||||
if (_applicationTask != null && !_applicationTask.IsCompleted)
|
var appToken = _applicationCancellationTokenSource?.Token;
|
||||||
|
while ((!_applicationTask?.IsCompleted ?? false)
|
||||||
|
&& !downloadToken.IsCancellationRequested
|
||||||
|
&& (!appToken?.IsCancellationRequested ?? false))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName);
|
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish",
|
||||||
|
applicationBase, _applicationId, PlayerName);
|
||||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
await Task.Delay(250).ConfigureAwait(false);
|
||||||
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
timeoutCts.Dispose();
|
|
||||||
combinedCts.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadToken.IsCancellationRequested)
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
||||||
{
|
{
|
||||||
_forceFullReapply = true;
|
_forceFullReapply = true;
|
||||||
RecordFailure("Application cancelled", "Cancellation");
|
RecordFailure("Application cancelled", "Cancellation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _applicationCancellationTokenSource.Token;
|
var token = _applicationCancellationTokenSource.Token;
|
||||||
|
|
||||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
||||||
@@ -1630,7 +2023,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
||||||
if (handlerForApply.Address != nint.Zero)
|
if (handlerForApply.Address != nint.Zero)
|
||||||
{
|
{
|
||||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token, FullyLoadedTimeoutMsPlayer).ConfigureAwait(false);
|
||||||
|
if (!fullyLoaded)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[BASE-{applicationId}] Timed out waiting for {handler} to fully load, caching data for later application",
|
||||||
|
applicationBase, GetLogIdentifier());
|
||||||
|
_cachedData = charaData;
|
||||||
|
_pairStateCache.Store(Ident, charaData);
|
||||||
|
_forceFullReapply = true;
|
||||||
|
RecordFailure("Actor not fully loaded within timeout", "FullyLoadedTimeout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
@@ -1692,7 +2095,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
foreach (var kind in updatedData)
|
foreach (var kind in updatedData)
|
||||||
{
|
{
|
||||||
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
var applied = await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||||
|
if (applied)
|
||||||
|
{
|
||||||
|
ClearOwnedObjectRetry(kind.Key);
|
||||||
|
}
|
||||||
|
else if (kind.Key != ObjectKind.Player)
|
||||||
|
{
|
||||||
|
ScheduleOwnedObjectRetry(kind.Key, kind.Value);
|
||||||
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1714,6 +2125,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
StorePerformanceMetrics(charaData);
|
StorePerformanceMetrics(charaData);
|
||||||
|
_lastSuccessfulDataHash = GetDataHashSafe(charaData);
|
||||||
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
||||||
ClearFailureState();
|
ClearFailureState();
|
||||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||||
@@ -1827,6 +2239,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
IsVisible = false;
|
IsVisible = false;
|
||||||
_charaHandler?.Invalidate();
|
_charaHandler?.Invalidate();
|
||||||
|
ClearAllOwnedObjectRetries();
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
_downloadCancellationTokenSource = null;
|
_downloadCancellationTokenSource = null;
|
||||||
if (logChange)
|
if (logChange)
|
||||||
@@ -1839,6 +2252,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
PlayerName = name;
|
PlayerName = name;
|
||||||
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult();
|
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult();
|
||||||
|
UpdateLastKnownActor(_charaHandler.Address, name);
|
||||||
|
|
||||||
var user = GetPrimaryUserData();
|
var user = GetPrimaryUserData();
|
||||||
if (!string.IsNullOrEmpty(user.UID))
|
if (!string.IsNullOrEmpty(user.UID))
|
||||||
@@ -2185,6 +2599,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
if (descriptor.Address == nint.Zero)
|
if (descriptor.Address == nint.Zero)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
UpdateLastKnownActor(descriptor);
|
||||||
RefreshTrackedHandler(descriptor);
|
RefreshTrackedHandler(descriptor);
|
||||||
QueueActorInitialization(descriptor);
|
QueueActorInitialization(descriptor);
|
||||||
}
|
}
|
||||||
@@ -2308,6 +2723,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return !string.IsNullOrEmpty(hashedCid);
|
return !string.IsNullOrEmpty(hashedCid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateLastKnownActor(ActorObjectService.ActorDescriptor descriptor)
|
||||||
|
{
|
||||||
|
_lastKnownAddress = descriptor.Address;
|
||||||
|
_lastKnownObjectIndex = descriptor.ObjectIndex;
|
||||||
|
if (!string.IsNullOrEmpty(descriptor.Name))
|
||||||
|
{
|
||||||
|
_lastKnownName = descriptor.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLastKnownActor(nint address, string? name)
|
||||||
|
{
|
||||||
|
if (address != nint.Zero)
|
||||||
|
{
|
||||||
|
_lastKnownAddress = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
_lastKnownName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||||
{
|
{
|
||||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using LightlessSync.Services.Mediator;
|
|||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -32,6 +33,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -42,6 +44,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
IFramework framework,
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
@@ -60,6 +63,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_framework = framework;
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
@@ -86,6 +90,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
|
_framework,
|
||||||
actorObjectService,
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||||
|
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
||||||
{
|
{
|
||||||
_fileTransferManager.CancelUpload();
|
_fileTransferManager.CancelUpload();
|
||||||
@@ -111,6 +112,20 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
_ = PushCharacterDataAsync(forced);
|
_ = PushCharacterDataAsync(forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandlePairOnline(PairUniqueIdentifier pairIdent)
|
||||||
|
{
|
||||||
|
if (!_apiController.IsConnected || !_pairLedger.IsPairVisible(pairIdent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pairLedger.GetHandler(pairIdent)?.UserData is { } user)
|
||||||
|
{
|
||||||
|
_usersToPushDataTo.Add(user);
|
||||||
|
PushCharacterData(forced: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PushCharacterDataAsync(bool forced = false)
|
private async Task PushCharacterDataAsync(bool forced = false)
|
||||||
{
|
{
|
||||||
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||||
@@ -152,5 +167,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
|
private List<UserData> GetVisibleUsers()
|
||||||
|
=> [.. _pairLedger.GetVisiblePairs().Where(connection => connection.IsOnline).Select(connection => connection.User)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(new WindowSystem("LightlessSync"));
|
services.AddSingleton(new WindowSystem("LightlessSync"));
|
||||||
services.AddSingleton<FileDialogManager>();
|
services.AddSingleton<FileDialogManager>();
|
||||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||||
|
services.AddSingleton(framework);
|
||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
services.AddSingleton(gameInteropProvider);
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
|
|||||||
@@ -213,18 +213,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
|
public async Task<bool> WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default, int timeOutMs = 30000)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||||
|
|
||||||
|
var timeoutAt = timeOutMs > 0 ? Environment.TickCount64 + timeOutMs : long.MaxValue;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
var loadState = await _framework.RunOnFrameworkThread(() => GetObjectLoadState(address)).ConfigureAwait(false);
|
||||||
if (!IsZoning && isLoaded)
|
if (!loadState.IsValid)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
|
if (!IsZoning && loadState.IsLoaded)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (Environment.TickCount64 >= timeoutAt)
|
||||||
|
return false;
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -1143,6 +1150,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoadState GetObjectLoadState(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return LoadState.Invalid;
|
||||||
|
|
||||||
|
var obj = _objectTable.CreateObjectReference(address);
|
||||||
|
if (obj is null || obj.Address != address)
|
||||||
|
return LoadState.Invalid;
|
||||||
|
|
||||||
|
return new LoadState(true, IsObjectFullyLoaded(address));
|
||||||
|
}
|
||||||
|
|
||||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -1169,6 +1188,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
||||||
|
{
|
||||||
|
public static LoadState Invalid => new(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record OwnedObjectSnapshot(
|
private sealed record OwnedObjectSnapshot(
|
||||||
IReadOnlyList<nint> RenderedPlayers,
|
IReadOnlyList<nint> RenderedPlayers,
|
||||||
IReadOnlyList<nint> RenderedCompanions,
|
IReadOnlyList<nint> RenderedCompanions,
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase;
|
|||||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||||
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
||||||
|
public record PairOnlineMessage(PairUniqueIdentifier PairIdent) : MessageBase;
|
||||||
public record CombatStartMessage : MessageBase;
|
public record CombatStartMessage : MessageBase;
|
||||||
public record CombatEndMessage : MessageBase;
|
public record CombatEndMessage : MessageBase;
|
||||||
public record PerformanceStartMessage : MessageBase;
|
public record PerformanceStartMessage : MessageBase;
|
||||||
|
|||||||
@@ -77,16 +77,39 @@ public sealed class TextureDownscaleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||||
|
=> ScheduleDownscale(hash, filePath, () => mapKind);
|
||||||
|
|
||||||
|
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
||||||
{
|
{
|
||||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
if (_activeJobs.ContainsKey(hash)) return;
|
if (_activeJobs.ContainsKey(hash)) return;
|
||||||
|
|
||||||
_activeJobs[hash] = Task.Run(async () =>
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
TextureMapKind mapKind;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mapKind = mapKindFactory();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to determine texture map kind for {Hash}; skipping downscale", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShouldScheduleDownscale(string filePath)
|
||||||
|
{
|
||||||
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
|
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
||||||
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
{
|
{
|
||||||
if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
||||||
|
|||||||
@@ -206,12 +206,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var dlQueue = 0;
|
var dlQueue = 0;
|
||||||
var dlProg = 0;
|
var dlProg = 0;
|
||||||
var dlDecomp = 0;
|
var dlDecomp = 0;
|
||||||
|
var dlComplete = 0;
|
||||||
|
|
||||||
foreach (var entry in transfer.Value)
|
foreach (var entry in transfer.Value)
|
||||||
{
|
{
|
||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
|
case DownloadStatus.Initializing:
|
||||||
|
dlQueue++;
|
||||||
|
break;
|
||||||
case DownloadStatus.WaitingForSlot:
|
case DownloadStatus.WaitingForSlot:
|
||||||
dlSlot++;
|
dlSlot++;
|
||||||
break;
|
break;
|
||||||
@@ -224,15 +228,20 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
case DownloadStatus.Decompressing:
|
case DownloadStatus.Decompressing:
|
||||||
dlDecomp++;
|
dlDecomp++;
|
||||||
break;
|
break;
|
||||||
|
case DownloadStatus.Completed:
|
||||||
|
dlComplete++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
|
||||||
|
|
||||||
string statusText;
|
string statusText;
|
||||||
if (dlProg > 0)
|
if (dlProg > 0)
|
||||||
{
|
{
|
||||||
statusText = "Downloading";
|
statusText = "Downloading";
|
||||||
}
|
}
|
||||||
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
|
else if (dlDecomp > 0)
|
||||||
{
|
{
|
||||||
statusText = "Decompressing";
|
statusText = "Decompressing";
|
||||||
}
|
}
|
||||||
@@ -244,6 +253,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
statusText = "Waiting for slot";
|
statusText = "Waiting for slot";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
statusText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
statusText = "Waiting";
|
statusText = "Waiting";
|
||||||
@@ -309,7 +322,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
fillPercent = transferredBytes / (double)totalBytes;
|
fillPercent = transferredBytes / (double)totalBytes;
|
||||||
showFill = true;
|
showFill = true;
|
||||||
}
|
}
|
||||||
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
|
else if (dlDecomp > 0 || dlComplete > 0 || transferredBytes >= totalBytes)
|
||||||
{
|
{
|
||||||
fillPercent = 1.0;
|
fillPercent = 1.0;
|
||||||
showFill = true;
|
showFill = true;
|
||||||
@@ -341,10 +354,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
downloadText =
|
downloadText =
|
||||||
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||||
}
|
}
|
||||||
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
|
else if (dlDecomp > 0)
|
||||||
{
|
{
|
||||||
downloadText = "Decompressing";
|
downloadText = "Decompressing";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
downloadText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Waiting states
|
// Waiting states
|
||||||
@@ -417,6 +434,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var totalDlQueue = 0;
|
var totalDlQueue = 0;
|
||||||
var totalDlProg = 0;
|
var totalDlProg = 0;
|
||||||
var totalDlDecomp = 0;
|
var totalDlDecomp = 0;
|
||||||
|
var totalDlComplete = 0;
|
||||||
|
|
||||||
var perPlayer = new List<(
|
var perPlayer = new List<(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -428,7 +446,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
int DlSlot,
|
int DlSlot,
|
||||||
int DlQueue,
|
int DlQueue,
|
||||||
int DlProg,
|
int DlProg,
|
||||||
int DlDecomp)>();
|
int DlDecomp,
|
||||||
|
int DlComplete)>();
|
||||||
|
|
||||||
foreach (var transfer in _currentDownloads)
|
foreach (var transfer in _currentDownloads)
|
||||||
{
|
{
|
||||||
@@ -450,12 +469,17 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var playerDlQueue = 0;
|
var playerDlQueue = 0;
|
||||||
var playerDlProg = 0;
|
var playerDlProg = 0;
|
||||||
var playerDlDecomp = 0;
|
var playerDlDecomp = 0;
|
||||||
|
var playerDlComplete = 0;
|
||||||
|
|
||||||
foreach (var entry in transfer.Value)
|
foreach (var entry in transfer.Value)
|
||||||
{
|
{
|
||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
|
case DownloadStatus.Initializing:
|
||||||
|
playerDlQueue++;
|
||||||
|
totalDlQueue++;
|
||||||
|
break;
|
||||||
case DownloadStatus.WaitingForSlot:
|
case DownloadStatus.WaitingForSlot:
|
||||||
playerDlSlot++;
|
playerDlSlot++;
|
||||||
totalDlSlot++;
|
totalDlSlot++;
|
||||||
@@ -472,6 +496,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
playerDlDecomp++;
|
playerDlDecomp++;
|
||||||
totalDlDecomp++;
|
totalDlDecomp++;
|
||||||
break;
|
break;
|
||||||
|
case DownloadStatus.Completed:
|
||||||
|
playerDlComplete++;
|
||||||
|
totalDlComplete++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +525,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
playerDlSlot,
|
playerDlSlot,
|
||||||
playerDlQueue,
|
playerDlQueue,
|
||||||
playerDlProg,
|
playerDlProg,
|
||||||
playerDlDecomp
|
playerDlDecomp,
|
||||||
|
playerDlComplete
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +550,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
// Overall texts
|
// Overall texts
|
||||||
var headerText =
|
var headerText =
|
||||||
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";
|
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}/C:{totalDlComplete}]";
|
||||||
|
|
||||||
var bytesText =
|
var bytesText =
|
||||||
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||||
@@ -544,7 +573,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
foreach (var p in perPlayer)
|
foreach (var p in perPlayer)
|
||||||
{
|
{
|
||||||
var line =
|
var line =
|
||||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||||
|
|
||||||
var lineSize = ImGui.CalcTextSize(line);
|
var lineSize = ImGui.CalcTextSize(line);
|
||||||
if (lineSize.X > contentWidth)
|
if (lineSize.X > contentWidth)
|
||||||
@@ -662,7 +691,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
&& p.TransferredBytes > 0;
|
&& p.TransferredBytes > 0;
|
||||||
|
|
||||||
var labelLine =
|
var labelLine =
|
||||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||||
|
|
||||||
if (!showBar)
|
if (!showBar)
|
||||||
{
|
{
|
||||||
@@ -721,13 +750,18 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
// Text inside bar: downloading vs decompressing
|
// Text inside bar: downloading vs decompressing
|
||||||
string barText;
|
string barText;
|
||||||
|
|
||||||
var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;
|
var isDecompressing = p.DlDecomp > 0;
|
||||||
|
var isAllComplete = p.DlComplete > 0 && p.DlProg == 0 && p.DlDecomp == 0 && p.DlQueue == 0 && p.DlSlot == 0;
|
||||||
|
|
||||||
if (isDecompressing)
|
if (isDecompressing)
|
||||||
{
|
{
|
||||||
// Keep bar full, static text showing decompressing
|
// Keep bar full, static text showing decompressing
|
||||||
barText = "Decompressing...";
|
barText = "Decompressing...";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
barText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var bytesInside =
|
var bytesInside =
|
||||||
@@ -808,6 +842,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var dlQueue = 0;
|
var dlQueue = 0;
|
||||||
var dlProg = 0;
|
var dlProg = 0;
|
||||||
var dlDecomp = 0;
|
var dlDecomp = 0;
|
||||||
|
var dlComplete = 0;
|
||||||
long totalBytes = 0;
|
long totalBytes = 0;
|
||||||
long transferredBytes = 0;
|
long transferredBytes = 0;
|
||||||
|
|
||||||
@@ -817,22 +852,29 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
|
case DownloadStatus.Initializing: dlQueue++; break;
|
||||||
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
||||||
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
||||||
case DownloadStatus.Downloading: dlProg++; break;
|
case DownloadStatus.Downloading: dlProg++; break;
|
||||||
case DownloadStatus.Decompressing: dlDecomp++; break;
|
case DownloadStatus.Decompressing: dlDecomp++; break;
|
||||||
|
case DownloadStatus.Completed: dlComplete++; break;
|
||||||
}
|
}
|
||||||
totalBytes += fileStatus.TotalBytes;
|
totalBytes += fileStatus.TotalBytes;
|
||||||
transferredBytes += fileStatus.TransferredBytes;
|
transferredBytes += fileStatus.TransferredBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||||
|
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
|
||||||
|
{
|
||||||
|
progress = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
string status;
|
string status;
|
||||||
if (dlDecomp > 0) status = "decompressing";
|
if (dlDecomp > 0) status = "decompressing";
|
||||||
else if (dlProg > 0) status = "downloading";
|
else if (dlProg > 0) status = "downloading";
|
||||||
else if (dlQueue > 0) status = "queued";
|
else if (dlQueue > 0) status = "queued";
|
||||||
else if (dlSlot > 0) status = "waiting";
|
else if (dlSlot > 0) status = "waiting";
|
||||||
|
else if (dlComplete > 0) status = "completed";
|
||||||
else status = "completed";
|
else status = "completed";
|
||||||
|
|
||||||
downloadStatus.Add((item.Key.Name, progress, status));
|
downloadStatus.Add((item.Key.Name, progress, status));
|
||||||
|
|||||||
@@ -863,10 +863,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
_uiShared.DrawHelpText(
|
_uiShared.DrawHelpText(
|
||||||
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
|
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
$"What do W/Q/P/D/C stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
||||||
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
|
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
|
||||||
$"P = Processing download (aka downloading){Environment.NewLine}" +
|
$"P = Processing download (aka downloading){Environment.NewLine}" +
|
||||||
$"D = Decompressing download");
|
$"D = Decompressing download{Environment.NewLine}" +
|
||||||
|
$"C = Completed download");
|
||||||
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
|
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
|
||||||
ImGui.Indent();
|
ImGui.Indent();
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ public static class VariousExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods,
|
||||||
|
bool suppressForcedRedrawOnForcedModApply = false)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
oldData ??= new();
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ public static class VariousExtensions
|
|||||||
|
|
||||||
bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null;
|
bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null;
|
||||||
bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null;
|
bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null;
|
||||||
|
var forceRedrawOnForcedApply = forceApplyMods && !suppressForcedRedrawOnForcedModApply;
|
||||||
|
|
||||||
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
|
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
|
||||||
{
|
{
|
||||||
@@ -100,7 +102,7 @@ public static class VariousExtensions
|
|||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
||||||
if (forceApplyMods || objectKind != ObjectKind.Player)
|
if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply)
|
||||||
{
|
{
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ public static class VariousExtensions
|
|||||||
if (objectKind != ObjectKind.Player) continue;
|
if (objectKind != ObjectKind.Player) continue;
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||||
if (manipDataDifferent || forceApplyMods)
|
if (manipDataDifferent || forceRedrawOnForcedApply)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
private readonly SemaphoreSlim _decompressGate =
|
|
||||||
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
|
|
||||||
|
|
||||||
private volatile bool _disableDirectDownloads;
|
private volatile bool _disableDirectDownloads;
|
||||||
private int _consecutiveDirectDownloadFailures;
|
private int _consecutiveDirectDownloadFailures;
|
||||||
@@ -502,18 +500,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveStatus(string key)
|
|
||||||
{
|
|
||||||
lock (_downloadStatusLock)
|
|
||||||
{
|
|
||||||
_downloadStatus.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DecompressBlockFileAsync(
|
private async Task DecompressBlockFileAsync(
|
||||||
string downloadStatusKey,
|
string downloadStatusKey,
|
||||||
string blockFilePath,
|
string blockFilePath,
|
||||||
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
string downloadLabel,
|
string downloadLabel,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
bool skipDownscale)
|
bool skipDownscale)
|
||||||
@@ -542,7 +533,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!replacementLookup.TryGetValue(fileHash, out var repl))
|
if (!replacementLookup.TryGetValue(fileHash, out var repl))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
|
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
|
||||||
fileBlockStream.Seek(len, SeekOrigin.Current);
|
// still need to skip bytes:
|
||||||
|
var skip = checked((int)fileLengthBytes);
|
||||||
|
fileBlockStream.Position += skip;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,37 +545,23 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
// read compressed data
|
// read compressed data
|
||||||
var compressed = new byte[len];
|
var compressed = new byte[len];
|
||||||
|
|
||||||
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
|
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (len == 0)
|
MungeBuffer(compressed);
|
||||||
|
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
||||||
|
|
||||||
|
if (rawSizeLookup.TryGetValue(fileHash, out var expectedRawSize)
|
||||||
|
&& expectedRawSize > 0
|
||||||
|
&& decompressed.LongLength != expectedRawSize)
|
||||||
{
|
{
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
Logger.LogWarning("{dlName}: Decompressed size mismatch for {fileHash} (expected {expected}, got {actual})",
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
downloadLabel, fileHash, expectedRawSize, decompressed.LongLength);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MungeBuffer(compressed);
|
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
||||||
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
||||||
// limit concurrent decompressions
|
|
||||||
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
|
|
||||||
// decompress
|
|
||||||
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
|
||||||
|
|
||||||
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
|
|
||||||
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
|
|
||||||
|
|
||||||
// write to file
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_decompressGate.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
@@ -594,6 +573,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetStatus(downloadStatusKey, DownloadStatus.Completed);
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
@@ -603,10 +584,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
RemoveStatus(downloadStatusKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
|
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
|
||||||
@@ -644,21 +621,25 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Logger.LogDebug("Files with size 0 or less: {files}",
|
||||||
|
string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||||
|
|
||||||
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
|
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
|
||||||
{
|
{
|
||||||
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
|
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
|
||||||
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
|
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDownloads = [.. downloadFileInfoFromService
|
CurrentDownloads = downloadFileInfoFromService
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Select(d => new DownloadFileTransfer(d))
|
.Select(d => new DownloadFileTransfer(d))
|
||||||
.Where(d => d.CanBeTransferred)];
|
.Where(d => d.CanBeTransferred)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return CurrentDownloads;
|
return CurrentDownloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record BatchChunk(string Key, List<DownloadFileTransfer> Items);
|
private sealed record BatchChunk(string HostKey, string StatusKey, List<DownloadFileTransfer> Items);
|
||||||
|
|
||||||
private static IEnumerable<List<T>> ChunkList<T>(List<T> items, int chunkSize)
|
private static IEnumerable<List<T>> ChunkList<T>(List<T> items, int chunkSize)
|
||||||
{
|
{
|
||||||
@@ -684,6 +665,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
var allowDirectDownloads = ShouldUseDirectDownloads();
|
var allowDirectDownloads = ShouldUseDirectDownloads();
|
||||||
var replacementLookup = BuildReplacementLookup(fileReplacement);
|
var replacementLookup = BuildReplacementLookup(fileReplacement);
|
||||||
|
var rawSizeLookup = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var download in CurrentDownloads)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(download.Hash))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawSizeLookup.TryGetValue(download.Hash, out var existing) || existing <= 0)
|
||||||
|
{
|
||||||
|
rawSizeLookup[download.Hash] = download.TotalRaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var directDownloads = new List<DownloadFileTransfer>();
|
var directDownloads = new List<DownloadFileTransfer>();
|
||||||
var batchDownloads = new List<DownloadFileTransfer>();
|
var batchDownloads = new List<DownloadFileTransfer>();
|
||||||
@@ -708,7 +703,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
var chunkSize = (int)Math.Ceiling(list.Count / (double)chunkCount);
|
var chunkSize = (int)Math.Ceiling(list.Count / (double)chunkCount);
|
||||||
|
|
||||||
return ChunkList(list, chunkSize)
|
return ChunkList(list, chunkSize)
|
||||||
.Select(chunk => new BatchChunk(g.Key, chunk));
|
.Select((chunk, index) => new BatchChunk(g.Key, $"{g.Key}#{index + 1}", chunk));
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
@@ -722,7 +717,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_downloadStatus[d.DirectDownloadUrl!] = new FileDownloadStatus
|
_downloadStatus[d.DirectDownloadUrl!] = new FileDownloadStatus
|
||||||
{
|
{
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
DownloadStatus = DownloadStatus.WaitingForSlot,
|
||||||
TotalBytes = d.Total,
|
TotalBytes = d.Total,
|
||||||
TotalFiles = 1,
|
TotalFiles = 1,
|
||||||
TransferredBytes = 0,
|
TransferredBytes = 0,
|
||||||
@@ -730,12 +725,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var g in batchChunks.GroupBy(c => c.Key, StringComparer.Ordinal))
|
foreach (var chunk in batchChunks)
|
||||||
{
|
{
|
||||||
_downloadStatus[g.Key] = new FileDownloadStatus
|
_downloadStatus[chunk.StatusKey] = new FileDownloadStatus
|
||||||
{
|
{
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
DownloadStatus = DownloadStatus.WaitingForQueue,
|
||||||
TotalBytes = g.SelectMany(x => x.Items).Sum(x => x.Total),
|
TotalBytes = chunk.Items.Sum(x => x.Total),
|
||||||
TotalFiles = 1,
|
TotalFiles = 1,
|
||||||
TransferredBytes = 0,
|
TransferredBytes = 0,
|
||||||
TransferredFiles = 0
|
TransferredFiles = 0
|
||||||
@@ -759,13 +754,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
Task batchTask = batchChunks.Length == 0
|
Task batchTask = batchChunks.Length == 0
|
||||||
? Task.CompletedTask
|
? Task.CompletedTask
|
||||||
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
||||||
async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, token, skipDownscale).ConfigureAwait(false));
|
async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false));
|
||||||
|
|
||||||
// direct downloads
|
// direct downloads
|
||||||
Task directTask = directDownloads.Count == 0
|
Task directTask = directDownloads.Count == 0
|
||||||
? Task.CompletedTask
|
? Task.CompletedTask
|
||||||
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
||||||
async (d, token) => await ProcessDirectAsync(d, replacementLookup, token, skipDownscale).ConfigureAwait(false));
|
async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false));
|
||||||
|
|
||||||
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
|
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -773,9 +768,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
ClearDownload();
|
ClearDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessBatchChunkAsync(BatchChunk chunk, Dictionary<string, (string Extension, string GamePath)> replacementLookup, CancellationToken ct, bool skipDownscale)
|
private async Task ProcessBatchChunkAsync(
|
||||||
|
BatchChunk chunk,
|
||||||
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool skipDownscale)
|
||||||
{
|
{
|
||||||
var statusKey = chunk.Key;
|
var statusKey = chunk.StatusKey;
|
||||||
|
|
||||||
// enqueue (no slot)
|
// enqueue (no slot)
|
||||||
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
||||||
@@ -803,10 +803,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
|
SetStatus(statusKey, DownloadStatus.Completed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, fi.Name, ct, skipDownscale).ConfigureAwait(false);
|
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -823,7 +824,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessDirectAsync(DownloadFileTransfer directDownload, Dictionary<string, (string Extension, string GamePath)> replacementLookup, CancellationToken ct, bool skipDownscale)
|
private async Task ProcessDirectAsync(
|
||||||
|
DownloadFileTransfer directDownload,
|
||||||
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool skipDownscale)
|
||||||
{
|
{
|
||||||
var progress = CreateInlineProgress(bytes =>
|
var progress = CreateInlineProgress(bytes =>
|
||||||
{
|
{
|
||||||
@@ -833,7 +839,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||||
{
|
{
|
||||||
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +867,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!replacementLookup.TryGetValue(directDownload.Hash, out var repl))
|
if (!replacementLookup.TryGetValue(directDownload.Hash, out var repl))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
||||||
|
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,13 +880,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
|
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
|
||||||
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
||||||
|
|
||||||
|
if (directDownload.TotalRaw > 0 && decompressedBytes.LongLength != directDownload.TotalRaw)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{directDownload.Hash}: Decompressed size mismatch (expected {directDownload.TotalRaw}, got {decompressedBytes.LongLength})");
|
||||||
|
}
|
||||||
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
|
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale);
|
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale);
|
||||||
|
|
||||||
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
|
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
|
||||||
|
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
|
||||||
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
||||||
|
|
||||||
RemoveStatus(directDownload.DirectDownloadUrl!);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
@@ -902,7 +914,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
||||||
{
|
{
|
||||||
@@ -929,6 +941,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private async Task ProcessDirectAsQueuedFallbackAsync(
|
private async Task ProcessDirectAsQueuedFallbackAsync(
|
||||||
DownloadFileTransfer directDownload,
|
DownloadFileTransfer directDownload,
|
||||||
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
IProgress<long> progress,
|
IProgress<long> progress,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
bool skipDownscale)
|
bool skipDownscale)
|
||||||
@@ -956,7 +969,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
||||||
|
|
||||||
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale)
|
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -1003,11 +1016,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var entry = _fileDbManager.CreateCacheEntry(filePath);
|
var entry = _fileDbManager.CreateCacheEntryWithKnownHash(filePath, fileHash);
|
||||||
var mapKind = _textureMetadataHelper.DetermineMapKind(gamePath, filePath);
|
|
||||||
|
|
||||||
if (!skipDownscale)
|
if (!skipDownscale && _textureDownscaleService.ShouldScheduleDownscale(filePath))
|
||||||
_textureDownscaleService.ScheduleDownscale(fileHash, filePath, mapKind);
|
{
|
||||||
|
_textureDownscaleService.ScheduleDownscale(
|
||||||
|
fileHash,
|
||||||
|
filePath,
|
||||||
|
() => _textureMetadataHelper.DetermineMapKind(gamePath, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public enum DownloadStatus
|
|||||||
WaitingForSlot,
|
WaitingForSlot,
|
||||||
WaitingForQueue,
|
WaitingForQueue,
|
||||||
Downloading,
|
Downloading,
|
||||||
Decompressing
|
Decompressing,
|
||||||
|
Completed
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user