diff --git a/MareSynchronos/FileCache/FileCacheEntity.cs b/MareSynchronos/FileCache/FileCacheEntity.cs index 47af1c0..19a9dfc 100644 --- a/MareSynchronos/FileCache/FileCacheEntity.cs +++ b/MareSynchronos/FileCache/FileCacheEntity.cs @@ -4,18 +4,22 @@ namespace MareSynchronos.FileCache; public class FileCacheEntity { - public FileCacheEntity(string hash, string path, string lastModifiedDateTicks) + public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null) { + Size = size; + CompressedSize = compressedSize; Hash = hash; PrefixedFilePath = path; LastModifiedDateTicks = lastModifiedDateTicks; } - public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}"; + public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}|{Size ?? -1}|{CompressedSize ?? -1}"; public string Hash { get; set; } public string LastModifiedDateTicks { get; set; } public string PrefixedFilePath { get; init; } public string ResolvedFilepath { get; private set; } = string.Empty; + public long? Size { get; set; } + public long? CompressedSize { get; set; } public void SetResolvedFilePath(string filePath) { diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 9beb83d..177a113 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -64,7 +64,20 @@ public sealed class FileCacheManager : IDisposable if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length); var path = splittedEntry[1]; var time = splittedEntry[2]; - AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time))); + long size = -1; + long compressed = -1; + if (splittedEntry.Length > 3) + { + if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result)) + { + size = result; + } + if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed)) + { + compressed = resultCompressed; + } + } + AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed))); } catch (Exception ex) { @@ -122,6 +135,21 @@ public sealed class FileCacheManager : IDisposable return null; } + public List GetAllFileCachesByHash(string hash) + { + List output = new(); + if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + { + foreach (var filecache in fileCacheEntities) + { + var validated = GetValidatedFileCache(filecache); + if (validated != null) output.Add(validated); + } + } + + return output; + } + public FileCacheEntity? GetFileCacheByPath(string path) { var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); @@ -158,11 +186,17 @@ public sealed class FileCacheManager : IDisposable } } - public void UpdateHashedFile(FileCacheEntity fileCache) + public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true) { _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); - fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); - fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); + if (computeProperties) + { + var fi = new FileInfo(fileCache.ResolvedFilepath); + fileCache.Size = fi.Length; + fileCache.CompressedSize = null; + fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); + } RemoveHashedFile(fileCache); AddHashedFile(fileCache); } @@ -247,7 +281,7 @@ public sealed class FileCacheManager : IDisposable private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { hash ??= Crypto.GetFileHash(fileInfo.FullName); - var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)); + var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); entity = ReplacePathPrefixes(entity); AddHashedFile(entity); lock (_fileWriteLock) diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index ac93635..ee89063 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -87,6 +87,10 @@ $(DalamudLibPath)CheapLoc.dll false + + $(DalamudLibPath)Dalamud.Interface.dll + false + diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index c762e79..42b37a3 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -106,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); + collection.AddScoped(); collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface.UiBuilder, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); @@ -118,7 +119,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService())); + s.GetRequiredService())); collection.AddScoped((s) => new NotificationService(s.GetRequiredService>(), s.GetRequiredService(), pluginInterface.UiBuilder, chatGui, s.GetRequiredService())); collection.AddScoped((s) => new UiSharedService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index 16319ba..6040305 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -1,4 +1,5 @@ using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; using MareSynchronos.FileCache; using MareSynchronos.Services.Mediator; using MareSynchronos.UI; @@ -10,82 +11,146 @@ namespace MareSynchronos.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { private readonly FileCacheManager _fileCacheManager; - private CharacterData? _lastCreatedData; private CancellationTokenSource? _analysisCts; + private string _lastDataHash = string.Empty; + internal Dictionary> LastAnalysis { get; } = new(); public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager) : base(logger, mediator) { Mediator.Subscribe(this, (msg) => { - _lastCreatedData = msg.CharacterData.DeepClone(); + _ = Task.Run(() => BaseAnalysis(msg.CharacterData.DeepClone())); }); _fileCacheManager = fileCacheManager; } public bool IsAnalysisRunning => _analysisCts != null; + public int CurrentFile { get; internal set; } + public int TotalFiles { get; internal set; } + public void CancelAnalyze() { _analysisCts?.CancelDispose(); _analysisCts = null; } - public async Task Analyze() + public async Task ComputeAnalysis(bool print = true) { + Logger.LogDebug("=== Calculating Character Analysis ==="); + _analysisCts = _analysisCts?.CancelRecreate() ?? new(); var cancelToken = _analysisCts.Token; - if (_lastCreatedData == null) return; - - Logger.LogInformation("=== Calculating Character Analysis, this may take a while ==="); - foreach (var obj in _lastCreatedData.FileReplacements) + var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList(); + if (allFiles.Exists(c => !c.IsComputed)) { - Logger.LogInformation("=== File Calculation for {obj} ===", obj.Key); - Dictionary> data = new(StringComparer.OrdinalIgnoreCase); - var totalFiles = obj.Value.Count(c => !string.IsNullOrEmpty(c.Hash)); - var currentFile = 1; - foreach (var hash in obj.Value.Select(c => c.Hash)) + var remaining = allFiles.Where(c => !c.IsComputed).ToList(); + TotalFiles = remaining.Count; + CurrentFile = 1; + Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); + + Mediator.Publish(new HaltScanMessage("CharacterAnalyzer")); + + foreach (var file in remaining) { - var fileCacheEntry = _fileCacheManager.GetFileCacheByHash(hash); - if (fileCacheEntry == null) continue; - - Logger.LogInformation("Computing File {x}/{y}: {hash}", currentFile, totalFiles, hash); - - Logger.LogInformation(" File Path: {path}", fileCacheEntry.ResolvedFilepath); - - var filePath = fileCacheEntry.ResolvedFilepath; - FileInfo fi = new(filePath); - var ext = fi.Extension; - if (!data.ContainsKey(ext)) data[ext] = new List(); - - (_, byte[] fileLength) = await _fileCacheManager.GetCompressedFileData(hash, cancelToken).ConfigureAwait(false); - - Logger.LogInformation(" Original Size: {size}, Compressed Size: {compr}", - UiSharedService.ByteToString(fi.Length), UiSharedService.ByteToString(fileLength.LongLength)); - - data[ext].Add(new DataEntry(fi.FullName, fi.Length, fileLength.LongLength)); - - currentFile++; - - cancelToken.ThrowIfCancellationRequested(); + await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); + CurrentFile++; } - Logger.LogInformation("=== Summary by file type for {obj} ===", obj.Key); - foreach (var entry in data) - { - Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Value.Count, - UiSharedService.ByteToString(entry.Value.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Value.Sum(v => v.CompressedSize))); - } - Logger.LogInformation("=== Total summary for {obj} ===", obj.Key); - Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", data.Values.Sum(c => c.Count), - UiSharedService.ByteToString(data.Values.Sum(v => v.Sum(c => c.OriginalSize))), UiSharedService.ByteToString(data.Values.Sum(v => v.Sum(c => c.CompressedSize)))); + _fileCacheManager.WriteOutFullCsv(); - Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); + Mediator.Publish(new ResumeScanMessage("CharacterAnalzyer")); } + Mediator.Publish(new CharacterDataAnalyzedMessage()); + _analysisCts.CancelDispose(); _analysisCts = null; + + if (print) PrintAnalysis(); + } + + private void BaseAnalysis(CharacterData charaData) + { + 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) + { + var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash); + if (fileCacheEntries.Count == 0) continue; + + var filePath = fileCacheEntries[0].ResolvedFilepath; + FileInfo fi = new(filePath); + var ext = fi.Extension[1..]; + + foreach (var entry in fileCacheEntries) + { + data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, + fileEntry.GamePaths.ToList(), + fileCacheEntries.Select(c => c.ResolvedFilepath).ToList(), + entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0); + } + } + + LastAnalysis[obj.Key] = data; + } + + Mediator.Publish(new CharacterDataAnalyzedMessage()); + + _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 {obj} ===", 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", 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)))); + Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); } public void Dispose() @@ -93,5 +158,23 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _analysisCts.CancelDispose(); } - private sealed record DataEntry(string filePath, long OriginalSize, long CompressedSize); + internal sealed record FileDataEntry(string Hash, string FileType, List GamePaths, List FilePaths, long OriginalSize, long CompressedSize) + { + public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; + public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token) + { + var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); + var normalSize = new FileInfo(FilePaths[0]).Length; + var entries = fileCacheManager.GetAllFileCachesByHash(Hash); + foreach (var entry in entries) + { + entry.Size = normalSize; + entry.CompressedSize = compressedsize.Item2.LongLength; + } + OriginalSize = normalSize; + CompressedSize = compressedsize.Item2.LongLength; + } + public long OriginalSize { get; private set; } = OriginalSize; + public long CompressedSize { get; private set; } = CompressedSize; + } } diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs index 5a4691c..31514e3 100644 --- a/MareSynchronos/Services/CommandManagerService.cs +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -14,7 +14,6 @@ public sealed class CommandManagerService : IDisposable private readonly ApiController _apiController; private readonly CommandManager _commandManager; private readonly MareMediator _mediator; - private readonly CharacterAnalyzer _characterAnalyzer; private readonly PerformanceCollectorService _performanceCollectorService; private readonly PeriodicFileScanner _periodicFileScanner; private readonly ServerConfigurationManager _serverConfigurationManager; @@ -22,7 +21,7 @@ public sealed class CommandManagerService : IDisposable public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService, UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, - ApiController apiController, MareMediator mediator, CharacterAnalyzer characterAnalyzer) + ApiController apiController, MareMediator mediator) { _commandManager = commandManager; _performanceCollectorService = performanceCollectorService; @@ -31,7 +30,6 @@ public sealed class CommandManagerService : IDisposable _periodicFileScanner = periodicFileScanner; _apiController = apiController; _mediator = mediator; - _characterAnalyzer = characterAnalyzer; _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) { HelpMessage = "Opens the Mare Synchronos UI" @@ -102,14 +100,7 @@ public sealed class CommandManagerService : IDisposable } else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase)) { - if (splitArgs.Length > 1 && string.Equals(splitArgs[1], "cancel", StringComparison.OrdinalIgnoreCase)) - { - _characterAnalyzer.CancelAnalyze(); - } - else - { - _ = _characterAnalyzer.Analyze(); - } + _mediator.Publish(new OpenDataAnalysisUiMessage()); } } } \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 05c11da..2c07710 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -13,6 +13,7 @@ namespace MareSynchronos.Services.Mediator; public record SwitchToIntroUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase; +public record OpenDataAnalysisUiMessage : MessageBase; public record DalamudLoginMessage : MessageBase; public record DalamudLogoutMessage : MessageBase; public record FrameworkUpdateMessage : SameThreadMessage; @@ -49,6 +50,7 @@ public record NotificationMessage public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; +public record CharacterDataAnalyzedMessage : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; public record HubReconnectingMessage(Exception? Exception) : MessageBase; diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 9d5e990..097f94e 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -520,6 +520,11 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.Text("No downloads in progress"); } + if (UiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Mare Character Data Analysis", WindowContentWidth)) + { + Mediator.Publish(new OpenDataAnalysisUiMessage()); + } + ImGui.SameLine(); } diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs new file mode 100644 index 0000000..dd512a7 --- /dev/null +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -0,0 +1,268 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class DataAnalysisUi : WindowMediatorSubscriberBase +{ + private readonly CharacterAnalyzer _characterAnalyzer; + private bool _hasUpdate = false; + private Dictionary>? _cachedAnalysis; + private string _selectedHash = string.Empty; + private ObjectKind _selectedTab; + + public DataAnalysisUi(ILogger logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer) : base(logger, mediator, "Mare Character Data Analysis") + { + _characterAnalyzer = characterAnalyzer; + + Mediator.Subscribe(this, (_) => + { + _hasUpdate = true; + }); + Mediator.Subscribe(this, (_) => Toggle()); + SizeConstraints = new() + { + MinimumSize = new() + { + X = 800, + Y = 600 + }, + MaximumSize = new() + { + X = 3840, + Y = 2160 + } + }; + } + + public override void OnOpen() + { + _hasUpdate = true; + _selectedHash = string.Empty; + } + + public override void Draw() + { + if (_hasUpdate) + { + _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); + _hasUpdate = false; + } + + UiSharedService.TextWrapped("This window shows you all files and their sizes that are currently in use through your character and associated entities in Mare"); + + if (_cachedAnalysis!.Count == 0) return; + + if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed))) + { + bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; + if (isAnalyzing) + { + UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", + ImGuiColors.DalamudYellow); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + { + _characterAnalyzer.CancelAnalyze(); + } + } + else + { + UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", + ImGuiColors.DalamudYellow); + if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis")) + { + _ = _characterAnalyzer.ComputeAnalysis(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 (uncompressed):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); + ImGui.TextUnformatted("Total size (compressed):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); + + ImGui.Separator(); + + using var tabbar = ImRaii.TabBar("objectSelection"); + foreach (var kvp in _cachedAnalysis) + { + 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) + { + ImGui.TextUnformatted("Files for " + kvp.Key); + 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 = ""; + var groupedfiles = kvp.Value.Select(v => v.Value).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($"{kvp.Key} size (uncompressed):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); + ImGui.TextUnformatted($"{kvp.Key} size (compressed):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + + ImGui.Separator(); + if (_selectedTab != kvp.Key) + { + _selectedHash = string.Empty; + _selectedTab = kvp.Key; + } + + using var table = ImRaii.Table("Analysis", 6, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(0, 300)); + if (!table.Success) continue; + ImGui.TableSetupColumn("Type"); + ImGui.TableSetupColumn("Hash"); + ImGui.TableSetupColumn("Filepaths"); + ImGui.TableSetupColumn("Gamepaths"); + ImGui.TableSetupColumn("Original Size"); + ImGui.TableSetupColumn("Compressed Size"); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsDirty) + { + var idx = sortSpecs.Specs.ColumnIndex; + + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.FileType, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.FileType, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + _cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + + sortSpecs.SpecsDirty = false; + } + + foreach (var item in kvp.Value) + { + using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Key, _selectedHash)); + using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.Value.IsComputed); + ImGui.TableNextColumn(); + if (!item.Value.IsComputed) + { + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudRed)); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudRed)); + } + if (string.Equals(_selectedHash, item.Key, StringComparison.Ordinal)) + { + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow)); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow)); + } + ImGui.TextUnformatted(item.Value.FileType); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Key); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Value.FilePaths.Count.ToString()); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Value.GamePaths.Count.ToString()); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(item.Value.OriginalSize)); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(item.Value.CompressedSize)); + if (ImGui.IsItemClicked()) _selectedHash = item.Key; + } + } + } + ImGui.Separator(); + ImGui.Text("Selected file:"); + ImGui.SameLine(); + UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); + if (_cachedAnalysis[_selectedTab].ContainsKey(_selectedHash)) + { + var filePaths = _cachedAnalysis[_selectedTab][_selectedHash].FilePaths; + ImGui.TextUnformatted("Local file path:"); + ImGui.SameLine(); + UiSharedService.TextWrapped(filePaths[0]); + if (filePaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)"); + ImGui.SameLine(); + UiSharedService.FontText(FontAwesomeIcon.InfoCircle.ToIconString(), UiBuilder.IconFont); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); + } + + var gamepaths = _cachedAnalysis[_selectedTab][_selectedHash].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.FontText(FontAwesomeIcon.InfoCircle.ToIconString(), UiBuilder.IconFont); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); + } + } + } +} diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs index a050dd8..05d23eb 100644 --- a/MareSynchronos/UI/DtrEntry.cs +++ b/MareSynchronos/UI/DtrEntry.cs @@ -32,7 +32,11 @@ public sealed class DtrEntry : IDisposable, IHostedService public void Dispose() { - _entry.Value.Dispose(); + if (_entry.IsValueCreated) + { + _logger.LogDebug("Disposing DtrEntry"); + Clear(); + } } public Task StartAsync(CancellationToken cancellationToken) @@ -51,15 +55,6 @@ public sealed class DtrEntry : IDisposable, IHostedService catch (OperationCanceledException) { } finally { - _logger.LogDebug("Disposing DtrEntry"); - if (_entry.IsValueCreated) - { - Clear(); - - _entry.Value.Remove(); - _entry.Value.Dispose(); - } - _cancellationTokenSource.Dispose(); } } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 9309f8a..efd2c24 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -29,7 +29,6 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly FileUploadManager _fileTransferManager; private readonly FileTransferOrchestrator _fileTransferOrchestrator; - private readonly CharacterAnalyzer _characterAnalyzer; private readonly MareCharaFileManager _mareCharaFileManager; private readonly PairManager _pairManager; private readonly PerformanceCollectorService _performanceCollector; @@ -51,8 +50,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ServerConfigurationManager serverConfigurationManager, MareMediator mediator, PerformanceCollectorService performanceCollector, FileUploadManager fileTransferManager, - FileTransferOrchestrator fileTransferOrchestrator, - CharacterAnalyzer characterAnalyzer) : base(logger, mediator, "Mare Synchronos Settings") + FileTransferOrchestrator fileTransferOrchestrator) : base(logger, mediator, "Mare Synchronos Settings") { _configService = configService; _mareCharaFileManager = mareCharaFileManager; @@ -61,7 +59,6 @@ public class SettingsUi : WindowMediatorSubscriberBase _performanceCollector = performanceCollector; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; - _characterAnalyzer = characterAnalyzer; _uiShared = uiShared; SizeConstraints = new WindowSizeConstraints() @@ -334,23 +331,6 @@ public class SettingsUi : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server."); - var isAnalyzing = _characterAnalyzer.IsAnalysisRunning; - if (isAnalyzing) ImGui.BeginDisabled(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.QuestionCircle, "[DEBUG] Analyze current character composition to /xllog")) - { - _ = _characterAnalyzer.Analyze(); - } - UiSharedService.AttachToolTip("This will compute your current \"Mare load\" and print it to the /xllog"); - if (isAnalyzing) ImGui.EndDisabled(); - ImGui.SameLine(); - if (!isAnalyzing) ImGui.BeginDisabled(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) - { - _characterAnalyzer.CancelAnalyze(); - } - UiSharedService.AttachToolTip("Cancels the current analysis of your character composition"); - if (!isAnalyzing) ImGui.EndDisabled(); - _uiShared.DrawCombo("Log Level", Enum.GetValues(), (l) => l.ToString(), (l) => { _configService.Current.LogLevel = l; diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index 217b5c5..de16729 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -313,7 +313,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; } - public static bool IconTextButton(FontAwesomeIcon icon, string text) + public static bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null) { var buttonClicked = false; @@ -322,9 +322,18 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase var padding = ImGui.GetStyle().FramePadding; var spacing = ImGui.GetStyle().ItemSpacing; - var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; + Vector2 buttonSize; var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; - var buttonSize = new Vector2(buttonSizeX, buttonSizeY); + + if (width == null) + { + var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; + buttonSize = new Vector2(buttonSizeX, buttonSizeY); + } + else + { + buttonSize = new Vector2(width.Value, buttonSizeY); + } if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) {