sigma update

This commit is contained in:
2026-01-16 11:00:58 +09:00
parent 59ed03a825
commit 96123d00a2
51 changed files with 6640 additions and 1382 deletions

View File

@@ -19,6 +19,7 @@ public class FileDownloadManagerFactory
private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly FileDownloadDeduplicator _downloadDeduplicator;
public FileDownloadManagerFactory(
ILoggerFactory loggerFactory,
@@ -29,7 +30,8 @@ public class FileDownloadManagerFactory
LightlessConfigService configService,
TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
TextureMetadataHelper textureMetadataHelper)
TextureMetadataHelper textureMetadataHelper,
FileDownloadDeduplicator downloadDeduplicator)
{
_loggerFactory = loggerFactory;
_lightlessMediator = lightlessMediator;
@@ -40,6 +42,7 @@ public class FileDownloadManagerFactory
_textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_textureMetadataHelper = textureMetadataHelper;
_downloadDeduplicator = downloadDeduplicator;
}
public FileDownloadManager Create()
@@ -53,6 +56,7 @@ public class FileDownloadManagerFactory
_configService,
_textureDownscaleService,
_modelDecimationService,
_textureMetadataHelper);
_textureMetadataHelper,
_downloadDeduplicator);
}
}

View File

@@ -476,7 +476,7 @@ public class PlayerDataFactory
if (transientPaths.Count == 0)
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
var resolved = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal))
.ConfigureAwait(false);
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
@@ -692,7 +692,6 @@ public class PlayerDataFactory
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
GameObjectHandler handler,
HashSet<string> forwardResolve,
HashSet<string> reverseResolve)
{
@@ -707,59 +706,6 @@ public class PlayerDataFactory
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player)
{
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
{
var idx = handler.GetGameObject()?.ObjectIndex;
if (!idx.HasValue)
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
var resolvedForward = new string[forwardPaths.Length];
for (int i = 0; i < forwardPaths.Length; i++)
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
var resolvedReverse = new string[reversePaths.Length][];
for (int i = 0; i < reversePaths.Length; i++)
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
return (idx, resolvedForward, resolvedReverse);
}).ConfigureAwait(false);
if (objectIndex.HasValue)
{
for (int i = 0; i < forwardPaths.Length; i++)
{
var filePath = forwardResolved[i]?.ToLowerInvariant();
if (string.IsNullOrEmpty(filePath))
continue;
if (resolvedPaths.TryGetValue(filePath, out var list))
list.Add(forwardPaths[i].ToLowerInvariant());
else
{
resolvedPaths[filePath] = [forwardPathsLower[i]];
}
}
for (int i = 0; i < reversePaths.Length; i++)
{
var filePath = reversePathsLower[i];
var reverseResolvedLower = new string[reverseResolved[i].Length];
for (var j = 0; j < reverseResolvedLower.Length; j++)
{
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
}
if (resolvedPaths.TryGetValue(filePath, out var list))
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
else
resolvedPaths[filePath] = [.. reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()];
}
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
}
}
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
for (int i = 0; i < forwardPaths.Length; i++)

View File

@@ -1,43 +1,44 @@
using LightlessSync.API.Data;
using LightlessSync.API.Data;
namespace LightlessSync.PlayerData.Pairs;
namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { get; }
int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { get; }
int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
void Initialize();
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}

View File

@@ -54,6 +54,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly SemaphoreSlim _metricsComputeGate = new(1, 1);
private readonly PairManager _pairManager;
private readonly IFramework _framework;
private CancellationTokenSource? _applicationCancellationTokenSource;
@@ -193,8 +194,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public string? LastFailureReason => _lastFailureReason;
public IReadOnlyList<string> LastBlockingConditions => _lastBlockingConditions;
public bool IsApplying => _applicationTask is { IsCompleted: false };
public bool IsDownloading => _downloadManager.IsDownloading;
public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count;
public bool IsDownloading => _downloadManager.IsDownloadingFor(_charaHandler);
public int PendingDownloadCount => _downloadManager.GetPendingDownloadCount(_charaHandler);
public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count;
public PairHandlerAdapter(
@@ -721,6 +722,74 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return true;
}
public async Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken)
{
EnsureInitialized();
if (LastReceivedCharacterData is null || IsApplying)
{
return;
}
if (LastAppliedApproximateVRAMBytes >= 0
&& LastAppliedDataTris >= 0
&& LastAppliedApproximateEffectiveVRAMBytes >= 0
&& LastAppliedApproximateEffectiveTris >= 0)
{
return;
}
await _metricsComputeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
cancellationToken.ThrowIfCancellationRequested();
if (LastReceivedCharacterData is null)
{
return;
}
if (LastAppliedApproximateVRAMBytes >= 0
&& LastAppliedDataTris >= 0
&& LastAppliedApproximateEffectiveVRAMBytes >= 0
&& LastAppliedApproximateEffectiveTris >= 0)
{
return;
}
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
if (sanitized is null)
{
return;
}
if (!string.IsNullOrEmpty(dataHash) && TryApplyCachedMetrics(dataHash))
{
_cachedData = sanitized;
_pairStateCache.Store(Ident, sanitized);
return;
}
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, sanitized, []);
}
if (LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0)
{
await _playerPerformanceService.CheckTriangleUsageThresholds(this, sanitized).ConfigureAwait(false);
}
StorePerformanceMetrics(sanitized);
_cachedData = sanitized;
_pairStateCache.Store(Ident, sanitized);
}
finally
{
_metricsComputeGate.Release();
}
}
private CharacterData? CloneAndSanitizeLastReceived(out string? dataHash)
{
dataHash = null;
@@ -1090,6 +1159,19 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
}
var forceModsForMissing = _pendingModReapply;
if (!forceModsForMissing && HasMissingCachedFiles(characterData))
{
forceModsForMissing = true;
}
if (forceModsForMissing)
{
_forceApplyMods = true;
}
var suppressForcedModRedrawOnForcedApply = suppressForcedModRedraw || forceModsForMissing;
SetUploading(false);
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), forceApplyCustomization, _forceApplyMods);
@@ -1106,7 +1188,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
"Applying Character Data")));
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this,
forceApplyCustomization, _forceApplyMods, suppressForcedModRedraw);
forceApplyCustomization, _forceApplyMods, suppressForcedModRedrawOnForcedApply);
if (handlerReady && _forceApplyMods)
{
@@ -1921,7 +2003,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
var handlerForDownload = _charaHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair, skipDecimationForPair).ConfigureAwait(false));
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, toDownloadFiles, downloadToken, skipDownscaleForPair, skipDecimationForPair).ConfigureAwait(false));
await _pairDownloadTask.ConfigureAwait(false);
@@ -2136,6 +2218,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
SplitPapMappings(moddedPaths, out var withoutPap, out var papOnly);
var hasPap = papOnly.Count > 0;
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false);
@@ -2148,22 +2231,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (handlerForApply.Address != nint.Zero)
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
var removedPap = await StripIncompatiblePapAsync(handlerForApply, charaData, papOnly, token).ConfigureAwait(false);
if (removedPap > 0)
if (hasPap)
{
Logger.LogTrace("[{applicationId}] Removed {removedPap} incompatible PAP mappings found for {handler}", _applicationId, removedPap, GetLogIdentifier());
var removedPap = await StripIncompatiblePapAsync(handlerForApply, charaData, papOnly, token).ConfigureAwait(false);
if (removedPap > 0)
{
Logger.LogTrace("[{applicationId}] Removed {removedPap} incompatible PAP mappings found for {handler}", _applicationId, removedPap, GetLogIdentifier());
}
var merged = new Dictionary<(string GamePath, string? Hash), string>(withoutPap, withoutPap.Comparer);
foreach (var kv in papOnly)
merged[kv.Key] = kv.Value;
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, _applicationId, penumbraCollection,
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal))
.ConfigureAwait(false);
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(merged, merged.Comparer);
}
else
{
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(withoutPap, withoutPap.Comparer);
}
var merged = new Dictionary<(string GamePath, string? Hash), string>(withoutPap, withoutPap.Comparer);
foreach (var kv in papOnly)
merged[kv.Key] = kv.Value;
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, _applicationId, penumbraCollection,
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal))
.ConfigureAwait(false);
_lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(merged, merged.Comparer);
LastAppliedDataBytes = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
@@ -2218,20 +2308,20 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_lastSuccessfulApplyAt = DateTime.UtcNow;
ClearFailureState();
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
}
catch (OperationCanceledException)
{
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
}
catch (Exception ex)
{
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
}
catch (OperationCanceledException)
{
IsVisible = false;
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
}
catch (Exception ex)
{
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
{
IsVisible = false;
_forceApplyMods = true;
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
@@ -2471,6 +2561,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
(item) =>
{
token.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(item.Hash))
{
Logger.LogTrace("[BASE-{appBase}] Skipping replacement with empty hash for paths: {paths}", applicationBase, string.Join(", ", item.GamePaths));
return;
}
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
if (fileCache is not null && !File.Exists(fileCache.ResolvedFilepath))
{
@@ -2698,10 +2793,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
}
});
{
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
}
});
}
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)

View File

@@ -271,7 +271,20 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
try
{
handler.ApplyLastReceivedData(forced: true);
_ = Task.Run(async () =>
{
try
{
await handler.EnsurePerformanceMetricsAsync(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
}
}
});
}
catch (Exception ex)
{