From eaaded1ed5907ffd276a15740e5f89df0b9f13e2 Mon Sep 17 00:00:00 2001 From: Loporrit <141286461+loporrit@users.noreply.github.com> Date: Sat, 22 Feb 2025 11:59:22 +0000 Subject: [PATCH] Add Pair Character Analysis for funsies --- MareSynchronos/FileCache/FileCacheManager.cs | 5 +- .../Factories/PairAnalyzerFactory.cs | 30 ++ .../Factories/PairHandlerFactory.cs | 6 +- .../PlayerData/Handlers/PairHandler.cs | 9 +- MareSynchronos/PlayerData/Pairs/Pair.cs | 2 + MareSynchronos/Plugin.cs | 1 + MareSynchronos/Services/CharacterAnalyzer.cs | 21 +- .../Services/Mediator/MareMediator.cs | 4 +- MareSynchronos/Services/Mediator/Messages.cs | 3 + MareSynchronos/Services/PairAnalyzer.cs | 211 ++++++++++ MareSynchronos/Services/UiFactory.cs | 6 + MareSynchronos/Services/UiService.cs | 11 + MareSynchronos/UI/Components/DrawGroupPair.cs | 5 + MareSynchronos/UI/Components/DrawUserPair.cs | 5 + MareSynchronos/UI/DataAnalysisUi.cs | 31 +- .../UI/Handlers/UidDisplayHandler.cs | 5 + MareSynchronos/UI/PlayerAnalysisUI.cs | 370 ++++++++++++++++++ .../WebAPI/Files/FileDownloadManager.cs | 6 +- .../WebAPI/Files/FileUploadManager.cs | 2 + 19 files changed, 701 insertions(+), 32 deletions(-) create mode 100644 MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs create mode 100644 MareSynchronos/Services/PairAnalyzer.cs create mode 100644 MareSynchronos/UI/PlayerAnalysisUI.cs diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index e920155..e87dd62 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -141,12 +141,13 @@ public sealed class FileCacheManager : IHostedService public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) { - var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; - using var fs = File.OpenRead(fileCache); + var fileCache = GetFileCacheByHash(fileHash)!; + using var fs = File.OpenRead(fileCache.ResolvedFilepath); var ms = new MemoryStream(64 * 1024); using var encstream = LZ4Stream.Encode(ms, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC}); await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false); encstream.Close(); + fileCache.CompressedSize = encstream.Length; return (fileHash, ms.ToArray()); } diff --git a/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs b/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs new file mode 100644 index 0000000..42b9cfa --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs @@ -0,0 +1,30 @@ +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class PairAnalyzerFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _modelAnalyzer; + + public PairAnalyzerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, + FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) + { + _loggerFactory = loggerFactory; + _fileCacheManager = fileCacheManager; + _mareMediator = mareMediator; + _modelAnalyzer = modelAnalyzer; + } + + public PairAnalyzer Create(Pair pair) + { + return new PairAnalyzer(_loggerFactory.CreateLogger(), pair, _mareMediator, + _fileCacheManager, _modelAnalyzer); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index 000a4b7..bc5b3d5 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -23,12 +23,13 @@ public class PairHandlerFactory private readonly PlayerPerformanceService _playerPerformanceService; private readonly ServerConfigurationManager _serverConfigManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; + private readonly PairAnalyzerFactory _pairAnalyzerFactory; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService, - ServerConfigurationManager serverConfigManager) + ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -41,11 +42,12 @@ public class PairHandlerFactory _mareMediator = mareMediator; _playerPerformanceService = playerPerformanceService; _serverConfigManager = serverConfigManager; + _pairAnalyzerFactory = pairAnalyzerFactory; } public PairHandler Create(Pair pair) { - return new PairHandler(_loggerFactory.CreateLogger(), pair, _gameObjectHandlerFactory, + return new PairHandler(_loggerFactory.CreateLogger(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager); } diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index 1175cb4..06b5acc 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -43,7 +43,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private Guid _penumbraCollection = Guid.Empty; private bool _redrawOnNextApplication = false; - public PairHandler(ILogger logger, Pair pair, + public PairHandler(ILogger logger, Pair pair, PairAnalyzer pairAnalyzer, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManager transferManager, PluginWarningNotificationService pluginWarningNotificationManager, @@ -53,6 +53,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase ServerConfigurationManager serverConfigManager) : base(logger, mediator) { Pair = pair; + PairAnalyzer = pairAnalyzer; _gameObjectHandlerFactory = gameObjectHandlerFactory; _ipcManager = ipcManager; _downloadManager = transferManager; @@ -129,6 +130,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase public long LastAppliedDataBytes { get; private set; } public Pair Pair { get; private set; } + public PairAnalyzer PairAnalyzer { get; private init; } public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero; public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero ? uint.MaxValue @@ -159,6 +161,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase .Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); _forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null); _cachedData = characterData; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData)); Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); return; } @@ -176,6 +179,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase .Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); _forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null); _cachedData = characterData; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData)); Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); return; } @@ -274,6 +278,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { PlayerName = null; _cachedData = null; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, null)); Logger.LogDebug("Disposing {name} complete", name); } } @@ -581,6 +586,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase } _cachedData = charaData; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData)); Logger.LogDebug("[{applicationId}] Application finished", _applicationId); } @@ -591,6 +597,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase IsVisible = false; _forceApplyMods = true; _cachedData = charaData; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData)); Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); } else diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs index 31e1228..4207124 100644 --- a/MareSynchronos/PlayerData/Pairs/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -9,6 +9,7 @@ using MareSynchronos.API.Dto.User; using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Utils; @@ -67,6 +68,7 @@ public class Pair : DisposableMediatorSubscriberBase public long LastAppliedDataTris { get; set; } = -1; public long LastAppliedApproximateVRAMBytes { get; set; } = -1; public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty; + public PairAnalyzer? PairAnalyzer => CachedPlayer?.PairAnalyzer; public UserData UserData { get; init; } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index fbb1729..77d76b4 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -79,6 +79,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index 7a26f98..4d81a51 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace MareSynchronos.Services; -public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable +public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase { private readonly FileCacheManager _fileCacheManager; private readonly XivDataAnalyzer _xivDataAnalyzer; @@ -63,7 +63,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable foreach (var file in remaining) { Logger.LogDebug("Computing file {file}", file.FilePaths[0]); - await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); + await file.ComputeSizes(_fileCacheManager, cancelToken, ignoreCacheEntries: true).ConfigureAwait(false); CurrentFile++; } @@ -88,9 +88,14 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable if (print) PrintAnalysis(); } - public void Dispose() + protected override void Dispose(bool disposing) { - _analysisCts.CancelDispose(); + base.Dispose(disposing); + + if (!disposing) return; + + _analysisCts?.CancelDispose(); + _baseAnalysisCts.CancelDispose(); } private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) @@ -191,11 +196,11 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable internal sealed record FileDataEntry(string Hash, string FileType, List GamePaths, List FilePaths, long OriginalSize, long CompressedSize, long Triangles) { public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; - public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token) + public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool ignoreCacheEntries = true) { var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); var normalSize = new FileInfo(FilePaths[0]).Length; - var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false); + var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: ignoreCacheEntries, validate: false); foreach (var entry in entries) { entry.Size = normalSize; @@ -220,7 +225,9 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable using var reader = new BinaryReader(stream); reader.BaseStream.Position = 4; var format = (TexFile.TextureFormat)reader.ReadInt32(); - return format.ToString(); + var width = reader.ReadInt16(); + var height = reader.ReadInt16(); + return $"{format} ({width}x{height})"; } catch { diff --git a/MareSynchronos/Services/Mediator/MareMediator.cs b/MareSynchronos/Services/Mediator/MareMediator.cs index 1e97d6f..de54e8c 100644 --- a/MareSynchronos/Services/Mediator/MareMediator.cs +++ b/MareSynchronos/Services/Mediator/MareMediator.cs @@ -109,7 +109,7 @@ public sealed class MareMediator : IHostedService throw new InvalidOperationException("Already subscribed"); } - _logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name); + _logger.LogTrace("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name); } } @@ -124,7 +124,7 @@ public sealed class MareMediator : IHostedService throw new InvalidOperationException("Already subscribed"); } - _logger.LogDebug("Subscriber added for message {message}:{key}: {sub}", typeof(T).Name, key, subscriber.GetType().Name); + _logger.LogTrace("Subscriber added for message {message}:{key}: {sub}", typeof(T).Name, key, subscriber.GetType().Name); } } diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 91d758f..548c1be 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -78,6 +78,7 @@ public record OpenReportPopupMessage(Pair PairToReport) : MessageBase; public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase; public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase; public record OpenPermissionWindow(Pair Pair) : MessageBase; +public record OpenPairAnalysisWindow(Pair Pair) : MessageBase; public record DownloadLimitChangedMessage() : SameThreadMessage; public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; public record TargetPairMessage(Pair Pair) : MessageBase; @@ -94,5 +95,7 @@ public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessa public record UnholdPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); public record HoldPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID); public record UnholdPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID); +public record PairDataAppliedMessage(string UID, CharacterData? CharacterData) : KeyedMessage(UID); +public record PairDataAnalyzedMessage(string UID) : KeyedMessage(UID); #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/PairAnalyzer.cs b/MareSynchronos/Services/PairAnalyzer.cs new file mode 100644 index 0000000..57a4bf7 --- /dev/null +++ b/MareSynchronos/Services/PairAnalyzer.cs @@ -0,0 +1,211 @@ +using Lumina.Data.Files; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class PairAnalyzer : DisposableMediatorSubscriberBase +{ + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _xivDataAnalyzer; + private CancellationTokenSource? _analysisCts; + private CancellationTokenSource _baseAnalysisCts = new(); + private string _lastDataHash = string.Empty; + + public PairAnalyzer(ILogger logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) + : base(logger, mediator) + { + Pair = pair; + Mediator.SubscribeKeyed(this, pair.UserData.UID, (msg) => + { + _baseAnalysisCts = _baseAnalysisCts.CancelRecreate(); + var token = _baseAnalysisCts.Token; + if (msg.CharacterData != null) + { + _ = BaseAnalysis(msg.CharacterData, token); + } + else + { + LastAnalysis.Clear(); + _lastDataHash = string.Empty; + } + }); + _fileCacheManager = fileCacheManager; + _xivDataAnalyzer = modelAnalyzer; + + var lastReceivedData = pair.LastReceivedCharacterData; + if (lastReceivedData != null) + _ = BaseAnalysis(lastReceivedData, _baseAnalysisCts.Token); + } + + public Pair Pair { get; init; } + public int CurrentFile { get; internal set; } + public bool IsAnalysisRunning => _analysisCts != null; + public int TotalFiles { get; internal set; } + internal Dictionary> LastAnalysis { get; } = []; + internal string LastPlayerName { get; set; } = string.Empty; + + public void CancelAnalyze() + { + _analysisCts?.CancelDispose(); + _analysisCts = null; + } + + public async Task ComputeAnalysis(bool print = true, bool recalculate = false) + { + Logger.LogDebug("=== Calculating Character Analysis ==="); + + _analysisCts = _analysisCts?.CancelRecreate() ?? new(); + + var cancelToken = _analysisCts.Token; + + var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList(); + if (allFiles.Exists(c => !c.IsComputed || recalculate)) + { + var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList(); + TotalFiles = remaining.Count; + CurrentFile = 1; + Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); + + Mediator.Publish(new HaltScanMessage(nameof(PairAnalyzer))); + try + { + foreach (var file in remaining) + { + Logger.LogDebug("Computing file {file}", file.FilePaths[0]); + await file.ComputeSizes(_fileCacheManager, cancelToken, ignoreCacheEntries: false).ConfigureAwait(false); + CurrentFile++; + } + + _fileCacheManager.WriteOutFullCsv(); + + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to analyze files"); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(PairAnalyzer))); + } + } + + LastPlayerName = Pair.PlayerName ?? string.Empty; + Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID)); + + _analysisCts.CancelDispose(); + _analysisCts = null; + + if (print) PrintAnalysis(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) return; + + _analysisCts?.CancelDispose(); + _baseAnalysisCts.CancelDispose(); + } + + private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) + { + if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return; + + LastAnalysis.Clear(); + + foreach (var obj in charaData.FileReplacements) + { + Dictionary data = new(StringComparer.OrdinalIgnoreCase); + foreach (var fileEntry in obj.Value) + { + token.ThrowIfCancellationRequested(); + + var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: false, validate: false).ToList(); + if (fileCacheEntries.Count == 0) continue; + + var filePath = fileCacheEntries[0].ResolvedFilepath; + FileInfo fi = new(filePath); + string ext = "unk?"; + try + { + ext = fi.Extension[1..]; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); + } + + var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); + + foreach (var entry in fileCacheEntries) + { + data[fileEntry.Hash] = new CharacterAnalyzer.FileDataEntry(fileEntry.Hash, ext, + [.. fileEntry.GamePaths], + fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), + entry.Size > 0 ? entry.Size.Value : 0, + entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, + tris); + } + } + + LastAnalysis[obj.Key] = data; + } + + Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID)); + + _lastDataHash = charaData.DataHash.Value; + } + + private void PrintAnalysis() + { + if (LastAnalysis.Count == 0) return; + foreach (var kvp in LastAnalysis) + { + int fileCounter = 1; + int totalFiles = kvp.Value.Count; + Logger.LogInformation("=== Analysis for {uid}:{obj} ===", Pair.UserData.UID, kvp.Key); + + foreach (var entry in kvp.Value.OrderBy(b => b.Value.GamePaths.OrderBy(p => p, StringComparer.Ordinal).First(), StringComparer.Ordinal)) + { + Logger.LogInformation("File {x}/{y}: {hash}", fileCounter++, totalFiles, entry.Key); + foreach (var path in entry.Value.GamePaths) + { + Logger.LogInformation(" Game Path: {path}", path); + } + if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected for {key}", entry.Key); + foreach (var filePath in entry.Value.FilePaths) + { + Logger.LogInformation(" File Path: {path}", filePath); + } + Logger.LogInformation(" Size: {size}, Compressed: {compressed}", UiSharedService.ByteToString(entry.Value.OriginalSize), + UiSharedService.ByteToString(entry.Value.CompressedSize)); + } + } + foreach (var kvp in LastAnalysis) + { + Logger.LogInformation("=== Detailed summary by file type for {obj} ===", kvp.Key); + foreach (var entry in kvp.Value.Select(v => v.Value).GroupBy(v => v.FileType, StringComparer.Ordinal)) + { + Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Count(), + UiSharedService.ByteToString(entry.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Sum(v => v.CompressedSize))); + } + Logger.LogInformation("=== Total summary for {obj} ===", kvp.Key); + Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", kvp.Value.Count, + UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.OriginalSize)), UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.CompressedSize))); + } + + Logger.LogInformation("=== Total summary for all currently present objects ==="); + Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", + LastAnalysis.Values.Sum(v => v.Values.Count), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize)))); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/UiFactory.cs b/MareSynchronos/Services/UiFactory.cs index 88f53ac..c518d4e 100644 --- a/MareSynchronos/Services/UiFactory.cs +++ b/MareSynchronos/Services/UiFactory.cs @@ -51,4 +51,10 @@ public class UiFactory return new PermissionWindowUI(_loggerFactory.CreateLogger(), pair, _mareMediator, _uiSharedService, _apiController, _performanceCollectorService); } + + public PlayerAnalysisUI CreatePlayerAnalysisUi(Pair pair) + { + return new PlayerAnalysisUI(_loggerFactory.CreateLogger(), pair, + _mareMediator, _uiSharedService, _performanceCollectorService); + } } diff --git a/MareSynchronos/Services/UiService.cs b/MareSynchronos/Services/UiService.cs index 6bf08ac..aa38c3b 100644 --- a/MareSynchronos/Services/UiService.cs +++ b/MareSynchronos/Services/UiService.cs @@ -76,6 +76,17 @@ public sealed class UiService : DisposableMediatorSubscriberBase } }); + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is PlayerAnalysisUI ui + && msg.Pair == ui.Pair)) + { + var window = _uiFactory.CreatePlayerAnalysisUi(msg.Pair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + Mediator.Subscribe(this, (msg) => { _windowSystem.RemoveWindow(msg.Window); diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs index c048665..de43c16 100644 --- a/MareSynchronos/UI/Components/DrawGroupPair.cs +++ b/MareSynchronos/UI/Components/DrawGroupPair.cs @@ -335,6 +335,11 @@ public class DrawGroupPair : DrawPairBase } if (_pair.IsVisible) { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis")) + { + _displayHandler.OpenAnalysis(_pair); + ImGui.CloseCurrentPopup(); + } if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) { _pair.ApplyLastReceivedData(forced: true); diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 4bfe12a..8f1cb20 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -225,6 +225,11 @@ public class DrawUserPair : DrawPairBase } if (entry.IsVisible) { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis")) + { + _displayHandler.OpenAnalysis(_pair); + ImGui.CloseCurrentPopup(); + } if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) { entry.ApplyLastReceivedData(forced: true); diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs index c826077..0197241 100644 --- a/MareSynchronos/UI/DataAnalysisUi.cs +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -26,6 +26,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private Task? _conversionTask; private bool _enableBc7ConversionMode = false; private bool _hasUpdate = false; + private bool _sortDirty = true; private bool _modalOpen = false; private string _selectedFileTypeTab = string.Empty; private string _selectedHash = string.Empty; @@ -102,11 +103,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); _hasUpdate = false; + _sortDirty = true; } UiSharedService.TextWrapped("This window shows you all files and their sizes that are currently in use through your character and associated entities"); - if (_cachedAnalysis!.Count == 0) return; + if (_cachedAnalysis == null || _cachedAnalysis.Count == 0) return; bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; bool needAnalysis = _cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)); @@ -161,7 +163,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("Total size (actual):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); - ImGui.TextUnformatted("Total size (compressed for up/download only):"); + ImGui.TextUnformatted("Total size (download size):"); ImGui.SameLine(); using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) { @@ -182,7 +184,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { using var id = ImRaii.PushId(kvp.Key.ToString()); string tabText = kvp.Key.ToString(); - if (kvp.Value.Any(f => !f.Value.IsComputed)) tabText += " (!)"; using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); if (tab.Success) { @@ -209,7 +210,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.TextUnformatted($"{kvp.Key} size (actual):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); - ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); + ImGui.TextUnformatted($"{kvp.Key} size (download size):"); ImGui.SameLine(); using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) { @@ -248,10 +249,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; var requiresCompute = fileGroup.Any(k => !k.IsComputed); using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute); - if (requiresCompute) - { - fileGroupText += " (!)"; - } ImRaii.IEndObject fileTab; using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) @@ -277,7 +274,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize))); - ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed for up/download only):"); + ImGui.TextUnformatted($"{fileGroup.Key} files size (download size):"); ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); @@ -376,10 +373,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase new Vector2(0, 300)); if (!table.Success) return; ImGui.TableSetupColumn("Hash"); - ImGui.TableSetupColumn("Filepaths"); - ImGui.TableSetupColumn("Gamepaths"); - ImGui.TableSetupColumn("Original Size"); - ImGui.TableSetupColumn("Compressed Size"); + ImGui.TableSetupColumn("Filepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Gamepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("File Size", ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Download Size", ImGuiTableColumnFlags.PreferSortDescending); if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) { ImGui.TableSetupColumn("Format"); @@ -387,13 +384,13 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) { - ImGui.TableSetupColumn("Triangles"); + ImGui.TableSetupColumn("Triangles", ImGuiTableColumnFlags.PreferSortDescending); } ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); var sortSpecs = ImGui.TableGetSortSpecs(); - if (sortSpecs.SpecsDirty) + if (sortSpecs.SpecsDirty || _sortDirty) { var idx = sortSpecs.Specs.ColumnIndex; @@ -427,6 +424,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); sortSpecs.SpecsDirty = false; + _sortDirty = false; } foreach (var item in fileGroup) @@ -462,7 +460,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase if (_enableBc7ConversionMode) { ImGui.TableNextColumn(); - if (string.Equals(item.Format.Value, "BC7", StringComparison.Ordinal)) + if (item.Format.Value.StartsWith("BC") || item.Format.Value.StartsWith("DXT") + || item.Format.Value.StartsWith("24864")) // BC4 { ImGui.TextUnformatted(""); continue; diff --git a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs index b3c37a5..056e78e 100644 --- a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs +++ b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs @@ -194,6 +194,11 @@ public class UidDisplayHandler _mediator.Publish(new ProfileOpenStandaloneMessage(entry)); } + internal void OpenAnalysis(Pair entry) + { + _mediator.Publish(new OpenPairAnalysisWindow(entry)); + } + private bool ShowUidInsteadOfName(Pair pair) { _showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); diff --git a/MareSynchronos/UI/PlayerAnalysisUI.cs b/MareSynchronos/UI/PlayerAnalysisUI.cs new file mode 100644 index 0000000..7bc465b --- /dev/null +++ b/MareSynchronos/UI/PlayerAnalysisUI.cs @@ -0,0 +1,370 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class PlayerAnalysisUI : WindowMediatorSubscriberBase +{ + private readonly UiSharedService _uiSharedService; + private Dictionary>? _cachedAnalysis; + private bool _hasUpdate = true; + private bool _sortDirty = true; + private string _selectedFileTypeTab = string.Empty; + private string _selectedHash = string.Empty; + private ObjectKind _selectedObjectTab; + + public PlayerAnalysisUI(ILogger logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, + PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Character Data Analysis for " + pair.UserData.AliasOrUID + "###LoporritPairAnalysis" + pair.UserData.UID, performanceCollectorService) + { + Pair = pair; + _uiSharedService = uiSharedService; + Mediator.SubscribeKeyed(this, Pair.UserData.UID, (_) => + { + _logger.LogInformation("PairDataAnalyzedMessage received for {uid}", Pair.UserData.UID); + _hasUpdate = true; + }); + SizeConstraints = new() + { + MinimumSize = new() + { + X = 800, + Y = 600 + }, + MaximumSize = new() + { + X = 3840, + Y = 2160 + } + }; + IsOpen = true; + } + + public Pair Pair { get; private init; } + public PairAnalyzer? PairAnalyzer => Pair.PairAnalyzer; + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } + + protected override void DrawInternal() + { + if (PairAnalyzer == null) return; + PairAnalyzer analyzer = PairAnalyzer!; + + if (_hasUpdate) + { + _cachedAnalysis = analyzer.LastAnalysis.DeepClone(); + _hasUpdate = false; + _sortDirty = true; + } + + UiSharedService.TextWrapped($"This window shows you all files and their sizes that are currently in use by {Pair.UserData.AliasOrUID} and associated entities"); + + if (_cachedAnalysis == null || _cachedAnalysis.Count == 0) return; + + bool isAnalyzing = analyzer.IsAnalysisRunning; + bool needAnalysis = _cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)); + if (isAnalyzing) + { + UiSharedService.ColorTextWrapped($"Analyzing {analyzer.CurrentFile}/{analyzer.TotalFiles}", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + { + analyzer.CancelAnalyze(); + } + } + else + { + if (needAnalysis) + { + UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to compute missing data", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) + { + _ = analyzer.ComputeAnalysis(print: false); + } + } + } + + ImGui.Separator(); + + ImGui.TextUnformatted("Total files:"); + ImGui.SameLine(); + ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString()); + ImGui.SameLine(); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + string text = ""; + var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal); + text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal) + .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) + + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); + ImGui.SetTooltip(text); + } + ImGui.TextUnformatted("Total size (actual):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); + ImGui.TextUnformatted("Total size (compressed for up/download only):"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}"); + ImGui.Separator(); + + var playerName = analyzer.LastPlayerName; + + if (playerName.Length == 0) + { + playerName = Pair.PlayerName ?? string.Empty; + analyzer.LastPlayerName = playerName; + } + + using var tabbar = ImRaii.TabBar("objectSelection"); + foreach (var kvp in _cachedAnalysis) + { + using var id = ImRaii.PushId(kvp.Key.ToString()); + string tabText = kvp.Key == ObjectKind.Player ? playerName : $"{playerName}'s {kvp.Key}"; + using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); + if (tab.Success) + { + var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal) + .OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); + + ImGui.TextUnformatted($"Files for {tabText}"); + + ImGui.SameLine(); + ImGui.TextUnformatted(kvp.Value.Count.ToString()); + ImGui.SameLine(); + + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + { + string text = ""; + text = string.Join(Environment.NewLine, groupedfiles + .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) + + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); + ImGui.SetTooltip(text); + } + ImGui.TextUnformatted($"{kvp.Key} size (actual):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); + ImGui.TextUnformatted($"{kvp.Key} size (download size):"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.SameLine(); + var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramUsage != null) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize))); + } + ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}"); + + ImGui.Separator(); + if (_selectedObjectTab != kvp.Key) + { + _selectedHash = string.Empty; + _selectedObjectTab = kvp.Key; + _selectedFileTypeTab = string.Empty; + } + + using var fileTabBar = ImRaii.TabBar("fileTabs"); + + foreach (IGrouping? fileGroup in groupedfiles) + { + string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; + var requiresCompute = fileGroup.Any(k => !k.IsComputed); + using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute); + ImRaii.IEndObject fileTab; + using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), + requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) + { + fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key); + } + + if (!fileTab) { fileTab.Dispose(); continue; } + + if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal)) + { + _selectedFileTypeTab = fileGroup.Key; + _selectedHash = string.Empty; + } + + ImGui.TextUnformatted($"{fileGroup.Key} files"); + ImGui.SameLine(); + ImGui.TextUnformatted(fileGroup.Count().ToString()); + + ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize))); + + ImGui.TextUnformatted($"{fileGroup.Key} files size (download size):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); + + ImGui.Separator(); + DrawTable(fileGroup); + + fileTab.Dispose(); + } + } + } + + ImGui.Separator(); + + ImGui.TextUnformatted("Selected file:"); + ImGui.SameLine(); + UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); + + if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) + { + var gamepaths = item.GamePaths; + ImGui.TextUnformatted("Used by game path:"); + ImGui.SameLine(); + UiSharedService.TextWrapped(gamepaths[0]); + if (gamepaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); + } + } + } + + private void DrawTable(IGrouping fileGroup) + { + var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) + ? 5 + : (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 5 : 4); + using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(0, 300)); + if (!table.Success) return; + ImGui.TableSetupColumn("Hash"); + ImGui.TableSetupColumn("Gamepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("File Size", ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Download Size", ImGuiTableColumnFlags.PreferSortDescending); + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) + { + ImGui.TableSetupColumn("Format"); + } + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + { + ImGui.TableSetupColumn("Triangles", ImGuiTableColumnFlags.PreferSortDescending); + } + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsDirty || _sortDirty) + { + var idx = sortSpecs.Specs.ColumnIndex; + + if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + + sortSpecs.SpecsDirty = false; + _sortDirty = false; + } + + foreach (var item in fileGroup) + { + using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal)); + using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); + ImGui.TableNextColumn(); + if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) + { + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow)); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow)); + } + ImGui.TextUnformatted(item.Hash); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.GamePaths.Count.ToString()); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize)); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, !item.IsComputed)) + ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize)); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Format.Value); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + } + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.TrisToString(item.Triangles)); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + } + } + } +} diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs index 4a9b3ce..3b481aa 100644 --- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -322,7 +322,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension); await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile.ToArray(), token).ConfigureAwait(false); - PersistFileToStorage(fileHash, filePath); + PersistFileToStorage(fileHash, filePath, fileLengthBytes); } catch (Exception e) { @@ -355,7 +355,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; } - private void PersistFileToStorage(string fileHash, string filePath) + private void PersistFileToStorage(string fileHash, string filePath, long? compressedSize = null) { var fi = new FileInfo(filePath); Func RandomDayInThePast() @@ -378,6 +378,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase File.Delete(filePath); _fileDbManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath); } + if (entry != null) + entry.CompressedSize = compressedSize; } catch (Exception ex) { diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs index b20f000..5611f17 100644 --- a/MareSynchronos/WebAPI/Files/FileUploadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -245,6 +245,8 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase var compressedSize = CurrentUploads.Sum(c => c.Total); Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize)); + + _fileDbManager.WriteOutFullCsv(); } foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))