diff --git a/.editorconfig b/.editorconfig index 5934d0b..4dedbb3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -239,7 +239,7 @@ dotnet_naming_style.fieldstyle.required_prefix = _ dotnet_naming_style.fieldstyle.required_suffix = dotnet_naming_style.fieldstyle.word_separator = dotnet_naming_style.fieldstyle.capitalization = camel_case -dotnet_diagnostic.MA0016.severity = suggestion +dotnet_diagnostic.MA0016.severity = silent dotnet_diagnostic.MA0026.severity = warning dotnet_diagnostic.MA0046.severity = suggestion dotnet_diagnostic.MA0051.severity = suggestion diff --git a/MareAPI b/MareAPI index 85bedb4..f8e647a 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 85bedb49e3b5d9fbccbb1f81929e366d515ca1a6 +Subproject commit f8e647af00be85404523e77920c03fc1a9eb894a diff --git a/MareSynchronos/.editorconfig b/MareSynchronos/.editorconfig new file mode 100644 index 0000000..d852067 --- /dev/null +++ b/MareSynchronos/.editorconfig @@ -0,0 +1,98 @@ + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion + +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +dotnet_diagnostic.MA0076.severity = silent +dotnet_diagnostic.MA0051.severity = silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +dotnet_diagnostic.S1075.severity = silent +dotnet_diagnostic.MA0007.severity = silent +dotnet_diagnostic.MA0075.severity = silent \ No newline at end of file diff --git a/MareSynchronos/Factories/CachedPlayerFactory.cs b/MareSynchronos/Factories/CachedPlayerFactory.cs deleted file mode 100644 index 57d72d2..0000000 --- a/MareSynchronos/Factories/CachedPlayerFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MareSynchronos.API.Dto.User; -using MareSynchronos.FileCache; -using MareSynchronos.Managers; -using MareSynchronos.Mediator; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.Factories; - -public class CachedPlayerFactory -{ - private readonly IpcManager _ipcManager; - private readonly DalamudUtil _dalamudUtil; - private readonly FileCacheManager _fileCacheManager; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly MareMediator _mediator; - private readonly ILoggerFactory _loggerFactory; - - public CachedPlayerFactory(IpcManager ipcManager, DalamudUtil dalamudUtil, FileCacheManager fileCacheManager, - GameObjectHandlerFactory gameObjectHandlerFactory, - MareMediator mediator, ILoggerFactory loggerFactory) - { - _ipcManager = ipcManager; - _dalamudUtil = dalamudUtil; - _fileCacheManager = fileCacheManager; - _gameObjectHandlerFactory = gameObjectHandlerFactory; - _mediator = mediator; - _loggerFactory = loggerFactory; - } - - public CachedPlayer Create(OnlineUserIdentDto dto, ApiController apiController) - { - return new CachedPlayer(_loggerFactory.CreateLogger(), dto, _gameObjectHandlerFactory, _ipcManager, apiController, _dalamudUtil, _fileCacheManager, _mediator); - } -} diff --git a/MareSynchronos/Factories/GameObjectHandlerFactory.cs b/MareSynchronos/Factories/GameObjectHandlerFactory.cs deleted file mode 100644 index 46c19a8..0000000 --- a/MareSynchronos/Factories/GameObjectHandlerFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MareSynchronos.API.Data.Enum; -using MareSynchronos.Mediator; -using MareSynchronos.Models; -using MareSynchronos.Utils; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.Factories; - -public class GameObjectHandlerFactory -{ - private readonly ILoggerFactory _loggerFactory; - private readonly MareMediator _mediator; - private readonly PerformanceCollector _performanceCollector; - private readonly DalamudUtil _dalamudUtil; - - public GameObjectHandlerFactory(ILoggerFactory loggerFactory, MareMediator mediator, PerformanceCollector performanceCollector, DalamudUtil dalamudUtil) - { - _loggerFactory = loggerFactory; - _mediator = mediator; - _performanceCollector = performanceCollector; - _dalamudUtil = dalamudUtil; - } - - public GameObjectHandler Create(ObjectKind objectKind, Func getAddress, bool isWatched) - { - return new GameObjectHandler(_loggerFactory.CreateLogger(), _performanceCollector, _mediator, _dalamudUtil, objectKind, getAddress, isWatched); - } -} \ No newline at end of file diff --git a/MareSynchronos/Factories/PairFactory.cs b/MareSynchronos/Factories/PairFactory.cs deleted file mode 100644 index 4eb7f5f..0000000 --- a/MareSynchronos/Factories/PairFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MareSynchronos.Managers; -using MareSynchronos.MareConfiguration; -using MareSynchronos.Models; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.Factories; - -public class PairFactory -{ - private readonly MareConfigService _configService; - private readonly ServerConfigurationManager _serverConfigurationManager; - private readonly CachedPlayerFactory _cachedPlayerFactory; - private readonly ILoggerFactory _loggerFactory; - - public PairFactory(MareConfigService configService, ServerConfigurationManager serverConfigurationManager, CachedPlayerFactory cachedPlayerFactory, ILoggerFactory loggerFactory) - { - _configService = configService; - _serverConfigurationManager = serverConfigurationManager; - _cachedPlayerFactory = cachedPlayerFactory; - _loggerFactory = loggerFactory; - } - - public Pair Create() - { - return new Pair(_loggerFactory.CreateLogger(), _cachedPlayerFactory, _configService, _serverConfigurationManager); - } -} diff --git a/MareSynchronos/FileCache/FileCacheEntity.cs b/MareSynchronos/FileCache/FileCacheEntity.cs index 26fac8e..47af1c0 100644 --- a/MareSynchronos/FileCache/FileCacheEntity.cs +++ b/MareSynchronos/FileCache/FileCacheEntity.cs @@ -1,17 +1,9 @@ #nullable disable - -using System.Globalization; - namespace MareSynchronos.FileCache; public class FileCacheEntity { - public string ResolvedFilepath { get; private set; } = string.Empty; - public string Hash { get; set; } - public string PrefixedFilePath { get; init; } - public string LastModifiedDateTicks { get; set; } - public FileCacheEntity(string hash, string path, string lastModifiedDateTicks) { Hash = hash; @@ -19,10 +11,14 @@ public class FileCacheEntity LastModifiedDateTicks = lastModifiedDateTicks; } + public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}"; + public string Hash { get; set; } + public string LastModifiedDateTicks { get; set; } + public string PrefixedFilePath { get; init; } + public string ResolvedFilepath { get; private set; } = string.Empty; + public void SetResolvedFilePath(string filePath) { ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal); } - - public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks.ToString(CultureInfo.InvariantCulture)}"; -} +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 70a12f2..8f35870 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -1,4 +1,4 @@ -using MareSynchronos.Managers; +using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; @@ -8,18 +8,17 @@ using System.Text; namespace MareSynchronos.FileCache; -public class FileCacheManager : IDisposable +public sealed class FileCacheManager : IDisposable { - private const string _penumbraPrefix = "{penumbra}"; + public const string CsvSplit = "|"; private const string _cachePrefix = "{cache}"; - private readonly ILogger _logger; - private readonly IpcManager _ipcManager; + private const string _penumbraPrefix = "{penumbra}"; private readonly MareConfigService _configService; private readonly string _csvPath; - private string CsvBakPath => _csvPath + ".bak"; private readonly ConcurrentDictionary _fileCaches = new(StringComparer.Ordinal); - public const string CsvSplit = "|"; private readonly object _fileWriteLock = new(); + private readonly IpcManager _ipcManager; + private readonly ILogger _logger; public FileCacheManager(ILogger logger, IpcManager ipcManager, MareConfigService configService) { @@ -51,12 +50,114 @@ public class FileCacheManager : IDisposable } catch (Exception) { - _logger.LogWarning($"Failed to initialize entry {entry}, ignoring"); + _logger.LogWarning("Failed to initialize entry {entry}, ignoring", entry); } } } } + private string CsvBakPath => _csvPath + ".bak"; + + public FileCacheEntity? CreateCacheEntry(string path) + { + _logger.LogTrace("Creating cache entry for {path}", path); + FileInfo fi = new(path); + if (!fi.Exists) return null; + var fullName = fi.FullName.ToLowerInvariant(); + if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), _cachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper(CultureInfo.InvariantCulture)); + } + + public FileCacheEntity? CreateFileEntry(string path) + { + _logger.LogTrace("Creating file entry for {path}", path); + FileInfo fi = new(path); + if (!fi.Exists) return null; + var fullName = fi.FullName.ToLowerInvariant(); + if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + return CreateFileCacheEntity(fi, prefixedPath); + } + + public void Dispose() + { + _logger.LogTrace("Disposing {type}", GetType()); + WriteOutFullCsv(); + GC.SuppressFinalize(this); + } + + public List GetAllFileCaches() => _fileCaches.Values.ToList(); + + public string GetCacheFilePath(string hash, bool isTemporaryFile) + { + return Path.Combine(_configService.Current.CacheFolder, hash + (isTemporaryFile ? ".tmp" : string.Empty)); + } + + public FileCacheEntity? GetFileCacheByHash(string hash) + { + if (_fileCaches.Any(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal))) + { + return GetValidatedFileCache(_fileCaches.Where(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)) + .OrderByDescending(f => f.Value.PrefixedFilePath.Length) + .FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)).Value); + } + + return null; + } + + public FileCacheEntity? GetFileCacheByPath(string path) + { + var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); + var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)); + + if (entry == null) + { + _logger.LogDebug("Found no entries for {path}", cleanedPath); + return CreateFileEntry(path); + } + + var validatedCacheEntry = GetValidatedFileCache(entry); + + return validatedCacheEntry; + } + + public void RemoveHash(FileCacheEntity entity) + { + _logger.LogTrace("Removing {path}", entity.ResolvedFilepath); + _fileCaches.Remove(entity.PrefixedFilePath, out _); + } + + public string ResolveFileReplacement(string gamePath) + { + return _ipcManager.PenumbraResolvePath(gamePath); + } + + public void UpdateHash(FileCacheEntity fileCache) + { + _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); + _fileCaches.Remove(fileCache.PrefixedFilePath, out _); + _fileCaches[fileCache.PrefixedFilePath] = fileCache; + } + + public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache) + { + fileCache = ReplacePathPrefixes(fileCache); + FileInfo fi = new(fileCache.ResolvedFilepath); + if (!fi.Exists) + { + return (FileState.RequireDeletion, fileCache); + } + if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + { + return (FileState.RequireUpdate, fileCache); + } + + return (FileState.Valid, fileCache); + } + public void WriteOutFullCsv() { StringBuilder sb = new(); @@ -82,74 +183,6 @@ public class FileCacheManager : IDisposable } } - public List GetAllFileCaches() => _fileCaches.Values.ToList(); - - public FileCacheEntity? GetFileCacheByHash(string hash) - { - if (_fileCaches.Any(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal))) - { - return GetValidatedFileCache(_fileCaches.Where(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)) - .OrderByDescending(f => f.Value.PrefixedFilePath.Length) - .FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)).Value); - } - - return null; - } - - public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache) - { - fileCache = ReplacePathPrefixes(fileCache); - FileInfo fi = new(fileCache.ResolvedFilepath); - if (!fi.Exists) - { - return (FileState.RequireDeletion, fileCache); - } - if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) - { - return (FileState.RequireUpdate, fileCache); - } - - return (FileState.Valid, fileCache); - } - - public FileCacheEntity? GetFileCacheByPath(string path) - { - var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); - var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)); - - if (entry == null) - { - _logger.LogDebug("Found no entries for " + cleanedPath); - return CreateFileEntry(path); - } - - var validatedCacheEntry = GetValidatedFileCache(entry); - - return validatedCacheEntry; - } - - public FileCacheEntity? CreateCacheEntry(string path) - { - _logger.LogTrace("Creating cache entry for " + path); - FileInfo fi = new(path); - if (!fi.Exists) return null; - var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; - string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), _cachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); - return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper(CultureInfo.InvariantCulture)); - } - - public FileCacheEntity? CreateFileEntry(string path) - { - _logger.LogTrace("Creating file entry for " + path); - FileInfo fi = new(path); - if (!fi.Exists) return null; - var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null; - string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); - return CreateFileCacheEntity(fi, prefixedPath); - } - private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { hash ??= Crypto.GetFileHash(fileInfo.FullName); @@ -161,7 +194,7 @@ public class FileCacheManager : IDisposable File.AppendAllLines(_csvPath, new[] { entity.CsvEntry }); } var result = GetFileCacheByPath(fileInfo.FullName); - _logger.LogDebug("Creating file cache for " + fileInfo.FullName + " success: " + (result != null)); + _logger.LogDebug("Creating file cache for {name} success: {success}", fileInfo.FullName, (result != null)); return result; } @@ -172,6 +205,20 @@ public class FileCacheManager : IDisposable return resulingFileCache; } + private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) + { + if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase)) + { + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal)); + } + else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase)) + { + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_cachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal)); + } + + return fileCache; + } + private FileCacheEntity? Validate(FileCacheEntity fileCache) { var file = new FileInfo(fileCache.ResolvedFilepath); @@ -188,44 +235,4 @@ public class FileCacheManager : IDisposable return fileCache; } - - public void RemoveHash(FileCacheEntity entity) - { - _logger.LogTrace("Removing " + entity.ResolvedFilepath); - _fileCaches.Remove(entity.PrefixedFilePath, out _); - } - - public void UpdateHash(FileCacheEntity fileCache) - { - _logger.LogTrace("Updating hash for " + fileCache.ResolvedFilepath); - fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); - fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); - _fileCaches.Remove(fileCache.PrefixedFilePath, out _); - _fileCaches[fileCache.PrefixedFilePath] = fileCache; - } - - private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) - { - if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase)) - { - fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal)); - } - else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase)) - { - fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_cachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal)); - } - - return fileCache; - } - - public string ResolveFileReplacement(string gamePath) - { - return _ipcManager.PenumbraResolvePath(gamePath); - } - - public void Dispose() - { - _logger.LogTrace($"Disposing {GetType()}"); - WriteOutFullCsv(); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/PeriodicFileScanner.cs b/MareSynchronos/FileCache/PeriodicFileScanner.cs index 63ed461..474c545 100644 --- a/MareSynchronos/FileCache/PeriodicFileScanner.cs +++ b/MareSynchronos/FileCache/PeriodicFileScanner.cs @@ -1,89 +1,60 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using MareSynchronos.Managers; +using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Utils; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; namespace MareSynchronos.FileCache; -public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable +public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase { - private readonly IpcManager _ipcManager; private readonly MareConfigService _configService; private readonly FileCacheManager _fileDbManager; - private readonly PerformanceCollector _performanceCollector; + private readonly IpcManager _ipcManager; + private readonly PerformanceCollectorService _performanceCollector; + private long _currentFileProgress = 0; + private bool _fileScanWasRunning = false; private CancellationTokenSource? _scanCancellationTokenSource; - private Task? _fileScannerTask = null; - public ConcurrentDictionary haltScanLocks = new(StringComparer.Ordinal); + private TimeSpan _timeUntilNextScan = TimeSpan.Zero; public PeriodicFileScanner(ILogger logger, IpcManager ipcManager, MareConfigService configService, - FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollector performanceCollector) : base(logger, mediator) + FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector) : base(logger, mediator) { - _logger.LogTrace("Creating " + nameof(PeriodicFileScanner)); _ipcManager = ipcManager; _configService = configService; _fileDbManager = fileDbManager; _performanceCollector = performanceCollector; Mediator.Subscribe(this, (_) => StartScan()); - Mediator.Subscribe(this, (msg) => HaltScan(((HaltScanMessage)msg).Source)); - Mediator.Subscribe(this, (msg) => ResumeScan(((ResumeScanMessage)msg).Source)); + Mediator.Subscribe(this, (msg) => HaltScan(msg.Source)); + Mediator.Subscribe(this, (msg) => ResumeScan(msg.Source)); Mediator.Subscribe(this, (_) => StartScan()); + Mediator.Subscribe(this, (_) => StartScan()); } - public void ResetLocks() - { - haltScanLocks.Clear(); - } + public long CurrentFileProgress => _currentFileProgress; + public long FileCacheSize { get; set; } + public ConcurrentDictionary HaltScanLocks { get; set; } = new(StringComparer.Ordinal); + public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0; - public void ResumeScan(string source) - { - if (!haltScanLocks.ContainsKey(source)) haltScanLocks[source] = 0; + public string TimeUntilNextScan => _timeUntilNextScan.ToString(@"mm\:ss"); - haltScanLocks[source]--; - if (haltScanLocks[source] < 0) haltScanLocks[source] = 0; + public long TotalFiles { get; private set; } - if (_fileScanWasRunning && haltScanLocks.All(f => f.Value == 0)) - { - _fileScanWasRunning = false; - InvokeScan(forced: true); - } - } + private int TimeBetweenScans => _configService.Current.TimeSpanBetweenScansInSeconds; public void HaltScan(string source) { - if (!haltScanLocks.ContainsKey(source)) haltScanLocks[source] = 0; - haltScanLocks[source]++; + if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; + HaltScanLocks[source]++; - if (IsScanRunning && haltScanLocks.Any(f => f.Value > 0)) + if (IsScanRunning && HaltScanLocks.Any(f => f.Value > 0)) { _scanCancellationTokenSource?.Cancel(); _fileScanWasRunning = true; } } - private bool _fileScanWasRunning = false; - private long _currentFileProgress = 0; - public long CurrentFileProgress => _currentFileProgress; - - public long FileCacheSize { get; set; } - - public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0; - - public long TotalFiles { get; private set; } - - public string TimeUntilNextScan => _timeUntilNextScan.ToString(@"mm\:ss"); - private TimeSpan _timeUntilNextScan = TimeSpan.Zero; - private int TimeBetweenScans => _configService.Current.TimeSpanBetweenScansInSeconds; - - public override void Dispose() - { - base.Dispose(); - - _scanCancellationTokenSource?.Cancel(); - } - public void InvokeScan(bool forced = false) { bool isForced = forced; @@ -92,11 +63,11 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable _scanCancellationTokenSource?.Cancel(); _scanCancellationTokenSource = new CancellationTokenSource(); var token = _scanCancellationTokenSource.Token; - _fileScannerTask = Task.Run(async () => + Task.Run(async () => { while (!token.IsCancellationRequested) { - while (haltScanLocks.Any(f => f.Value > 0) || !_ipcManager.CheckPenumbraApi()) + while (HaltScanLocks.Any(f => f.Value > 0) || !_ipcManager.CheckPenumbraApi()) { await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } @@ -150,6 +121,38 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable return true; } + public void ResetLocks() + { + HaltScanLocks.Clear(); + } + + public void ResumeScan(string source) + { + if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; + + HaltScanLocks[source]--; + if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0; + + if (_fileScanWasRunning && HaltScanLocks.All(f => f.Value == 0)) + { + _fileScanWasRunning = false; + InvokeScan(forced: true); + } + } + + public void StartScan() + { + if (!_ipcManager.Initialized || !_configService.Current.HasValidSetup()) return; + Logger.LogTrace("Penumbra is active, configuration is valid, scan"); + InvokeScan(forced: true); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _scanCancellationTokenSource?.Cancel(); + } + private void PeriodicFileScan(CancellationToken ct) { TotalFiles = 1; @@ -159,19 +162,19 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir)) { penDirExists = false; - _logger.LogWarning("Penumbra directory is not set or does not exist."); + Logger.LogWarning("Penumbra directory is not set or does not exist."); } if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder)) { cacheDirExists = false; - _logger.LogWarning("Mare Cache directory is not set or does not exist."); + Logger.LogWarning("Mare Cache directory is not set or does not exist."); } if (!penDirExists || !cacheDirExists) { return; } - _logger.LogDebug("Getting files from " + penumbraDir + " and " + _configService.Current.CacheFolder); + Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder); string[] ext = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk" }; var scannedFiles = new ConcurrentDictionary(Directory.EnumerateFiles(penumbraDir!, "*.*", SearchOption.AllDirectories) @@ -207,18 +210,18 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable scannedFiles[validatedCacheResult.Item2.ResolvedFilepath] = true; if (validatedCacheResult.Item1 == FileState.RequireUpdate) { - _logger.LogTrace("To update: {path}", validatedCacheResult.Item2.ResolvedFilepath); + Logger.LogTrace("To update: {path}", validatedCacheResult.Item2.ResolvedFilepath); entitiesToUpdate.Add(validatedCacheResult.Item2); } else if (validatedCacheResult.Item1 == FileState.RequireDeletion) { - _logger.LogTrace("To delete: {path}", validatedCacheResult.Item2.ResolvedFilepath); + Logger.LogTrace("To delete: {path}", validatedCacheResult.Item2.ResolvedFilepath); entitiesToRemove.Add(validatedCacheResult.Item2); } } catch (Exception ex) { - _logger.LogWarning(ex, "Failed validating {path}", cache.ResolvedFilepath); + Logger.LogWarning(ex, "Failed validating {path}", cache.ResolvedFilepath); } Interlocked.Increment(ref _currentFileProgress); @@ -227,7 +230,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable if (!_ipcManager.CheckPenumbraApi()) { - _logger.LogWarning("Penumbra not available"); + Logger.LogWarning("Penumbra not available"); return; } @@ -240,14 +243,14 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "Error during enumerating FileCaches"); + Logger.LogWarning(ex, "Error during enumerating FileCaches"); } Task.WaitAll(dbTasks); if (!_ipcManager.CheckPenumbraApi()) { - _logger.LogWarning("Penumbra not available"); + Logger.LogWarning("Penumbra not available"); return; } @@ -266,18 +269,18 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable _fileDbManager.WriteOutFullCsv(); } - _logger.LogTrace("Scanner validated existing db files"); + Logger.LogTrace("Scanner validated existing db files"); if (!_ipcManager.CheckPenumbraApi()) { - _logger.LogWarning("Penumbra not available"); + Logger.LogWarning("Penumbra not available"); return; } if (ct.IsCancellationRequested) return; // scan new files - foreach (var c in scannedFiles.Where(c => c.Value == false)) + foreach (var c in scannedFiles.Where(c => !c.Value)) { var idx = Task.WaitAny(dbTasks, ct); dbTasks[idx] = Task.Run(() => @@ -289,7 +292,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "Failed adding {file}", c.Key); + Logger.LogWarning(ex, "Failed adding {file}", c.Key); } Interlocked.Increment(ref _currentFileProgress); @@ -298,7 +301,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable if (!_ipcManager.CheckPenumbraApi()) { - _logger.LogWarning("Penumbra not available"); + Logger.LogWarning("Penumbra not available"); return; } @@ -307,9 +310,9 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable Task.WaitAll(dbTasks); - _logger.LogTrace("Scanner added new files to db"); + Logger.LogTrace("Scanner added new files to db"); - _logger.LogDebug("Scan complete"); + Logger.LogDebug("Scan complete"); TotalFiles = 0; _currentFileProgress = 0; entitiesToRemove.Clear(); @@ -322,11 +325,4 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable _configService.Save(); } } - - public void StartScan() - { - if (!_ipcManager.Initialized || !_configService.Current.HasValidSetup()) return; - _logger.LogTrace("Penumbra is active, configuration is valid, scan"); - InvokeScan(forced: true); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs similarity index 77% rename from MareSynchronos/Managers/TransientResourceManager.cs rename to MareSynchronos/FileCache/TransientResourceManager.cs index 9f45e17..c1d19ed 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -1,27 +1,24 @@ using MareSynchronos.API.Data.Enum; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Models; -using MareSynchronos.Utils; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -namespace MareSynchronos.Managers; +namespace MareSynchronos.FileCache; - -public class TransientResourceManager : MediatorSubscriberBase, IDisposable +public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { private readonly TransientConfigService _configurationService; - private readonly DalamudUtil _dalamudUtil; + private readonly DalamudUtilService _dalamudUtil; - public HashSet PlayerRelatedPointers = new(); private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" }; - private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId; + private readonly HashSet _playerRelatedPointers = new(); - private ConcurrentDictionary> TransientResources { get; } = new(); - private ConcurrentDictionary> SemiTransientResources { get; } = new(); public TransientResourceManager(ILogger logger, TransientConfigService configurationService, - DalamudUtil dalamudUtil, MareMediator mediator) : base(logger, mediator) + DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) { _configurationService = configurationService; _dalamudUtil = dalamudUtil; @@ -36,66 +33,36 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable try { - _logger.LogDebug("Loaded persistent transient resource {path}", gamePath); + Logger.LogDebug("Loaded persistent transient resource {path}", gamePath); SemiTransientResources[ObjectKind.Player].Add(gamePath); restored++; } catch (Exception ex) { - _logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath); + Logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath); } - } - _logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count()); + Logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count); } - Mediator.Subscribe(this, (msg) => Manager_PenumbraResourceLoadEvent((PenumbraResourceLoadMessage)msg)); + Mediator.Subscribe(this, Manager_PenumbraResourceLoadEvent); Mediator.Subscribe(this, (_) => Manager_PenumbraModSettingChanged()); Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe(this, (_) => DalamudUtil_ClassJobChanged()); Mediator.Subscribe(this, (msg) => { - var actualMsg = ((AddWatchedGameObjectHandler)msg); - PlayerRelatedPointers.Add(actualMsg.Handler); + _playerRelatedPointers.Add(msg.Handler); }); Mediator.Subscribe(this, (msg) => { - var actualMsg = ((RemoveWatchedGameObjectHandler)msg); - PlayerRelatedPointers.Remove(actualMsg.Handler); + _playerRelatedPointers.Remove(msg.Handler); }); } - private void Manager_PenumbraModSettingChanged() - { - Task.Run(() => - { - _logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); - foreach (var item in SemiTransientResources) - { - Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer)); - } - }); - } + private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId; - private void DalamudUtil_ClassJobChanged() - { - if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) - { - SemiTransientResources[ObjectKind.Pet].Clear(); - } - } - - private void DalamudUtil_FrameworkUpdate() - { - foreach (var item in TransientResources.ToList()) - { - if (!_dalamudUtil.IsGameObjectPresent(item.Key)) - { - _logger.LogDebug("Object not present anymore: " + item.Key.ToString("X")); - TransientResources.TryRemove(item.Key, out _); - } - } - } + private ConcurrentDictionary> SemiTransientResources { get; } = new(); + private ConcurrentDictionary> TransientResources { get; } = new(); public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) { @@ -114,6 +81,16 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable } } + public HashSet GetSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.TryGetValue(objectKind, out var result)) + { + return result ?? new HashSet(StringComparer.Ordinal); + } + + return new HashSet(StringComparer.Ordinal); + } + public List GetTransientResources(IntPtr gameObject) { if (TransientResources.TryGetValue(gameObject, out var result)) @@ -124,14 +101,91 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable return new List(); } - public HashSet GetSemiTransientResources(ObjectKind objectKind) + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) { - if (SemiTransientResources.TryGetValue(objectKind, out var result)) + if (!SemiTransientResources.ContainsKey(objectKind)) { - return result ?? new HashSet(StringComparer.Ordinal); + SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); } - return new HashSet(StringComparer.Ordinal); + if (!TransientResources.TryGetValue(gameObject, out var resources)) + { + return; + } + + var transientResources = resources.ToList(); + Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); + foreach (var gamePath in transientResources) + { + SemiTransientResources[objectKind].Add(gamePath); + } + + if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) + { + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal); + _configurationService.Save(); + } + TransientResources[gameObject].Clear(); + } + + internal void AddSemiTransientResource(ObjectKind objectKind, string item) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); + } + + SemiTransientResources[objectKind].Add(item.ToLowerInvariant()); + } + + internal void ClearTransientPaths(IntPtr ptr, List list) + { + if (TransientResources.TryGetValue(ptr, out var set)) + { + set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + TransientResources.Clear(); + SemiTransientResources.Clear(); + if (SemiTransientResources.ContainsKey(ObjectKind.Player)) + { + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player]; + _configurationService.Save(); + } + } + + private void DalamudUtil_ClassJobChanged() + { + if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet? value)) + { + value?.Clear(); + } + } + + private void DalamudUtil_FrameworkUpdate() + { + foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList()) + { + Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X")); + TransientResources.TryRemove(item, out _); + } + } + + private void Manager_PenumbraModSettingChanged() + { + Task.Run(() => + { + Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); + foreach (var item in SemiTransientResources) + { + Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer)); + } + }); } private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg) @@ -143,7 +197,7 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable { return; } - if (!PlayerRelatedPointers.Select(p => p.CurrentAddress).Contains(gameObject)) + if (!_playerRelatedPointers.Select(p => p.CurrentAddress).Contains(gameObject)) { //_logger.LogDebug("Got resource " + gamePath + " for ptr " + gameObject.ToString("X")); return; @@ -167,70 +221,13 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable if (TransientResources[gameObject].Contains(replacedGamePath) || SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase)))) { - _logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); + Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); } else { TransientResources[gameObject].Add(replacedGamePath); - _logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath); + Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath); Mediator.Publish(new TransientResourceChangedMessage(gameObject)); } } - - public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); - } - - if (!TransientResources.TryGetValue(gameObject, out var resources)) - { - return; - } - - var transientResources = resources.ToList(); - _logger.LogDebug("Persisting " + transientResources.Count + " transient resources"); - foreach (var gamePath in transientResources) - { - SemiTransientResources[objectKind].Add(gamePath); - } - - if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) - { - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal); - _configurationService.Save(); - } - TransientResources[gameObject].Clear(); - } - - public override void Dispose() - { - base.Dispose(); - TransientResources.Clear(); - SemiTransientResources.Clear(); - if (SemiTransientResources.ContainsKey(ObjectKind.Player)) - { - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player]; - _configurationService.Save(); - } - } - - internal void AddSemiTransientResource(ObjectKind objectKind, string item) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); - } - - SemiTransientResources[objectKind].Add(item.ToLowerInvariant()); - } - - internal void ClearTransientPaths(IntPtr ptr, List list) - { - if (TransientResources.TryGetValue(ptr, out var set)) - { - set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); - } - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Utils/DalamudLogger.cs b/MareSynchronos/Interop/DalamudLogger.cs similarity index 92% rename from MareSynchronos/Utils/DalamudLogger.cs rename to MareSynchronos/Interop/DalamudLogger.cs index 18cfdcb..8a01354 100644 --- a/MareSynchronos/Utils/DalamudLogger.cs +++ b/MareSynchronos/Interop/DalamudLogger.cs @@ -3,19 +3,26 @@ using MareSynchronos.MareConfiguration; using Microsoft.Extensions.Logging; using System.Text; -namespace MareSynchronos.Utils; +namespace MareSynchronos.Interop; -internal class DalamudLogger : ILogger +internal sealed class DalamudLogger : ILogger { - private readonly string _name; private readonly MareConfigService _mareConfigService; + private readonly string _name; public DalamudLogger(string name, MareConfigService mareConfigService) { - this._name = name; + _name = name; _mareConfigService = mareConfigService; } + public IDisposable BeginScope(TState state) => default!; + + public bool IsEnabled(LogLevel logLevel) + { + return (int)_mareConfigService.Current.LogLevel <= (int)logLevel; + } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; @@ -35,11 +42,4 @@ internal class DalamudLogger : ILogger PluginLog.Fatal(sb.ToString()); } } - - public bool IsEnabled(LogLevel logLevel) - { - return (int)_mareConfigService.Current.LogLevel <= (int)logLevel; - } - - public IDisposable BeginScope(TState state) => default!; -} +} \ No newline at end of file diff --git a/MareSynchronos/Utils/DalamudLoggingProvider.cs b/MareSynchronos/Interop/DalamudLoggingProvider.cs similarity index 89% rename from MareSynchronos/Utils/DalamudLoggingProvider.cs rename to MareSynchronos/Interop/DalamudLoggingProvider.cs index 00ece8d..b1c4594 100644 --- a/MareSynchronos/Utils/DalamudLoggingProvider.cs +++ b/MareSynchronos/Interop/DalamudLoggingProvider.cs @@ -2,13 +2,14 @@ using MareSynchronos.MareConfiguration; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Utils; +namespace MareSynchronos.Interop; [ProviderAlias("Dalamud")] -public class DalamudLoggingProvider : ILoggerProvider +public sealed class DalamudLoggingProvider : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + private readonly MareConfigService _mareConfigService; public DalamudLoggingProvider(MareConfigService mareConfigService) @@ -34,5 +35,6 @@ public class DalamudLoggingProvider : ILoggerProvider public void Dispose() { _loggers.Clear(); + GC.SuppressFinalize(this); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Utils/DalamudLoggingProviderExtensions.cs b/MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs similarity index 93% rename from MareSynchronos/Utils/DalamudLoggingProviderExtensions.cs rename to MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs index 2dd6add..5d50b57 100644 --- a/MareSynchronos/Utils/DalamudLoggingProviderExtensions.cs +++ b/MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Utils; +namespace MareSynchronos.Interop; public static class DalamudLoggingProviderExtensions { diff --git a/MareSynchronos/Interop/CharaExt.cs b/MareSynchronos/Interop/FFXIV/CharaExt.cs similarity index 87% rename from MareSynchronos/Interop/CharaExt.cs rename to MareSynchronos/Interop/FFXIV/CharaExt.cs index e6fa4ec..94c24a6 100644 --- a/MareSynchronos/Interop/CharaExt.cs +++ b/MareSynchronos/Interop/FFXIV/CharaExt.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game.Character; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharaExt diff --git a/MareSynchronos/Interop/HumanExt.cs b/MareSynchronos/Interop/FFXIV/HumanExt.cs similarity index 89% rename from MareSynchronos/Interop/HumanExt.cs rename to MareSynchronos/Interop/FFXIV/HumanExt.cs index 2be2e15..eec2adf 100644 --- a/MareSynchronos/Interop/HumanExt.cs +++ b/MareSynchronos/Interop/FFXIV/HumanExt.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct HumanExt diff --git a/MareSynchronos/Interop/Material.cs b/MareSynchronos/Interop/FFXIV/Material.cs similarity index 81% rename from MareSynchronos/Interop/Material.cs rename to MareSynchronos/Interop/FFXIV/Material.cs index badbe5e..41cd349 100644 --- a/MareSynchronos/Interop/Material.cs +++ b/MareSynchronos/Interop/FFXIV/Material.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct Material diff --git a/MareSynchronos/Interop/MaterialData.cs b/MareSynchronos/Interop/FFXIV/MaterialData.cs similarity index 79% rename from MareSynchronos/Interop/MaterialData.cs rename to MareSynchronos/Interop/FFXIV/MaterialData.cs index 1ebdc02..bc7bfac 100644 --- a/MareSynchronos/Interop/MaterialData.cs +++ b/MareSynchronos/Interop/FFXIV/MaterialData.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct MaterialData diff --git a/MareSynchronos/Interop/MtrlResource.cs b/MareSynchronos/Interop/FFXIV/MtrlResource.cs similarity index 93% rename from MareSynchronos/Interop/MtrlResource.cs rename to MareSynchronos/Interop/FFXIV/MtrlResource.cs index 866029e..02947bb 100644 --- a/MareSynchronos/Interop/MtrlResource.cs +++ b/MareSynchronos/Interop/FFXIV/MtrlResource.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct MtrlResource diff --git a/MareSynchronos/Interop/RenderModel.cs b/MareSynchronos/Interop/FFXIV/RenderModel.cs similarity index 75% rename from MareSynchronos/Interop/RenderModel.cs rename to MareSynchronos/Interop/FFXIV/RenderModel.cs index 76deebc..17ce6ba 100644 --- a/MareSynchronos/Interop/RenderModel.cs +++ b/MareSynchronos/Interop/FFXIV/RenderModel.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct RenderModel @@ -24,14 +24,6 @@ public unsafe struct RenderModel [FieldOffset(0x60)] public int BoneListCount; - [FieldOffset(0x70)] - private void* UnkDXBuffer1; - - [FieldOffset(0x78)] - private void* UnkDXBuffer2; - - [FieldOffset(0x80)] - private void* UnkDXBuffer3; [FieldOffset(0x98)] public void** Materials; diff --git a/MareSynchronos/Interop/ResourceHandle.cs b/MareSynchronos/Interop/FFXIV/ResourceHandle.cs similarity index 93% rename from MareSynchronos/Interop/ResourceHandle.cs rename to MareSynchronos/Interop/FFXIV/ResourceHandle.cs index a9ebe7a..973d633 100644 --- a/MareSynchronos/Interop/ResourceHandle.cs +++ b/MareSynchronos/Interop/FFXIV/ResourceHandle.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.System.Resource; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct ResourceHandle diff --git a/MareSynchronos/Interop/Weapon.cs b/MareSynchronos/Interop/FFXIV/Weapon.cs similarity index 89% rename from MareSynchronos/Interop/Weapon.cs rename to MareSynchronos/Interop/FFXIV/Weapon.cs index b72da28..b740875 100644 --- a/MareSynchronos/Interop/Weapon.cs +++ b/MareSynchronos/Interop/FFXIV/Weapon.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct Weapon diff --git a/MareSynchronos/Interop/WeaponDrawObject.cs b/MareSynchronos/Interop/FFXIV/WeaponDrawObject.cs similarity index 81% rename from MareSynchronos/Interop/WeaponDrawObject.cs rename to MareSynchronos/Interop/FFXIV/WeaponDrawObject.cs index 7d155ee..d4fc435 100644 --- a/MareSynchronos/Interop/WeaponDrawObject.cs +++ b/MareSynchronos/Interop/FFXIV/WeaponDrawObject.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace MareSynchronos.Interop; +namespace MareSynchronos.Interop.FFXIV; [StructLayout(LayoutKind.Explicit)] public unsafe struct WeaponDrawObject diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Interop/IpcManager.cs similarity index 90% rename from MareSynchronos/Managers/IpcManager.cs rename to MareSynchronos/Interop/IpcManager.cs index 2460701..e054ddb 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Interop/IpcManager.cs @@ -1,95 +1,88 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Game.ClientState.Objects.Types; -using MareSynchronos.Utils; using Action = System.Action; using System.Collections.Concurrent; using System.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; -using MareSynchronos.Mediator; using Dalamud.Interface.Internal.Notifications; -using MareSynchronos.Models; using Microsoft.Extensions.Logging; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services; -namespace MareSynchronos.Managers; +namespace MareSynchronos.Interop; -public class IpcManager : MediatorSubscriberBase, IDisposable +public sealed class IpcManager : DisposableMediatorSubscriberBase { + private readonly ICallGateSubscriber _customizePlusApiVersion; + private readonly ICallGateSubscriber _customizePlusGetBodyScale; + private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; + private readonly ICallGateSubscriber _customizePlusRevert; + private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; + private readonly DalamudUtilService _dalamudUtil; private readonly ICallGateSubscriber _glamourerApiVersion; private readonly ICallGateSubscriber? _glamourerApplyAll; + private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; + private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; private readonly ICallGateSubscriber? _glamourerGetAllCustomization; private readonly ICallGateSubscriber _glamourerRevertCustomization; - private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; - private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; - - private readonly FuncSubscriber<(int, int)> _penumbraApiVersion; - private readonly FuncSubscriber _penumbraCreateNamedTemporaryCollection; - private readonly FuncSubscriber _penumbraGetMetaManipulations; - private readonly EventSubscriber _penumbraInit; - private readonly EventSubscriber _penumbraDispose; - private readonly EventSubscriber _penumbraObjectIsRedrawn; - private readonly ActionSubscriber _penumbraRedraw; - private readonly ActionSubscriber _penumbraRedrawObject; - private readonly FuncSubscriber _penumbraRemoveTemporaryCollection; - private readonly FuncSubscriber _penumbraRemoveTemporaryMod; - private readonly FuncSubscriber _penumbraAssignTemporaryCollection; - private readonly FuncSubscriber _penumbraResolveModDir; - private readonly FuncSubscriber _penumbraResolvePlayer; - private readonly FuncSubscriber _reverseResolvePlayer; - private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; - private readonly FuncSubscriber _penumbraResolvePaths; - private readonly FuncSubscriber _penumbraEnabled; - private readonly EventSubscriber _penumbraModSettingChanged; - private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; - + private readonly ConcurrentQueue _gposeActionQueue = new(); private readonly ICallGateSubscriber _heelsGetApiVersion; private readonly ICallGateSubscriber _heelsGetOffset; private readonly ICallGateSubscriber _heelsOffsetUpdate; private readonly ICallGateSubscriber _heelsRegisterPlayer; private readonly ICallGateSubscriber _heelsUnregisterPlayer; - - private readonly ICallGateSubscriber _customizePlusApiVersion; - private readonly ICallGateSubscriber _customizePlusGetBodyScale; - private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; - private readonly ICallGateSubscriber _customizePlusRevert; - private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; - + private readonly ConcurrentQueue _normalQueue = new(); private readonly ICallGateSubscriber _palettePlusApiVersion; private readonly ICallGateSubscriber _palettePlusBuildCharaPalette; - private readonly ICallGateSubscriber _palettePlusSetCharaPalette; - private readonly ICallGateSubscriber _palettePlusRemoveCharaPalette; private readonly ICallGateSubscriber _palettePlusPaletteChanged; - private readonly DalamudUtil _dalamudUtil; - private bool _inGposeQueueMode = false; - private ConcurrentQueue ActionQueue => _inGposeQueueMode ? _gposeActionQueue : _normalQueue; - private readonly ConcurrentQueue _normalQueue = new(); - private readonly ConcurrentQueue _gposeActionQueue = new(); - + private readonly ICallGateSubscriber _palettePlusRemoveCharaPalette; + private readonly ICallGateSubscriber _palettePlusSetCharaPalette; + private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; + private readonly FuncSubscriber<(int, int)> _penumbraApiVersion; + private readonly FuncSubscriber _penumbraAssignTemporaryCollection; + private readonly FuncSubscriber _penumbraCreateNamedTemporaryCollection; + private readonly EventSubscriber _penumbraDispose; + private readonly FuncSubscriber _penumbraEnabled; + private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; + private readonly FuncSubscriber _penumbraGetMetaManipulations; + private readonly EventSubscriber _penumbraInit; + private readonly EventSubscriber _penumbraModSettingChanged; + private readonly EventSubscriber _penumbraObjectIsRedrawn; + private readonly ActionSubscriber _penumbraRedraw; + private readonly ActionSubscriber _penumbraRedrawObject; private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); - private CancellationTokenSource _disposalCts = new(); - - private bool _penumbraAvailable = false; - private bool _glamourerAvailable = false; + private readonly FuncSubscriber _penumbraRemoveTemporaryCollection; + private readonly FuncSubscriber _penumbraRemoveTemporaryMod; + private readonly FuncSubscriber _penumbraResolveModDir; + private readonly FuncSubscriber _penumbraResolvePaths; + private readonly FuncSubscriber _penumbraResolvePlayer; + private readonly FuncSubscriber _reverseResolvePlayer; private bool _customizePlusAvailable = false; + private CancellationTokenSource _disposalCts = new(); + private bool _glamourerAvailable = false; private bool _heelsAvailable = false; + private bool _inGposeQueueMode = false; private bool _palettePlusAvailable = false; + private bool _penumbraAvailable = false; + private bool _shownGlamourerUnavailable = false; + private bool _shownPenumbraUnavailable = false; - public IpcManager(ILogger logger, DalamudPluginInterface pi, DalamudUtil dalamudUtil, MareMediator mediator) : base(logger, mediator) + public IpcManager(ILogger logger, DalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) { _dalamudUtil = dalamudUtil; - _logger.LogTrace("Creating " + nameof(IpcManager)); - - _penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, () => PenumbraInit()); - _penumbraDispose = Penumbra.Api.Ipc.Disposed.Subscriber(pi, () => PenumbraDispose()); + _penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, PenumbraInit); + _penumbraDispose = Penumbra.Api.Ipc.Disposed.Subscriber(pi, PenumbraDispose); _penumbraResolvePlayer = Penumbra.Api.Ipc.ResolvePlayerPath.Subscriber(pi); _penumbraResolveModDir = Penumbra.Api.Ipc.GetModDirectory.Subscriber(pi); _penumbraRedraw = Penumbra.Api.Ipc.RedrawObjectByName.Subscriber(pi); _penumbraRedrawObject = Penumbra.Api.Ipc.RedrawObject.Subscriber(pi); _reverseResolvePlayer = Penumbra.Api.Ipc.ReverseResolvePlayerPath.Subscriber(pi); _penumbraApiVersion = Penumbra.Api.Ipc.ApiVersions.Subscriber(pi); - _penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, (ptr, idx) => RedrawEvent((IntPtr)ptr, idx)); + _penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, RedrawEvent); _penumbraGetMetaManipulations = Penumbra.Api.Ipc.GetPlayerMetaManipulations.Subscriber(pi); _penumbraAddTemporaryMod = Penumbra.Api.Ipc.AddTemporaryMod.Subscriber(pi); _penumbraCreateNamedTemporaryCollection = Penumbra.Api.Ipc.CreateNamedTemporaryCollection.Subscriber(pi); @@ -104,7 +97,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable Mediator.Publish(new PenumbraModSettingChangedMessage()); }); - _penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2)); + _penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); @@ -148,64 +141,26 @@ public class IpcManager : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => PeriodicApiStateCheck()); } - private void PeriodicApiStateCheck() - { - _glamourerAvailable = CheckGlamourerApiInternal(); - _penumbraAvailable = CheckPenumbraApiInternal(); - _heelsAvailable = CheckHeelsApiInternal(); - _customizePlusAvailable = CheckCustomizePlusApiInternal(); - _palettePlusAvailable = CheckPalettePlusApiInternal(); - PenumbraModDirectory = GetPenumbraModDirectory(); - } - - private void HandleGposeActionQueue() - { - if (_gposeActionQueue.TryDequeue(out var action)) - { - if (action == null) return; - _logger.LogDebug("Execution action in gpose queue: {method}", action.Method); - action(); - } - } - - public void ToggleGposeQueueMode(bool on) - { - _inGposeQueueMode = on; - } - - private void ClearActionQueue() - { - ActionQueue.Clear(); - _gposeActionQueue.Clear(); - } - - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) - { - Task.Run(() => - { - if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0) - { - Mediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); - } - }); - } - - private void HandleActionQueue() - { - if (ActionQueue.TryDequeue(out var action)) - { - if (action == null) return; - _logger.LogDebug("Execution action in queue: {method}", action.Method); - action(); - } - } - public bool Initialized => CheckPenumbraApiInternal() && CheckGlamourerApiInternal(); + public string? PenumbraModDirectory { get; private set; } + private ConcurrentQueue ActionQueue => _inGposeQueueMode ? _gposeActionQueue : _normalQueue; + + public bool CheckCustomizePlusApi() => _customizePlusAvailable; + + public bool CheckCustomizePlusApiInternal() + { + try + { + return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal); + } + catch + { + return false; + } + } public bool CheckGlamourerApi() => _glamourerAvailable; - private bool _shownGlamourerUnavailable = false; - public bool CheckGlamourerApiInternal() { bool apiAvailable = false; @@ -229,9 +184,36 @@ public class IpcManager : MediatorSubscriberBase, IDisposable } } + public bool CheckHeelsApi() => _heelsAvailable; + + public bool CheckHeelsApiInternal() + { + try + { + return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal); + } + catch + { + return false; + } + } + + public bool CheckPalettePlusApi() => _palettePlusAvailable; + + public bool CheckPalettePlusApiInternal() + { + try + { + return string.Equals(_palettePlusApiVersion.InvokeFunc(), "1.1.0", StringComparison.Ordinal); + } + catch + { + return false; + } + } + public bool CheckPenumbraApi() => _penumbraAvailable; - private bool _shownPenumbraUnavailable = false; public bool CheckPenumbraApiInternal() { bool apiAvailable = false; @@ -255,108 +237,31 @@ public class IpcManager : MediatorSubscriberBase, IDisposable } } - public bool CheckHeelsApi() => _heelsAvailable; - - public bool CheckHeelsApiInternal() + public async Task CustomizePlusRevert(IntPtr character) { - try - { - return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal); - } - catch - { - return false; - } - } - - public bool CheckCustomizePlusApi() => _customizePlusAvailable; - - public bool CheckCustomizePlusApiInternal() - { - try - { - return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal); - } - catch - { - return false; - } - } - - public bool CheckPalettePlusApi() => _palettePlusAvailable; - - public bool CheckPalettePlusApiInternal() - { - try - { - return string.Equals(_palettePlusApiVersion.InvokeFunc(), "1.1.0", StringComparison.Ordinal); - } - catch - { - return false; - } - } - - public override void Dispose() - { - base.Dispose(); - - _disposalCts.Cancel(); - - int totalSleepTime = 0; - while (!ActionQueue.IsEmpty && totalSleepTime < 2000) - { - _logger.LogTrace("Waiting for actionqueue to clear..."); - HandleActionQueue(); - Thread.Sleep(16); - totalSleepTime += 16; - } - - if (totalSleepTime >= 2000) - { - _logger.LogTrace("Action queue clear or not, disposing"); - } - - ActionQueue.Clear(); - - _penumbraModSettingChanged.Dispose(); - _penumbraGameObjectResourcePathResolved.Dispose(); - _penumbraDispose.Dispose(); - _penumbraInit.Dispose(); - _penumbraObjectIsRedrawn.Dispose(); - _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); - } - - public float GetHeelsOffset() - { - if (!CheckHeelsApi()) return 0.0f; - return _heelsGetOffset.InvokeFunc(); - } - - public async Task HeelsSetOffsetForPlayer(IntPtr character, float offset) - { - if (!CheckHeelsApi()) return; + if (!CheckCustomizePlusApi()) return; await _dalamudUtil.RunOnFrameworkThread(() => { var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) + if (gameObj is Character c) { - _logger.LogTrace("Applying Heels data to {chara}", character.ToString("X")); - _heelsRegisterPlayer.InvokeAction(gameObj, offset); + Logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X")); + _customizePlusRevert!.InvokeAction(c); } }).ConfigureAwait(false); } - public async Task HeelsRestoreOffsetForPlayer(IntPtr character) + public async Task CustomizePlusSetBodyScale(IntPtr character, string scale) { - if (!CheckHeelsApi()) return; + if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return; await _dalamudUtil.RunOnFrameworkThread(() => { var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) + if (gameObj is Character c) { - _logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X")); - _heelsUnregisterPlayer.InvokeAction(gameObj); + string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale)); + Logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X")); + _customizePlusSetBodyScaleToCharacter!.InvokeAction(decodedScale, c); } }).ConfigureAwait(false); } @@ -369,57 +274,16 @@ public class IpcManager : MediatorSubscriberBase, IDisposable return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); } - public async Task CustomizePlusSetBodyScale(IntPtr character, string scale) + public float GetHeelsOffset() { - if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale)); - _logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X")); - _customizePlusSetBodyScaleToCharacter!.InvokeAction(decodedScale, c); - } - }).ConfigureAwait(false); + if (!CheckHeelsApi()) return 0.0f; + return _heelsGetOffset.InvokeFunc(); } - public async Task CustomizePlusRevert(IntPtr character) + public string? GetPenumbraModDirectory() { - if (!CheckCustomizePlusApi()) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - _logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X")); - _customizePlusRevert!.InvokeAction(c); - } - }).ConfigureAwait(false); - } - - private async Task PenumbraRedrawAction(ILogger logger, GameObjectHandler obj, Guid applicationId, Action action, bool fireAndForget, CancellationToken token) - { - Mediator.Publish(new PenumbraStartRedrawMessage(obj.Address)); - - _penumbraRedrawRequests[obj.Address] = !fireAndForget; - - ActionQueue.Enqueue(action); - - if (!fireAndForget) - { - var disposeToken = _disposalCts.Token; - var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(disposeToken, token).Token; - - await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); - - if (!combinedToken.IsCancellationRequested) - await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, obj, applicationId, 30000, combinedToken).ConfigureAwait(false); - - _penumbraRedrawRequests[obj.Address] = false; - } - Mediator.Publish(new PenumbraEndRedrawMessage(obj.Address)); - + if (!CheckPenumbraApi()) return null; + return _penumbraResolveModDir!.Invoke().ToLowerInvariant(); } public async Task GlamourerApplyAll(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) @@ -428,17 +292,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable var gameObj = _dalamudUtil.CreateGameObject(handler.Address); if (gameObj is Character c) { - await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyAll!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); - } - } - - public async Task GlamourerApplyOnlyEquipment(ILogger logger, GameObjectHandler handler, string customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; - var gameObj = _dalamudUtil.CreateGameObject(handler.Address); - if (gameObj is Character c) - { - await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyOnlyEquipment!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); + await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyAll!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); } } @@ -448,7 +302,17 @@ public class IpcManager : MediatorSubscriberBase, IDisposable var gameObj = _dalamudUtil.CreateGameObject(handler.Address); if (gameObj is Character c) { - await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyOnlyCustomization!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); + await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyOnlyCustomization!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); + } + } + + public async Task GlamourerApplyOnlyEquipment(ILogger logger, GameObjectHandler handler, string customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; + var gameObj = _dalamudUtil.CreateGameObject(handler.Address); + if (gameObj is Character c) + { + await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyOnlyEquipment!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false); } } @@ -481,27 +345,93 @@ public class IpcManager : MediatorSubscriberBase, IDisposable ActionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); } + public async Task HeelsRestoreOffsetForPlayer(IntPtr character) + { + if (!CheckHeelsApi()) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X")); + _heelsUnregisterPlayer.InvokeAction(gameObj); + } + }).ConfigureAwait(false); + } + + public async Task HeelsSetOffsetForPlayer(IntPtr character, float offset) + { + if (!CheckHeelsApi()) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.LogTrace("Applying Heels data to {chara}", character.ToString("X")); + _heelsRegisterPlayer.InvokeAction(gameObj, offset); + } + }).ConfigureAwait(false); + } + + public string PalettePlusBuildPalette() + { + if (!CheckPalettePlusApi()) return string.Empty; + var palette = _palettePlusBuildCharaPalette.InvokeFunc(_dalamudUtil.PlayerCharacter); + if (string.IsNullOrEmpty(palette)) return string.Empty; + return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette)); + } + + public async Task PalettePlusRemovePalette(IntPtr character) + { + if (!CheckPalettePlusApi()) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + Logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X")); + _palettePlusRemoveCharaPalette!.InvokeAction(c); + } + }).ConfigureAwait(false); + } + + public async Task PalettePlusSetPalette(IntPtr character, string palette) + { + if (!CheckPalettePlusApi()) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette)); + + if (string.IsNullOrEmpty(decodedPalette)) + { + Logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X")); + _palettePlusRemoveCharaPalette!.InvokeAction(c); + } + else + { + Logger.LogTrace("PalettePlus applying for {addr}", c.Address.ToString("X")); + _palettePlusSetCharaPalette!.InvokeAction(c, decodedPalette); + } + } + }).ConfigureAwait(false); + } + public string PenumbraGetMetaManipulations() { if (!CheckPenumbraApi()) return string.Empty; return _penumbraGetMetaManipulations.Invoke(); } - public string? PenumbraModDirectory; - - public string? GetPenumbraModDirectory() - { - if (!CheckPenumbraApi()) return null; - return _penumbraResolveModDir!.Invoke().ToLowerInvariant(); - } - public async Task PenumbraRedraw(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token, bool fireAndForget = false) { if (!CheckPenumbraApi() || _dalamudUtil.IsZoning) return; var gameObj = _dalamudUtil.CreateGameObject(handler.Address); if (gameObj is Character c) { - await PenumbraRedrawAction(logger, handler, applicationId, () => _penumbraRedrawObject!.Invoke(c, RedrawType.Redraw), fireAndForget, token).ConfigureAwait(false); + await PenumbraRedrawAsync(logger, handler, applicationId, () => _penumbraRedrawObject!.Invoke(c, RedrawType.Redraw), fireAndForget, token).ConfigureAwait(false); } } @@ -523,6 +453,11 @@ public class IpcManager : MediatorSubscriberBase, IDisposable return resolvedPath ?? path; } + public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse) + { + return _penumbraResolvePaths.Invoke(forward, reverse); + } + public string[] PenumbraReverseResolvePlayer(string path) { if (!CheckPenumbraApi()) return new[] { path }; @@ -557,28 +492,81 @@ public class IpcManager : MediatorSubscriberBase, IDisposable logger.LogTrace("[{applicationId}] Setting temp mods for {collName}, Success: {ret2}", applicationId, collName, ret2); } - public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse) + public void ToggleGposeQueueMode(bool on) { - return _penumbraResolvePaths.Invoke(forward, reverse); + _inGposeQueueMode = on; } - private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) + internal bool RequestedRedraw(nint address) { - bool wasRequested = false; - if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest) + if (_penumbraRedrawRequests.TryGetValue(address, out var requested)) { - _penumbraRedrawRequests[objectAddress] = false; + return requested; } - else + + return false; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _disposalCts.Cancel(); + + int totalSleepTime = 0; + while (!ActionQueue.IsEmpty && totalSleepTime < 2000) { - Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested)); + Logger.LogTrace("Waiting for actionqueue to clear..."); + PeriodicApiStateCheck(); + if (CheckPenumbraApi()) + { + HandleActionQueue(); + } + Thread.Sleep(16); + totalSleepTime += 16; + } + + if (totalSleepTime >= 2000) + { + Logger.LogTrace("Action queue clear or not, disposing"); + } + + ActionQueue.Clear(); + + _penumbraModSettingChanged.Dispose(); + _penumbraGameObjectResourcePathResolved.Dispose(); + _penumbraDispose.Dispose(); + _penumbraInit.Dispose(); + _penumbraObjectIsRedrawn.Dispose(); + _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); + _palettePlusPaletteChanged.Unsubscribe(OnPalettePlusPaletteChange); + _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange); + } + + private void ClearActionQueue() + { + ActionQueue.Clear(); + _gposeActionQueue.Clear(); + } + + private void HandleActionQueue() + { + if (ActionQueue.TryDequeue(out var action)) + { + if (action == null) return; + Logger.LogDebug("Execution action in queue: {method}", action.Method); + action(); } } - private void PenumbraInit() + private void HandleGposeActionQueue() { - Mediator.Publish(new PenumbraInitializedMessage()); - _penumbraRedraw!.Invoke("self", RedrawType.Redraw); + if (_gposeActionQueue.TryDequeue(out var action)) + { + if (action == null) return; + Logger.LogDebug("Execution action in gpose queue: {method}", action.Method); + action(); + } } private void HeelsOffsetChange(float offset) @@ -596,52 +584,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable Mediator.Publish(new PalettePlusMessage()); } - public async Task PalettePlusSetPalette(IntPtr character, string palette) - { - if (!CheckPalettePlusApi()) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette)); - - if (string.IsNullOrEmpty(decodedPalette)) - { - _logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X")); - _palettePlusRemoveCharaPalette!.InvokeAction(c); - } - else - { - _logger.LogTrace("PalettePlus applying for {addr}", c.Address.ToString("X")); - _palettePlusSetCharaPalette!.InvokeAction(c, decodedPalette); - } - } - }).ConfigureAwait(false); - } - - public string PalettePlusBuildPalette() - { - if (!CheckPalettePlusApi()) return string.Empty; - var palette = _palettePlusBuildCharaPalette.InvokeFunc(_dalamudUtil.PlayerCharacter); - if (string.IsNullOrEmpty(palette)) return string.Empty; - return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette)); - } - - public async Task PalettePlusRemovePalette(IntPtr character) - { - if (!CheckPalettePlusApi()) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - _logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X")); - _palettePlusRemoveCharaPalette!.InvokeAction(c); - } - }).ConfigureAwait(false); - } - private void PenumbraDispose() { _disposalCts.Cancel(); @@ -651,13 +593,66 @@ public class IpcManager : MediatorSubscriberBase, IDisposable _disposalCts = new(); } - internal bool RequestedRedraw(nint address) + private void PenumbraInit() { - if (_penumbraRedrawRequests.TryGetValue(address, out var requested)) - { - return requested; - } - - return false; + Mediator.Publish(new PenumbraInitializedMessage()); + _penumbraRedraw!.Invoke("self", RedrawType.Redraw); } -} + + private async Task PenumbraRedrawAsync(ILogger logger, GameObjectHandler obj, Guid applicationId, Action action, bool fireAndForget, CancellationToken token) + { + Mediator.Publish(new PenumbraStartRedrawMessage(obj.Address)); + + _penumbraRedrawRequests[obj.Address] = !fireAndForget; + + ActionQueue.Enqueue(action); + + if (!fireAndForget) + { + var disposeToken = _disposalCts.Token; + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(disposeToken, token).Token; + + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + + if (!combinedToken.IsCancellationRequested) + await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, obj, applicationId, 30000, combinedToken).ConfigureAwait(false); + + _penumbraRedrawRequests[obj.Address] = false; + } + Mediator.Publish(new PenumbraEndRedrawMessage(obj.Address)); + } + + private void PeriodicApiStateCheck() + { + _glamourerAvailable = CheckGlamourerApiInternal(); + _penumbraAvailable = CheckPenumbraApiInternal(); + _heelsAvailable = CheckHeelsApiInternal(); + _customizePlusAvailable = CheckCustomizePlusApiInternal(); + _palettePlusAvailable = CheckPalettePlusApiInternal(); + PenumbraModDirectory = GetPenumbraModDirectory(); + } + + private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) + { + bool wasRequested = false; + if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest) + { + _penumbraRedrawRequests[objectAddress] = false; + } + else + { + Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested)); + } + } + + private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) + { + Task.Run(() => + { + if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0) + { + Mediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); + } + }); + } +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs b/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs index 1a6d34a..a876578 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs @@ -10,4 +10,4 @@ public static class ConfigurationExtensions && !string.IsNullOrEmpty(configuration.CacheFolder) && Directory.Exists(configuration.CacheFolder); } -} +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs index 1d0edc2..ae4e8ac 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs @@ -2,12 +2,14 @@ using MareSynchronos.MareConfiguration.Configurations; using MareSynchronos.MareConfiguration.Configurations.Obsolete; using MareSynchronos.MareConfiguration.Models; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace MareSynchronos.MareConfiguration; +#pragma warning disable CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones -public class ConfigurationMigrator +public class ConfigurationMigrator : IHostedService { private readonly ILogger _logger; private readonly DalamudPluginInterface _pi; @@ -20,7 +22,6 @@ public class ConfigurationMigrator public void Migrate() { -#pragma warning disable CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones if (_pi.GetPluginConfig() is Configuration oldConfig) { _logger.LogInformation("Migrating Configuration from old config style to 1"); @@ -41,6 +42,24 @@ public class ConfigurationMigrator } } + public Task StartAsync(CancellationToken cancellationToken) + { + Migrate(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private static void SaveConfig(IMareConfiguration config, string path) + { + File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented)); + } + + private string ConfigurationPath(string configName) => Path.Combine(_pi.ConfigDirectory.FullName, configName); + private void MigrateMareConfigV0ToV1(MareConfigV0 mareConfigV0) { _logger.LogInformation("Migrating Configuration from version 0 to 1"); @@ -82,13 +101,6 @@ public class ConfigurationMigrator SaveConfig(tagConfig, ConfigurationPath(ServerTagConfigService.ConfigName)); SaveConfig(notesConfig, ConfigurationPath(NotesConfigService.ConfigName)); } -#pragma warning restore CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones +} - private string ConfigurationPath(string configName) => Path.Combine(_pi.ConfigDirectory.FullName, configName); - - - private void SaveConfig(IMareConfiguration config, string path) - { - File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented)); - } -} \ No newline at end of file +#pragma warning restore CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs index 63d32e5..ad3fb92 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs @@ -1,26 +1,18 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; +using MareSynchronos.MareConfiguration.Configurations; +using System.Text.Json; namespace MareSynchronos.MareConfiguration; public abstract class ConfigurationServiceBase : IDisposable where T : IMareConfiguration { - protected abstract string ConfigurationName { get; } - public string ConfigurationDirectory => _pluginInterface.ConfigDirectory.FullName; - public T Current => _currentConfigInternal.Value; - - protected readonly DalamudPluginInterface _pluginInterface; private readonly CancellationTokenSource _periodicCheckCts = new(); - private DateTime _configLastWriteTime; private bool _configIsDirty = false; + private DateTime _configLastWriteTime; private Lazy _currentConfigInternal; - protected string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName); - protected ConfigurationServiceBase(DalamudPluginInterface pluginInterface) + protected ConfigurationServiceBase(string configurationDirectory) { - _pluginInterface = pluginInterface; + ConfigurationDirectory = configurationDirectory; Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token); Task.Run(CheckForDirtyConfigInternal, _periodicCheckCts.Token); @@ -28,39 +20,26 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC _currentConfigInternal = LazyConfig(); } - private Lazy LazyConfig() - { - _configLastWriteTime = GetConfigLastWriteTime(); - return new Lazy(() => LoadConfig()); - } - private DateTime GetConfigLastWriteTime() => new FileInfo(ConfigurationPath).LastWriteTimeUtc; + public string ConfigurationDirectory { get; init; } + public T Current => _currentConfigInternal.Value; + protected abstract string ConfigurationName { get; } + protected string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName); - private async Task CheckForConfigUpdatesInternal() + public void Dispose() { - while (!_periodicCheckCts.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false); - - var lastWriteTime = GetConfigLastWriteTime(); - if (lastWriteTime != _configLastWriteTime) - { - //_logger.LogDebug($"Config {ConfigurationName} changed, reloading config"); - _currentConfigInternal = LazyConfig(); - } - } + Dispose(disposing: true); + GC.SuppressFinalize(this); } - private async Task CheckForDirtyConfigInternal() + public void Save() { - while (!_periodicCheckCts.IsCancellationRequested) - { - if (_configIsDirty) - { - SaveDirtyConfig(); - } + _configIsDirty = true; + } - await Task.Delay(TimeSpan.FromSeconds(1), _periodicCheckCts.Token).ConfigureAwait(false); - } + protected virtual void Dispose(bool disposing) + { + _periodicCheckCts.Cancel(); + _periodicCheckCts.Dispose(); } protected T LoadConfig() @@ -73,7 +52,7 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC } else { - config = JsonConvert.DeserializeObject(File.ReadAllText(ConfigurationPath)); + config = JsonSerializer.Deserialize(File.ReadAllText(ConfigurationPath)); if (config == null) { config = (T)Activator.CreateInstance(typeof(T))!; @@ -98,26 +77,54 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC } } - //_logger.LogDebug("Saving dirty config " + ConfigurationName); - try { File.Copy(ConfigurationPath, ConfigurationPath + ".bak." + DateTime.Now.ToString("yyyyMMddHHmmss"), overwrite: true); } - catch { } + catch + { + // ignore if file cannot be backupped once + } - File.WriteAllText(ConfigurationPath, JsonConvert.SerializeObject(Current, Formatting.Indented)); + File.WriteAllText(ConfigurationPath, JsonSerializer.Serialize(Current, new JsonSerializerOptions() + { + WriteIndented = true + })); _configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc; } - public void Save() + private async Task CheckForConfigUpdatesInternal() { - _configIsDirty = true; + while (!_periodicCheckCts.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false); + + var lastWriteTime = GetConfigLastWriteTime(); + if (lastWriteTime != _configLastWriteTime) + { + _currentConfigInternal = LazyConfig(); + } + } } - public void Dispose() + private async Task CheckForDirtyConfigInternal() { - //_logger.LogTrace($"Disposing {GetType()}"); - _periodicCheckCts.Cancel(); + while (!_periodicCheckCts.IsCancellationRequested) + { + if (_configIsDirty) + { + SaveDirtyConfig(); + } + + await Task.Delay(TimeSpan.FromSeconds(1), _periodicCheckCts.Token).ConfigureAwait(false); + } } -} + + private DateTime GetConfigLastWriteTime() => new FileInfo(ConfigurationPath).LastWriteTimeUtc; + + private Lazy LazyConfig() + { + _configLastWriteTime = GetConfigLastWriteTime(); + return new Lazy(LoadConfig); + } +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 59396e4..cc30cf4 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -6,25 +6,31 @@ namespace MareSynchronos.MareConfiguration.Configurations; [Serializable] public class MareConfig : IMareConfiguration { - public int Version { get; set; } = 1; public bool AcceptedAgreement { get; set; } = false; public string CacheFolder { get; set; } = string.Empty; - public double MaxLocalCacheInGiB { get; set; } = 20; - public bool ReverseUserSort { get; set; } = false; - public int TimeSpanBetweenScansInSeconds { get; set; } = 30; - public bool FileScanPaused { get; set; } = false; - public bool InitialScanComplete { get; set; } = false; public bool DisableOptionalPluginWarnings { get; set; } = false; + public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; + public bool FileScanPaused { get; set; } = false; + public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast; + public bool InitialScanComplete { get; set; } = false; + public LogLevel LogLevel { get; set; } = LogLevel.Information; + public bool LogPerformance { get; set; } = false; + public double MaxLocalCacheInGiB { get; set; } = 20; public bool OpenGposeImportOnGposeStart { get; set; } = false; - public bool ShowTransferWindow { get; set; } = true; public bool OpenPopupOnAdd { get; set; } = true; + public int ParallelDownloads { get; set; } = 10; + public bool ReverseUserSort { get; set; } = false; + public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; + public bool ShowOfflineUsersSeparately { get; set; } = true; public bool ShowOnlineNotifications { get; set; } = false; public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true; public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; - public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; - public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast; + public bool ShowTransferBars { get; set; } = true; + public bool ShowTransferWindow { get; set; } = false; + public bool ShowUploading { get; set; } = true; + public bool ShowUploadingBigText { get; set; } = true; + public bool ShowVisibleUsersSeparately { get; set; } = true; + public int TimeSpanBetweenScansInSeconds { get; set; } = 30; + public int Version { get; set; } = 1; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; - public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; - public LogLevel LogLevel { get; set; } = LogLevel.Information; - public bool LogPerformance { get; set; } = false; -} +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs b/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs index 79b6ad0..53c5bf1 100644 --- a/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs +++ b/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs @@ -1,6 +1,6 @@ using Dalamud.Configuration; -using Dalamud.Plugin; using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.MareConfiguration.Models.Obsolete; using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; @@ -46,12 +46,12 @@ public class Configuration : IPluginConfiguration /// The dictionary first maps a server URL to a dictionary, and that /// dictionary maps the OtherUID of the to a list of tags. /// - public Dictionary>> UidServerPairedUserTags = new(StringComparer.Ordinal); + public Dictionary>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); /// /// A dictionary that maps a server URL to the tags the user has added for that server. /// - public Dictionary> ServerAvailablePairTags = new(StringComparer.Ordinal); - public HashSet OpenPairTags = new(StringComparer.Ordinal); + public Dictionary> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); + public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); public MareConfigV0 ToMareConfig(ILogger logger) { @@ -75,7 +75,7 @@ public class Configuration : IPluginConfiguration // create all server storage based on current clientsecret foreach (var secret in ClientSecret) { - logger.LogDebug("Migrating " + secret.Key); + logger.LogDebug("Migrating {key}", secret.Key); var apiuri = secret.Key; var secretkey = secret.Value; ServerStorageV0 toAdd = new(); @@ -121,9 +121,4 @@ public class Configuration : IPluginConfiguration return newConfig; } - - public void Migrate() - { - - } } diff --git a/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs b/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs index bbb07c0..9fcb8c5 100644 --- a/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs +++ b/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs @@ -1,4 +1,5 @@ using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.MareConfiguration.Models.Obsolete; namespace MareSynchronos.MareConfiguration.Configurations.Obsolete; diff --git a/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs index f0b1045..508301f 100644 --- a/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs @@ -5,5 +5,5 @@ namespace MareSynchronos.MareConfiguration.Configurations; public class UidNotesConfig : IMareConfiguration { public int Version { get; set; } = 0; - public Dictionary ServerNotes = new(StringComparer.Ordinal); + public Dictionary ServerNotes { get; set; } = new(StringComparer.Ordinal); } diff --git a/MareSynchronos/MareConfiguration/MareConfigService.cs b/MareSynchronos/MareConfiguration/MareConfigService.cs index 526509d..7e60baf 100644 --- a/MareSynchronos/MareConfiguration/MareConfigService.cs +++ b/MareSynchronos/MareConfiguration/MareConfigService.cs @@ -1,13 +1,14 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; +using MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration; public class MareConfigService : ConfigurationServiceBase { public const string ConfigName = "config.json"; - protected override string ConfigurationName => ConfigName; - public MareConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { } -} + public MareConfigService(string configDir) : base(configDir) + { + } + + protected override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs index a3a0d44..7864d33 100644 --- a/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs +++ b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs @@ -1,4 +1,4 @@ -namespace MareSynchronos.MareConfiguration.Models; +namespace MareSynchronos.MareConfiguration.Models.Obsolete; [Serializable] [Obsolete("Deprecated, use ServerStorage")] @@ -9,7 +9,7 @@ public class ServerStorageV0 public List Authentications { get; set; } = new(); public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); public Dictionary GidServerComments { get; set; } = new(StringComparer.Ordinal); - public Dictionary> UidServerPairedUserTags = new(StringComparer.Ordinal); + public Dictionary> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); public HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); public Dictionary SecretKeys { get; set; } = new(); @@ -19,11 +19,11 @@ public class ServerStorageV0 { return new ServerStorage() { - ServerUri = this.ServerUri, - ServerName = this.ServerName, - Authentications = this.Authentications.ToList(), - FullPause = this.FullPause, - SecretKeys = this.SecretKeys.ToDictionary(p => p.Key, p => p.Value) + ServerUri = ServerUri, + ServerName = ServerName, + Authentications = Authentications.ToList(), + FullPause = FullPause, + SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value) }; } } diff --git a/MareSynchronos/MareConfiguration/Models/UidNoteStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs similarity index 100% rename from MareSynchronos/MareConfiguration/Models/UidNoteStorage.cs rename to MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs diff --git a/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs index 261caec..96e985e 100644 --- a/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs +++ b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs @@ -3,7 +3,7 @@ [Serializable] public class ServerTagStorage { - public Dictionary> UidServerPairedUserTags = new(StringComparer.Ordinal); + public Dictionary> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); public HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); } diff --git a/MareSynchronos/MareConfiguration/NotesConfigService.cs b/MareSynchronos/MareConfiguration/NotesConfigService.cs index bc5823e..ed6cab8 100644 --- a/MareSynchronos/MareConfiguration/NotesConfigService.cs +++ b/MareSynchronos/MareConfiguration/NotesConfigService.cs @@ -1,12 +1,14 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; +using MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration; public class NotesConfigService : ConfigurationServiceBase { public const string ConfigName = "notes.json"; + + public NotesConfigService(string configDir) : base(configDir) + { + } + protected override string ConfigurationName => ConfigName; - public NotesConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { } } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerConfigService.cs b/MareSynchronos/MareConfiguration/ServerConfigService.cs index 0475c8f..c45120e 100644 --- a/MareSynchronos/MareConfiguration/ServerConfigService.cs +++ b/MareSynchronos/MareConfiguration/ServerConfigService.cs @@ -1,12 +1,14 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; +using MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration; public class ServerConfigService : ConfigurationServiceBase { public const string ConfigName = "server.json"; + + public ServerConfigService(string configDir) : base(configDir) + { + } + protected override string ConfigurationName => ConfigName; - public ServerConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { } -} +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerTagConfigService.cs b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs index 7527e66..2f868b3 100644 --- a/MareSynchronos/MareConfiguration/ServerTagConfigService.cs +++ b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs @@ -1,12 +1,14 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; +using MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration; public class ServerTagConfigService : ConfigurationServiceBase { public const string ConfigName = "servertags.json"; + + public ServerTagConfigService(string configDir) : base(configDir) + { + } + protected override string ConfigurationName => ConfigName; - public ServerTagConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { } } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/TransientConfigService.cs b/MareSynchronos/MareConfiguration/TransientConfigService.cs index ba41f87..d16da74 100644 --- a/MareSynchronos/MareConfiguration/TransientConfigService.cs +++ b/MareSynchronos/MareConfiguration/TransientConfigService.cs @@ -1,12 +1,14 @@ -using Dalamud.Plugin; -using MareSynchronos.MareConfiguration.Configurations; -using Microsoft.Extensions.Logging; +using MareSynchronos.MareConfiguration.Configurations; namespace MareSynchronos.MareConfiguration; public class TransientConfigService : ConfigurationServiceBase { public const string ConfigName = "transient.json"; + + public TransientConfigService(string configDir) : base(configDir) + { + } + protected override string ConfigurationName => ConfigName; - public TransientConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { } -} +} \ No newline at end of file diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs index 594dcf6..fee157e 100644 --- a/MareSynchronos/MarePlugin.cs +++ b/MareSynchronos/MarePlugin.cs @@ -1,28 +1,27 @@ -using Dalamud.Game.Command; -using Dalamud.Plugin; -using Dalamud.Interface.ImGuiFileDialog; -using MareSynchronos.Managers; -using MareSynchronos.WebAPI; -using Dalamud.Interface.Windowing; -using MareSynchronos.UI; -using MareSynchronos.Utils; -using MareSynchronos.FileCache; +using MareSynchronos.FileCache; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.PlayerData.Services; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Reflection; namespace MareSynchronos; +#pragma warning disable S125 // Sections of code should not be commented out /* - (..,,...,,,,,+/, ,,.....,,+ - ..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+. - ...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./ - ,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,. - ,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,.., - +..////////////+++++++... .../##(,,////////////////++,,,+/(((+, - +,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,, - /+.+//////////++++/++++++++++,,...,++///////////////////////////((((##, + (..,,...,,,,,+/, ,,.....,,+ + ..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+. + ...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./ + ,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,. + ,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,.., + +..////////////+++++++... .../##(,,////////////////++,,,+/(((+, + +,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,, + /+.+//////////++++/++++++++++,,...,++///////////////////////////((((##, /,.////////+++++++++++++++++++++////////+++//////++/+++++//////////((((#(+, /+.+////////+++++++++++++++++++++++++++++++++++++++++++++++++++++/////((((##+ +,.///////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///((((%/ @@ -30,202 +29,137 @@ namespace MareSynchronos; +,./////////////////+++++++++++++++++++++++++++++++++++++++++++++++,,+++++///((, ...////////++/++++++++++++++++++++++++,,++++++++++++++++++++++++++++++++++++//(,, ..//+,+///++++++++++++++++++,,,,+++,,,,,,,,,,,,++++++++,,+++++++++++++++++++//,,+ - ..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++... - ..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,. - ..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+ - ....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,, - .....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/ - .....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+ - ...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,, - ....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,.. - ...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++...... - ...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,.. - ...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,.. - ..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,.. - ...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,... - ..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,... - ..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,... + ..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++... + ..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,. + ..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+ + ....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,, + .....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/ + .....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+ + ...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,, + ....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,.. + ...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++...... + ...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,.. + ...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,.. + ..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,.. + ...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,... + ..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,... + ..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,... ...+//(((((//////////++++++..+++++++++++++++,......,,,,++++++,,,..+++////////////////////////+,.... ...,,,,,,,... - ..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,.. - ...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,... - ..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,... - .,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........ - .+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++.. - .,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++.. - .++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++... - .++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ . - .++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,.. - .++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++.. - .+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,.. - ..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,... - ..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,... - ...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,.. - ...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,.. - ...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,... - ,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,.. + ..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,.. + ...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,... + ..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,... + .,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........ + .+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++.. + .,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++.. + .++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++... + .++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ . + .++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,.. + .++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++.. + .+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,.. + ..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,... + ..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,... + ...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,.. + ...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,.. + ...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,... + ,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,.. */ +#pragma warning restore S125 // Sections of code should not be commented out -public class MarePlugin : MediatorSubscriberBase, IDisposable +public class MarePlugin : MediatorSubscriberBase, IHostedService { - private readonly ServiceProvider _serviceProvider; - private const string _commandName = "/mare"; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareConfigService _mareConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly IServiceScopeFactory _serviceScopeFactory; private IServiceScope? _runtimeServiceScope; - public MarePlugin(ILogger logger, ServiceProvider serviceProvider, MareMediator mediator) : base(logger, mediator) + public MarePlugin(ILogger logger, MareConfigService mareConfigService, + ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtil, + IServiceScopeFactory serviceScopeFactory, MareMediator mediator) : base(logger, mediator) { - _serviceProvider = serviceProvider; - - _serviceProvider.GetRequiredService().Migrate(); - - mediator.Subscribe(this, (_) => Task.Run(WaitForPlayerAndLaunchCharacterManager)); - mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); - mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); - - serviceProvider.GetRequiredService(); - serviceProvider.GetRequiredService(); - serviceProvider.GetRequiredService(); - serviceProvider.GetRequiredService(); - serviceProvider.GetRequiredService(); - serviceProvider.GetRequiredService(); + _mareConfigService = mareConfigService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtil = dalamudUtil; + _serviceScopeFactory = serviceScopeFactory; } - public override void Dispose() + public Task StartAsync(CancellationToken cancellationToken) { - base.Dispose(); + var version = Assembly.GetExecutingAssembly().GetName().Version!; + Logger.LogInformation("Launching {name} {major}.{minor}.{build}", "Mare Synchronos", version.Major, version.Minor, version.Build); - _serviceProvider.GetRequiredService().RemoveHandler(_commandName); + Mediator.Subscribe(this, (_) => Task.Run(WaitForPlayerAndLaunchCharacterManager)); + Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); + Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); - _runtimeServiceScope?.Dispose(); - _serviceProvider.Dispose(); + return Task.CompletedTask; + } - _logger.LogDebug("Shut down"); + public Task StopAsync(CancellationToken cancellationToken) + { + UnsubscribeAll(); + + DalamudUtilOnLogOut(); + + Logger.LogDebug("Halting MarePlugin"); + + return Task.CompletedTask; } private void DalamudUtilOnLogIn() { - _logger?.LogDebug("Client login"); + Logger?.LogDebug("Client login"); - var pi = _serviceProvider.GetRequiredService(); - pi.UiBuilder.Draw += Draw; - pi.UiBuilder.OpenConfigUi += OpenUi; - _serviceProvider.GetRequiredService().AddHandler(_commandName, new CommandInfo(OnCommand) - { - HelpMessage = "Opens the Mare Synchronos UI", - }); - - if (!_serviceProvider.GetRequiredService().Current.HasValidSetup() - || !_serviceProvider.GetRequiredService().HasValidConfig()) - { - _serviceProvider.GetRequiredService().Publish(new SwitchToIntroUiMessage()); - return; - } - - _serviceProvider.GetRequiredService().StartScan(); Task.Run(WaitForPlayerAndLaunchCharacterManager); } private void DalamudUtilOnLogOut() { - _logger?.LogDebug("Client logout"); + Logger?.LogDebug("Client logout"); _runtimeServiceScope?.Dispose(); - var pi = _serviceProvider.GetRequiredService(); - pi.UiBuilder.Draw -= Draw; - pi.UiBuilder.OpenConfigUi -= OpenUi; - _serviceProvider.GetRequiredService().RemoveHandler(_commandName); } private async Task WaitForPlayerAndLaunchCharacterManager() { - var dalamudUtil = _serviceProvider.GetRequiredService(); - while (!dalamudUtil.IsPlayerPresent) + while (!_dalamudUtil.IsPlayerPresent) { await Task.Delay(100).ConfigureAwait(false); } try { - _logger?.LogDebug("Launching Managers"); + Logger?.LogDebug("Launching Managers"); _runtimeServiceScope?.Dispose(); - _runtimeServiceScope = _serviceProvider.CreateScope(); + _runtimeServiceScope = _serviceScopeFactory.CreateScope(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + if (!_mareConfigService.Current.HasValidSetup() || !_serverConfigurationManager.HasValidConfig()) + { + Mediator.Publish(new SwitchToIntroUiMessage()); + return; + } + _runtimeServiceScope.ServiceProvider.GetRequiredService().StartScan(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + +#if !DEBUG + if (_mareConfigService.Current.LogLevel != LogLevel.Information) + { + Mediator.Publish(new NotificationMessage("Abnormal Log Level", + $"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Mare Settings -> Debug\" unless instructed otherwise.", + Dalamud.Interface.Internal.Notifications.NotificationType.Error, 15000)); + } +#endif } catch (Exception ex) { - _logger?.LogCritical(ex, "Error during launch of managers"); + Logger?.LogCritical(ex, "Error during launch of managers"); } } - - private void Draw() - { - _serviceProvider.GetRequiredService().Draw(); - _serviceProvider.GetRequiredService().Draw(); - } - - private void OnCommand(string command, string args) - { - var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); - - if (splitArgs == null || splitArgs.Length == 0) - { - // Interpret this as toggling the UI - OpenUi(); - return; - } - - if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase)) - { - var serverConfigurationManager = _serviceProvider.GetRequiredService(); - if (serverConfigurationManager.CurrentServer == null) return; - var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch - { - "on" => false, - "off" => true, - _ => !serverConfigurationManager.CurrentServer.FullPause, - } : !serverConfigurationManager.CurrentServer.FullPause; - - if (fullPause != serverConfigurationManager.CurrentServer.FullPause) - { - serverConfigurationManager.CurrentServer.FullPause = fullPause; - serverConfigurationManager.Save(); - _ = _serviceProvider.GetRequiredService().CreateConnections(); - } - } - else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase)) - { - _serviceProvider.GetRequiredService().Toggle(); - } - else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase)) - { - _serviceProvider.GetRequiredService().InvokeScan(forced: true); - } - else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase)) - { - if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds)) - { - _serviceProvider.GetRequiredService().PrintPerformanceStats(limitBySeconds); - } - else - { - _serviceProvider.GetRequiredService().PrintPerformanceStats(); - } - } - else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase)) - { - _serviceProvider.GetRequiredService().PrintSubscriberInfo(); - } - } - - private void OpenUi() - { - - if (_serviceProvider.GetRequiredService().Current.HasValidSetup()) - _serviceProvider.GetRequiredService().Toggle(); - else - _serviceProvider.GetRequiredService().Toggle(); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 3358d74..92e01ae 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.7.42 + 0.8.0 https://github.com/Penumbra-Sync/client @@ -26,7 +26,7 @@ - + all @@ -34,8 +34,13 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MareSynchronos/MareSynchronos.json b/MareSynchronos/MareSynchronos.json index 9875750..317ce25 100644 --- a/MareSynchronos/MareSynchronos.json +++ b/MareSynchronos/MareSynchronos.json @@ -10,4 +10,4 @@ ], "IconUrl": "https://raw.githubusercontent.com/Penumbra-Sync/client/main/MareSynchronos/images/logo.png", "RepoUrl": "https://github.com/Penumbra-Sync/client" -} +} \ No newline at end of file diff --git a/MareSynchronos/Mediator/IMediatorSubscriber.cs b/MareSynchronos/Mediator/IMediatorSubscriber.cs deleted file mode 100644 index f4d46f2..0000000 --- a/MareSynchronos/Mediator/IMediatorSubscriber.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MareSynchronos.Mediator; - -public interface IMediatorSubscriber : IDisposable -{ - MareMediator Mediator { get; } -} diff --git a/MareSynchronos/Mediator/IMessage.cs b/MareSynchronos/Mediator/IMessage.cs deleted file mode 100644 index 68e4d6c..0000000 --- a/MareSynchronos/Mediator/IMessage.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MareSynchronos.Mediator; - -public interface IMessage { } \ No newline at end of file diff --git a/MareSynchronos/Mediator/WindowMediatorSubscriberBase.cs b/MareSynchronos/Mediator/WindowMediatorSubscriberBase.cs deleted file mode 100644 index 946a6eb..0000000 --- a/MareSynchronos/Mediator/WindowMediatorSubscriberBase.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Dalamud.Interface.Windowing; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.Mediator; - -public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber -{ - protected readonly ILogger _logger; - - public MareMediator Mediator { get; } - protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name) : base(name) - { - _logger = logger; - Mediator = mediator; - } - - public virtual void Dispose() - { - _logger.LogTrace($"Disposing {GetType()}"); - Mediator.UnsubscribeAll(this); - } -} diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/PlayerData/Data/CharacterData.cs similarity index 95% rename from MareSynchronos/Models/CharacterData.cs rename to MareSynchronos/PlayerData/Data/CharacterData.cs index 5eb43df..abef2be 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/PlayerData/Data/CharacterData.cs @@ -2,21 +2,19 @@ using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data; -namespace MareSynchronos.Models; +namespace MareSynchronos.PlayerData.Data; public class CharacterData { + public string CustomizePlusScale { get; set; } = string.Empty; public Dictionary> FileReplacements { get; set; } = new(); public Dictionary GlamourerString { get; set; } = new(); - public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); - public string ManipulationString { get; set; } = string.Empty; - public float HeelsOffset { get; set; } = 0f; + public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); - public string CustomizePlusScale { get; set; } = string.Empty; - + public string ManipulationString { get; set; } = string.Empty; public string PalettePlusPalette { get; set; } = string.Empty; public API.Data.CharacterData ToAPI() @@ -52,8 +50,8 @@ public class CharacterData StringBuilder stringBuilder = new(); foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths.First(), StringComparer.Ordinal)) { - stringBuilder.AppendLine(fileReplacement.ToString()); + stringBuilder.Append(fileReplacement).AppendLine(); } return stringBuilder.ToString(); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/PlayerData/Data/FileReplacement.cs similarity index 60% rename from MareSynchronos/Models/FileReplacement.cs rename to MareSynchronos/PlayerData/Data/FileReplacement.cs index e9be801..a8d5a6a 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/PlayerData/Data/FileReplacement.cs @@ -2,30 +2,28 @@ using MareSynchronos.FileCache; using MareSynchronos.API.Data; -namespace MareSynchronos.Models; +namespace MareSynchronos.PlayerData.Data; -public class FileReplacement +public partial class FileReplacement { + private readonly Lazy _hashLazy; + public FileReplacement(List gamePaths, string filePath, FileCacheManager fileDbManager) { GamePaths = gamePaths.Select(g => g.Replace('\\', '/')).ToHashSet(StringComparer.Ordinal); ResolvedPath = filePath.Replace('\\', '/'); - HashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty); + _hashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty); } public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); - public HashSet GamePaths { get; init; } = new(); + public HashSet GamePaths { get; init; } public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal)); - public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths.First(), ResolvedPath, StringComparison.Ordinal); - - public string Hash => HashLazy.Value; - - private Lazy HashLazy; - - public string ResolvedPath { get; init; } = string.Empty; + public string Hash => _hashLazy.Value; + public bool IsFileSwap => !LocalPathRegex().IsMatch(ResolvedPath) && !string.Equals(GamePaths.First(), ResolvedPath, StringComparison.Ordinal); + public string ResolvedPath { get; init; } public FileReplacementData ToFileReplacementDto() { @@ -41,4 +39,7 @@ public class FileReplacement { return $"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"; } -} + + [GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)] + private static partial Regex LocalPathRegex(); +} \ No newline at end of file diff --git a/MareSynchronos/Utils/FileReplacementComparer.cs b/MareSynchronos/PlayerData/Data/FileReplacementComparer.cs similarity index 82% rename from MareSynchronos/Utils/FileReplacementComparer.cs rename to MareSynchronos/PlayerData/Data/FileReplacementComparer.cs index e0a614d..6d16553 100644 --- a/MareSynchronos/Utils/FileReplacementComparer.cs +++ b/MareSynchronos/PlayerData/Data/FileReplacementComparer.cs @@ -1,12 +1,14 @@ -using MareSynchronos.Models; - -namespace MareSynchronos.Utils; +namespace MareSynchronos.PlayerData.Data; public class FileReplacementComparer : IEqualityComparer { + private static readonly FileReplacementComparer _instance = new(); + + private FileReplacementComparer() + { } + public static FileReplacementComparer Instance => _instance; - private static FileReplacementComparer _instance = new(); - private FileReplacementComparer() { } + public bool Equals(FileReplacement? x, FileReplacement? y) { if (x == null || y == null) return false; @@ -18,18 +20,7 @@ public class FileReplacementComparer : IEqualityComparer return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths)); } - private static int GetOrderIndependentHashCode(IEnumerable source) - { - int hash = 0; - foreach (T element in source) - { - hash = unchecked(hash + - EqualityComparer.Default.GetHashCode(element)); - } - return hash; - } - - private bool CompareLists(HashSet list1, HashSet list2) + private static bool CompareLists(HashSet list1, HashSet list2) { if (list1.Count != list2.Count) return false; @@ -42,4 +33,15 @@ public class FileReplacementComparer : IEqualityComparer return true; } + + private static int GetOrderIndependentHashCode(IEnumerable source) + { + int hash = 0; + foreach (T element in source) + { + hash = unchecked(hash + + EqualityComparer.Default.GetHashCode(element)); + } + return hash; + } } \ No newline at end of file diff --git a/MareSynchronos/Utils/FileReplacementDataComparer.cs b/MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs similarity index 70% rename from MareSynchronos/Utils/FileReplacementDataComparer.cs rename to MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs index 500886a..8bf04e2 100644 --- a/MareSynchronos/Utils/FileReplacementDataComparer.cs +++ b/MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs @@ -1,16 +1,20 @@ using MareSynchronos.API.Data; -namespace MareSynchronos.Utils; +namespace MareSynchronos.PlayerData.Data; public class FileReplacementDataComparer : IEqualityComparer { + private static readonly FileReplacementDataComparer _instance = new(); + + private FileReplacementDataComparer() + { } + public static FileReplacementDataComparer Instance => _instance; - private static FileReplacementDataComparer _instance = new(); - private FileReplacementDataComparer() { } + public bool Equals(FileReplacementData? x, FileReplacementData? y) { if (x == null || y == null) return false; - return x.Hash.Equals(y.Hash) && CompareLists(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal); + return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal); } public int GetHashCode(FileReplacementData obj) @@ -18,18 +22,7 @@ public class FileReplacementDataComparer : IEqualityComparer(IEnumerable source) - { - int hash = 0; - foreach (T element in source) - { - hash = unchecked(hash + - EqualityComparer.Default.GetHashCode(element)); - } - return hash; - } - - private bool CompareLists(HashSet list1, HashSet list2) + private static bool CompareHashSets(HashSet list1, HashSet list2) { if (list1.Count != list2.Count) return false; @@ -42,4 +35,15 @@ public class FileReplacementDataComparer : IEqualityComparer(IEnumerable source) + { + int hash = 0; + foreach (T element in source) + { + hash = unchecked(hash + + EqualityComparer.Default.GetHashCode(element)); + } + return hash; + } } \ No newline at end of file diff --git a/MareSynchronos/Export/MareCharaFileData.cs b/MareSynchronos/PlayerData/Export/MareCharaFileData.cs similarity index 89% rename from MareSynchronos/Export/MareCharaFileData.cs rename to MareSynchronos/PlayerData/Export/MareCharaFileData.cs index 2a3645a..a139794 100644 --- a/MareSynchronos/Export/MareCharaFileData.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileData.cs @@ -1,10 +1,10 @@ using MareSynchronos.API.Data; using MareSynchronos.API.Data.Enum; using MareSynchronos.FileCache; -using Newtonsoft.Json; using System.Text; +using System.Text.Json; -namespace MareSynchronos.Export; +namespace MareSynchronos.PlayerData.Export; public record MareCharaFileData { @@ -52,15 +52,15 @@ public record MareCharaFileData public byte[] ToByteArray() { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this)); + return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(this)); } public static MareCharaFileData FromByteArray(byte[] data) { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data))!; + return JsonSerializer.Deserialize(Encoding.UTF8.GetString(data))!; } public record FileSwap(IEnumerable GamePaths, string FileSwapPath); public record FileData(IEnumerable GamePaths, long Length); -} +} \ No newline at end of file diff --git a/MareSynchronos/Export/MareCharaFileDataFactory.cs b/MareSynchronos/PlayerData/Export/MareCharaFileDataFactory.cs similarity index 82% rename from MareSynchronos/Export/MareCharaFileDataFactory.cs rename to MareSynchronos/PlayerData/Export/MareCharaFileDataFactory.cs index ef96d26..7e011cf 100644 --- a/MareSynchronos/Export/MareCharaFileDataFactory.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileDataFactory.cs @@ -1,9 +1,9 @@ using MareSynchronos.API.Data; using MareSynchronos.FileCache; -namespace MareSynchronos.Export; +namespace MareSynchronos.PlayerData.Export; -internal class MareCharaFileDataFactory +internal sealed class MareCharaFileDataFactory { private readonly FileCacheManager _fileCacheManager; @@ -16,4 +16,4 @@ internal class MareCharaFileDataFactory { return new MareCharaFileData(_fileCacheManager, description, characterCacheDto); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Export/MareCharaFileHeader.cs b/MareSynchronos/PlayerData/Export/MareCharaFileHeader.cs similarity index 89% rename from MareSynchronos/Export/MareCharaFileHeader.cs rename to MareSynchronos/PlayerData/Export/MareCharaFileHeader.cs index f230307..e49f9cc 100644 --- a/MareSynchronos/Export/MareCharaFileHeader.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileHeader.cs @@ -1,4 +1,4 @@ -namespace MareSynchronos.Export; +namespace MareSynchronos.PlayerData.Export; public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) { @@ -23,7 +23,7 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) public static MareCharaFileHeader? FromBinaryReader(string path, BinaryReader reader) { var chars = new string(reader.ReadChars(4)); - if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new System.Exception("Not a Mare Chara File"); + if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new InvalidDataException("Not a Mare Chara File"); MareCharaFileHeader? decoded = null; @@ -41,7 +41,7 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) return decoded; } - public void AdvanceReaderToData(BinaryReader reader) + public static void AdvanceReaderToData(BinaryReader reader) { reader.ReadChars(4); var version = reader.ReadByte(); @@ -51,4 +51,4 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) _ = reader.ReadBytes(length); } } -} +} \ No newline at end of file diff --git a/MareSynchronos/Export/MareCharaFileManager.cs b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs similarity index 83% rename from MareSynchronos/Export/MareCharaFileManager.cs rename to MareSynchronos/PlayerData/Export/MareCharaFileManager.cs index bea1bc9..7354f97 100644 --- a/MareSynchronos/Export/MareCharaFileManager.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs @@ -1,37 +1,32 @@ using Dalamud.Game.ClientState.Objects.Types; using LZ4; using MareSynchronos.FileCache; -using MareSynchronos.Managers; -using MareSynchronos.Utils; using MareSynchronos.API.Data.Enum; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Models; using CharacterData = MareSynchronos.API.Data.CharacterData; using Microsoft.Extensions.Logging; -using MareSynchronos.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Interop; +using MareSynchronos.Services; + +namespace MareSynchronos.PlayerData.Export; -namespace MareSynchronos.Export; public class MareCharaFileManager { - private readonly ILogger _logger; - private readonly MareMediator _mediator; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly FileCacheManager _manager; - private readonly IpcManager _ipcManager; private readonly MareConfigService _configService; - private readonly DalamudUtil _dalamudUtil; + private readonly DalamudUtilService _dalamudUtil; private readonly MareCharaFileDataFactory _factory; - public MareCharaFileHeader? LoadedCharaFile { get; private set; } - public bool CurrentlyWorking { get; private set; } = false; - private static int GlobalFileCounter = 0; + private readonly Func, bool, GameObjectHandler> _gameObjectHandlerFactory; + private readonly IpcManager _ipcManager; + private readonly ILogger _logger; + private readonly FileCacheManager _manager; + private int _globalFileCounter = 0; - public MareCharaFileManager(ILogger logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, - FileCacheManager manager, IpcManager ipcManager, MareConfigService configService, DalamudUtil dalamudUtil) + public MareCharaFileManager(ILogger logger, Func, bool, GameObjectHandler> gameObjectHandlerFactory, + FileCacheManager manager, IpcManager ipcManager, MareConfigService configService, DalamudUtilService dalamudUtil) { _factory = new(manager); _logger = logger; - _mediator = mediator; _gameObjectHandlerFactory = gameObjectHandlerFactory; _manager = manager; _ipcManager = ipcManager; @@ -39,6 +34,58 @@ public class MareCharaFileManager _dalamudUtil = dalamudUtil; } + public bool CurrentlyWorking { get; private set; } = false; + public MareCharaFileHeader? LoadedCharaFile { get; private set; } + + public async Task ApplyMareCharaFile(GameObject? charaTarget) + { + Dictionary extractedFiles = new(StringComparer.Ordinal); + CurrentlyWorking = true; + try + { + if (LoadedCharaFile == null || charaTarget == null || !File.Exists(LoadedCharaFile.FilePath)) return; + var unwrapped = File.OpenRead(LoadedCharaFile.FilePath); + await using (unwrapped.ConfigureAwait(false)) + { + CancellationTokenSource disposeCts = new(); + using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); + using var reader = new BinaryReader(lz4Stream); + MareCharaFileHeader.AdvanceReaderToData(reader); + _logger.LogDebug("Applying to {chara}", charaTarget.Name.TextValue); + extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader); + Dictionary fileSwaps = new(StringComparer.Ordinal); + foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps) + { + foreach (var path in fileSwap.GamePaths) + { + fileSwaps.Add(path, fileSwap.FileSwapPath); + } + } + var applicationId = Guid.NewGuid(); + _ipcManager.ToggleGposeQueueMode(on: true); + _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue); + _ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, charaTarget.Name.TextValue, + extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal), + LoadedCharaFile.CharaFileData.ManipulationData); + using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => charaTarget.Address, false); + await _ipcManager.GlamourerApplyAll(_logger, tempHandler, LoadedCharaFile.CharaFileData.GlamourerData, applicationId, disposeCts.Token).ConfigureAwait(false); + _dalamudUtil.WaitWhileGposeCharacterIsDrawing(charaTarget.Address, 30000); + _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue); + _ipcManager.ToggleGposeQueueMode(on: false); + } + } + finally + { + CurrentlyWorking = false; + + _logger.LogDebug("Clearing local files"); + foreach (var file in extractedFiles) + { + File.Delete(file.Value); + } + } + } + public void ClearMareCharaFile() { LoadedCharaFile = null; @@ -69,7 +116,7 @@ public class MareCharaFileManager wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); }*/ _logger.LogInformation("Read Mare Chara File"); - _logger.LogInformation("Version: " + (LoadedCharaFile?.Version ?? -1)); + _logger.LogInformation("Version: {ver}", (LoadedCharaFile?.Version ?? -1)); long expectedLength = 0; if (LoadedCharaFile != null) { @@ -78,7 +125,7 @@ public class MareCharaFileManager { foreach (var gamePath in item.GamePaths) { - _logger.LogTrace("Swap: " + gamePath + " => " + item.FileSwapPath); + _logger.LogTrace("Swap: {gamePath} => {fileSwapPath}", gamePath, item.FileSwapPath); } } @@ -89,99 +136,16 @@ public class MareCharaFileManager expectedLength += item.Length; foreach (var gamePath in item.GamePaths) { - _logger.LogTrace($"File {itemNr}: " + gamePath + " = " + item.Length); + _logger.LogTrace("File {itemNr}: {gamePath} = {len}", itemNr, gamePath, item.Length); } } - _logger.LogInformation("Expected length: " + expectedLength); + _logger.LogInformation("Expected length: {expected}", expectedLength); } - } - catch { throw; } finally { CurrentlyWorking = false; } } - public async Task ApplyMareCharaFile(GameObject? charaTarget) - { - Dictionary extractedFiles = new(StringComparer.Ordinal); - CurrentlyWorking = true; - try - { - if (LoadedCharaFile == null || charaTarget == null || !File.Exists(LoadedCharaFile.FilePath)) return; - var unwrapped = File.OpenRead(LoadedCharaFile.FilePath); - await using (unwrapped.ConfigureAwait(false)) - { - CancellationTokenSource disposeCts = new(); - using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); - using var reader = new BinaryReader(lz4Stream); - LoadedCharaFile.AdvanceReaderToData(reader); - _logger.LogDebug("Applying to " + charaTarget.Name.TextValue); - extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader); - Dictionary fileSwaps = new(StringComparer.Ordinal); - foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps) - { - foreach (var path in fileSwap.GamePaths) - { - fileSwaps.Add(path, fileSwap.FileSwapPath); - } - } - var applicationId = Guid.NewGuid(); - _ipcManager.ToggleGposeQueueMode(on: true); - _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue); - _ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, charaTarget.Name.TextValue, - extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal), - LoadedCharaFile.CharaFileData.ManipulationData); - using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => charaTarget.Address, isWatched: false); - await _ipcManager.GlamourerApplyAll(_logger, tempHandler, LoadedCharaFile.CharaFileData.GlamourerData, applicationId, disposeCts.Token).ConfigureAwait(false); - _dalamudUtil.WaitWhileGposeCharacterIsDrawing(charaTarget.Address, 30000); - _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue); - _ipcManager.ToggleGposeQueueMode(on: false); - } - } - catch { throw; } - finally - { - CurrentlyWorking = false; - - _logger.LogDebug("Clearing local files"); - foreach (var file in extractedFiles) - { - File.Delete(file.Value); - } - } - } - - private Dictionary ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader) - { - Dictionary gamePathToFilePath = new(StringComparer.Ordinal); - foreach (var fileData in charaFileHeader.CharaFileData.Files) - { - var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + (GlobalFileCounter++) + ".tmp"); - var length = fileData.Length; - var bufferSize = 4 * 1024 * 1024; - var buffer = new byte[bufferSize]; - using var fs = File.OpenWrite(fileName); - using var wr = new BinaryWriter(fs); - int chunk = 0; - while (length > 0) - { - if (length < bufferSize) bufferSize = (int)length; - _logger.LogTrace($"Reading chunk {chunk++} {bufferSize}/{length} of {fileName}"); - buffer = reader.ReadBytes(bufferSize); - wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); - length -= bufferSize; - } - wr.Flush(); - foreach (var path in fileData.GamePaths) - { - gamePathToFilePath[path] = fileName; - _logger.LogTrace(path + " => " + fileName); - } - } - - return gamePathToFilePath; - } - public void SaveMareCharaFile(CharacterData? dto, string description, string filePath) { CurrentlyWorking = true; @@ -204,7 +168,6 @@ public class MareCharaFileManager { var itemFromData = playerReplacements.First(f => f.GamePaths.Any(p => item.GamePaths.Contains(p, StringComparer.OrdinalIgnoreCase))); var file = _manager.GetFileCacheByHash(itemFromData.Hash)!; - var length = new FileInfo(file!.ResolvedFilepath).Length; using var fsRead = File.OpenRead(file.ResolvedFilepath); using var br = new BinaryReader(fsRead); int readBytes = 0; @@ -214,7 +177,36 @@ public class MareCharaFileManager } } } - catch { throw; } finally { CurrentlyWorking = false; } } -} + + private Dictionary ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader) + { + Dictionary gamePathToFilePath = new(StringComparer.Ordinal); + foreach (var fileData in charaFileHeader.CharaFileData.Files) + { + var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp"); + var length = fileData.Length; + var bufferSize = 4 * 1024 * 1024; + using var fs = File.OpenWrite(fileName); + using var wr = new BinaryWriter(fs); + int chunk = 0; + while (length > 0) + { + if (length < bufferSize) bufferSize = (int)length; + _logger.LogTrace("Reading chunk {chunk} {bufferSize}/{length} of {fileName}", chunk++, bufferSize, length, fileName); + var buffer = reader.ReadBytes(bufferSize); + wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); + length -= bufferSize; + } + wr.Flush(); + foreach (var path in fileData.GamePaths) + { + gamePathToFilePath[path] = fileName; + _logger.LogTrace("{path} => {fileName}", path, fileName); + } + } + + return gamePathToFilePath; + } +} \ No newline at end of file diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs similarity index 91% rename from MareSynchronos/Factories/CharacterDataFactory.cs rename to MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 2fb557f..b0a7019 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -4,43 +4,40 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.API.Data.Enum; using MareSynchronos.Interop; -using MareSynchronos.Managers; -using MareSynchronos.Models; -using MareSynchronos.Utils; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Penumbra.String; -using Weapon = MareSynchronos.Interop.Weapon; +using Weapon = MareSynchronos.Interop.FFXIV.Weapon; using MareSynchronos.FileCache; -using MareSynchronos.Mediator; -using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using System.Globalization; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Interop.FFXIV; +using MareSynchronos.Services; -namespace MareSynchronos.Factories; +namespace MareSynchronos.PlayerData.Factories; -public class CharacterDataFactory : MediatorSubscriberBase +public class PlayerDataFactory { - private readonly DalamudUtil _dalamudUtil; - private readonly IpcManager _ipcManager; - private readonly TransientResourceManager _transientResourceManager; + private readonly DalamudUtilService _dalamudUtil; private readonly FileCacheManager _fileCacheManager; - private readonly PerformanceCollector _performanceCollector; + private readonly IpcManager _ipcManager; + private readonly ILogger _logger; + private readonly PerformanceCollectorService _performanceCollector; + private readonly TransientResourceManager _transientResourceManager; - public CharacterDataFactory(ILogger logger, DalamudUtil dalamudUtil, IpcManager ipcManager, - TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, MareMediator mediator, - PerformanceCollector performanceCollector) : base(logger, mediator) + public PlayerDataFactory(ILogger logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, + TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, + PerformanceCollectorService performanceCollector) { - _logger.LogTrace("Creating " + nameof(CharacterDataFactory)); + _logger = logger; _dalamudUtil = dalamudUtil; _ipcManager = ipcManager; _transientResourceManager = transientResourceManager; _fileCacheManager = fileReplacementFactory; _performanceCollector = performanceCollector; - } - private unsafe bool CheckForNullDrawObject(IntPtr playerPointer) - { - return ((Character*)playerPointer)->GameObject.DrawObject == null; + _logger.LogTrace("Creating " + nameof(PlayerDataFactory)); } public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) @@ -102,131 +99,14 @@ public class CharacterDataFactory : MediatorSubscriberBase previousData.FileReplacements = previousFileReplacements; previousData.GlamourerString = previousGlamourerData; - return; } - private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + private static unsafe bool CheckForNullDrawObject(IntPtr playerPointer) { - var objectKind = playerRelatedObject.ObjectKind; - var charaPointer = playerRelatedObject.Address; - - _logger.LogDebug("Building character data for {obj}", playerRelatedObject); - - if (!previousData.FileReplacements.ContainsKey(objectKind)) - { - previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); - } - else - { - previousData.FileReplacements[objectKind].Clear(); - } - - // wait until chara is not drawing and present so nothing spontaneously explodes - await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false); - int totalWaitTime = 10000; - while (!DalamudUtil.IsObjectPresent(_dalamudUtil.CreateGameObject(charaPointer)) && totalWaitTime > 0) - { - _logger.LogTrace("Character is null but it shouldn't be, waiting"); - await Task.Delay(50).ConfigureAwait(false); - totalWaitTime -= 50; - } - - Stopwatch st = Stopwatch.StartNew(); - - // gather up data from ipc - previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); - previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); - Task getGlamourerData = Task.Run(() => _ipcManager.GlamourerGetCharacterCustomization(playerRelatedObject.Address)); - Task getCustomizeData = Task.Run(() => _ipcManager.GetCustomizePlusScale()); - Task getPalettePlusData = Task.Run(() => _ipcManager.PalettePlusBuildPalette()); - previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); - _logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]); - previousData.CustomizePlusScale = await getCustomizeData.ConfigureAwait(false); - _logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale); - previousData.PalettePlusPalette = await getPalettePlusData.ConfigureAwait(false); - _logger.LogDebug("Palette is now: {data}", previousData.PalettePlusPalette); - - // gather static replacements from render model - var (forwardResolve, reverseResolve) = BuildDataFromModel(objectKind, charaPointer, token); - Dictionary> resolvedPaths = GetFileReplacementsFromPaths(forwardResolve, reverseResolve); - previousData.FileReplacements[objectKind] = - new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance) - .Where(p => p.HasFileReplacement).ToHashSet(); - - _logger.LogDebug("== Static Replacements =="); - foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) - { - _logger.LogDebug("=> {repl}", replacement); - } - - // if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times - // or we get into redraw city for every change and nothing works properly - if (objectKind == ObjectKind.Pet) - { - foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) - { - _transientResourceManager.AddSemiTransientResource(objectKind, item); - } - } - - _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); - - // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving - _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); - - // get all remaining paths and resolve them - var transientPaths = ManageSemiTransientData(objectKind, charaPointer); - var resolvedTransientPaths = GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)); - - _logger.LogDebug("== Transient Replacements =="); - foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) - { - _logger.LogDebug("=> {repl}", replacement); - previousData.FileReplacements[objectKind].Add(replacement); - } - - // clean up all semi transient resources that don't have any file replacement (aka null resolve) - _transientResourceManager.CleanUpSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList()); - - // make sure we only return data that actually has file replacements - foreach (var item in previousData.FileReplacements) - { - previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); - } - - st.Stop(); - _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(st.ElapsedTicks).TotalMilliseconds); - - return previousData; + return ((Character*)playerPointer)->GameObject.DrawObject == null; } - private unsafe (HashSet forwardResolve, HashSet reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token) - { - HashSet forwardResolve = new(StringComparer.Ordinal); - HashSet reverseResolve = new(StringComparer.Ordinal); - var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); - for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) - { - var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve); - } - - if (objectKind == ObjectKind.Player) - { - AddPlayerSpecificReplacements(objectKind, charaPointer, human, forwardResolve, reverseResolve); - } - - return (forwardResolve, reverseResolve); - } - - private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human, HashSet forwardResolve, HashSet reverseResolve) + private unsafe void AddPlayerSpecificReplacements(Human* human, HashSet forwardResolve, HashSet reverseResolve) { var weaponObject = (Weapon*)((Object*)human)->ChildObject; @@ -275,44 +155,12 @@ public class CharacterDataFactory : MediatorSubscriberBase } } - - private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, HashSet forwardResolve, HashSet reverseResolve) - { - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - return; - } - - string mdlPath; - try - { - mdlPath = new ByteString(mdl->ResourceHandle->FileName()).ToString(); - } - catch - { - _logger.LogWarning("Could not get model data"); - return; - } - _logger.LogTrace("Checking File Replacement for Model {path}", mdlPath); - - reverseResolve.Add(mdlPath); - - for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mdl->Materials[mtrlIdx]; - if (mtrl == null) continue; - - AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve); - } - } - private unsafe void AddReplacementsFromMaterial(Material* mtrl, HashSet forwardResolve, HashSet reverseResolve) { string fileName; try { fileName = new ByteString(mtrl->ResourceHandle->FileName()).ToString(); - } catch { @@ -357,6 +205,36 @@ public class CharacterDataFactory : MediatorSubscriberBase } } + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, HashSet forwardResolve, HashSet reverseResolve) + { + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + return; + } + + string mdlPath; + try + { + mdlPath = new ByteString(mdl->ResourceHandle->FileName()).ToString(); + } + catch + { + _logger.LogWarning("Could not get model data"); + return; + } + _logger.LogTrace("Checking File Replacement for Model {path}", mdlPath); + + reverseResolve.Add(mdlPath); + + for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) + { + var mtrl = (Material*)mdl->Materials[mtrlIdx]; + if (mtrl == null) continue; + + AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve); + } + } + private void AddReplacementsFromTexture(string texPath, HashSet forwardResolve, HashSet reverseResolve, bool doNotReverseResolve = true) { if (string.IsNullOrEmpty(texPath)) return; @@ -382,20 +260,131 @@ public class CharacterDataFactory : MediatorSubscriberBase string raceSexIdString = raceSexId.ToString("0000", CultureInfo.InvariantCulture); string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; + + _logger.LogTrace("Checking skeleton {path}", skeletonPath); + forwardResolve.Add(skeletonPath); } - private HashSet ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) + private unsafe (HashSet forwardResolve, HashSet reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token) { - _transientResourceManager.PersistTransientResources(charaPointer, objectKind); - - HashSet pathsToResolve = new(StringComparer.Ordinal); - foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path))) + HashSet forwardResolve = new(StringComparer.Ordinal); + HashSet reverseResolve = new(StringComparer.Ordinal); + var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); + for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) { - pathsToResolve.Add(path); + var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve); } - return pathsToResolve; + if (objectKind == ObjectKind.Player) + { + AddPlayerSpecificReplacements(human, forwardResolve, reverseResolve); + } + + return (forwardResolve, reverseResolve); + } + + private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + { + var objectKind = playerRelatedObject.ObjectKind; + var charaPointer = playerRelatedObject.Address; + + _logger.LogDebug("Building character data for {obj}", playerRelatedObject); + + if (!previousData.FileReplacements.ContainsKey(objectKind)) + { + previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); + } + else + { + previousData.FileReplacements[objectKind].Clear(); + } + + // wait until chara is not drawing and present so nothing spontaneously explodes + await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false); + int totalWaitTime = 10000; + while (!DalamudUtilService.IsObjectPresent(_dalamudUtil.CreateGameObject(charaPointer)) && totalWaitTime > 0) + { + _logger.LogTrace("Character is null but it shouldn't be, waiting"); + await Task.Delay(50, token).ConfigureAwait(false); + totalWaitTime -= 50; + } + + Stopwatch st = Stopwatch.StartNew(); + + // gather up data from ipc + previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); + previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); + Task getGlamourerData = Task.Run(() => _ipcManager.GlamourerGetCharacterCustomization(playerRelatedObject.Address)); + Task getCustomizeData = Task.Run(_ipcManager.GetCustomizePlusScale); + Task getPalettePlusData = Task.Run(_ipcManager.PalettePlusBuildPalette); + previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); + _logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]); + previousData.CustomizePlusScale = await getCustomizeData.ConfigureAwait(false); + _logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale); + previousData.PalettePlusPalette = await getPalettePlusData.ConfigureAwait(false); + _logger.LogDebug("Palette is now: {data}", previousData.PalettePlusPalette); + + // gather static replacements from render model + var (forwardResolve, reverseResolve) = BuildDataFromModel(objectKind, charaPointer, token); + Dictionary> resolvedPaths = GetFileReplacementsFromPaths(forwardResolve, reverseResolve); + previousData.FileReplacements[objectKind] = + new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance) + .Where(p => p.HasFileReplacement).ToHashSet(); + + _logger.LogDebug("== Static Replacements =="); + foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + { + _logger.LogDebug("=> {repl}", replacement); + } + + // if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times + // or we get into redraw city for every change and nothing works properly + if (objectKind == ObjectKind.Pet) + { + foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) + { + _transientResourceManager.AddSemiTransientResource(objectKind, item); + } + } + + _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); + + // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving + _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + + // get all remaining paths and resolve them + var transientPaths = ManageSemiTransientData(objectKind, charaPointer); + var resolvedTransientPaths = GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)); + + _logger.LogDebug("== Transient Replacements =="); + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) + { + _logger.LogDebug("=> {repl}", replacement); + previousData.FileReplacements[objectKind].Add(replacement); + } + + // clean up all semi transient resources that don't have any file replacement (aka null resolve) + _transientResourceManager.CleanUpSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList()); + + // make sure we only return data that actually has file replacements + foreach (var item in previousData.FileReplacements) + { + previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); + } + + st.Stop(); + _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(st.ElapsedTicks).TotalMilliseconds); + + return previousData; } private Dictionary> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) @@ -432,4 +421,17 @@ public class CharacterDataFactory : MediatorSubscriberBase return resolvedPaths; } -} + + private HashSet ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) + { + _transientResourceManager.PersistTransientResources(charaPointer, objectKind); + + HashSet pathsToResolve = new(StringComparer.Ordinal); + foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path))) + { + pathsToResolve.Add(path); + } + + return pathsToResolve; + } +} \ No newline at end of file diff --git a/MareSynchronos/Models/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs similarity index 73% rename from MareSynchronos/Models/GameObjectHandler.cs rename to MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs index 224c752..211ca85 100644 --- a/MareSynchronos/Models/GameObjectHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -1,33 +1,31 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using MareSynchronos.Mediator; -using MareSynchronos.Utils; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using Penumbra.String; using System.Runtime.InteropServices; using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind; -namespace MareSynchronos.Models; +namespace MareSynchronos.PlayerData.Handlers; -public class GameObjectHandler : MediatorSubscriberBase +public sealed class GameObjectHandler : DisposableMediatorSubscriberBase { + private readonly DalamudUtilService _dalamudUtil; private readonly Func _getAddress; private readonly bool _isOwnedObject; - private readonly MareMediator _mediator; - private readonly DalamudUtil _dalamudUtil; - private readonly PerformanceCollector _performanceCollector; + private readonly PerformanceCollectorService _performanceCollector; private CancellationTokenSource? _clearCts = new(); - private Task? _clearTask; private Task? _delayedZoningTask; private bool _haltProcessing = false; private bool _ignoreSendAfterRedraw = false; private CancellationTokenSource _zoningCts = new(); - public GameObjectHandler(ILogger logger, PerformanceCollector performanceCollector, - MareMediator mediator, DalamudUtil dalamudUtil, ObjectKind objectKind, Func getAddress, bool watchedObject = true) : base(logger, mediator) + + public GameObjectHandler(ILogger logger, PerformanceCollectorService performanceCollector, + MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func getAddress, bool watchedObject = true) : base(logger, mediator) { _performanceCollector = performanceCollector; - _mediator = mediator; ObjectKind = objectKind; _dalamudUtil = dalamudUtil; _getAddress = getAddress; @@ -40,8 +38,7 @@ public class GameObjectHandler : MediatorSubscriberBase { if (_delayedZoningTask?.IsCompleted ?? true) { - var actualMsg = (TransientResourceChangedMessage)msg; - if (actualMsg.Address != Address) return; + if (msg.Address != Address) return; Mediator.Publish(new CreateCacheForObjectMessage(this)); } }); @@ -63,14 +60,14 @@ public class GameObjectHandler : MediatorSubscriberBase }); Mediator.Subscribe(this, (msg) => { - if (((PenumbraStartRedrawMessage)msg).Address == Address) + if (msg.Address == Address) { _haltProcessing = true; } }); Mediator.Subscribe(this, (msg) => { - if (((PenumbraEndRedrawMessage)msg).Address == Address) + if (msg.Address == Address) { _haltProcessing = false; Task.Run(async () => @@ -87,7 +84,7 @@ public class GameObjectHandler : MediatorSubscriberBase public IntPtr Address { get; set; } public unsafe Character* Character => (Character*)Address; - + public Lazy GameObjectLazy { get; private set; } public IntPtr CurrentAddress => _getAddress.Invoke(); public string Name { get; private set; } public ObjectKind ObjectKind { get; } @@ -95,37 +92,6 @@ public class GameObjectHandler : MediatorSubscriberBase private IntPtr DrawObjectAddress { get; set; } private byte[] EquipSlotData { get; set; } = new byte[40]; - private byte? HatState { get; set; } - - private byte? VisorWeaponState { get; set; } - - public override void Dispose() - { - base.Dispose(); - if (_isOwnedObject) - Mediator.Publish(new RemoveWatchedGameObjectHandler(this)); - } - - public override string ToString() - { - var owned = (_isOwnedObject ? "Self" : "Other"); - return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})"; - } - - private unsafe IntPtr GetDrawObj() - { - return (IntPtr)((GameObject*)_getAddress.Invoke())->GetDrawObject(); - } - - private unsafe bool IsBeingDrawn(IntPtr drawObj, IntPtr curPtr) - { - _logger.LogTrace("IsBeingDrawn for ptr {curPtr} : {drawObj}", curPtr.ToString("X"), drawObj.ToString("X")); - return drawObj == IntPtr.Zero - || (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0) - || (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0) - || (((GameObject*)curPtr)->RenderFlags & 0b100000000000) == 0b100000000000; - } - public async Task IsBeingDrawnRunOnFramework() { return await _dalamudUtil.RunOnFrameworkThread(() => @@ -137,7 +103,7 @@ public class GameObjectHandler : MediatorSubscriberBase var drawObj = GetDrawObj(); return IsBeingDrawn(drawObj, curPtr); } - catch (Exception ex) + catch (Exception) { if (curPtr != IntPtr.Zero) { @@ -149,6 +115,20 @@ public class GameObjectHandler : MediatorSubscriberBase }).ConfigureAwait(false); } + public override string ToString() + { + var owned = _isOwnedObject ? "Self" : "Other"; + return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})"; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_isOwnedObject) + Mediator.Publish(new RemoveWatchedGameObjectHandler(this)); + } + private unsafe void CheckAndUpdateObject() { if (_haltProcessing) return; @@ -166,69 +146,66 @@ public class GameObjectHandler : MediatorSubscriberBase } catch (Exception ex) { - var name = new ByteString(((Character*)curPtr)->GameObject.Name).ToString(); - _logger.LogError(ex, "Error during checking for draw object for {name}", this); + Logger.LogError(ex, "Error during checking for draw object for {name}", this); } if (curPtr != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero) { if (_clearCts != null) { - _logger.LogDebug("[{this}] Cancelling Clear Task", this); + Logger.LogDebug("[{this}] Cancelling Clear Task", this); _clearCts?.Cancel(); _clearCts = null; } bool addrDiff = Address != curPtr; Address = curPtr; + if (addrDiff) + { + GameObjectLazy = new(() => _dalamudUtil.CreateGameObject(curPtr)); + } var chara = (Character*)curPtr; var name = new ByteString(chara->GameObject.Name).ToString(); - bool nameChange = (!string.Equals(name, Name, StringComparison.Ordinal)); + bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal); Name = name; bool equipDiff = CompareAndUpdateEquipByteData(chara->EquipSlotData); - if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self + if (equipDiff && !_isOwnedObject && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self { - if (!_ignoreSendAfterRedraw) - { - _logger.LogTrace("[{this}] Changed", this); - Mediator.Publish(new CharacterChangedMessage(this)); - return; - } + Logger.LogTrace("[{this}] Changed", this); + Mediator.Publish(new CharacterChangedMessage(this)); + return; } var customizeDiff = CompareAndUpdateCustomizeData(chara->CustomizeData); - if (addrDiff || equipDiff || customizeDiff || drawObjDiff || nameChange) + if ((addrDiff || equipDiff || customizeDiff || drawObjDiff || nameChange) && _isOwnedObject) { - if (_isOwnedObject) - { - _logger.LogTrace("[{this}] Changed", this); + Logger.LogTrace("[{this}] Changed", this); - _logger.LogDebug("[{this}] Sending CreateCacheObjectMessage", this); - Mediator.Publish(new CreateCacheForObjectMessage(this)); - } + Logger.LogDebug("[{this}] Sending CreateCacheObjectMessage", this); + Mediator.Publish(new CreateCacheForObjectMessage(this)); } } else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) { Address = IntPtr.Zero; DrawObjectAddress = IntPtr.Zero; - _logger.LogTrace("[{this}] Changed -> Null", this); + Logger.LogTrace("[{this}] Changed -> Null", this); if (_isOwnedObject && ObjectKind != ObjectKind.Player) { _clearCts?.Cancel(); _clearCts?.Dispose(); _clearCts = new(); var token = _clearCts.Token; - _clearTask = Task.Run(() => ClearTask(token), token); + _ = Task.Run(() => ClearTask(token), token); } } } private async Task ClearTask(CancellationToken token) { - _logger.LogDebug("[{this}] Running Clear Task", this); + Logger.LogDebug("[{this}] Running Clear Task", this); await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); - _logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this); + Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this); Mediator.Publish(new ClearCacheForObjectMessage(this)); _clearCts = null; } @@ -277,10 +254,24 @@ public class GameObjectHandler : MediatorSubscriberBase } catch (Exception ex) { - _logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this); + Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this); } } + private unsafe IntPtr GetDrawObj() + { + return (IntPtr)((GameObject*)_getAddress.Invoke())->GetDrawObject(); + } + + private unsafe bool IsBeingDrawn(IntPtr drawObj, IntPtr curPtr) + { + Logger.LogTrace("IsBeingDrawn for ptr {curPtr} : {drawObj}", curPtr.ToString("X"), drawObj.ToString("X")); + return drawObj == IntPtr.Zero + || (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0) + || (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0) + || (((GameObject*)curPtr)->RenderFlags & 0b100000000000) == 0b100000000000; + } + private void ZoneSwitchEnd() { if (!_isOwnedObject || _haltProcessing) return; @@ -296,17 +287,20 @@ public class GameObjectHandler : MediatorSubscriberBase if (!_isOwnedObject || _haltProcessing) return; _zoningCts = new(); - _logger.LogDebug("[{obj}] Starting Delay After Zoning", this); + Logger.LogDebug("[{obj}] Starting Delay After Zoning", this); _delayedZoningTask = Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false); } - catch { } + catch + { + // ignore cancelled + } finally { - _logger.LogDebug("[{this}] Delay after zoning complete", this); + Logger.LogDebug("[{this}] Delay after zoning complete", this); _zoningCts.Dispose(); } }); diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs similarity index 59% rename from MareSynchronos/Managers/CachedPlayer.cs rename to MareSynchronos/PlayerData/Pairs/CachedPlayer.cs index e9fcbf7..3a5ff99 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs @@ -3,52 +3,71 @@ using Dalamud.Logging; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Dto.User; -using MareSynchronos.Factories; using MareSynchronos.FileCache; -using MareSynchronos.Mediator; -using MareSynchronos.Models; +using MareSynchronos.Interop; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; -using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Managers; +namespace MareSynchronos.PlayerData.Pairs; -public class CachedPlayer : MediatorSubscriberBase, IDisposable +public sealed class CachedPlayer : DisposableMediatorSubscriberBase { - private readonly ApiController _apiController; - private readonly DalamudUtil _dalamudUtil; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly IpcManager _ipcManager; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; - private API.Data.CharacterData _cachedData = new(); - private GameObjectHandler? _currentOtherChara; + private readonly Func, bool, GameObjectHandler> _gameObjectHandlerFactory; + private readonly IpcManager _ipcManager; + private readonly IHostApplicationLifetime _lifetime; + private Guid _applicationId; + private Task? _applicationTask; + private CharacterData _cachedData = new(); + private GameObjectHandler? _charaHandler; private CancellationTokenSource? _downloadCancellationTokenSource = new(); private string _lastGlamourerData = string.Empty; private string _originalGlamourerData = string.Empty; - public CachedPlayer(ILogger logger, OnlineUserIdentDto onlineUser, GameObjectHandlerFactory gameObjectHandlerFactory, - IpcManager ipcManager, ApiController apiController, - DalamudUtil dalamudUtil, FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator) + private CancellationTokenSource _redrawCts = new(); + + public CachedPlayer(ILogger logger, OnlineUserIdentDto onlineUser, + Func, bool, GameObjectHandler> gameObjectHandlerFactory, + IpcManager ipcManager, FileDownloadManager transferManager, + DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator) { OnlineUser = onlineUser; _gameObjectHandlerFactory = gameObjectHandlerFactory; _ipcManager = ipcManager; - _apiController = apiController; + _downloadManager = transferManager; _dalamudUtil = dalamudUtil; + _lifetime = lifetime; _fileDbManager = fileDbManager; } - private OnlineUserIdentDto OnlineUser { get; set; } - private IntPtr PlayerCharacter => _currentOtherChara?.Address ?? IntPtr.Zero; + private enum PlayerChanges + { + Heels = 1, + Customize = 2, + Palette = 3, + Mods = 4 + } + public string? PlayerName { get; private set; } public string PlayerNameHash => OnlineUser.Ident; + private OnlineUserIdentDto OnlineUser { get; set; } + private IntPtr PlayerCharacter => _charaHandler?.Address ?? IntPtr.Zero; - public void ApplyCharacterData(API.Data.CharacterData characterData, OptionalPluginWarning warning, bool forced = false) + public void ApplyCharacterData(CharacterData characterData, OptionalPluginWarning warning, bool forced = false) { - _logger.LogDebug("Received data for {player}", this); + SetUploading(false); - _logger.LogDebug("Checking for files to download for player {name}", this); - _logger.LogDebug("Hash for data is {newHash}, current cache hash is {oldHash}", characterData.DataHash.Value, _cachedData.DataHash.Value); + Logger.LogDebug("Received data for {player}", this); + + Logger.LogDebug("Checking for files to download for player {name}", this); + Logger.LogDebug("Hash for data is {newHash}, current cache hash is {oldHash}", characterData.DataHash.Value, _cachedData.DataHash.Value); if (!_ipcManager.CheckPenumbraApi()) return; if (!_ipcManager.CheckGlamourerApi()) return; @@ -62,12 +81,167 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable NotifyForMissingPlugins(playerChanges, warning); } + Logger.LogDebug("Downloading and applying character for {name}", this); + DownloadAndApplyCharacter(characterData, charaDataToUpdate); _cachedData = characterData; } - private Dictionary> CheckUpdatedData(API.Data.CharacterData oldData, API.Data.CharacterData newData, bool forced) + public bool CheckExistence() + { + if (PlayerName == null || _charaHandler == null + || !string.Equals(PlayerName, _charaHandler.Name, StringComparison.Ordinal) + || _charaHandler.CurrentAddress == IntPtr.Zero) + { + return false; + } + + return true; + } + + public void Initialize(string name) + { + PlayerName = name; + _charaHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, false); + + _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); + _lastGlamourerData = _originalGlamourerData; + Mediator.Subscribe(this, IpcManagerOnPenumbraRedrawEvent); + Mediator.Subscribe(this, (msg) => + { + if (msg.GameObjectHandler == _charaHandler && (_applicationTask?.IsCompleted ?? true)) + { + Logger.LogTrace("Saving new Glamourer Data for {this}", this); + _lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); + } + }); + + Logger.LogDebug("Initializing Player {obj}", this); + } + + public override string ToString() + { + return OnlineUser == null + ? (base.ToString() ?? string.Empty) + : (OnlineUser.User.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != IntPtr.Zero ? "HasChar" : "NoChar")); + } + + internal void SetUploading(bool isUploading = true) + { + Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading); + if (_charaHandler != null) + { + Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading)); + } + } + + protected override void Dispose(bool disposing) + { + if (string.IsNullOrEmpty(PlayerName)) return; // already disposed + + base.Dispose(disposing); + + SetUploading(false); + _downloadManager.Dispose(); + var name = PlayerName; + Logger.LogDebug("Disposing {name} ({user})", name, OnlineUser); + try + { + Guid applicationId = Guid.NewGuid(); + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource?.Dispose(); + _downloadCancellationTokenSource = null; + nint ptr = PlayerCharacter; + _charaHandler?.Dispose(); + _charaHandler = null; + if (!_lifetime.ApplicationStopping.IsCancellationRequested && ptr != IntPtr.Zero && !_dalamudUtil.IsZoning) + { + Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); + _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name); + + foreach (KeyValuePair> item in _cachedData.FileReplacements) + { + RevertCustomizationData(ptr, item.Key, name, applicationId).GetAwaiter().GetResult(); + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error on disposal of {name}", name); + } + finally + { + PlayerName = null; + _cachedData = new(); + Logger.LogDebug("Disposing {name} complete", name); + } + } + + private async Task ApplyBaseData(Guid applicationId, Dictionary moddedPaths, string manipulationData) + { + await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, PlayerName!)).ConfigureAwait(false); + await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraSetTemporaryMods(Logger, applicationId, PlayerName!, moddedPaths, manipulationData)).ConfigureAwait(false); + } + + private async Task ApplyCustomizationData(Guid applicationId, KeyValuePair> changes, API.Data.CharacterData charaData) + { + if (PlayerCharacter == IntPtr.Zero) return; + var ptr = PlayerCharacter; + var handler = changes.Key switch + { + ObjectKind.Player => _charaHandler!, + ObjectKind.Companion => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetCompanion(ptr), false), + ObjectKind.MinionOrMount => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), false), + ObjectKind.Pet => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetPet(ptr), false), + _ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key) + }; + + CancellationTokenSource applicationTokenSource = new(); + applicationTokenSource.CancelAfter(TimeSpan.FromSeconds(30)); + + if (handler.Address == IntPtr.Zero) + { + if (handler != _charaHandler) handler.Dispose(); + return; + } + + Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler); + await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000).ConfigureAwait(false); + foreach (var change in changes.Value) + { + Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler); + switch (change) + { + case PlayerChanges.Palette: + await _ipcManager.PalettePlusSetPalette(handler.Address, charaData.PalettePlusData).ConfigureAwait(false); + break; + + case PlayerChanges.Customize: + await _ipcManager.CustomizePlusSetBodyScale(handler.Address, charaData.CustomizePlusData).ConfigureAwait(false); + break; + + case PlayerChanges.Heels: + await _ipcManager.HeelsSetOffsetForPlayer(handler.Address, charaData.HeelsOffset).ConfigureAwait(false); + break; + + case PlayerChanges.Mods: + if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData)) + { + await _ipcManager.GlamourerApplyAll(Logger, handler, glamourerData, applicationId, applicationTokenSource.Token).ConfigureAwait(false); + } + else + { + await _ipcManager.PenumbraRedraw(Logger, handler, applicationId, applicationTokenSource.Token).ConfigureAwait(false); + } + break; + } + } + + if (handler != _charaHandler) handler.Dispose(); + } + + private Dictionary> CheckUpdatedData(CharacterData oldData, CharacterData newData, bool forced) { var charaDataToUpdate = new Dictionary>(); foreach (ObjectKind objectKind in Enum.GetValues()) @@ -89,7 +263,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData) { - _logger.LogDebug("Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," + + Logger.LogDebug("Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," + " OldButNotNewFiles:{hasOldButNotNewFileReplacements}, NewButNotOldGlam:{hasNewButNotOldGlamourerData}, OldButNotNewGlam:{hasOldButNotNewGlamourerData}) => {change}", this, objectKind, hasNewButNotOldFileReplacements, hasOldButNotNewFileReplacements, hasNewButNotOldGlamourerData, hasOldButNotNewGlamourerData, PlayerChanges.Mods); charaDataToUpdate[objectKind].Add(PlayerChanges.Mods); @@ -98,10 +272,10 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable { if (hasNewAndOldFileReplacements) { - bool listsAreEqual = Enumerable.SequenceEqual(oldData.FileReplacements[objectKind], newData.FileReplacements[objectKind], FileReplacementDataComparer.Instance); + bool listsAreEqual = oldData.FileReplacements[objectKind].SequenceEqual(newData.FileReplacements[objectKind], Data.FileReplacementDataComparer.Instance); if (!listsAreEqual || forced) { - _logger.LogDebug("Updating {object}/{kind} (FileReplacements not equal) => {change}", this, objectKind, PlayerChanges.Mods); + Logger.LogDebug("Updating {object}/{kind} (FileReplacements not equal) => {change}", this, objectKind, PlayerChanges.Mods); charaDataToUpdate[objectKind].Add(PlayerChanges.Mods); } } @@ -111,7 +285,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable bool glamourerDataDifferent = !string.Equals(oldData.GlamourerData[objectKind], newData.GlamourerData[objectKind], StringComparison.Ordinal); if (glamourerDataDifferent || forced) { - _logger.LogDebug("Updating {object}/{kind} (Glamourer different) => {change}", this, objectKind, PlayerChanges.Mods); + Logger.LogDebug("Updating {object}/{kind} (Glamourer different) => {change}", this, objectKind, PlayerChanges.Mods); charaDataToUpdate[objectKind].Add(PlayerChanges.Mods); } } @@ -122,28 +296,28 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); if (manipDataDifferent || forced) { - _logger.LogDebug("Updating {object}/{kind} (Diff manip data) => {change}", this, objectKind, PlayerChanges.Mods); + Logger.LogDebug("Updating {object}/{kind} (Diff manip data) => {change}", this, objectKind, PlayerChanges.Mods); charaDataToUpdate[objectKind].Add(PlayerChanges.Mods); } bool heelsOffsetDifferent = oldData.HeelsOffset != newData.HeelsOffset; if (heelsOffsetDifferent || forced) { - _logger.LogDebug("Updating {object}/{kind} (Diff heels data) => {change}", this, objectKind, PlayerChanges.Heels); + Logger.LogDebug("Updating {object}/{kind} (Diff heels data) => {change}", this, objectKind, PlayerChanges.Heels); charaDataToUpdate[objectKind].Add(PlayerChanges.Heels); } bool customizeDataDifferent = !string.Equals(oldData.CustomizePlusData, newData.CustomizePlusData, StringComparison.Ordinal); if (customizeDataDifferent || forced) { - _logger.LogDebug("Updating {object}/{kind} (Diff customize data) => {change}", this, objectKind, PlayerChanges.Customize); + Logger.LogDebug("Updating {object}/{kind} (Diff customize data) => {change}", this, objectKind, PlayerChanges.Customize); charaDataToUpdate[objectKind].Add(PlayerChanges.Customize); } bool palettePlusDataDifferent = !string.Equals(oldData.PalettePlusData, newData.PalettePlusData, StringComparison.Ordinal); if (palettePlusDataDifferent || forced) { - _logger.LogDebug("Updating {object}/{kind} (Diff palette data) => {change}", this, objectKind, PlayerChanges.Palette); + Logger.LogDebug("Updating {object}/{kind} (Diff palette data) => {change}", this, objectKind, PlayerChanges.Palette); charaDataToUpdate[objectKind].Add(PlayerChanges.Palette); } } @@ -157,195 +331,11 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable return charaDataToUpdate; } - private enum PlayerChanges - { - Heels = 1, - Customize = 2, - Palette = 3, - Mods = 4 - } - - private void NotifyForMissingPlugins(HashSet changes, OptionalPluginWarning warning) - { - List missingPluginsForData = new(); - if (changes.Contains(PlayerChanges.Heels)) - { - if (!warning.ShownHeelsWarning && !_ipcManager.CheckHeelsApi()) - { - missingPluginsForData.Add("Heels"); - warning.ShownHeelsWarning = true; - } - } - if (changes.Contains(PlayerChanges.Customize)) - { - if (!warning.ShownCustomizePlusWarning && !_ipcManager.CheckCustomizePlusApi()) - { - missingPluginsForData.Add("Customize+"); - warning.ShownCustomizePlusWarning = true; - } - } - - if (changes.Contains(PlayerChanges.Palette)) - { - if (!warning.ShownPalettePlusWarning && !_ipcManager.CheckPalettePlusApi()) - { - missingPluginsForData.Add("Palette+"); - warning.ShownPalettePlusWarning = true; - } - } - - if (missingPluginsForData.Any()) - { - Mediator.Publish(new NotificationMessage("Missing plugins for " + PlayerName, - $"Received data for {PlayerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.", - NotificationType.Warning, 10000)); - } - } - - public bool CheckExistence() - { - if (PlayerName == null || _currentOtherChara == null - || !string.Equals(PlayerName, _currentOtherChara.Name, StringComparison.Ordinal) - || _currentOtherChara.CurrentAddress == IntPtr.Zero) - { - return false; - } - - return true; - } - - public override void Dispose() - { - if (string.IsNullOrEmpty(PlayerName)) return; // already disposed - - base.Dispose(); - var name = PlayerName; - PlayerName = null; - _logger.LogDebug("Disposing {name} ({user})", name, OnlineUser); - try - { - Guid applicationId = Guid.NewGuid(); - _logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); - _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, name); - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource?.Dispose(); - _downloadCancellationTokenSource = null; - nint ptr = PlayerCharacter; - _currentOtherChara?.Dispose(); - - if (ptr != IntPtr.Zero && !_dalamudUtil.IsZoning) - { - foreach (KeyValuePair> item in _cachedData.FileReplacements) - { - Task.Run(async () => await RevertCustomizationData(ptr, item.Key, name, applicationId).ConfigureAwait(false)); - } - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error on disposal of {name}", name); - } - finally - { - _currentOtherChara = null; - _cachedData = new(); - _logger.LogDebug("Disposing {name} complete", name); - PlayerName = null; - } - } - - public void Initialize(string name) - { - PlayerName = name; - _currentOtherChara = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, isWatched: false); - - _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); - _lastGlamourerData = _originalGlamourerData; - Mediator.Subscribe(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg))); - Mediator.Subscribe(this, (msg) => - { - var actualMsg = (CharacterChangedMessage)msg; - if (actualMsg.GameObjectHandler == _currentOtherChara && (_applicationTask?.IsCompleted ?? true)) - { - _logger.LogTrace("Saving new Glamourer Data for {this}", this); - _lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); - } - }); - - _logger.LogDebug("Initializing Player {obj}", this); - } - - public override string ToString() - { - return OnlineUser.User.AliasOrUID + ":" + PlayerName + ":" + ((PlayerCharacter != IntPtr.Zero) ? "HasChar" : "NoChar"); - } - - private async Task ApplyBaseData(Guid applicationId, Dictionary moddedPaths, string manipulationData) - { - await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, PlayerName!)).ConfigureAwait(false); - await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, PlayerName!, moddedPaths, manipulationData)).ConfigureAwait(false); - } - - private async Task ApplyCustomizationData(Guid applicationId, KeyValuePair> changes, API.Data.CharacterData charaData) - { - if (PlayerCharacter == IntPtr.Zero) return; - var ptr = PlayerCharacter; - var handler = changes.Key switch - { - ObjectKind.Player => _currentOtherChara!, - ObjectKind.Companion => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanion(ptr), isWatched: false), - ObjectKind.MinionOrMount => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false), - ObjectKind.Pet => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false), - _ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key) - }; - - - CancellationTokenSource applicationTokenSource = new(); - applicationTokenSource.CancelAfter(TimeSpan.FromSeconds(30)); - - if (handler.Address == IntPtr.Zero) - { - if (handler != _currentOtherChara) handler.Dispose(); - return; - } - - _logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler); - await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, handler, applicationId, 30000).ConfigureAwait(false); - foreach (var change in changes.Value) - { - _logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler); - switch (change) - { - case PlayerChanges.Palette: - await _ipcManager.PalettePlusSetPalette(handler.Address, charaData.PalettePlusData).ConfigureAwait(false); - break; - case PlayerChanges.Customize: - await _ipcManager.CustomizePlusSetBodyScale(handler.Address, charaData.CustomizePlusData).ConfigureAwait(false); - break; - case PlayerChanges.Heels: - await _ipcManager.HeelsSetOffsetForPlayer(handler.Address, charaData.HeelsOffset).ConfigureAwait(false); - break; - case PlayerChanges.Mods: - if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData)) - { - await _ipcManager.GlamourerApplyAll(_logger, handler, glamourerData, applicationId, applicationTokenSource.Token).ConfigureAwait(false); - } - else - { - await _ipcManager.PenumbraRedraw(_logger, handler, applicationId, applicationTokenSource.Token).ConfigureAwait(false); - } - break; - } - } - - if (handler != _currentOtherChara) handler.Dispose(); - } - - private void DownloadAndApplyCharacter(API.Data.CharacterData charaData, Dictionary> updatedData) + private void DownloadAndApplyCharacter(CharacterData charaData, Dictionary> updatedData) { if (!updatedData.Any()) { - _logger.LogDebug("Nothing to update for {obj}", this); + Logger.LogDebug("Nothing to update for {obj}", this); } var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.Mods)); @@ -355,7 +345,6 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable _downloadCancellationTokenSource = new CancellationTokenSource(); var downloadToken = _downloadCancellationTokenSource.Token; - var downloadId = _apiController.GetDownloadId(); Task.Run(async () => { List toDownloadReplacements; @@ -367,22 +356,22 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable int attempts = 0; while ((toDownloadReplacements = TryCalculateModdedDictionary(charaData, out moddedPaths)).Count > 0 && attempts++ <= 10) { - downloadId = _apiController.GetDownloadId(); - _logger.LogDebug("Downloading missing files for player {name}, {kind}", PlayerName, updatedData); + _downloadManager.CancelDownload(); + Logger.LogDebug("Downloading missing files for player {name}, {kind}", PlayerName, updatedData); if (toDownloadReplacements.Any()) { - await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken).ConfigureAwait(false); - _apiController.CancelDownload(downloadId); + await _downloadManager.DownloadFiles(_charaHandler, toDownloadReplacements, downloadToken).ConfigureAwait(false); + _downloadManager.CancelDownload(); } if (downloadToken.IsCancellationRequested) { - _logger.LogTrace("Detected cancellation"); - _apiController.CancelDownload(downloadId); + Logger.LogTrace("Detected cancellation"); + _downloadManager.CancelDownload(); return; } - if ((TryCalculateModdedDictionary(charaData, out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) + if (TryCalculateModdedDictionary(charaData, out moddedPaths).All(c => _downloadManager.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) { break; } @@ -391,10 +380,10 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable } } - while (!_applicationTask?.IsCompleted ?? false && !downloadToken.IsCancellationRequested) + while ((!_applicationTask?.IsCompleted ?? false) && !downloadToken.IsCancellationRequested) { // block until current application is done - _logger.LogDebug("Waiting for current data application (Id: {id}) to finish", _applicationId); + Logger.LogDebug("Waiting for current data application (Id: {id}) to finish", _applicationId); await Task.Delay(250).ConfigureAwait(false); } @@ -403,7 +392,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable _applicationTask = Task.Run(async () => { _applicationId = Guid.NewGuid(); - _logger.LogDebug("[{applicationId}] Starting application task", _applicationId); + Logger.LogDebug("[{applicationId}] Starting application task", _applicationId); if (updateModdedPaths && (moddedPaths.Any() || !string.IsNullOrEmpty(charaData.ManipulationData))) { @@ -415,16 +404,11 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable await ApplyCustomizationData(_applicationId, kind, charaData).ConfigureAwait(false); } - _logger.LogDebug("[{applicationId}] Application finished", _applicationId); + Logger.LogDebug("[{applicationId}] Application finished", _applicationId); }); }, downloadToken); } - private Task? _applicationTask; - - private CancellationTokenSource _redrawCts = new(); - private Guid _applicationId; - private void IpcManagerOnPenumbraRedrawEvent(PenumbraRedrawMessage msg) { var player = _dalamudUtil.GetCharacterFromObjectTableByIndex(msg.ObjTblIdx); @@ -438,14 +422,42 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable Task.Run(async () => { var applicationId = Guid.NewGuid(); - await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, _currentOtherChara!, applicationId, ct: token).ConfigureAwait(false); - _logger.LogDebug("Unauthorized character change detected"); + await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, applicationId, ct: token).ConfigureAwait(false); + Logger.LogDebug("Unauthorized character change detected"); await ApplyCustomizationData(applicationId, new(ObjectKind.Player, new HashSet(new[] { PlayerChanges.Palette, PlayerChanges.Customize, PlayerChanges.Heels, PlayerChanges.Mods })), _cachedData).ConfigureAwait(false); }, token); } + private void NotifyForMissingPlugins(HashSet changes, OptionalPluginWarning warning) + { + List missingPluginsForData = new(); + if (changes.Contains(PlayerChanges.Heels) && !warning.ShownHeelsWarning && !_ipcManager.CheckHeelsApi()) + { + missingPluginsForData.Add("Heels"); + warning.ShownHeelsWarning = true; + } + if (changes.Contains(PlayerChanges.Customize) && !warning.ShownCustomizePlusWarning && !_ipcManager.CheckCustomizePlusApi()) + { + missingPluginsForData.Add("Customize+"); + warning.ShownCustomizePlusWarning = true; + } + + if (changes.Contains(PlayerChanges.Palette) && !warning.ShownPalettePlusWarning && !_ipcManager.CheckPalettePlusApi()) + { + missingPluginsForData.Add("Palette+"); + warning.ShownPalettePlusWarning = true; + } + + if (missingPluginsForData.Any()) + { + Mediator.Publish(new NotificationMessage("Missing plugins for " + PlayerName, + $"Received data for {PlayerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.", + NotificationType.Warning, 10000)); + } + } + private async Task RevertCustomizationData(IntPtr address, ObjectKind objectKind, string name, Guid applicationId) { if (address == IntPtr.Zero) return; @@ -453,20 +465,20 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable var cancelToken = new CancellationTokenSource(); cancelToken.CancelAfter(TimeSpan.FromSeconds(10)); - _logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind); + Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind); if (objectKind == ObjectKind.Player) { - using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, false); - _logger.LogDebug("[{applicationId}] Restoring Customization for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _originalGlamourerData); - await _ipcManager.GlamourerApplyOnlyCustomization(_logger, tempHandler, _originalGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); - _logger.LogDebug("[{applicationId}] Restoring Equipment for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _lastGlamourerData); - await _ipcManager.GlamourerApplyOnlyEquipment(_logger, tempHandler, _lastGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); - _logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => address, false); + Logger.LogDebug("[{applicationId}] Restoring Customization for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _originalGlamourerData); + await _ipcManager.GlamourerApplyOnlyCustomization(Logger, tempHandler, _originalGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); + Logger.LogDebug("[{applicationId}] Restoring Equipment for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _lastGlamourerData); + await _ipcManager.GlamourerApplyOnlyEquipment(Logger, tempHandler, _lastGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); + Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); await _ipcManager.HeelsRestoreOffsetForPlayer(address).ConfigureAwait(false); - _logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); await _ipcManager.CustomizePlusRevert(address).ConfigureAwait(false); - _logger.LogDebug("[{applicationId}] Restoring Palette+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); + Logger.LogDebug("[{applicationId}] Restoring Palette+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); await _ipcManager.PalettePlusRemovePalette(address).ConfigureAwait(false); } else if (objectKind == ObjectKind.MinionOrMount) @@ -474,8 +486,8 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable var minionOrMount = _dalamudUtil.GetMinionOrMount(address); if (minionOrMount != IntPtr.Zero) { - using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false); - await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); + using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.MinionOrMount, () => minionOrMount, false); + await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); } } else if (objectKind == ObjectKind.Pet) @@ -483,8 +495,8 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable var pet = _dalamudUtil.GetPet(address); if (pet != IntPtr.Zero) { - using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false); - await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); + using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Pet, () => pet, false); + await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); } } else if (objectKind == ObjectKind.Companion) @@ -492,13 +504,13 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable var companion = _dalamudUtil.GetCompanion(address); if (companion != IntPtr.Zero) { - using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false); - await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); + using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Pet, () => companion, false); + await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false); } } } - private List TryCalculateModdedDictionary(API.Data.CharacterData charaData, out Dictionary moddedDictionary) + private List TryCalculateModdedDictionary(CharacterData charaData, out Dictionary moddedDictionary) { List missingFiles = new(); moddedDictionary = new Dictionary(StringComparer.Ordinal); @@ -515,7 +527,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable } else { - _logger.LogTrace("Missing file: {hash}", item.Hash); + Logger.LogTrace("Missing file: {hash}", item.Hash); missingFiles.Add(item); } } @@ -525,7 +537,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable { foreach (var gamePath in item.GamePaths) { - _logger.LogTrace("Adding file swap for {path}: {fileSwap}", gamePath, item.FileSwapPath); + Logger.LogTrace("Adding file swap for {path}: {fileSwap}", gamePath, item.FileSwapPath); moddedDictionary[gamePath] = item.FileSwapPath; } } @@ -534,7 +546,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable { PluginLog.Error(ex, "Something went wrong during calculation replacements"); } - _logger.LogDebug("ModdedPaths calculated, missing files: {count}", missingFiles.Count); + Logger.LogDebug("ModdedPaths calculated, missing files: {count}", missingFiles.Count); return missingFiles; } } \ No newline at end of file diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs similarity index 52% rename from MareSynchronos/Managers/OnlinePlayerManager.cs rename to MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs index fefee9e..0cc9430 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs @@ -1,57 +1,46 @@ using MareSynchronos.API.Data; -using MareSynchronos.FileCache; -using MareSynchronos.Mediator; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Managers; +namespace MareSynchronos.PlayerData.Pairs; -public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable +public class OnlinePlayerManager : DisposableMediatorSubscriberBase { private readonly ApiController _apiController; - private readonly DalamudUtil _dalamudUtil; - private readonly FileCacheManager _fileDbManager; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileUploadManager _fileTransferManager; private readonly PairManager _pairManager; private CharacterData? _lastSentData; - public OnlinePlayerManager(ILogger logger, ApiController apiController, DalamudUtil dalamudUtil, - FileCacheManager fileDbManager, PairManager pairManager, MareMediator mediator) : base(logger, mediator) + public OnlinePlayerManager(ILogger logger, ApiController apiController, DalamudUtilService dalamudUtil, + PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator) { - _logger.LogTrace("Creating " + nameof(OnlinePlayerManager)); _apiController = apiController; _dalamudUtil = dalamudUtil; - _fileDbManager = fileDbManager; _pairManager = pairManager; - - Mediator.Subscribe(this, (msg) => PlayerManagerOnPlayerHasChanged((PlayerChangedMessage)msg)); + _fileTransferManager = fileTransferManager; + Mediator.Subscribe(this, (_) => PlayerManagerOnPlayerHasChanged()); Mediator.Subscribe(this, (_) => FrameworkOnUpdate()); Mediator.Subscribe(this, (msg) => { - var newData = ((CharacterDataCreatedMessage)msg).CharacterData; - if (_lastSentData == null || _lastSentData != null && !string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal)) + var newData = msg.CharacterData; + if (_lastSentData == null || (!string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal))) { - _logger.LogDebug("Pushing data for visible players"); + Logger.LogDebug("Pushing data for visible players"); _lastSentData = newData; - PushCharacterData(_pairManager.VisibleUsers); + PushCharacterData(_pairManager.GetVisibleUsers()); } else { - _logger.LogDebug("Not sending data for " + newData.DataHash.Value); + Logger.LogDebug("Not sending data for {hash}", newData.DataHash.Value); } }); } - private void PlayerManagerOnPlayerHasChanged(PlayerChangedMessage msg) - { - PushCharacterData(_pairManager.VisibleUsers); - } - - public override void Dispose() - { - base.Dispose(); - } - private void FrameworkOnUpdate() { if (!_dalamudUtil.IsPlayerPresent || !_apiController.IsConnected) return; @@ -71,18 +60,24 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable if (newVisiblePlayers.Any()) { - _logger.LogTrace("Has new visible players, pushing character data"); + Logger.LogTrace("Has new visible players, pushing character data"); PushCharacterData(newVisiblePlayers); } } + private void PlayerManagerOnPlayerHasChanged() + { + PushCharacterData(_pairManager.GetVisibleUsers()); + } + private void PushCharacterData(List visiblePlayers) { if (visiblePlayers.Any() && _lastSentData != null) { Task.Run(async () => { - await _apiController.PushCharacterData(_lastSentData, visiblePlayers).ConfigureAwait(false); + var dataToSend = await _fileTransferManager.UploadFiles(_lastSentData.DeepClone(), visiblePlayers).ConfigureAwait(false); + await _apiController.PushCharacterData(dataToSend, visiblePlayers).ConfigureAwait(false); }); } } diff --git a/MareSynchronos/Models/OptionalPluginWarning.cs b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs similarity index 83% rename from MareSynchronos/Models/OptionalPluginWarning.cs rename to MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs index a8813a8..7be98ad 100644 --- a/MareSynchronos/Models/OptionalPluginWarning.cs +++ b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs @@ -1,4 +1,4 @@ -namespace MareSynchronos.Models; +namespace MareSynchronos.PlayerData.Pairs; public record OptionalPluginWarning { diff --git a/MareSynchronos/Models/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs similarity index 66% rename from MareSynchronos/Models/Pair.cs rename to MareSynchronos/PlayerData/Pairs/Pair.cs index 7b2c21b..27d7b4d 100644 --- a/MareSynchronos/Models/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -4,24 +4,23 @@ using MareSynchronos.API.Data.Comparer; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.User; -using MareSynchronos.Factories; -using MareSynchronos.Managers; using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Utils; -using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Models; +namespace MareSynchronos.PlayerData.Pairs; public class Pair { - private readonly ILogger _logger; - private readonly CachedPlayerFactory _cachedPlayerFactory; + private readonly Func _cachedPlayerFactory; private readonly MareConfigService _configService; + private readonly ILogger _logger; private readonly ServerConfigurationManager _serverConfigurationManager; + private OnlineUserIdentDto? _onlineUserIdentDto = null; private OptionalPluginWarning? _pluginWarnings; - public Pair(ILogger logger, CachedPlayerFactory cachedPlayerFactory, MareConfigService configService, ServerConfigurationManager serverConfigurationManager) + public Pair(ILogger logger, Func cachedPlayerFactory, MareConfigService configService, ServerConfigurationManager serverConfigurationManager) { _logger = logger; _cachedPlayerFactory = cachedPlayerFactory; @@ -29,52 +28,21 @@ public class Pair _serverConfigurationManager = serverConfigurationManager; } - public UserPairDto? UserPair { get; set; } - private CachedPlayer? CachedPlayer { get; set; } - public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName); - public API.Data.CharacterData? LastReceivedCharacterData { get; set; } + public bool CachedPlayerExists => CachedPlayer?.CheckExistence() ?? false; public Dictionary GroupPair { get; set; } = new(GroupDtoComparer.Instance); - public string PlayerNameHash => CachedPlayer?.PlayerNameHash ?? string.Empty; - public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; - public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; + public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName); public bool IsOnline => CachedPlayer != null; - public bool IsVisible => CachedPlayer?.PlayerName != null; - public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? (UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused()) + + public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused() : GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused()); - public string? GetNote() - { - return _serverConfigurationManager.GetNoteForUid(UserData.UID); - } - - public void SetNote(string note) - { - _serverConfigurationManager.SetNoteForUid(UserData.UID, note); - } - - public bool HasAnyConnection() - { - return UserPair != null || GroupPair.Any(); - } - - public bool InitializePair(string name) - { - if (!PlayerName.IsNullOrEmpty()) return false; - - if (CachedPlayer == null) throw new InvalidOperationException("CachedPlayer not initialized"); - _pluginWarnings ??= new() - { - ShownCustomizePlusWarning = _configService.Current.DisableOptionalPluginWarnings, - ShownHeelsWarning = _configService.Current.DisableOptionalPluginWarnings, - ShownPalettePlusWarning = _configService.Current.DisableOptionalPluginWarnings, - }; - - CachedPlayer.Initialize(name); - - ApplyLastReceivedData(); - - return true; - } + public bool IsVisible => CachedPlayer?.PlayerName != null; + public CharacterData? LastReceivedCharacterData { get; set; } + public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; + public string PlayerNameHash => CachedPlayer?.PlayerNameHash ?? string.Empty; + public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; + public UserPairDto? UserPair { get; set; } + private CachedPlayer? CachedPlayer { get; set; } public void ApplyData(OnlineUserCharaDataDto data) { @@ -102,21 +70,91 @@ public class Pair CachedPlayer.ApplyCharacterData(RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, _pluginWarnings, forced); } - private API.Data.CharacterData? RemoveNotSyncedFiles(API.Data.CharacterData? data) + public string? GetNote() + { + return _serverConfigurationManager.GetNoteForUid(UserData.UID); + } + + public bool HasAnyConnection() + { + return UserPair != null || GroupPair.Any(); + } + + public bool InitializePair(string name) + { + if (!PlayerName.IsNullOrEmpty()) return false; + + if (CachedPlayer == null) throw new InvalidOperationException("CachedPlayer not initialized"); + _pluginWarnings ??= new() + { + ShownCustomizePlusWarning = _configService.Current.DisableOptionalPluginWarnings, + ShownHeelsWarning = _configService.Current.DisableOptionalPluginWarnings, + ShownPalettePlusWarning = _configService.Current.DisableOptionalPluginWarnings, + }; + + CachedPlayer.Initialize(name); + + ApplyLastReceivedData(); + + return true; + } + + public void MarkOffline() + { + _onlineUserIdentDto = null; + LastReceivedCharacterData = null; + CachedPlayer?.Dispose(); + CachedPlayer = null; + } + + public void RecreateCachedPlayer(OnlineUserIdentDto? dto = null) + { + if (dto == null && _onlineUserIdentDto == null) return; + if (dto != null) + { + _onlineUserIdentDto = dto; + } + + CachedPlayer?.Dispose(); + CachedPlayer = _cachedPlayerFactory(_onlineUserIdentDto!); + } + + public void SetNote(string note) + { + _serverConfigurationManager.SetNoteForUid(UserData.UID, note); + } + + internal void SetIsUploading() + { + CachedPlayer?.SetUploading(); + } + + private CharacterData? RemoveNotSyncedFiles(CharacterData? data) { _logger.LogTrace("Removing not synced files"); - if (data == null || (UserPair != null && UserPair.OtherPermissions.IsPaired())) + if (data == null) { - _logger.LogTrace("Nothing to remove or user is paired directly"); + _logger.LogTrace("Nothing to remove"); return data; } - bool disableAnimations = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations()); - bool disableSounds = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds()); + bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations()); + bool disableGroupAnimations = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations()); + + bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations); + + bool disableIndividualSounds = UserPair != null && (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds()); + bool disableGroupSounds = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds()); + + bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds); + + _logger.LogTrace("Individual Sounds: {disableIndividualSounds}, Individual Anims: {disableIndividualAnims}; " + + "Group Sounds: {disableGroupSounds}, Group Anims: {disableGroupAnims} => Disable Sounds: {disableSounds}, Disable Anims: {disableAnims}", + disableIndividualSounds, disableIndividualAnimations, disableGroupSounds, disableGroupAnimations, disableSounds, disableAnimations); if (disableAnimations || disableSounds) { - _logger.LogTrace($"Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}"); + _logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}", disableAnimations, disableSounds); foreach (var kvp in data.FileReplacements) { if (disableSounds) @@ -132,30 +170,4 @@ public class Pair return data; } - - public bool CachedPlayerExists => CachedPlayer?.CheckExistence() ?? false; - - private OnlineUserIdentDto? _onlineUserIdentDto = null; - private ApiController? _apiController = null; - - public void RecreateCachedPlayer(OnlineUserIdentDto? dto = null, ApiController? controller = null) - { - if ((dto == null && _onlineUserIdentDto == null) || (_apiController == null && controller == null)) return; - if (dto != null || controller != null) - { - _onlineUserIdentDto = dto; - _apiController = controller; - } - CachedPlayer?.Dispose(); - CachedPlayer = null; - CachedPlayer = _cachedPlayerFactory.Create(_onlineUserIdentDto!, _apiController!); - } - - public void MarkOffline() - { - _onlineUserIdentDto = null; - LastReceivedCharacterData = null; - CachedPlayer?.Dispose(); - CachedPlayer = null; - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Managers/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs similarity index 80% rename from MareSynchronos/Managers/PairManager.cs rename to MareSynchronos/PlayerData/Pairs/PairManager.cs index 4069a9f..e8628ba 100644 --- a/MareSynchronos/Managers/PairManager.cs +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -6,26 +6,26 @@ using MareSynchronos.API.Data.Comparer; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.User; -using MareSynchronos.Factories; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Models; +using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; -using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -namespace MareSynchronos.Managers; +namespace MareSynchronos.PlayerData.Pairs; -public class PairManager : MediatorSubscriberBase, IDisposable +public sealed class PairManager : DisposableMediatorSubscriberBase { private readonly ConcurrentDictionary _allClientPairs = new(UserDataComparer.Instance); private readonly ConcurrentDictionary _allGroups = new(GroupDataComparer.Instance); - private readonly PairFactory _pairFactory; private readonly MareConfigService _configurationService; + private readonly Func _pairFactory; + private Lazy> _directPairsInternal; - public PairManager(ILogger logger, PairFactory pairFactory, - MareConfigService configurationService, MareMediator mediator) : base(logger, mediator) + private Lazy>> _groupPairsInternal; + + public PairManager(ILogger logger, Func pairFactory, + MareConfigService configurationService, MareMediator mediator) : base(logger, mediator) { _pairFactory = pairFactory; _configurationService = configurationService; @@ -36,33 +36,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable _groupPairsInternal = GroupPairsLazy(); } - private void RecreateLazy() - { - _directPairsInternal = DirectPairsLazy(); - _groupPairsInternal = GroupPairsLazy(); - } - - private Lazy> _directPairsInternal; - private Lazy>> _groupPairsInternal; - public Dictionary> GroupPairs => _groupPairsInternal.Value; public List DirectPairs => _directPairsInternal.Value; - private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value).Where(k => k.UserPair != null).ToList()); - private Lazy>> GroupPairsLazy() - { - return new Lazy>>(() => - { - Dictionary> outDict = new(); - foreach (var group in _allGroups) - { - outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList(); - } - return outDict; - }); - } - - public List OnlineUserPairs => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.PlayerNameHash)).Select(p => p.Value).ToList(); - public List VisibleUsers => _allClientPairs.Where(p => p.Value.HasCachedPlayer).Select(p => p.Key).ToList(); + public Dictionary> GroupPairs => _groupPairsInternal.Value; public Pair? LastAddedUser { get; internal set; } @@ -72,33 +48,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable RecreateLazy(); } - public void RemoveGroup(GroupData data) - { - _allGroups.TryRemove(data, out _); - foreach (var item in _allClientPairs.ToList()) - { - foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).ToList()) - { - if (GroupDataComparer.Instance.Equals(grpPair.Group, data)) - { - _allClientPairs[item.Key].GroupPair.Remove(grpPair); - } - } - - if (!_allClientPairs[item.Key].HasAnyConnection()) - { - if (_allClientPairs.TryRemove(item.Key, out var pair)) - { - pair.MarkOffline(); - } - } - } - RecreateLazy(); - } - public void AddGroupPair(GroupPairFullInfoDto dto) { - if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Create(); + if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Invoke(); var group = _allGroups[dto.Group]; _allClientPairs[dto.User].GroupPair[group] = dto; @@ -109,7 +61,7 @@ public class PairManager : MediatorSubscriberBase, IDisposable { if (!_allClientPairs.ContainsKey(dto.User)) { - _allClientPairs[dto.User] = _pairFactory.Create(); + _allClientPairs[dto.User] = _pairFactory.Invoke(); } else { @@ -125,39 +77,24 @@ public class PairManager : MediatorSubscriberBase, IDisposable public void ClearPairs() { - _logger.LogDebug("Clearing all Pairs"); + Logger.LogDebug("Clearing all Pairs"); DisposePairs(); _allClientPairs.Clear(); _allGroups.Clear(); RecreateLazy(); } - public override void Dispose() - { - base.Dispose(); - DisposePairs(); - } - - private void DisposePairs(bool recreate = false) - { - _logger.LogDebug("Disposing all Pairs"); - foreach (var item in _allClientPairs) - { - if (recreate) - item.Value.RecreateCachedPlayer(); - else - item.Value.MarkOffline(); - } - RecreateLazy(); - } - public Pair? FindPair(PlayerCharacter? pChar) { if (pChar == null) return null; var hash = pChar.GetHash256(); - return OnlineUserPairs.Find(p => string.Equals(p.PlayerNameHash, hash, StringComparison.Ordinal)); + return GetOnlineUserPairs().Find(p => string.Equals(p.PlayerNameHash, hash, StringComparison.Ordinal)); } + public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.PlayerNameHash)).Select(p => p.Value).ToList(); + + public List GetVisibleUsers() => _allClientPairs.Where(p => p.Value.HasCachedPlayer).Select(p => p.Key).ToList(); + public void MarkPairOffline(UserData user) { if (_allClientPairs.TryGetValue(user, out var pair)) @@ -167,14 +104,14 @@ public class PairManager : MediatorSubscriberBase, IDisposable } } - public void MarkPairOnline(OnlineUserIdentDto dto, ApiController controller, bool sendNotif = true) + public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true) { if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto); var pair = _allClientPairs[dto.User]; if (pair.HasCachedPlayer) return; if (sendNotif && _configurationService.Current.ShowOnlineNotifications - && ((_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null) + && (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null || !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs) && (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote()) || !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs)) @@ -186,7 +123,7 @@ public class PairManager : MediatorSubscriberBase, IDisposable Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, 5000)); } - pair.RecreateCachedPlayer(dto, controller); + pair.RecreateCachedPlayer(dto); RecreateLazy(); } @@ -205,6 +142,24 @@ public class PairManager : MediatorSubscriberBase, IDisposable } } + public void RemoveGroup(GroupData data) + { + _allGroups.TryRemove(data, out _); + foreach (var item in _allClientPairs.ToList()) + { + foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList()) + { + _allClientPairs[item.Key].GroupPair.Remove(grpPair); + } + + if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair)) + { + pair.MarkOffline(); + } + } + RecreateLazy(); + } + public void RemoveGroupPair(GroupPairDto dto) { if (_allClientPairs.TryGetValue(dto.User, out var pair)) @@ -241,6 +196,14 @@ public class PairManager : MediatorSubscriberBase, IDisposable } } + public void SetGroupInfo(GroupInfoDto dto) + { + _allGroups[dto.Group].Group = dto.Group; + _allGroups[dto.Group].Owner = dto.Owner; + _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; + RecreateLazy(); + } + public void UpdatePairPermissions(UserPermissionsDto dto) { if (!_allClientPairs.TryGetValue(dto.User, out var pair)) @@ -251,10 +214,10 @@ public class PairManager : MediatorSubscriberBase, IDisposable if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); pair.UserPair.OtherPermissions = dto.Permissions; - if (!pair.UserPair.OtherPermissions.IsPaired()) - { - pair.ApplyLastReceivedData(); - } + + Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}", + pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds()); + pair.ApplyLastReceivedData(); } public void UpdateSelfPairPermissions(UserPermissionsDto dto) @@ -267,29 +230,38 @@ public class PairManager : MediatorSubscriberBase, IDisposable if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); pair.UserPair.OwnPermissions = dto.Permissions; + + Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}", + pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds()); + + pair.ApplyLastReceivedData(); } - private void DalamudUtilOnDelayedFrameworkUpdate() + internal void ReceiveUploadStatus(UserDto dto) { - foreach (Pair pair in _allClientPairs.Select(p => p.Value).Where(p => p.HasCachedPlayer).ToList()) + if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible) { - if (!pair.CachedPlayerExists) - { - pair.RecreateCachedPlayer(); - } + existingPair.SetIsUploading(); } } - private void DalamudUtilOnZoneSwitched() + internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) { - DisposePairs(true); + var group = _allGroups[dto.Group]; + _allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo; + RecreateLazy(); } - public void SetGroupInfo(GroupInfoDto dto) + internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto) { - _allGroups[dto.Group].Group = dto.Group; - _allGroups[dto.Group].Owner = dto.Owner; - _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; + var group = _allGroups[dto.Group]; + var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions; + _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions; + if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() + || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()) + { + _allClientPairs[dto.User].ApplyLastReceivedData(); + } RecreateLazy(); } @@ -307,17 +279,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable RecreateLazy(); } - internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto) + internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) { - var group = _allGroups[dto.Group]; - var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions; - _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions; - if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() - || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()) - { - _allClientPairs[dto.User].ApplyLastReceivedData(); - } - RecreateLazy(); + _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; } internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto) @@ -334,15 +298,60 @@ public class PairManager : MediatorSubscriberBase, IDisposable RecreateLazy(); } - internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) + protected override void Dispose(bool disposing) { - _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; + base.Dispose(disposing); + + DisposePairs(); } - internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) + private void DalamudUtilOnDelayedFrameworkUpdate() { - var group = _allGroups[dto.Group]; - _allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo; + foreach (Pair pair in _allClientPairs.Select(p => p.Value).Where(p => p.HasCachedPlayer).ToList()) + { + if (!pair.CachedPlayerExists) + { + pair.RecreateCachedPlayer(); + } + } + } + + private void DalamudUtilOnZoneSwitched() + { + DisposePairs(recreate: true); + } + + private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value).Where(k => k.UserPair != null).ToList()); + + private void DisposePairs(bool recreate = false) + { + Logger.LogDebug("Disposing all Pairs"); + foreach (var item in _allClientPairs) + { + if (recreate) + item.Value.RecreateCachedPlayer(); + else + item.Value.MarkOffline(); + } RecreateLazy(); } + + private Lazy>> GroupPairsLazy() + { + return new Lazy>>(() => + { + Dictionary> outDict = new(); + foreach (var group in _allGroups) + { + outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList(); + } + return outDict; + }); + } + + private void RecreateLazy() + { + _directPairsInternal = DirectPairsLazy(); + _groupPairsInternal = GroupPairsLazy(); + } } \ No newline at end of file diff --git a/MareSynchronos/Managers/CacheCreationService.cs b/MareSynchronos/PlayerData/Services/CacheCreationService.cs similarity index 67% rename from MareSynchronos/Managers/CacheCreationService.cs rename to MareSynchronos/PlayerData/Services/CacheCreationService.cs index dc3e65c..c50f8b9 100644 --- a/MareSynchronos/Managers/CacheCreationService.cs +++ b/MareSynchronos/PlayerData/Services/CacheCreationService.cs @@ -1,51 +1,50 @@ using MareSynchronos.API.Data.Enum; -using MareSynchronos.Factories; -using MareSynchronos.Mediator; -using MareSynchronos.Models; -using MareSynchronos.Utils; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Managers; +namespace MareSynchronos.PlayerData.Services; -public class CacheCreationService : MediatorSubscriberBase, IDisposable +public sealed class CacheCreationService : DisposableMediatorSubscriberBase { - private readonly CharacterDataFactory _characterDataFactory; - private Task? _cacheCreationTask; + private readonly SemaphoreSlim _cacheCreateLock = new(1); private readonly Dictionary _cachesToCreate = new(); - private readonly CharacterData _playerData = new(); + private readonly PlayerDataFactory _characterDataFactory; private readonly CancellationTokenSource _cts = new(); + private readonly CharacterData _playerData = new(); private readonly List _playerRelatedObjects = new(); + private Task? _cacheCreationTask; private CancellationTokenSource _palettePlusCts = new(); - private SemaphoreSlim _cacheCreateLock = new(1); - public CacheCreationService(ILogger logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, - CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) : base(logger, mediator) + public CacheCreationService(ILogger logger, MareMediator mediator, Func, bool, GameObjectHandler> gameObjectHandlerFactory, + PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator) { _characterDataFactory = characterDataFactory; Mediator.Subscribe(this, (msg) => { - var actualMsg = (CreateCacheForObjectMessage)msg; _cacheCreateLock.Wait(); - _cachesToCreate[actualMsg.ObjectToCreateFor.ObjectKind] = actualMsg.ObjectToCreateFor; + _cachesToCreate[msg.ObjectToCreateFor.ObjectKind] = msg.ObjectToCreateFor; _cacheCreateLock.Release(); }); _playerRelatedObjects.AddRange(new List() { - gameObjectHandlerFactory.Create(ObjectKind.Player, () => dalamudUtil.PlayerPointer, isWatched: true), - gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true), - gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), isWatched: true), - gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true), + gameObjectHandlerFactory(ObjectKind.Player, () => dalamudUtil.PlayerPointer, true), + gameObjectHandlerFactory(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), true), + gameObjectHandlerFactory(ObjectKind.Pet, () => dalamudUtil.GetPet(), true), + gameObjectHandlerFactory(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), true), }); Mediator.Subscribe(this, (msg) => { Task.Run(() => { - var actualMsg = (ClearCacheForObjectMessage)msg; - _playerData.FileReplacements.Remove(actualMsg.ObjectToCreateFor.ObjectKind); - _playerData.GlamourerString.Remove(actualMsg.ObjectToCreateFor.ObjectKind); + _playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind); + _playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind); Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); }); }); @@ -57,6 +56,14 @@ public class CacheCreationService : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, async (msg) => await AddPlayerCacheToCreate().ConfigureAwait(false)); } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _playerRelatedObjects.ForEach(p => p.Dispose()); + _cts.Dispose(); + } + private async Task AddPlayerCacheToCreate() { await _cacheCreateLock.WaitAsync().ConfigureAwait(false); @@ -101,31 +108,24 @@ public class CacheCreationService : MediatorSubscriberBase, IDisposable { await Task.Delay(100).ConfigureAwait(false); maxWaitingTime -= 100; - _logger.LogTrace("Waiting for Cache to be ready"); + Logger.LogTrace("Waiting for Cache to be ready"); } Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); } catch (Exception ex) { - _logger.LogCritical(ex, "Error during Cache Creation Processing"); + Logger.LogCritical(ex, "Error during Cache Creation Processing"); } finally { - _logger.LogDebug("Cache Creation complete"); + Logger.LogDebug("Cache Creation complete"); } }, _cts.Token); } else if (_cachesToCreate.Any()) { - _logger.LogDebug("Cache Creation stored until previous creation finished"); + Logger.LogDebug("Cache Creation stored until previous creation finished"); } } - - public override void Dispose() - { - base.Dispose(); - _playerRelatedObjects.ForEach(p => p.Dispose()); - _cts.Dispose(); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 405716d..bed06c7 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -1,109 +1,150 @@ -using Dalamud.Game.Command; -using Dalamud.Plugin; -using MareSynchronos.Factories; +using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState; -using Dalamud.Interface.ImGuiFileDialog; -using MareSynchronos.Managers; -using MareSynchronos.WebAPI; -using Dalamud.Interface.Windowing; -using MareSynchronos.UI; -using MareSynchronos.Utils; using Dalamud.Game.ClientState.Conditions; -using MareSynchronos.FileCache; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Command; using Dalamud.Game.Gui; -using MareSynchronos.Export; -using Dalamud.Data; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto.User; +using MareSynchronos.FileCache; +using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; +using MareSynchronos.PlayerData.Export; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.PlayerData.Services; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MareSynchronos; public sealed class Plugin : IDalamudPlugin { - private readonly MarePlugin _plugin; - public string Name => "Mare Synchronos"; - private readonly ILogger _pluginLogger; + private readonly CancellationTokenSource _pluginCts = new(); public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, DataManager gameData, - Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition, ChatGui chatGui) + Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition, ChatGui chatGui, + GameGui gameGui) { - IServiceCollection collection = new ServiceCollection(); - collection.AddLogging(o => + new HostBuilder() + .UseContentRoot(pluginInterface.ConfigDirectory.FullName) + .ConfigureLogging(lb => { - o.AddDalamudLogging(); - o.SetMinimumLevel(LogLevel.Trace); - }); + lb.ClearProviders(); + lb.AddDalamudLogging(); + lb.SetMinimumLevel(LogLevel.Trace); + }) + .ConfigureServices(collection => + { + collection.AddSingleton(new WindowSystem("MareSynchronos")); + collection.AddSingleton(); + collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true)); - // inject dalamud stuff - collection.AddSingleton(pluginInterface); - collection.AddSingleton(commandManager); - collection.AddSingleton(gameData); - collection.AddSingleton(framework); - collection.AddSingleton(objectTable); - collection.AddSingleton(clientState); - collection.AddSingleton(condition); - collection.AddSingleton(chatGui); - collection.AddSingleton(pluginInterface.UiBuilder); - collection.AddSingleton(new WindowSystem("MareSynchronos")); - collection.AddSingleton(); + // add mare related singletons + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService>(), + clientState, objectTable, framework, gameGui, condition, gameData, + s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton((s) => new IpcManager(s.GetRequiredService>(), + pluginInterface, s.GetRequiredService(), s.GetRequiredService())); - // add mare related stuff - collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true)); + collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), pluginInterface)); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); + // func factory method singletons + collection.AddSingleton(s => + new Func, bool, GameObjectHandler>((o, f, b) + => new GameObjectHandler(s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService(), + o, f, b))); + collection.AddSingleton(s => + new Func((o) + => new CachedPlayer(s.GetRequiredService>(), + o, + s.GetRequiredService, bool, GameObjectHandler>>(), + s.GetRequiredService(), + s.GetRequiredService>().Invoke(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService()))); + collection.AddSingleton(s => + new Func(() + => new Pair(s.GetRequiredService>(), + s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService()))); + collection.AddSingleton(s => + new Func(() + => new FileDownloadManager(s.GetRequiredService>(), + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService()))); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); + // add scoped services + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), + s.GetRequiredService(), s.GetServices(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new CommandManagerService(commandManager, 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(), + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - - var serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true, ValidateScopes = true }); - - _pluginLogger = serviceProvider.GetRequiredService>(); - _pluginLogger.LogDebug("Launching " + Name); - - serviceProvider.GetRequiredService().SetupWithLangCode("en"); - serviceProvider.GetRequiredService().UiBuilder.DisableGposeUiHide = true; - - var mediator = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); - _plugin = new MarePlugin(logger, serviceProvider, mediator); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + }) + .Build() + .RunAsync(_pluginCts.Token); } + public string Name => "Mare Synchronos"; + public void Dispose() { - _pluginLogger.LogTrace($"Disposing {GetType()}"); - _plugin.Dispose(); + _pluginCts.Cancel(); + _pluginCts.Dispose(); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs new file mode 100644 index 0000000..a6ff39b --- /dev/null +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -0,0 +1,97 @@ +using Dalamud.Game.Command; +using MareSynchronos.FileCache; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.Services; + +public sealed class CommandManagerService : IDisposable +{ + private const string _commandName = "/mare"; + + private readonly ApiController _apiController; + private readonly CommandManager _commandManager; + private readonly MareMediator _mediator; + private readonly PerformanceCollectorService _performanceCollectorService; + private readonly PeriodicFileScanner _periodicFileScanner; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly UiService _uiService; + + public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService, + UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, + ApiController apiController, MareMediator mediator) + { + _commandManager = commandManager; + _performanceCollectorService = performanceCollectorService; + _uiService = uiService; + _serverConfigurationManager = serverConfigurationManager; + _periodicFileScanner = periodicFileScanner; + _apiController = apiController; + _mediator = mediator; + + _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Mare Synchronos UI" + }); + } + + public void Dispose() + { + _commandManager.RemoveHandler(_commandName); + } + + private void OnCommand(string command, string args) + { + var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); + + if (splitArgs == null || splitArgs.Length == 0) + { + // Interpret this as toggling the UI + _uiService.ToggleUi(); + return; + } + + if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase)) + { + if (_serverConfigurationManager.CurrentServer == null) return; + var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch + { + "on" => false, + "off" => true, + _ => !_serverConfigurationManager.CurrentServer.FullPause, + } : !_serverConfigurationManager.CurrentServer.FullPause; + + if (fullPause != _serverConfigurationManager.CurrentServer.FullPause) + { + _serverConfigurationManager.CurrentServer.FullPause = fullPause; + _serverConfigurationManager.Save(); + _ = _apiController.CreateConnections(); + } + } + else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase)) + { + _mediator.Publish(new UiToggleMessage(typeof(GposeUi))); + } + else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase)) + { + _periodicFileScanner.InvokeScan(forced: true); + } + else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase)) + { + if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds)) + { + _performanceCollectorService.PrintPerformanceStats(limitBySeconds); + } + else + { + _performanceCollectorService.PrintPerformanceStats(); + } + } + else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase)) + { + _mediator.PrintSubscriberInfo(); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Services/DalamudUtilService.cs similarity index 84% rename from MareSynchronos/Utils/DalamudUtil.cs rename to MareSynchronos/Services/DalamudUtilService.cs index ef9225b..a1b7d84 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -3,31 +3,144 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; -using MareSynchronos.Mediator; -using MareSynchronos.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Diagnostics; +using System.Numerics; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -namespace MareSynchronos.Utils; +namespace MareSynchronos.Services; -public class DalamudUtil : IDisposable +public class DalamudUtilService : IHostedService { - private readonly ILogger _logger; private readonly ClientState _clientState; - private readonly ObjectTable _objectTable; - private readonly Framework _framework; private readonly Condition _condition; + private readonly Framework _framework; + private readonly GameGui _gameGui; + private readonly ILogger _logger; private readonly MareMediator _mediator; - private readonly PerformanceCollector _performanceCollector; + private readonly ObjectTable _objectTable; + private readonly PerformanceCollectorService _performanceCollector; private uint? _classJobId = 0; private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; private bool _sentBetweenAreas = false; + + public DalamudUtilService(ILogger logger, ClientState clientState, ObjectTable objectTable, Framework framework, + GameGui gameGui, Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollectorService performanceCollector) + { + _logger = logger; + _clientState = clientState; + _objectTable = objectTable; + _framework = framework; + _gameGui = gameGui; + _condition = condition; + _mediator = mediator; + _performanceCollector = performanceCollector; + WorldData = new(() => + { + return gameData.GetExcelSheet(Dalamud.ClientLanguage.English)! + .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) + .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + }); + } + + public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget; + public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex]; public bool IsInCutscene { get; private set; } = false; - public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; public bool IsInGpose { get; private set; } = false; + public bool IsLoggedIn { get; private set; } + public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; + public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; + + public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; + + public string PlayerNameHashed => (PlayerName + _clientState.LocalPlayer!.HomeWorld.Id).GetHash256(); + + public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; + + public Lazy> WorldData { get; private set; } + + public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id; + + public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) + { + return obj != null && obj.IsValid(); + } + + public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) + { + return _objectTable.CreateObjectReference(reference); + } + + public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) + { + var objTableObj = _objectTable[index]; + if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; + return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; + } + + public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + playerPointer ??= PlayerPointer; + return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer); + } + + public int? GetIndexFromObjectTableByName(string characterName) + { + for (int i = 0; i < _objectTable.Length; i++) + { + if (_objectTable[i] == null) continue; + if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i; + } + + return null; + } + + public unsafe IntPtr GetMinion(IntPtr? playerPointer = null) + { + playerPointer ??= PlayerPointer; + return (IntPtr)((Character*)playerPointer)->CompanionObject; + } + + public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null) + { + playerPointer ??= PlayerPointer; + if (playerPointer == IntPtr.Zero) return IntPtr.Zero; + return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); + } + + public unsafe IntPtr GetPet(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + playerPointer ??= PlayerPointer; + return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer); + } + + public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) + { + foreach (var item in _objectTable) + { + if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item; + } + + return null; + } + + public List GetPlayerCharacters() + { + return _objectTable.Where(obj => + obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && + !string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Cast().ToList(); + } public unsafe bool IsGameObjectPresent(IntPtr key) { @@ -42,30 +155,92 @@ public class DalamudUtil : IDisposable return false; } - public DalamudUtil(ILogger logger, ClientState clientState, ObjectTable objectTable, Framework framework, - Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollector performanceCollector) + public async Task RunOnFrameworkThread(Action act) + { + _logger.LogTrace("Running Action on framework thread: {act}", act); + await _framework.RunOnFrameworkThread(act).ConfigureAwait(false); + } + + public async Task RunOnFrameworkThread(Func func) + { + _logger.LogTrace("Running Func on framework thread: {func}", func); + return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); + } + + public Task StartAsync(CancellationToken cancellationToken) { - _logger = logger; - _clientState = clientState; - _objectTable = objectTable; - _framework = framework; - _condition = condition; - _mediator = mediator; - _performanceCollector = performanceCollector; _framework.Update += FrameworkOnUpdate; if (IsLoggedIn) { _classJobId = _clientState.LocalPlayer!.ClassJob.Id; } - WorldData = new(() => - { - return gameData.GetExcelSheet(Dalamud.ClientLanguage.English)! - .Where(w => w.IsPublic && !w.Name.RawData.IsEmpty) - .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); - }); + + return Task.CompletedTask; } - public Lazy> WorldData { get; private set; } + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("Stopping {type}", GetType()); + + _framework.Update -= FrameworkOnUpdate; + return Task.CompletedTask; + } + + public Vector2 WorldToScreen(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) + { + if (obj == null) return Vector2.Zero; + return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero; + } + + public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null) + { + if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return; + + logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); + + const int tick = 250; + int curWaitTime = 0; + try + { + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while ((!ct?.IsCancellationRequested ?? true) + && curWaitTime < timeOut + && await handler.IsBeingDrawnRunOnFramework().ConfigureAwait(true)) // 0b100000000000 is "still rendering" or something + { + logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); + curWaitTime += tick; + await Task.Delay(tick).ConfigureAwait(true); + } + + logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); + } + catch (NullReferenceException ex) + { + logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); + } + catch (AccessViolationException ex) + { + logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); + } + } + + public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000) + { + Thread.Sleep(500); + var obj = (GameObject*)characterAddress; + const int tick = 250; + int curWaitTime = 0; + _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) + { + _logger.LogTrace($"Waiting for gpose actor to finish drawing"); + curWaitTime += tick; + Thread.Sleep(tick); + } + + Thread.Sleep(tick * 2); + } private void FrameworkOnUpdate(Framework framework) { @@ -93,7 +268,6 @@ public class DalamudUtil : IDisposable IsInCutscene = true; _mediator.Publish(new CutsceneStartMessage()); _mediator.Publish(new HaltScanMessage("Cutscene")); - } else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) { @@ -160,163 +334,4 @@ public class DalamudUtil : IDisposable _delayedFrameworkUpdateCheck = DateTime.Now; } - - public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) - { - return _objectTable.CreateObjectReference(reference); - } - - public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget; - - public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex]; - - public bool IsLoggedIn { get; private set; } - - public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); - - public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) - { - return obj != null && obj.IsValid(); - } - - public unsafe IntPtr GetMinion(IntPtr? playerPointer = null) - { - playerPointer ??= PlayerPointer; - return (IntPtr)((Character*)playerPointer)->CompanionObject; - } - - public unsafe IntPtr GetPet(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - playerPointer ??= PlayerPointer; - return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer); - } - - public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - playerPointer ??= PlayerPointer; - return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer); - } - - public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null) - { - playerPointer ??= PlayerPointer; - if (playerPointer == IntPtr.Zero) return IntPtr.Zero; - return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); - } - - public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; - public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id; - - public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; - - public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; - - public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); - - public List GetPlayerCharacters() - { - return _objectTable.Where(obj => - obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && - !string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Cast().ToList(); - } - - public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) - { - var objTableObj = _objectTable[index]; - if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; - return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; - } - - public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) - { - foreach (var item in _objectTable) - { - if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item; - } - - return null; - } - - public int? GetIndexFromObjectTableByName(string characterName) - { - for (int i = 0; i < _objectTable.Length; i++) - { - if (_objectTable[i] == null) continue; - if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i; - } - - return null; - } - - public async Task RunOnFrameworkThread(Action act) - { - _logger.LogTrace("Running Action on framework thread: {act}", act); - await _framework.RunOnFrameworkThread(act).ConfigureAwait(false); - } - - public async Task RunOnFrameworkThread(Func func) - { - _logger.LogTrace("Running Func on framework thread: {func}", func); - return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); - } - - public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null) - { - if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return; - - logger.LogTrace($"[{redrawId}] Starting wait for {handler} to draw"); - - const int tick = 250; - int curWaitTime = 0; - try - { - // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while ((!ct?.IsCancellationRequested ?? true) - && curWaitTime < timeOut - && await handler.IsBeingDrawnRunOnFramework().ConfigureAwait(true)) // 0b100000000000 is "still rendering" or something - { - logger.LogTrace($"[{redrawId}] Waiting for {handler} to finish drawing"); - curWaitTime += tick; - await Task.Delay(tick).ConfigureAwait(true); - } - - logger.LogTrace($"[{redrawId}] Finished drawing after {curWaitTime}ms"); - } - catch (NullReferenceException ex) - { - logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?"); - } - catch (AccessViolationException ex) - { - logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?"); - } - } - - public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000) - { - Thread.Sleep(500); - var obj = (GameObject*)characterAddress; - const int tick = 250; - int curWaitTime = 0; - _logger.LogTrace("RenderFlags:" + obj->RenderFlags.ToString("X")); - // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) - { - _logger.LogTrace($"Waiting for gpose actor to finish drawing"); - curWaitTime += tick; - Thread.Sleep(tick); - } - - Thread.Sleep(tick * 2); - } - - public void Dispose() - { - _logger.LogTrace($"Disposing {GetType()}"); - _framework.Update -= FrameworkOnUpdate; - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs new file mode 100644 index 0000000..f76e04e --- /dev/null +++ b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Mediator; + +public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable +{ + protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator) + { + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this); + UnsubscribeAll(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs b/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs new file mode 100644 index 0000000..9f03cfa --- /dev/null +++ b/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.Services.Mediator; + +public interface IMediatorSubscriber +{ + MareMediator Mediator { get; } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/IMessage.cs b/MareSynchronos/Services/Mediator/IMessage.cs new file mode 100644 index 0000000..d7306aa --- /dev/null +++ b/MareSynchronos/Services/Mediator/IMessage.cs @@ -0,0 +1,3 @@ +namespace MareSynchronos.Services.Mediator; + +public interface IMessage { } \ No newline at end of file diff --git a/MareSynchronos/Mediator/MareMediator.cs b/MareSynchronos/Services/Mediator/MareMediator.cs similarity index 72% rename from MareSynchronos/Mediator/MareMediator.cs rename to MareSynchronos/Services/Mediator/MareMediator.cs index 81c9af4..3ac9a0a 100644 --- a/MareSynchronos/Mediator/MareMediator.cs +++ b/MareSynchronos/Services/Mediator/MareMediator.cs @@ -1,36 +1,78 @@ -using MareSynchronos.Utils; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System.Text; -namespace MareSynchronos.Mediator; +namespace MareSynchronos.Services.Mediator; -public class MareMediator : IDisposable +public sealed class MareMediator : IDisposable { - private class SubscriberAction - { - public IMediatorSubscriber Subscriber { get; } - public Action Action { get; } - - public SubscriberAction(IMediatorSubscriber subscriber, Action action) - { - Subscriber = subscriber; - Action = action; - } - } - - private readonly Dictionary> _subscriberDict = new(); - private readonly ILogger _logger; - private readonly PerformanceCollector _performanceCollector; private readonly object _addRemoveLock = new(); + private readonly Dictionary _lastErrorTime = new(); - public MareMediator(ILogger logger, PerformanceCollector performanceCollector) + private readonly ILogger _logger; + + private readonly PerformanceCollectorService _performanceCollector; + + private readonly Dictionary> _subscriberDict = new(); + + public MareMediator(ILogger logger, PerformanceCollectorService performanceCollector) { _logger = logger; _performanceCollector = performanceCollector; } - public void Subscribe(IMediatorSubscriber subscriber, Action action) where T : IMessage + public void Dispose() + { + _logger.LogTrace("Disposing {type}", GetType()); + _subscriberDict.Clear(); + GC.SuppressFinalize(this); + } + + public void PrintSubscriberInfo() + { + foreach (var kvp in _subscriberDict.SelectMany(c => c.Value.Select(v => v)) + .DistinctBy(p => p.Subscriber).OrderBy(p => p.Subscriber.GetType().FullName, StringComparer.Ordinal).ToList()) + { + _logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().Name, kvp.Subscriber.ToString()); + StringBuilder sb = new(); + sb.Append("=> "); + foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == kvp.Subscriber)).ToList()) + { + sb.Append(item.Key.Name).Append(", "); + } + + if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal)) + _logger.LogInformation("{sb}", sb.ToString()); + _logger.LogInformation("---"); + } + } + + public void Publish(T message) where T : IMessage + { + if (_subscriberDict.TryGetValue(message.GetType(), out HashSet? subscribers) && subscribers != null && subscribers.Any()) + { + _performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}", () => + { + foreach (SubscriberAction subscriber in subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet()) + { + try + { + _performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}", () => ((Action)subscriber.Action).Invoke(message)); + } + catch (Exception ex) + { + if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow) + continue; + + _logger.LogCritical(ex, "Error executing {type} for subscriber {subscriber}", message.GetType().Name, subscriber.Subscriber.GetType().Name); + _lastErrorTime[subscriber] = DateTime.UtcNow; + } + } + }); + } + } + + public void Subscribe(IMediatorSubscriber subscriber, Action action) where T : IMessage { lock (_addRemoveLock) { @@ -41,7 +83,7 @@ public class MareMediator : IDisposable throw new InvalidOperationException("Already subscribed"); } - _logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T), subscriber); + _logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name); } } @@ -56,33 +98,6 @@ public class MareMediator : IDisposable } } - public void Publish(IMessage message) - { - if (_subscriberDict.TryGetValue(message.GetType(), out HashSet? subscribers) && subscribers != null && subscribers.Any()) - { - _performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}", () => - { - foreach (SubscriberAction subscriber in subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet()) - { - try - { - _performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}", () => subscriber.Action.Invoke(message)); - } - catch (Exception ex) - { - if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime)) - { - if (lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow) continue; - } - - _logger.LogCritical(ex, "Error executing {type} for subscriber {subscriber}", message.GetType().Name, subscriber.Subscriber.GetType().Name); - _lastErrorTime[subscriber] = DateTime.UtcNow; - } - } - }); - } - } - internal void UnsubscribeAll(IMediatorSubscriber subscriber) { lock (_addRemoveLock) @@ -92,41 +107,25 @@ public class MareMediator : IDisposable int unSubbed = _subscriberDict[kvp.Key]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0; if (unSubbed > 0) { - _logger.LogDebug("{sub} unsubscribed from {msg}", subscriber, kvp.Key.Name); - _logger.LogTrace("Remaining Subscribers:"); - foreach (var item in _subscriberDict[kvp.Key]) + _logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Key.Name); + if (_subscriberDict[kvp.Key].Any()) { - _logger.LogTrace("{Key}: {item}", kvp.Key, item.Subscriber); + _logger.LogTrace("Remaining Subscribers: {item}", string.Join(", ", _subscriberDict[kvp.Key].Select(k => k.Subscriber.GetType().Name))); } } } } } - public void PrintSubscriberInfo() + private sealed class SubscriberAction { - foreach (var kvp in _subscriberDict.ToList().SelectMany(c => c.Value.Select(v => v)) - .DistinctBy(p => p.Subscriber).OrderBy(p => p.Subscriber.GetType().FullName, StringComparer.Ordinal)) + public SubscriberAction(IMediatorSubscriber subscriber, object action) { - _logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().FullName, kvp.Subscriber.ToString()); - StringBuilder sb = new(); - sb.Append("=> "); - foreach (var item in _subscriberDict.ToList()) - { - if (item.Value.Any(v => v.Subscriber == kvp.Subscriber)) - { - sb.Append(item.Key.Name + ", "); - } - } - if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal)) - _logger.LogInformation("{sb}", sb.ToString()); - _logger.LogInformation("---"); + Subscriber = subscriber; + Action = action; } - } - public void Dispose() - { - _logger.LogTrace("Disposing {type}", GetType()); - _subscriberDict.Clear(); + public object Action { get; } + public IMediatorSubscriber Subscriber { get; } } } \ No newline at end of file diff --git a/MareSynchronos/Mediator/MediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs similarity index 50% rename from MareSynchronos/Mediator/MediatorSubscriberBase.cs rename to MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs index c8ad718..f45fee4 100644 --- a/MareSynchronos/Mediator/MediatorSubscriberBase.cs +++ b/MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs @@ -1,20 +1,23 @@ using Microsoft.Extensions.Logging; -namespace MareSynchronos.Mediator; +namespace MareSynchronos.Services.Mediator; public abstract class MediatorSubscriberBase : IMediatorSubscriber { - protected ILogger _logger { get; } - public MareMediator Mediator { get; } protected MediatorSubscriberBase(ILogger logger, MareMediator mediator) { - _logger = logger; + Logger = logger; + + Logger.LogTrace("Creating {type} ({this})", GetType().Name, this); Mediator = mediator; } - public virtual void Dispose() + public MareMediator Mediator { get; } + protected ILogger Logger { get; } + + protected void UnsubscribeAll() { - _logger.LogTrace("Disposing {type} ({this})", GetType(), this); + Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this); Mediator.UnsubscribeAll(this); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs similarity index 76% rename from MareSynchronos/Mediator/Messages.cs rename to MareSynchronos/Services/Mediator/Messages.cs index 720f6fe..cdb3533 100644 --- a/MareSynchronos/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -1,7 +1,9 @@ using Dalamud.Interface.Internal.Notifications; -using MareSynchronos.Models; +using MareSynchronos.API.Dto; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.WebAPI.Files.Models; -namespace MareSynchronos.Mediator; +namespace MareSynchronos.Services.Mediator; #pragma warning disable MA0048 // File name must match type name public record SwitchToIntroUiMessage : IMessage; @@ -19,16 +21,16 @@ public record GposeStartMessage : IMessage; public record GposeEndMessage : IMessage; public record CutsceneEndMessage : IMessage; public record CutsceneFrameworkUpdateMessage : IMessage; -public record ConnectedMessage : IMessage; +public record ConnectedMessage(ConnectionDto Connection) : IMessage; public record DisconnectedMessage : IMessage; public record PenumbraModSettingChangedMessage : IMessage; public record PenumbraInitializedMessage : IMessage; public record PenumbraDisposedMessage : IMessage; public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : IMessage; -public record HeelsOffsetMessage() : IMessage; +public record HeelsOffsetMessage : IMessage; public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : IMessage; -public record CustomizePlusMessage() : IMessage; -public record PalettePlusMessage() : IMessage; +public record CustomizePlusMessage : IMessage; +public record PalettePlusMessage : IMessage; public record PlayerChangedMessage(API.Data.CharacterData Data) : IMessage; public record CharacterChangedMessage(GameObjectHandler GameObjectHandler) : IMessage; public record TransientResourceChangedMessage(IntPtr Address) : IMessage; @@ -46,4 +48,10 @@ public record PenumbraEndRedrawMessage(IntPtr Address) : IMessage; public record HubReconnectingMessage(Exception? Exception) : IMessage; public record HubReconnectedMessage(string? Arg) : IMessage; public record HubClosedMessage(Exception? Exception) : IMessage; +public record DownloadReadyMessage(Guid RequestId) : IMessage; +public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary DownloadStatus) : IMessage; +public record DownloadFinishedMessage(GameObjectHandler DownloadId) : IMessage; +public record UiToggleMessage(Type UiType) : IMessage; +public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : IMessage; + #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs new file mode 100644 index 0000000..1d7424d --- /dev/null +++ b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs @@ -0,0 +1,45 @@ +using Dalamud.Interface.Windowing; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Mediator; + +public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable +{ + protected readonly ILogger _logger; + + protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name) : base(name) + { + _logger = logger; + Mediator = mediator; + + _logger.LogTrace("Creating {type}", GetType()); + + Mediator.Subscribe(this, (msg) => + { + if (msg.UiType == GetType()) + { + Toggle(); + } + }); + } + + public MareMediator Mediator { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public virtual Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + protected virtual void Dispose(bool disposing) + { + _logger.LogTrace("Disposing {type}", GetType()); + + Mediator.UnsubscribeAll(this); + } +} \ No newline at end of file diff --git a/MareSynchronos/Managers/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs similarity index 92% rename from MareSynchronos/Managers/NotificationService.cs rename to MareSynchronos/Services/NotificationService.cs index 975dbe8..89cbbf2 100644 --- a/MareSynchronos/Managers/NotificationService.cs +++ b/MareSynchronos/Services/NotificationService.cs @@ -4,84 +4,30 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.Mediator; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Managers; -public class NotificationService : MediatorSubscriberBase +namespace MareSynchronos.Services; + +public class NotificationService : DisposableMediatorSubscriberBase { - private readonly UiBuilder _uiBuilder; private readonly ChatGui _chatGui; private readonly MareConfigService _configurationService; + private readonly UiBuilder _uiBuilder; public NotificationService(ILogger logger, MareMediator mediator, UiBuilder uiBuilder, ChatGui chatGui, MareConfigService configurationService) : base(logger, mediator) { _uiBuilder = uiBuilder; _chatGui = chatGui; _configurationService = configurationService; - mediator.Subscribe(this, (msg) => ShowNotification((NotificationMessage)msg)); + + Mediator.Subscribe(this, ShowNotification); } - private void ShowNotification(NotificationMessage msg) + private void PrintErrorChat(string? message) { - _logger.LogInformation(msg.ToString()); - - switch (msg.Type) - { - case NotificationType.Info: - case NotificationType.Success: - case NotificationType.None: - ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); - break; - case NotificationType.Warning: - ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); - break; - case NotificationType.Error: - ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); - break; - } - } - - private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) - { - switch (location) - { - case NotificationLocation.Toast: - ShowToast(msg); - break; - case NotificationLocation.Chat: - ShowChat(msg); - break; - case NotificationLocation.Both: - ShowToast(msg); - ShowChat(msg); - break; - case NotificationLocation.Nowhere: - break; - } - } - - private void ShowToast(NotificationMessage msg) - { - _uiBuilder.AddNotification(msg.Message ?? string.Empty, "[Mare Synchronos] " + msg.Title, msg.Type, msg.TimeShownOnScreen); - } - - private void ShowChat(NotificationMessage msg) - { - switch (msg.Type) - { - case NotificationType.Info: - case NotificationType.Success: - case NotificationType.None: - PrintInfoChat(msg.Message); - break; - case NotificationType.Warning: - PrintWarnChat(msg.Message); - break; - case NotificationType.Error: - PrintErrorChat(msg.Message); - break; - } + SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message); + _chatGui.PrintError(se.BuiltString); } private void PrintInfoChat(string? message) @@ -96,9 +42,72 @@ public class NotificationService : MediatorSubscriberBase _chatGui.Print(se.BuiltString); } - private void PrintErrorChat(string? message) + private void ShowChat(NotificationMessage msg) { - SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message); - _chatGui.PrintError(se.BuiltString); + switch (msg.Type) + { + case NotificationType.Info: + case NotificationType.Success: + case NotificationType.None: + PrintInfoChat(msg.Message); + break; + + case NotificationType.Warning: + PrintWarnChat(msg.Message); + break; + + case NotificationType.Error: + PrintErrorChat(msg.Message); + break; + } } -} + + private void ShowNotification(NotificationMessage msg) + { + Logger.LogInformation("{msg}", msg.ToString()); + + switch (msg.Type) + { + case NotificationType.Info: + case NotificationType.Success: + case NotificationType.None: + ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); + break; + + case NotificationType.Warning: + ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); + break; + + case NotificationType.Error: + ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); + break; + } + } + + private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) + { + switch (location) + { + case NotificationLocation.Toast: + ShowToast(msg); + break; + + case NotificationLocation.Chat: + ShowChat(msg); + break; + + case NotificationLocation.Both: + ShowToast(msg); + ShowChat(msg); + break; + + case NotificationLocation.Nowhere: + break; + } + } + + private void ShowToast(NotificationMessage msg) + { + _uiBuilder.AddNotification(msg.Message ?? string.Empty, "[Mare Synchronos] " + msg.Title, msg.Type, msg.TimeShownOnScreen); + } +} \ No newline at end of file diff --git a/MareSynchronos/Utils/PerformanceCollector.cs b/MareSynchronos/Services/PerformanceCollectorService.cs similarity index 88% rename from MareSynchronos/Utils/PerformanceCollector.cs rename to MareSynchronos/Services/PerformanceCollectorService.cs index 07e744d..47700cc 100644 --- a/MareSynchronos/Utils/PerformanceCollector.cs +++ b/MareSynchronos/Services/PerformanceCollectorService.cs @@ -1,55 +1,26 @@ using MareSynchronos.MareConfiguration; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.Text; -namespace MareSynchronos.Utils; +namespace MareSynchronos.Services; -public class PerformanceCollector : IDisposable +public sealed class PerformanceCollectorService : IHostedService { - private readonly ConcurrentDictionary>> _performanceCounters = new(StringComparer.Ordinal); - private readonly ILogger _logger; - private readonly MareConfigService _mareConfigService; private const string _counterSplit = "=>"; + private readonly ILogger _logger; + private readonly MareConfigService _mareConfigService; + private readonly ConcurrentDictionary>> _performanceCounters = new(StringComparer.Ordinal); private readonly CancellationTokenSource _periodicLogPruneTask = new(); - public PerformanceCollector(ILogger logger, MareConfigService mareConfigService) + public PerformanceCollectorService(ILogger logger, MareConfigService mareConfigService) { _logger = logger; _mareConfigService = mareConfigService; - _ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token); - } - - public void Dispose() - { - _logger.LogTrace("Disposing {this}", GetType()); - _periodicLogPruneTask.Cancel(); - } - - private async Task PeriodicLogPrune() - { - while (!_periodicLogPruneTask.Token.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTask.Token).ConfigureAwait(false); - - foreach (var entries in _performanceCounters.ToList()) - { - try - { - var last = entries.Value.ToList().Last(); - if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now)) - { - _performanceCounters.Remove(entries.Key, out _); - } - } - catch (Exception e) - { - _logger.LogDebug("Error removing performance counter {counter}", entries.Key); - } - } - } } public T LogPerformance(object sender, string counterName, Func func) @@ -68,10 +39,6 @@ public class PerformanceCollector : IDisposable { return func.Invoke(); } - catch - { - throw; - } finally { st.Stop(); @@ -95,16 +62,23 @@ public class PerformanceCollector : IDisposable { act.Invoke(); } - catch - { - throw; - } finally { st.Stop(); list.Add(new(TimeOnly.FromDateTime(DateTime.Now), st.ElapsedTicks)); } + } + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _periodicLogPruneTask.Cancel(); + return Task.CompletedTask; } internal void PrintPerformanceStats(int limitBySeconds = 0) @@ -138,7 +112,7 @@ public class PerformanceCollector : IDisposable sb.Append("-Counter Name".PadRight(longestCounterName, '-')); sb.AppendLine(); var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList(); - var previousCaller = orderedData.First().Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; + var previousCaller = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; foreach (var entry in orderedData) { var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; @@ -196,4 +170,28 @@ public class PerformanceCollector : IDisposable sb.Append("".PadRight(longestCounterName, '-')); sb.AppendLine(); } -} + + private async Task PeriodicLogPrune() + { + while (!_periodicLogPruneTask.Token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTask.Token).ConfigureAwait(false); + + foreach (var entries in _performanceCounters.ToList()) + { + try + { + var last = entries.Value.ToList()[^1]; + if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now)) + { + _performanceCounters.Remove(entries.Key, out _); + } + } + catch (Exception e) + { + _logger.LogDebug(e, "Error removing performance counter {counter}", entries.Key); + } + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Models/JwtCache.cs b/MareSynchronos/Services/ServerConfiguration/JwtCache.cs similarity index 61% rename from MareSynchronos/Models/JwtCache.cs rename to MareSynchronos/Services/ServerConfiguration/JwtCache.cs index cc06809..c28a86f 100644 --- a/MareSynchronos/Models/JwtCache.cs +++ b/MareSynchronos/Services/ServerConfiguration/JwtCache.cs @@ -1,3 +1,3 @@ -namespace MareSynchronos.Models; +namespace MareSynchronos.Services.ServerConfiguration; public record JwtCache(string ApiUrl, string PlayerName, uint WorldId, string SecretKey); diff --git a/MareSynchronos/Managers/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs similarity index 96% rename from MareSynchronos/Managers/ServerConfigurationManager.cs rename to MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index 807f9b8..1b980be 100644 --- a/MareSynchronos/Managers/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -1,38 +1,22 @@ using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.Models; -using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; using System.Diagnostics; -namespace MareSynchronos.Managers; +namespace MareSynchronos.Services.ServerConfiguration; public class ServerConfigurationManager { - private readonly Dictionary _tokenDictionary = new(); - private readonly ILogger _logger; private readonly ServerConfigService _configService; - private readonly ServerTagConfigService _serverTagConfig; + private readonly DalamudUtilService _dalamudUtil; + private readonly ILogger _logger; private readonly NotesConfigService _notesConfig; - private readonly DalamudUtil _dalamudUtil; - - public string CurrentApiUrl => string.IsNullOrEmpty(_configService.Current.CurrentServer) ? ApiController.MainServiceUri : _configService.Current.CurrentServer; - public ServerStorage? CurrentServer => (_configService.Current.ServerStorage.ContainsKey(CurrentApiUrl) ? _configService.Current.ServerStorage[CurrentApiUrl] : null); - private ServerTagStorage CurrentServerTagStorage() - { - TryCreateCurrentServerTagStorage(); - return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl]; - } - - private ServerNotesStorage CurrentNotesStorage() - { - TryCreateCurrentNotesStorage(); - return _notesConfig.Current.ServerNotes[CurrentApiUrl]; - } + private readonly ServerTagConfigService _serverTagConfig; + private readonly Dictionary _tokenDictionary = new(); public ServerConfigurationManager(ILogger logger, ServerConfigService configService, - ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtil dalamudUtil) + ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil) { _logger = logger; _configService = configService; @@ -41,58 +25,14 @@ public class ServerConfigurationManager _dalamudUtil = dalamudUtil; } - public bool HasValidConfig() - { - return CurrentServer != null; - } - - public string[] GetServerApiUrls() - { - return _configService.Current.ServerStorage.Keys.ToArray(); - } - - public string[] GetServerNames() - { - return _configService.Current.ServerStorage.Values.Select(v => v.ServerName).ToArray(); - } - - public ServerStorage GetServerByIndex(int idx) - { - try - { - return _configService.Current.ServerStorage.ElementAt(idx).Value; - } - catch - { - _configService.Current.CurrentServer = ApiController.MainServiceUri; - if (!_configService.Current.ServerStorage.ContainsKey(ApiController.MainServer)) - { - _configService.Current.ServerStorage.Add(_configService.Current.CurrentServer, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer }); - } - Save(); - return CurrentServer!; - } - } + public string CurrentApiUrl => string.IsNullOrEmpty(_configService.Current.CurrentServer) ? ApiController.MainServiceUri : _configService.Current.CurrentServer; + public ServerStorage? CurrentServer => _configService.Current.ServerStorage.TryGetValue(CurrentApiUrl, out ServerStorage? value) ? value : null; public int GetCurrentServerIndex() { return Array.IndexOf(_configService.Current.ServerStorage.Keys.ToArray(), CurrentApiUrl); } - public void Save() - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - _logger.LogDebug(caller + " Calling config save"); - _configService.Save(); - } - - public void SelectServer(int idx) - { - _configService.Current.CurrentServer = GetServerByIndex(idx).ServerUri; - CurrentServer!.FullPause = false; - Save(); - } - public string? GetSecretKey(int serverIdx = -1) { ServerStorage? currentServer; @@ -128,6 +68,34 @@ public class ServerConfigurationManager return null; } + public string[] GetServerApiUrls() + { + return _configService.Current.ServerStorage.Keys.ToArray(); + } + + public ServerStorage GetServerByIndex(int idx) + { + try + { + return _configService.Current.ServerStorage.ElementAt(idx).Value; + } + catch + { + _configService.Current.CurrentServer = ApiController.MainServiceUri; + if (!_configService.Current.ServerStorage.ContainsKey(ApiController.MainServer)) + { + _configService.Current.ServerStorage.Add(_configService.Current.CurrentServer, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer }); + } + Save(); + return CurrentServer!; + } + } + + public string[] GetServerNames() + { + return _configService.Current.ServerStorage.Values.Select(v => v.ServerName).ToArray(); + } + public string? GetToken() { var charaName = _dalamudUtil.PlayerName; @@ -142,6 +110,18 @@ public class ServerConfigurationManager return null; } + public bool HasValidConfig() + { + return CurrentServer != null; + } + + public void Save() + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + _logger.LogDebug(caller + " Calling config save"); + _configService.Save(); + } + public void SaveToken(string token) { var charaName = _dalamudUtil.PlayerName; @@ -151,6 +131,13 @@ public class ServerConfigurationManager _tokenDictionary[new JwtCache(CurrentApiUrl, charaName, worldId, secretKey)] = token; } + public void SelectServer(int idx) + { + _configService.Current.CurrentServer = GetServerByIndex(idx).ServerUri; + CurrentServer!.FullPause = false; + Save(); + } + internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1, bool addLastSecretKey = false) { if (serverSelectionIndex == -1) serverSelectionIndex = GetCurrentServerIndex(); @@ -171,11 +158,10 @@ public class ServerConfigurationManager Save(); } - internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item) + internal void AddOpenPairTag(string tag) { - var server = GetServerByIndex(serverSelectionIndex); - server.Authentications.Remove(item); - Save(); + CurrentServerTagStorage().OpenPairTags.Add(tag); + _serverTagConfig.Save(); } internal void AddServer(ServerStorage serverStorage) @@ -184,71 +170,12 @@ public class ServerConfigurationManager Save(); } - internal void DeleteServer(ServerStorage selectedServer) - { - _configService.Current.ServerStorage.Remove(selectedServer.ServerUri); - Save(); - } - - internal void AddOpenPairTag(string tag) - { - CurrentServerTagStorage().OpenPairTags.Add(tag); - _serverTagConfig.Save(); - } - - internal void RemoveOpenPairTag(string tag) - { - CurrentServerTagStorage().OpenPairTags.Remove(tag); - _serverTagConfig.Save(); - } - - internal bool ContainsOpenPairTag(string tag) - { - return CurrentServerTagStorage().OpenPairTags.Contains(tag); - } - - internal Dictionary> GetUidServerPairedUserTags() - { - return CurrentServerTagStorage().UidServerPairedUserTags; - } - - internal HashSet GetServerAvailablePairTags() - { - return CurrentServerTagStorage().ServerAvailablePairTags; - } - internal void AddTag(string tag) { CurrentServerTagStorage().ServerAvailablePairTags.Add(tag); _serverTagConfig.Save(); } - internal void RemoveTag(string tag) - { - CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag); - foreach (var uid in GetUidsForTag(tag)) - { - RemoveTagForUid(uid, tag, save: false); - } - _serverTagConfig.Save(); - } - - private void TryCreateCurrentServerTagStorage() - { - if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl)) - { - _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new(); - } - } - - private void TryCreateCurrentNotesStorage() - { - if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl)) - { - _notesConfig.Current.ServerNotes[CurrentApiUrl] = new(); - } - } - internal void AddTagForUid(string uid, string tagName) { if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) @@ -263,14 +190,9 @@ public class ServerConfigurationManager _serverTagConfig.Save(); } - internal void RemoveTagForUid(string uid, string tagName, bool save = true) + internal bool ContainsOpenPairTag(string tag) { - if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) - { - tags.Remove(tagName); - if (save) - _serverTagConfig.Save(); - } + return CurrentServerTagStorage().OpenPairTags.Contains(tag); } internal bool ContainsTag(string uid, string tag) @@ -283,41 +205,10 @@ public class ServerConfigurationManager return false; } - internal bool HasTags(string uid) + internal void DeleteServer(ServerStorage selectedServer) { - if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) - { - return tags.Any(); - } - - return false; - } - - internal HashSet GetUidsForTag(string tag) - { - return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal); - } - - internal string? GetNoteForUid(string uid) - { - if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note)) - { - if (string.IsNullOrEmpty(note)) return null; - return note; - } - return null; - } - - internal void SetNoteForUid(string uid, string note, bool save = true) - { - CurrentNotesStorage().UidServerComments[uid] = note; - if (save) - _notesConfig.Save(); - } - - internal void SaveNotes() - { - _notesConfig.Save(); + _configService.Current.ServerStorage.Remove(selectedServer.ServerUri); + Save(); } internal string? GetNoteForGid(string gID) @@ -331,10 +222,118 @@ public class ServerConfigurationManager return null; } + internal string? GetNoteForUid(string uid) + { + if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note)) + { + if (string.IsNullOrEmpty(note)) return null; + return note; + } + return null; + } + + internal HashSet GetServerAvailablePairTags() + { + return CurrentServerTagStorage().ServerAvailablePairTags; + } + + internal Dictionary> GetUidServerPairedUserTags() + { + return CurrentServerTagStorage().UidServerPairedUserTags; + } + + internal HashSet GetUidsForTag(string tag) + { + return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal); + } + + internal bool HasTags(string uid) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + return tags.Any(); + } + + return false; + } + + internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item) + { + var server = GetServerByIndex(serverSelectionIndex); + server.Authentications.Remove(item); + Save(); + } + + internal void RemoveOpenPairTag(string tag) + { + CurrentServerTagStorage().OpenPairTags.Remove(tag); + _serverTagConfig.Save(); + } + + internal void RemoveTag(string tag) + { + CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag); + foreach (var uid in GetUidsForTag(tag)) + { + RemoveTagForUid(uid, tag, save: false); + } + _serverTagConfig.Save(); + } + + internal void RemoveTagForUid(string uid, string tagName, bool save = true) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + tags.Remove(tagName); + if (save) + _serverTagConfig.Save(); + } + } + + internal void SaveNotes() + { + _notesConfig.Save(); + } + internal void SetNoteForGid(string gid, string note, bool save = true) { CurrentNotesStorage().GidServerComments[gid] = note; if (save) _notesConfig.Save(); } -} + + internal void SetNoteForUid(string uid, string note, bool save = true) + { + CurrentNotesStorage().UidServerComments[uid] = note; + if (save) + _notesConfig.Save(); + } + + private ServerNotesStorage CurrentNotesStorage() + { + TryCreateCurrentNotesStorage(); + return _notesConfig.Current.ServerNotes[CurrentApiUrl]; + } + + private ServerTagStorage CurrentServerTagStorage() + { + TryCreateCurrentServerTagStorage(); + return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl]; + } + + private void TryCreateCurrentNotesStorage() + { + if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl)) + { + _notesConfig.Current.ServerNotes[CurrentApiUrl] = new(); + } + } + + private void TryCreateCurrentServerTagStorage() + { + if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl)) + { + _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new(); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/UiService.cs b/MareSynchronos/Services/UiService.cs new file mode 100644 index 0000000..dd39a08 --- /dev/null +++ b/MareSynchronos/Services/UiService.cs @@ -0,0 +1,66 @@ +using Dalamud.Plugin; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Windowing; +using MareSynchronos.UI; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class UiService : IDisposable +{ + private readonly DalamudPluginInterface _dalamudPluginInterface; + private readonly FileDialogManager _fileDialogManager; + private readonly ILogger _logger; + private readonly MareConfigService _mareConfigService; + private readonly MareMediator _mareMediator; + private readonly WindowSystem _windowSystem; + + public UiService(ILogger logger, DalamudPluginInterface dalamudPluginInterface, + MareConfigService mareConfigService, WindowSystem windowSystem, + IEnumerable windows, + FileDialogManager fileDialogManager, MareMediator mareMediator) + { + _logger = logger; + _logger.LogTrace("Creating {type}", GetType().Name); + _dalamudPluginInterface = dalamudPluginInterface; + _mareConfigService = mareConfigService; + _windowSystem = windowSystem; + _fileDialogManager = fileDialogManager; + _mareMediator = mareMediator; + + _dalamudPluginInterface.UiBuilder.DisableGposeUiHide = true; + _dalamudPluginInterface.UiBuilder.Draw += Draw; + _dalamudPluginInterface.UiBuilder.OpenConfigUi += ToggleUi; + + foreach (var window in windows) + { + _windowSystem.AddWindow(window); + } + } + + public void Dispose() + { + _logger.LogTrace("Disposing {type}", GetType().Name); + + _windowSystem.RemoveAllWindows(); + + _dalamudPluginInterface.UiBuilder.Draw -= Draw; + _dalamudPluginInterface.UiBuilder.OpenConfigUi -= ToggleUi; + } + + public void ToggleUi() + { + if (_mareConfigService.Current.HasValidSetup()) + _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi))); + else + _mareMediator.Publish(new UiToggleMessage(typeof(IntroUi))); + } + + private void Draw() + { + _windowSystem.Draw(); + _fileDialogManager.Draw(); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 73fb02c..9503a4b 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -1,126 +1,117 @@ -using System.Diagnostics; +using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.Numerics; using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.Windowing; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto.User; -using MareSynchronos.Managers; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI.Components; using MareSynchronos.UI.Handlers; using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; namespace MareSynchronos.UI; -public class CompactUi : WindowMediatorSubscriberBase, IDisposable +public class CompactUi : WindowMediatorSubscriberBase { - private readonly ApiController _apiController; - private readonly PairManager _pairManager; - private readonly ServerConfigurationManager _serverManager; - private readonly MareConfigService _configService; - private readonly TagHandler _tagHandler; public readonly Dictionary ShowUidForEntry = new(StringComparer.Ordinal); - private readonly UiShared _uiShared; - private readonly WindowSystem _windowSystem; - private string _characterOrCommentFilter = string.Empty; - - public string EditUserComment = string.Empty; public string EditNickEntry = string.Empty; - - private string _pairToAdd = string.Empty; - - private readonly Stopwatch _timeout = new(); - private bool _buttonState; - + public string EditUserComment = string.Empty; public float TransferPartHeight; public float WindowContentWidth; - private bool _showModalForUserAddition; - private bool _wasOpen; - - private bool _showSyncShells; + private readonly ApiController _apiController; + private readonly MareConfigService _configService; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly FileUploadManager _fileTransferManager; private readonly GroupPanel _groupPanel; - private Pair? _lastAddedUser; - private string _lastAddedUserComment = string.Empty; - + private readonly PairGroupsUi _pairGroupsUi; + private readonly PairManager _pairManager; private readonly SelectGroupForPairUi _selectGroupForPairUi; private readonly SelectPairForGroupUi _selectPairsForGroupUi; - private readonly PairGroupsUi _pairGroupsUi; + private readonly ServerConfigurationManager _serverManager; + private readonly Stopwatch _timeout = new(); + private readonly UiSharedService _uiShared; + private bool _buttonState; + private string _characterOrCommentFilter = string.Empty; + private Pair? _lastAddedUser; + private string _lastAddedUserComment = string.Empty; + private string _pairToAdd = string.Empty; + private int _secretKeyIdx = 0; + private bool _showModalForUserAddition; + private bool _showSyncShells; + private bool _wasOpen; - public CompactUi(ILogger logger, WindowSystem windowSystem, - UiShared uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, - ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator, "###MareSynchronosMainUI") + public CompactUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, + ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator, "###MareSynchronosMainUI") { - -#if DEBUG - string dev = "Dev Build"; - var ver = Assembly.GetExecutingAssembly().GetName().Version; - this.WindowName = $"Mare Synchronos {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###MareSynchronosMainUI"; - Toggle(); -#else - var ver = Assembly.GetExecutingAssembly().GetName().Version; - this.WindowName = "Mare Synchronos " + ver.Major + "." + ver.Minor + "." + ver.Build + "###MareSynchronosMainUI"; -#endif - _logger.LogTrace("Creating " + nameof(CompactUi)); - - _windowSystem = windowSystem; _uiShared = uiShared; _configService = configService; _apiController = apiController; _pairManager = pairManager; _serverManager = serverManager; - _tagHandler = new(_serverManager); + _fileTransferManager = fileTransferManager; + var tagHandler = new TagHandler(_serverManager); _groupPanel = new(this, uiShared, _pairManager, _serverManager, _configService); - _selectGroupForPairUi = new(_tagHandler); - _selectPairsForGroupUi = new(_tagHandler); - _pairGroupsUi = new(_tagHandler, DrawPairedClient, apiController, _selectPairsForGroupUi); + _selectGroupForPairUi = new(tagHandler); + _selectPairsForGroupUi = new(tagHandler); + _pairGroupsUi = new(configService, tagHandler, DrawPairedClient, apiController, _selectPairsForGroupUi); +#if DEBUG + string dev = "Dev Build"; + var ver = Assembly.GetExecutingAssembly().GetName().Version!; + WindowName = $"Mare Synchronos {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###MareSynchronosMainUI"; + Toggle(); +#else + var ver = Assembly.GetExecutingAssembly().GetName().Version; + WindowName = "Mare Synchronos " + ver.Major + "." + ver.Minor + "." + ver.Build + "###MareSynchronosMainUI"; +#endif Mediator.Subscribe(this, (_) => IsOpen = true); Mediator.Subscribe(this, (_) => IsOpen = false); - Mediator.Subscribe(this, (_) => UiShared_GposeStart()); - Mediator.Subscribe(this, (_) => UiShared_GposeEnd()); + Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); + Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(350, 400), MaximumSize = new Vector2(350, 2000), }; - - windowSystem.AddWindow(this); - } - - private void UiShared_GposeEnd() - { - IsOpen = _wasOpen; - } - - private void UiShared_GposeStart() - { - _wasOpen = IsOpen; - IsOpen = false; - } - - public override void Dispose() - { - base.Dispose(); - _windowSystem.RemoveWindow(this); } public override void Draw() { - WindowContentWidth = UiShared.GetWindowContentRegionWidth(); - UiShared.DrawWithID("header", DrawUIDHeader); + WindowContentWidth = UiSharedService.GetWindowContentRegionWidth(); + if (!_apiController.IsCurrentVersion) + { + var ver = _apiController.CurrentClientVersion; + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + var unsupported = "UNSUPPORTED VERSION"; + var uidTextSize = ImGui.CalcTextSize(unsupported); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2); + ImGui.TextColored(ImGuiColors.DalamudRed, unsupported); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + UiSharedService.ColorTextWrapped($"Your Mare Synchronos installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " + + $"It is highly recommended to keep Mare Synchronos up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed); + } + + UiSharedService.DrawWithID("header", DrawUIDHeader); ImGui.Separator(); - UiShared.DrawWithID("serverstatus", DrawServerStatus); + UiSharedService.DrawWithID("serverstatus", DrawServerStatus); if (_apiController.ServerState is ServerState.Connected) { @@ -131,7 +122,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); } - if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) + if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) { _showSyncShells = false; } @@ -140,7 +131,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable ImGui.PopStyleColor(); } ImGui.PopFont(); - UiShared.AttachToolTip("Individual pairs"); + UiSharedService.AttachToolTip("Individual pairs"); ImGui.SameLine(); @@ -149,7 +140,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); } - if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) + if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) { _showSyncShells = true; } @@ -159,23 +150,22 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable } ImGui.PopFont(); - UiShared.AttachToolTip("Syncshells"); + UiSharedService.AttachToolTip("Syncshells"); ImGui.Separator(); if (!hasShownSyncShells) { - UiShared.DrawWithID("pairlist", DrawPairList); + UiSharedService.DrawWithID("pairlist", DrawPairList); } else { - UiShared.DrawWithID("syncshells", _groupPanel.DrawSyncshells); - + UiSharedService.DrawWithID("syncshells", _groupPanel.DrawSyncshells); } ImGui.Separator(); - UiShared.DrawWithID("transfers", DrawTransfers); + UiSharedService.DrawWithID("transfers", DrawTransfers); TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; - UiShared.DrawWithID("group-user-popup", () => _selectPairsForGroupUi.Draw(_pairManager.DirectPairs, ShowUidForEntry)); - UiShared.DrawWithID("grouping-popup", () => _selectGroupForPairUi.Draw(ShowUidForEntry)); + UiSharedService.DrawWithID("group-user-popup", () => _selectPairsForGroupUi.Draw(_pairManager.DirectPairs, ShowUidForEntry)); + UiSharedService.DrawWithID("grouping-popup", () => _selectGroupForPairUi.Draw(ShowUidForEntry)); } if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) @@ -187,7 +177,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _lastAddedUserComment = string.Empty; } - if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiShared.PopupWindowFlags)) + if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags)) { if (_lastAddedUser == null) { @@ -195,9 +185,9 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable } else { - UiShared.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:"); + UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:"); ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100); - if (UiShared.IconTextButton(FontAwesomeIcon.Save, "Save Note")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note")) { _serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment); _lastAddedUser = null; @@ -205,7 +195,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _showModalForUserAddition = false; } } - UiShared.SetScaledWindowSize(275); + UiSharedService.SetScaledWindowSize(275); ImGui.EndPopup(); } } @@ -217,12 +207,42 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable base.OnClose(); } + private void DrawAddCharacter() + { + ImGui.Dummy(new(10)); + var keys = _serverManager.CurrentServer!.SecretKeys; + if (keys.TryGetValue(_secretKeyIdx, out var secretKey)) + { + var friendlyName = secretKey.FriendlyName; + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) + { + _serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication() + { + CharacterName = _uiShared.PlayerName, + WorldId = _uiShared.WorldId, + SecretKeyIdx = _secretKeyIdx + }); + + _serverManager.Save(); + + _ = _apiController.CreateConnections(forceGetToken: true); + } + + _uiShared.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key); + } + else + { + UiSharedService.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow); + } + } + private void DrawAddPair() { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20); - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); var canAdd = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal)); if (!canAdd) { @@ -235,7 +255,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _ = _apiController.UserAddPair(new(new(_pairToAdd))); _pairToAdd = string.Empty; } - UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); + UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); } ImGuiHelpers.ScaledDummy(2); @@ -243,8 +263,8 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable private void DrawFilter() { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp); - var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play); + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowUp); + var playButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Play); if (!_configService.Current.ReverseUserSort) { if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) @@ -252,7 +272,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _configService.Current.ReverseUserSort = true; _configService.Save(); } - UiShared.AttachToolTip("Sort by name descending"); + UiSharedService.AttachToolTip("Sort by name descending"); } else { @@ -261,7 +281,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _configService.Current.ReverseUserSort = false; _configService.Save(); } - UiShared.AttachToolTip("Sort by name ascending"); + UiSharedService.AttachToolTip("Sort by name ascending"); } ImGui.SameLine(); @@ -288,12 +308,15 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable case true when !pausedUsers.Any(): _buttonState = false; break; + case false when !resumedUsers.Any(): _buttonState = true; break; + case true: users = pausedUsers; break; + case false: users = resumedUsers; break; @@ -305,28 +328,25 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { _timeout.Reset(); - if (ImGuiComponents.IconButton(button)) + if (ImGuiComponents.IconButton(button) && UiSharedService.CtrlPressed()) { - if (UiShared.CtrlPressed()) + foreach (var entry in users) { - foreach (var entry in users) - { - var perm = entry.UserPair!.OwnPermissions; - perm.SetPaused(!perm.IsPaused()); - _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, perm)); - } - - _timeout.Start(); - _buttonState = !_buttonState; + var perm = entry.UserPair!.OwnPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, perm)); } + + _timeout.Start(); + _buttonState = !_buttonState; } - UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); + UiSharedService.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); } else { var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000; ImGuiComponents.DisabledButton(button); - UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds"); + UiSharedService.AttachToolTip($"Next execution is available at {availableAt} seconds"); } } @@ -335,14 +355,14 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (entry.UserPair == null) return; var pauseIcon = entry.UserPair!.OwnPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - var pauseIconSize = UiShared.GetIconButtonSize(pauseIcon); - var barButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars); + var pauseIconSize = UiSharedService.GetIconButtonSize(pauseIcon); + var barButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); var entryUID = entry.UserData.AliasOrUID; var textSize = ImGui.CalcTextSize(entryUID); var originalY = ImGui.GetCursorPosY(); var buttonSizes = pauseIconSize.Y + barButtonSize.Y; var spacingX = ImGui.GetStyle().ItemSpacing.X; - var windowEndX = ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth(); + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); var textPos = originalY + pauseIconSize.Y / 2 - textSize.Y / 2; ImGui.SetCursorPosY(textPos); @@ -369,17 +389,17 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable } ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(connectionIcon.ToIconString(), connectionColor); + UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor); ImGui.PopFont(); - UiShared.AttachToolTip(connectionText); + UiSharedService.AttachToolTip(connectionText); ImGui.SameLine(); ImGui.SetCursorPosY(textPos); if (entry is { IsOnline: true, IsVisible: true }) { ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); + UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); ImGui.PopFont(); - UiShared.AttachToolTip(entryUID + " is visible: " + entry.PlayerName!); + UiSharedService.AttachToolTip(entryUID + " is visible: " + entry.PlayerName!); } var textIsUid = true; @@ -414,7 +434,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); ImGui.TextUnformatted(playerText); if (textIsUid) ImGui.PopFont(); - UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + + UiSharedService.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + "Right click to change nick for " + entryUID); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { @@ -429,7 +449,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - var pair = _pairManager.DirectPairs.Find(p => p.UserData.UID == EditNickEntry); + var pair = _pairManager.DirectPairs.Find(p => string.Equals(p.UserData.UID, EditNickEntry, StringComparison.Ordinal)); if (pair != null) { pair.SetNote(EditUserComment); @@ -443,7 +463,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { ImGui.SetCursorPosY(originalY); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); if (ImGui.InputTextWithHint("", "Nick/Notes", ref EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) { _serverManager.SetNoteForUid(entry.UserPair!.User.UID, EditUserComment); @@ -454,12 +474,58 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { EditNickEntry = string.Empty; } - UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); } - // Pause Button + // Pause Button && sound warnings if (entry.UserPair!.OwnPermissions.IsPaired() && entry.UserPair!.OtherPermissions.IsPaired()) { + var individualSoundsDisabled = (entry.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (entry.UserPair?.OtherPermissions.IsDisableSounds() ?? false); + var individualAnimDisabled = (entry.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (entry.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); + + if (individualAnimDisabled || individualSoundsDisabled) + { + var infoIconPosDist = windowEndX - barButtonSize.X - spacingX - pauseIconSize.X - spacingX; + var icon = FontAwesomeIcon.ExclamationTriangle; + var iconwidth = UiSharedService.GetIconSize(icon); + + ImGui.SameLine(infoIconPosDist - iconwidth.X); + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + ImGui.PopStyleColor(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.Text("Individual User permissions"); + + if (individualSoundsDisabled) + { + var userSoundsText = "Sound sync disabled with " + entry.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userSoundsText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text("You: " + (entry.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (entry.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); + } + + if (individualAnimDisabled) + { + var userAnimText = "Animation sync disabled with " + entry.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userAnimText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text("You: " + (entry.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (entry.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled")); + } + + ImGui.EndTooltip(); + } + } + ImGui.SameLine(windowEndX - barButtonSize.X - spacingX - pauseIconSize.X); ImGui.SetCursorPosY(originalY); if (ImGuiComponents.IconButton(pauseIcon)) @@ -468,7 +534,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable perm.SetPaused(!perm.IsPaused()); _ = _apiController.UserSetPairPermissions(new(entry.UserData, perm)); } - UiShared.AttachToolTip(!entry.UserPair!.OwnPermissions.IsPaused() + UiSharedService.AttachToolTip(!entry.UserPair!.OwnPermissions.IsPaused() ? "Pause pairing with " + entryUID : "Resume pairing with " + entryUID); } @@ -483,7 +549,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable } if (ImGui.BeginPopup("User Flyout Menu")) { - UiShared.DrawWithID($"buttons-{entry.UserPair!.User.UID}", () => DrawPairedClientMenu(entry)); + UiSharedService.DrawWithID($"buttons-{entry.UserPair!.User.UID}", () => DrawPairedClientMenu(entry)); ImGui.EndPopup(); } } @@ -492,37 +558,54 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable { if (entry.IsVisible) { - if (UiShared.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) { entry.ApplyLastReceivedData(forced: true); ImGui.CloseCurrentPopup(); } - UiShared.AttachToolTip("This reapplies the last received character data to this character"); + UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); } var entryUID = entry.UserData.AliasOrUID; - if (UiShared.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) { _selectGroupForPairUi.Open(entry); } - UiShared.AttachToolTip("Choose pair groups for " + entryUID); + UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently")) + var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds(); + string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync"; + var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute; + if (UiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText)) { - if (UiShared.CtrlPressed()) - { - _ = _apiController.UserRemovePair(new(entry.UserData)); - } + var permissions = entry.UserPair.OwnPermissions; + permissions.SetDisableSounds(!isDisableSounds); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); } - UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); + + var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations(); + string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync"; + var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop; + if (UiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText)) + { + var permissions = entry.UserPair.OwnPermissions; + permissions.SetDisableAnimations(!isDisableAnims); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); + } + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed()) + { + _ = _apiController.UserRemovePair(new(entry.UserData)); + } + UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); } private void DrawPairList() { - UiShared.DrawWithID("addpair", DrawAddPair); - UiShared.DrawWithID("pairs", DrawPairs); + UiSharedService.DrawWithID("addpair", DrawAddPair); + UiSharedService.DrawWithID("pairs", DrawPairs); TransferPartHeight = ImGui.GetCursorPosY(); - UiShared.DrawWithID("filter", DrawFilter); + UiSharedService.DrawWithID("filter", DrawFilter); } private void DrawPairs() @@ -552,20 +635,9 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable ImGui.EndChild(); } - private List GetFilteredUsers() - { - return _pairManager.DirectPairs.Where(p => - { - if (_characterOrCommentFilter.IsNullOrEmpty()) return true; - return p.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) || - (p.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) || - (p.PlayerName?.Contains(_characterOrCommentFilter) ?? false); - }).ToList(); - } - private void DrawServerStatus() { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link); + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture); var userSize = ImGui.CalcTextSize(userCount); var textSize = ImGui.CalcTextSize("Users Online"); @@ -579,7 +651,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (_apiController.ServerState is ServerState.Connected) { - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); if (!printShard) ImGui.AlignTextToFramePadding(); ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); ImGui.SameLine(); @@ -595,16 +667,16 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (printShard) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - shardTextSize.X / 2); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X) / 2 - shardTextSize.X / 2); ImGui.TextUnformatted(shardConnection); } - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); if (printShard) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); } - var color = UiShared.GetBoolColor(!_serverManager.CurrentServer!.FullPause); + var color = UiSharedService.GetBoolColor(!_serverManager.CurrentServer!.FullPause); var connectedIcon = !_serverManager.CurrentServer.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; if (_apiController.ServerState != ServerState.Reconnecting) @@ -617,13 +689,13 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _ = _apiController.CreateConnections(); } ImGui.PopStyleColor(); - UiShared.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); + UiSharedService.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); } } private void DrawTransfers() { - var currentUploads = _apiController.CurrentUploads.ToList(); + var currentUploads = _fileTransferManager.CurrentUploads.ToList(); ImGui.PushFont(UiBuilder.IconFont); ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); ImGui.PopFont(); @@ -638,7 +710,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable var totalToUpload = currentUploads.Sum(c => c.Total); ImGui.Text($"{doneUploads}/{totalUploads}"); - var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})"; + var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})"; var textSize = ImGui.CalcTextSize(uploadText); ImGui.SameLine(WindowContentWidth - textSize.X); ImGui.Text(uploadText); @@ -648,7 +720,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable ImGui.Text("No uploads in progress"); } - var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList(); + var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); ImGui.PushFont(UiBuilder.IconFont); ImGui.Text(FontAwesomeIcon.Download.ToIconString()); ImGui.PopFont(); @@ -656,14 +728,14 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (currentDownloads.Any()) { - var totalDownloads = currentDownloads.Count(); - var doneDownloads = currentDownloads.Count(c => c.IsTransferred); - var totalDownloaded = currentDownloads.Sum(c => c.Transferred); - var totalToDownload = currentDownloads.Sum(c => c.Total); + var totalDownloads = currentDownloads.Sum(c => c.TotalFiles); + var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles); + var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes); + var totalToDownload = currentDownloads.Sum(c => c.TotalBytes); ImGui.Text($"{doneDownloads}/{totalDownloads}"); var downloadText = - $"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})"; + $"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})"; var textSize = ImGui.CalcTextSize(downloadText); ImGui.SameLine(WindowContentWidth - textSize.X); ImGui.Text(downloadText); @@ -687,28 +759,28 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable var originalPos = ImGui.GetCursorPos(); ImGui.SetWindowFontScale(1.5f); - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog); + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog); buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) { Mediator.Publish(new OpenSettingsUiMessage()); } - UiShared.AttachToolTip("Open the Mare Synchronos Settings"); + UiSharedService.AttachToolTip("Open the Mare Synchronos Settings"); ImGui.SameLine(); //Important to draw the uidText consistently ImGui.SetCursorPos(originalPos); if (_apiController.ServerState is ServerState.Connected) { - buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; + buttonSizeX += UiSharedService.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) { ImGui.SetClipboardText(_apiController.DisplayName); } - UiShared.AttachToolTip("Copy your UID to clipboard"); + UiSharedService.AttachToolTip("Copy your UID to clipboard"); ImGui.SameLine(); } ImGui.SetWindowFontScale(1f); @@ -721,7 +793,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable if (_apiController.ServerState is not ServerState.Connected) { - UiShared.ColorTextWrapped(GetServerError(), GetUidColor()); + UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); if (_apiController.ServerState is ServerState.NoSecretKey) { DrawAddCharacter(); @@ -729,38 +801,17 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable } } - private void DrawAddCharacter() + private List GetFilteredUsers() { - ImGui.Dummy(new(10)); - var keys = _serverManager.CurrentServer!.SecretKeys; - if (keys.TryGetValue(secretKeyIdx, out var secretKey)) + return _pairManager.DirectPairs.Where(p => { - var friendlyName = secretKey.FriendlyName; - - if (UiShared.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) - { - _serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication() - { - CharacterName = _uiShared.PlayerName, - WorldId = _uiShared.WorldId, - SecretKeyIdx = secretKeyIdx - }); - - _serverManager.Save(); - - _ = _apiController.CreateConnections(forceGetToken: true); - } - - _uiShared.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => secretKeyIdx = f.Key); - } - else - { - UiShared.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow); - } + if (_characterOrCommentFilter.IsNullOrEmpty()) return true; + return p.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) || + (p.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.PlayerName?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false); + }).ToList(); } - private int secretKeyIdx = 0; - private string GetServerError() { return _apiController.ServerState switch @@ -812,4 +863,15 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable _ => string.Empty }; } -} + + private void UiSharedService_GposeEnd() + { + IsOpen = _wasOpen; + } + + private void UiSharedService_GposeStart() + { + _wasOpen = IsOpen; + IsOpen = false; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs new file mode 100644 index 0000000..8420405 --- /dev/null +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -0,0 +1,1060 @@ +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface; +using Dalamud.Utility; +using ImGuiNET; +using MareSynchronos.WebAPI; +using System.Numerics; +using System.Globalization; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.ServerConfiguration; + +namespace MareSynchronos.UI; + +internal sealed class GroupPanel +{ + private readonly MareConfigService _configService; + private readonly Dictionary _expandedGroupState = new(StringComparer.Ordinal); + private readonly CompactUi _mainUi; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly Dictionary _showGidForEntry = new(StringComparer.Ordinal); + private readonly UiSharedService _uiShared; + private List _bannedUsers = new(); + private string _banReason = string.Empty; + private bool _banUserPopupOpen; + private int _bulkInviteCount = 10; + private List _bulkOneTimeInvites = new(); + private string _editGroupComment = string.Empty; + private string _editGroupEntry = string.Empty; + private bool _errorGroupCreate = false; + private bool _errorGroupJoin; + private bool _isPasswordValid; + private GroupPasswordDto? _lastCreatedGroup = null; + private bool _modalBanListOpened; + private bool _modalBulkOneTimeInvitesOpened; + private bool _modalChangePwOpened; + private string _newSyncShellPassword = string.Empty; + private bool _showModalBanList = false; + private bool _showModalBanUser; + private bool _showModalBulkOneTimeInvites = false; + private bool _showModalChangePassword; + private bool _showModalCreateGroup; + private bool _showModalEnterPassword; + private string _syncShellPassword = string.Empty; + private string _syncShellToJoin = string.Empty; + + public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ServerConfigurationManager serverConfigurationManager, MareConfigService configurationService) + { + _mainUi = mainUi; + _uiShared = uiShared; + _pairManager = pairManager; + _serverConfigurationManager = serverConfigurationManager; + _configService = configurationService; + } + + private ApiController ApiController => _uiShared.ApiController; + + public void DrawSyncshells() + { + UiSharedService.DrawWithID("addsyncshell", DrawAddSyncshell); + UiSharedService.DrawWithID("syncshelllist", DrawSyncshellList); + _mainUi.TransferPartHeight = ImGui.GetCursorPosY(); + } + + private void DrawAddSyncshell() + { + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); + + bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; + bool userCanCreateMoreGroups = _pairManager.GroupPairs.Count(u => string.Equals(u.Key.Owner.UID, ApiController.UID, StringComparison.Ordinal)) < ApiController.ServerInfo.MaxGroupsCreatedByUser; + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + if (_pairManager.GroupPairs.All(w => !string.Equals(w.Key.Group.GID, _syncShellToJoin, StringComparison.Ordinal) && !string.Equals(w.Key.Group.Alias, _syncShellToJoin, StringComparison.Ordinal)) + && !string.IsNullOrEmpty(_syncShellToJoin)) + { + if (userCanJoinMoreGroups) + { + _errorGroupJoin = false; + _showModalEnterPassword = true; + ImGui.OpenPopup("Enter Syncshell Password"); + } + } + else + { + if (userCanCreateMoreGroups) + { + _lastCreatedGroup = null; + _errorGroupCreate = false; + _showModalCreateGroup = true; + ImGui.OpenPopup("Create Syncshell"); + } + } + } + UiSharedService.AttachToolTip(_syncShellToJoin.IsNullOrEmpty() + ? (userCanCreateMoreGroups ? "Create Syncshell" : $"You cannot create more than {ApiController.ServerInfo.MaxGroupsCreatedByUser} Syncshells") + : (userCanJoinMoreGroups ? "Join Syncshell" + _syncShellToJoin : $"You cannot join more than {ApiController.ServerInfo.MaxGroupsJoinedByUser} Syncshells")); + + if (ImGui.BeginPopupModal("Enter Syncshell Password", ref _showModalEnterPassword, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell."); + ImGui.Separator(); + UiSharedService.TextWrapped("Enter the password for Syncshell " + _syncShellToJoin + ":"); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##password", _syncShellToJoin + " Password", ref _syncShellPassword, 255, ImGuiInputTextFlags.Password); + if (_errorGroupJoin) + { + UiSharedService.ColorTextWrapped($"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({ApiController.ServerInfo.MaxGroupsJoinedByUser}), " + + $"it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({ApiController.ServerInfo.MaxGroupUserCount} users) or the Syncshell has closed invites.", + new Vector4(1, 0, 0, 1)); + } + if (ImGui.Button("Join " + _syncShellToJoin)) + { + var shell = _syncShellToJoin; + var pw = _syncShellPassword; + _errorGroupJoin = !ApiController.GroupJoin(new(new GroupData(shell), pw)).Result; + if (!_errorGroupJoin) + { + _syncShellToJoin = string.Empty; + _showModalEnterPassword = false; + } + _syncShellPassword = string.Empty; + } + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Press the button below to create a new Syncshell."); + ImGui.SetNextItemWidth(200); + if (ImGui.Button("Create Syncshell")) + { + try + { + _lastCreatedGroup = ApiController.GroupCreate().Result; + } + catch + { + _lastCreatedGroup = null; + _errorGroupCreate = true; + } + } + + if (_lastCreatedGroup != null) + { + ImGui.Separator(); + _errorGroupCreate = false; + ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_lastCreatedGroup.Password); + } + UiSharedService.TextWrapped("You can change the Syncshell password later at any time."); + } + + if (_errorGroupCreate) + { + UiSharedService.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.", + new Vector4(1, 0, 0, 1)); + } + + UiSharedService.SetScaledWindowSize(350); + ImGui.EndPopup(); + } + + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawSyncshell(GroupFullInfoDto groupDto, List pairsInGroup) + { + var name = groupDto.Group.Alias ?? groupDto.GID; + if (!_expandedGroupState.TryGetValue(groupDto.GID, out bool isExpanded)) + { + isExpanded = false; + _expandedGroupState.Add(groupDto.GID, isExpanded); + } + var icon = isExpanded ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + var collapseButton = UiSharedService.GetIconButtonSize(icon); + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0)); + if (ImGuiComponents.IconButton(icon)) + { + _expandedGroupState[groupDto.GID] = !_expandedGroupState[groupDto.GID]; + } + ImGui.PopStyleColor(2); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + collapseButton.X); + var pauseIcon = groupDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + if (ImGuiComponents.IconButton(pauseIcon)) + { + var userPerm = groupDto.GroupUserPermissions ^ GroupUserPermissions.Paused; + _ = ApiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupDto.Group, new UserData(ApiController.UID), userPerm)); + } + UiSharedService.AttachToolTip((groupDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " pairing with all users in this Syncshell"); + ImGui.SameLine(); + + var textIsGid = true; + string groupName = groupDto.GroupAliasOrGID; + + if (string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal)) + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Crown.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("You are the owner of Syncshell " + groupName); + ImGui.SameLine(); + } + else if (groupDto.GroupUserInfo.IsModerator()) + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.UserShield.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("You are a moderator of Syncshell " + groupName); + ImGui.SameLine(); + } + + _showGidForEntry.TryGetValue(groupDto.GID, out var showGidInsteadOfName); + var groupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID); + if (!showGidInsteadOfName && !string.IsNullOrEmpty(groupComment)) + { + groupName = groupComment; + textIsGid = false; + } + + if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) + { + if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(groupName); + if (textIsGid) ImGui.PopFont(); + UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine + + "Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine + + "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsGid; + if (_showGidForEntry.ContainsKey(groupDto.GID)) + { + prevState = _showGidForEntry[groupDto.GID]; + } + + _showGidForEntry[groupDto.GID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _serverConfigurationManager.SetNoteForGid(_editGroupEntry, _editGroupComment); + _editGroupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID) ?? string.Empty; + _editGroupEntry = groupDto.GID; + } + } + else + { + var buttonSizes = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X + UiSharedService.GetIconSize(FontAwesomeIcon.LockOpen).X; + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _serverConfigurationManager.SetNoteForGid(groupDto.GID, _editGroupComment); + _editGroupEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editGroupEntry = string.Empty; + } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + UiSharedService.DrawWithID(groupDto.GID + "settings", () => DrawSyncShellButtons(groupDto, pairsInGroup)); + + if (_showModalBanList && !_modalBanListOpened) + { + _modalBanListOpened = true; + ImGui.OpenPopup("Manage Banlist for " + groupDto.GID); + } + + if (!_showModalBanList) _modalBanListOpened = false; + + if (ImGui.BeginPopupModal("Manage Banlist for " + groupDto.GID, ref _showModalBanList, UiSharedService.PopupWindowFlags)) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; + } + + if (ImGui.BeginTable("bannedusertable" + groupDto.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) + { + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); + ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); + + ImGui.TableHeadersRow(); + + foreach (var bannedUser in _bannedUsers.ToList()) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.UID); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.BannedBy); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); + ImGui.TableNextColumn(); + UiSharedService.TextWrapped(bannedUser.Reason); + ImGui.TableNextColumn(); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) + { + _ = ApiController.GroupUnbanUser(bannedUser); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + } + + ImGui.EndTable(); + } + UiSharedService.SetScaledWindowSize(700, 300); + ImGui.EndPopup(); + } + + if (_showModalChangePassword && !_modalChangePwOpened) + { + _modalChangePwOpened = true; + ImGui.OpenPopup("Change Syncshell Password"); + } + + if (!_showModalChangePassword) _modalChangePwOpened = false; + + if (ImGui.BeginPopupModal("Change Syncshell Password", ref _showModalChangePassword, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Enter the new Syncshell password for Syncshell " + name + " here."); + UiSharedService.TextWrapped("This action is irreversible"); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##changepw", "New password for " + name, ref _newSyncShellPassword, 255); + if (ImGui.Button("Change password")) + { + var pw = _newSyncShellPassword; + _isPasswordValid = ApiController.GroupChangePassword(new(groupDto.Group, pw)).Result; + _newSyncShellPassword = string.Empty; + if (_isPasswordValid) _showModalChangePassword = false; + } + + if (!_isPasswordValid) + { + UiSharedService.ColorTextWrapped("The selected password is too short. It must be at least 10 characters.", new Vector4(1, 0, 0, 1)); + } + + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + if (_showModalBulkOneTimeInvites && !_modalBulkOneTimeInvitesOpened) + { + _modalBulkOneTimeInvitesOpened = true; + ImGui.OpenPopup("Create Bulk One-Time Invites"); + } + + if (!_showModalBulkOneTimeInvites) _modalBulkOneTimeInvitesOpened = false; + + if (ImGui.BeginPopupModal("Create Bulk One-Time Invites", ref _showModalBulkOneTimeInvites, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("This allows you to create up to 100 one-time invites at once for the Syncshell " + name + "." + Environment.NewLine + + "The invites are valid for 24h after creation and will automatically expire."); + ImGui.Separator(); + if (_bulkOneTimeInvites.Count == 0) + { + ImGui.SetNextItemWidth(-1); + ImGui.SliderInt("Amount##bulkinvites", ref _bulkInviteCount, 1, 100); + if (UiSharedService.IconTextButton(FontAwesomeIcon.MailBulk, "Create invites")) + { + _bulkOneTimeInvites = ApiController.GroupCreateTempInvite(groupDto, _bulkInviteCount).Result; + } + } + else + { + UiSharedService.TextWrapped("A total of " + _bulkOneTimeInvites.Count + " invites have been created."); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard")) + { + ImGui.SetClipboardText(string.Join(Environment.NewLine, _bulkOneTimeInvites)); + } + } + + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + ImGui.Indent(collapseButton.X); + if (_expandedGroupState[groupDto.GID]) + { + var visibleUsers = pairsInGroup.Where(u => u.IsVisible) + .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) + .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + var onlineUsers = pairsInGroup.Where(u => u.IsOnline && !u.IsVisible) + .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) + .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + var offlineUsers = pairsInGroup.Where(u => !u.IsOnline && !u.IsVisible) + .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) + .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (visibleUsers.Any()) + { + ImGui.Text("Visible"); + ImGui.Separator(); + foreach (var entry in visibleUsers) + { + UiSharedService.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( + entry, + entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, + groupDto.OwnerUID, + string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), + groupDto.GroupUserInfo.IsModerator())); + } + } + + if (onlineUsers.Any()) + { + ImGui.Text("Online"); + ImGui.Separator(); + foreach (var entry in onlineUsers) + { + UiSharedService.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( + entry, + entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, + groupDto.OwnerUID, + string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), + groupDto.GroupUserInfo.IsModerator())); + } + } + + if (offlineUsers.Any()) + { + ImGui.Text("Offline/Unknown"); + ImGui.Separator(); + foreach (var entry in offlineUsers) + { + UiSharedService.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( + entry, + entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, + groupDto.OwnerUID, + string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), + groupDto.GroupUserInfo.IsModerator())); + } + } + + ImGui.Separator(); + ImGui.Unindent(ImGui.GetStyle().ItemSpacing.X / 2); + } + ImGui.Unindent(collapseButton.X); + } + + private void DrawSyncShellButtons(GroupFullInfoDto groupDto, List groupPairs) + { + var infoIcon = FontAwesomeIcon.InfoCircle; + + bool invitesEnabled = !groupDto.GroupPermissions.IsDisableInvites(); + var soundsDisabled = groupDto.GroupPermissions.IsDisableSounds(); + var animDisabled = groupDto.GroupPermissions.IsDisableAnimations(); + + var userSoundsDisabled = groupDto.GroupUserPermissions.IsDisableSounds(); + var userAnimDisabled = groupDto.GroupUserPermissions.IsDisableAnimations(); + + bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || userSoundsDisabled || userAnimDisabled; + + var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; + var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; + var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; + var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; + var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; + + var iconSize = UiSharedService.GetIconSize(infoIcon); + var diffLockUnlockIcons = showInfoIcon ? (UiSharedService.GetIconSize(infoIcon).X - iconSize.X) / 2 : 0; + var barbuttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal); + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - barbuttonSize.X - (showInfoIcon ? iconSize.X : 0) - diffLockUnlockIcons - (showInfoIcon ? ImGui.GetStyle().ItemSpacing.X : 0)); + if (showInfoIcon) + { + UiSharedService.FontText(infoIcon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (!invitesEnabled || soundsDisabled || animDisabled) + { + ImGui.Text("Syncshell permissions"); + + if (!invitesEnabled) + { + var lockedText = "Syncshell is closed for joining"; + UiSharedService.FontText(lockedIcon.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(lockedText); + } + + if (soundsDisabled) + { + var soundsText = "Sound sync disabled through owner"; + UiSharedService.FontText(soundsIcon.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(soundsText); + } + + if (animDisabled) + { + var animText = "Animation sync disabled through owner"; + UiSharedService.FontText(animIcon.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(animText); + } + } + + if (userSoundsDisabled || userAnimDisabled) + { + if (!invitesEnabled || soundsDisabled || animDisabled) + ImGui.Separator(); + + ImGui.Text("Your permissions"); + + if (userSoundsDisabled) + { + var userSoundsText = "Sound sync disabled through you"; + UiSharedService.FontText(userSoundsIcon.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userSoundsText); + } + + if (userAnimDisabled) + { + var userAnimText = "Animation sync disabled through you"; + UiSharedService.FontText(userAnimIcon.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userAnimText); + } + + if (!invitesEnabled || soundsDisabled || animDisabled) + UiSharedService.TextWrapped("Note that syncshell permissions for disabling take precedence over your own set permissions"); + } + ImGui.EndTooltip(); + } + ImGui.SameLine(); + } + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + diffLockUnlockIcons); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("ShellPopup"); + } + + if (ImGui.BeginPopup("ShellPopup")) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell") && UiSharedService.CtrlPressed()) + { + _ = ApiController.GroupLeave(groupDto); + } + UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine + + "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID")) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(groupDto.GroupAliasOrGID); + } + UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes")) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(UiSharedService.GetNotes(groupPairs)); + } + UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> Privacy -> Import Notes from Clipboard"); + + var soundsText = userSoundsDisabled ? "Enable sound sync" : "Disable sound sync"; + if (UiSharedService.IconTextButton(userSoundsIcon, soundsText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupUserPermissions; + perm.SetDisableSounds(!perm.IsDisableSounds()); + _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); + } + UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." + + Environment.NewLine + "Disabling the synchronization will stop applying sound modifications for users of this syncshell." + + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." + + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); + + var animText = userAnimDisabled ? "Enable animations sync" : "Disable animations sync"; + if (UiSharedService.IconTextButton(userAnimIcon, animText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupUserPermissions; + perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); + } + UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." + + Environment.NewLine + "Disabling the synchronization will stop applying animations modifications for users of this syncshell." + + Environment.NewLine + "Note: this setting might also affect sound synchronization" + + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." + + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); + + if (isOwner || groupDto.GroupUserInfo.IsModerator()) + { + ImGui.Separator(); + + var changedToIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; + if (UiSharedService.IconTextButton(changedToIcon, invitesEnabled ? "Lock Syncshell" : "Unlock Syncshell")) + { + ImGui.CloseCurrentPopup(); + var groupPerm = groupDto.GroupPermissions; + groupPerm.SetDisableInvites(invitesEnabled); + _ = ApiController.GroupChangeGroupPermissionState(new GroupPermissionDto(groupDto.Group, groupPerm)); + } + UiSharedService.AttachToolTip("Change Syncshell joining permissions" + Environment.NewLine + "Syncshell is currently " + (invitesEnabled ? "open" : "closed") + " for people to join"); + + if (isOwner) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) + { + ImGui.CloseCurrentPopup(); + _isPasswordValid = true; + _showModalChangePassword = true; + } + UiSharedService.AttachToolTip("Change Syncshell Password"); + } + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell") && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + _ = ApiController.GroupClear(groupDto); + } + UiSharedService.AttachToolTip("Hold CTRL and click to clear this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible." + Environment.NewLine + + "Clearing the Syncshell will remove all not pinned users from it."); + + var groupSoundsText = soundsDisabled ? "Enable syncshell sound sync" : "Disable syncshell sound sync"; + if (UiSharedService.IconTextButton(soundsIcon, groupSoundsText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupPermissions; + perm.SetDisableSounds(!perm.IsDisableSounds()); + _ = ApiController.GroupChangeGroupPermissionState(new(groupDto.Group, perm)); + } + UiSharedService.AttachToolTip("Sets syncshell-wide allowance for sound synchronization for all users." + Environment.NewLine + + "Note: users that are individually paired with others in the syncshell will ignore this setting." + Environment.NewLine + + "Note: if the synchronization is enabled, users can individually override this setting to disabled."); + + var groupAnimText = animDisabled ? "Enable syncshell animations sync" : "Disable syncshell animations sync"; + if (UiSharedService.IconTextButton(animIcon, groupAnimText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupPermissions; + perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _ = ApiController.GroupChangeGroupPermissionState(new(groupDto.Group, perm)); + } + UiSharedService.AttachToolTip("Sets syncshell-wide allowance for animations synchronization for all users." + Environment.NewLine + + "Note: users that are individually paired with others in the syncshell will ignore this setting." + Environment.NewLine + + "Note: if the synchronization is enabled, users can individually override this setting to disabled."); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(ApiController.GroupCreateTempInvite(groupDto, 1).Result.FirstOrDefault() ?? string.Empty); + } + UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.MailBulk, "Bulk one-time invites")) + { + ImGui.CloseCurrentPopup(); + _showModalBulkOneTimeInvites = true; + _bulkOneTimeInvites.Clear(); + } + UiSharedService.AttachToolTip("Opens a dialog to create up to 100 single-use passwords for joining the syncshell."); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Manage Banlist")) + { + ImGui.CloseCurrentPopup(); + _showModalBanList = true; + _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; + } + + if (isOwner) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + ImGui.CloseCurrentPopup(); + _ = ApiController.GroupDelete(groupDto); + } + UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); + } + } + + ImGui.EndPopup(); + } + } + + private void DrawSyncshellList() + { + var ySize = _mainUi.TransferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY(); + ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false); + foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList()) + { + UiSharedService.DrawWithID(entry.Key.Group.GID, () => DrawSyncshell(entry.Key, entry.Value)); + } + ImGui.EndChild(); + } + + private void DrawSyncshellPairedClient(Pair pair, GroupPairFullInfoDto entry, string ownerUid, bool userIsOwner, bool userIsModerator) + { + var plusButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); + var barButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var entryUID = entry.UserAliasOrUID; + var textSize = ImGui.CalcTextSize(entryUID); + var originalY = ImGui.GetCursorPosY(); + var entryIsMod = entry.GroupPairStatusInfo.IsModerator(); + var entryIsOwner = string.Equals(pair.UserData.UID, ownerUid, StringComparison.Ordinal); + var entryIsPinned = entry.GroupPairStatusInfo.IsPinned(); + var presenceIcon = pair.IsVisible ? FontAwesomeIcon.Eye : (pair.IsOnline ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink); + var presenceColor = (pair.IsOnline || pair.IsVisible) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var presenceText = entryUID + " is offline"; + + var soundsDisabled = entry.GroupUserPermissions.IsDisableSounds(); + var animDisabled = entry.GroupUserPermissions.IsDisableAnimations(); + var individualSoundsDisabled = (pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); + var individualAnimDisabled = (pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); + + var textPos = originalY + barButtonSize.Y / 2 - textSize.Y / 2; + ImGui.SetCursorPosY(textPos); + if (pair.IsPaused) + { + presenceIcon = FontAwesomeIcon.Question; + presenceColor = ImGuiColors.DalamudGrey; + presenceText = entryUID + " online status is unknown (paused)"; + + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); + ImGui.PopFont(); + + UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused"); + } + else + { + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); + ImGui.PopFont(); + + UiSharedService.AttachToolTip("You are paired with " + entryUID); + } + + if (pair.IsOnline && !pair.IsVisible) presenceText = entryUID + " is online"; + else if (pair.IsOnline && pair.IsVisible) presenceText = entryUID + " is visible: " + pair.PlayerName; + + ImGui.SameLine(); + ImGui.SetCursorPosY(textPos); + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor); + ImGui.PopFont(); + UiSharedService.AttachToolTip(presenceText); + + if (entryIsOwner) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPos); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is owner of this Syncshell"); + } + else if (entryIsMod) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPos); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is moderator of this Syncshell"); + } + else if (entryIsPinned) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPos); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is pinned in this Syncshell"); + } + + var textIsUid = true; + _mainUi.ShowUidForEntry.TryGetValue(entry.UID, out var showUidInsteadOfName); + var playerText = _serverConfigurationManager.GetNoteForUid(entry.UID); + if (showUidInsteadOfName || string.IsNullOrEmpty(playerText)) + { + playerText = entryUID; + } + else + { + textIsUid = false; + } + + if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && pair.IsVisible && !showUidInsteadOfName) + { + playerText = pair.PlayerName; + textIsUid = false; + } + + bool plusButtonShown = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, entry.UID, StringComparison.Ordinal)); + + ImGui.SameLine(); + if (!string.Equals(_mainUi.EditNickEntry, entry.UID, StringComparison.Ordinal)) + { + ImGui.SetCursorPosY(textPos); + if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(playerText); + if (textIsUid) ImGui.PopFont(); + UiSharedService.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + + "Right click to change nick for " + entryUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (_mainUi.ShowUidForEntry.ContainsKey(entry.UID)) + { + prevState = _mainUi.ShowUidForEntry[entry.UID]; + } + + _mainUi.ShowUidForEntry[entry.UID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _serverConfigurationManager.SetNoteForUid(_mainUi.EditNickEntry, _mainUi.EditUserComment); + _mainUi.EditUserComment = _serverConfigurationManager.GetNoteForUid(entry.UID) ?? string.Empty; + _mainUi.EditNickEntry = entry.UID; + } + } + else + { + ImGui.SetCursorPosY(originalY); + var buttonSizes = (plusButtonShown ? plusButtonSize.X : 0) + barButtonSize.X; + var buttons = plusButtonShown ? 2 : 1; + + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * buttons); + if (ImGui.InputTextWithHint("", "Nick/Notes", ref _mainUi.EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _serverConfigurationManager.SetNoteForUid(entry.UID, _mainUi.EditUserComment); + _mainUi.EditNickEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _mainUi.EditNickEntry = string.Empty; + } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + if (plusButtonShown) + { + var barWidth = userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner) + ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X + : 0; + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - plusButtonSize.X - barWidth); + ImGui.SetCursorPosY(originalY); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + _ = ApiController.UserAddPair(new UserDto(entry.User)); + } + UiSharedService.AttachToolTip("Pair with " + entryUID + " individually"); + } + + if (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) + { + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - barButtonSize.X); + ImGui.SetCursorPosY(originalY); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("Popup"); + } + } + + if (individualAnimDisabled || individualSoundsDisabled) + { + var infoIconPosDist = (plusButtonShown ? plusButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0) + + ((userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0); + var icon = FontAwesomeIcon.ExclamationTriangle; + var iconwidth = UiSharedService.GetIconSize(icon); + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - infoIconPosDist - iconwidth.X); + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + ImGui.PopStyleColor(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.Text("Individual User permissions"); + + if (individualSoundsDisabled) + { + var userSoundsText = "Sound sync disabled with " + pair.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userSoundsText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text("You: " + (pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); + } + + if (individualAnimDisabled) + { + var userAnimText = "Animation sync disabled with " + pair.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userAnimText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text("You: " + (pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled")); + } + + ImGui.EndTooltip(); + } + } + else if ((animDisabled || soundsDisabled)) + { + var infoIconPosDist = (plusButtonShown ? plusButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0) + + ((userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0); + var icon = FontAwesomeIcon.InfoCircle; + var iconwidth = UiSharedService.GetIconSize(icon); + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - infoIconPosDist - iconwidth.X); + ImGui.SetCursorPosY(originalY); + + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.Text("Sycnshell User permissions"); + + if (soundsDisabled) + { + var userSoundsText = "Sound sync disabled by " + pair.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userSoundsText); + } + + if (animDisabled) + { + var userAnimText = "Animation sync disabled by " + pair.UserData.AliasOrUID; + UiSharedService.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.Text(userAnimText); + } + + ImGui.EndTooltip(); + } + } + + if (!plusButtonShown && !(userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner))) + { + ImGui.SameLine(); + ImGui.Dummy(barButtonSize with { X = 0 }); + } + + if (ImGui.BeginPopup("Popup")) + { + if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner)) + { + var pinText = entryIsPinned ? "Unpin user" : "Pin user"; + if (UiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText)) + { + ImGui.CloseCurrentPopup(); + var userInfo = entry.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; + _ = ApiController.GroupSetUserInfo(new GroupPairUserInfoDto(entry.Group, entry.User, userInfo)); + } + UiSharedService.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove user") && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + _ = ApiController.GroupRemoveUser(entry); + } + + UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (entry.UserAliasOrUID) + " from Syncshell"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + { + _showModalBanUser = true; + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("Ban user from this Syncshell"); + } + + if (userIsOwner) + { + string modText = entryIsMod ? "Demod user" : "Mod user"; + if (UiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText) && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + var userInfo = entry.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; + _ = ApiController.GroupSetUserInfo(new GroupPairUserInfoDto(entry.Group, entry.User, userInfo)); + } + UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (entry.UserAliasOrUID) + Environment.NewLine + + "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + ImGui.CloseCurrentPopup(); + _ = ApiController.GroupChangeOwnership(entry); + } + UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (entry.UserAliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); + } + ImGui.EndPopup(); + } + + if (_showModalBanUser && !_banUserPopupOpen) + { + ImGui.OpenPopup("Ban User"); + _banUserPopupOpen = true; + } + + if (!_showModalBanUser) _banUserPopupOpen = false; + + if (ImGui.BeginPopupModal("Ban User", ref _showModalBanUser, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("User " + (entry.UserAliasOrUID) + " will be banned and removed from this Syncshell."); + ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255); + if (ImGui.Button("Ban User")) + { + ImGui.CloseCurrentPopup(); + var reason = _banReason; + _ = ApiController.GroupBanUser(entry, reason); + _banReason = string.Empty; + } + UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."); + UiSharedService.SetScaledWindowSize(300); + ImGui.EndPopup(); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/PairGroupsUi.cs b/MareSynchronos/UI/Components/PairGroupsUi.cs index 8dc295d..2a617d7 100644 --- a/MareSynchronos/UI/Components/PairGroupsUi.cs +++ b/MareSynchronos/UI/Components/PairGroupsUi.cs @@ -2,215 +2,236 @@ using Dalamud.Interface.Components; using ImGuiNET; using MareSynchronos.API.Data.Extensions; -using MareSynchronos.Models; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; using MareSynchronos.UI.Handlers; using MareSynchronos.WebAPI; -namespace MareSynchronos.UI.Components +namespace MareSynchronos.UI.Components; + +public class PairGroupsUi { - public class PairGroupsUi + private readonly ApiController _apiController; + private readonly Action _clientRenderFn; + private readonly MareConfigService _mareConfig; + private readonly SelectPairForGroupUi _selectGroupForPairUi; + private readonly TagHandler _tagHandler; + + public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, Action clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi) { - private readonly Action _clientRenderFn; - private readonly TagHandler _tagHandler; - private readonly ApiController _apiController; - private readonly SelectPairForGroupUi _selectGroupForPairUi; + _clientRenderFn = clientRenderFn; + _mareConfig = mareConfig; + _tagHandler = tagHandler; + _apiController = apiController; + _selectGroupForPairUi = selectGroupForPairUi; + } - public PairGroupsUi(TagHandler tagHandler, Action clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi) + public void Draw(List visibleUsers, List onlineUsers, List offlineUsers) + { + // Only render those tags that actually have pairs in them, otherwise + // we can end up with a bunch of useless pair groups + var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted(); + var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList(); + if (_mareConfig.Current.ShowVisibleUsersSeparately) { - _clientRenderFn = clientRenderFn; - _tagHandler = tagHandler; - _apiController = apiController; - _selectGroupForPairUi = selectGroupForPairUi; + UiSharedService.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers)); } - - public void Draw(List visibleUsers, List onlineUsers, List offlineUsers) + foreach (var tag in tagsWithPairsInThem) { - // Only render those tags that actually have pairs in them, otherwise - // we can end up with a bunch of useless pair groups - var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted(); - var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList(); - UiShared.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers)); - foreach (var tag in tagsWithPairsInThem) + if (_mareConfig.Current.ShowOfflineUsersSeparately) { - UiShared.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers)); - } - UiShared.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers)); - UiShared.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag, offlineUsers, allUsers)); - } - - private void DrawCategory(string tag, List onlineUsers, List allUsers, List? visibleUsers = null) - { - List usersInThisTag; - HashSet? otherUidsTaggedWithTag = null; - bool isSpecialTag = false; - int visibleInThisTag = 0; - if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag) - { - usersInThisTag = onlineUsers; - isSpecialTag = true; + UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers)); } else { - otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag); - usersInThisTag = onlineUsers - .Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID)) - .ToList(); - visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0; - } - - if (isSpecialTag && !usersInThisTag.Any()) return; - - DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count); - if (!isSpecialTag) - UiShared.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList())); - - if (!_tagHandler.IsTagOpen(tag)) return; - - ImGui.Indent(20); - DrawPairs(tag, usersInThisTag); - ImGui.Unindent(20); - } - - private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total) - { - string displayedName = tag switch - { - TagHandler.CustomOfflineTag => "Offline/Unpaired", - TagHandler.CustomOnlineTag => "Online/Paused", - TagHandler.CustomVisibleTag => "Visible", - _ => tag - }; - - string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)"; - - // FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight - var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; - UiShared.FontText(icon.ToIconString(), UiBuilder.IconFont); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - ToggleTagOpen(tag); - } - ImGui.SameLine(); - UiShared.FontText(resultFolderName, UiBuilder.DefaultFont); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - ToggleTagOpen(tag); - } - - if (!isSpecialTag && ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.TextUnformatted($"Group {tag}"); - ImGui.Separator(); - ImGui.TextUnformatted($"{visible} Pairs visible"); - ImGui.TextUnformatted($"{online} Pairs online/paused"); - ImGui.TextUnformatted($"{total} Pairs total"); - ImGui.EndTooltip(); + UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers)); } } - - private void DrawButtons(string tag, List availablePairsInThisTag) + if (_mareConfig.Current.ShowOfflineUsersSeparately) { - var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused()); - var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - var flyoutMenuX = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X; - var pauseButtonX = UiShared.GetIconButtonSize(pauseButton).X; - var windowX = ImGui.GetWindowContentRegionMin().X; - var windowWidth = UiShared.GetWindowContentRegionWidth(); - var spacingX = ImGui.GetStyle().ItemSpacing.X; + UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers)); + UiSharedService.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag, + offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers)); + } + else + { + UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Concat(offlineUsers).Where(u => u.UserPair!.OtherPermissions.IsPaired() && !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers)); + } + UiSharedService.DrawWithID($"group-UnpairedCustomTag", () => DrawCategory(TagHandler.CustomUnpairedTag, + offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers)); + } - var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX; - ImGui.SameLine(buttonPauseOffset); - if (ImGuiComponents.IconButton(pauseButton)) - { - // If all of the currently visible pairs (after applying filters to the pairs) - // are paused we display a resume button to resume all currently visible (after filters) - // pairs. Otherwise, we just pause all the remaining pairs. - if (allArePaused) - { - // If all are paused => resume all - ResumeAllPairs(availablePairsInThisTag); - } - else - { - // otherwise pause all remaining - PauseRemainingPairs(availablePairsInThisTag); - } - } + private void DrawButtons(string tag, List availablePairsInThisTag) + { + var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused()); + var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var flyoutMenuX = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; + var pauseButtonX = UiSharedService.GetIconButtonSize(pauseButton).X; + var windowX = ImGui.GetWindowContentRegionMin().X; + var windowWidth = UiSharedService.GetWindowContentRegionWidth(); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + + var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX; + ImGui.SameLine(buttonPauseOffset); + if (ImGuiComponents.IconButton(pauseButton)) + { + // If all of the currently visible pairs (after applying filters to the pairs) + // are paused we display a resume button to resume all currently visible (after filters) + // pairs. Otherwise, we just pause all the remaining pairs. if (allArePaused) { - UiShared.AttachToolTip($"Resume pairing with all pairs in {tag}"); + // If all are paused => resume all + ResumeAllPairs(availablePairsInThisTag); } else { - UiShared.AttachToolTip($"Pause pairing with all pairs in {tag}"); - } - - var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX; - ImGui.SameLine(buttonDeleteOffset); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) - { - ImGui.OpenPopup("Group Flyout Menu"); - - } - - if (ImGui.BeginPopup("Group Flyout Menu")) - { - UiShared.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag)); - ImGui.EndPopup(); + // otherwise pause all remaining + PauseRemainingPairs(availablePairsInThisTag); } } - - private void DrawGroupMenu(string tag) + if (allArePaused) { - if (UiShared.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag)) - { - _selectGroupForPairUi.Open(tag); - } - UiShared.AttachToolTip($"Add more users to Group {tag}"); - - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag)) - { - if (UiShared.CtrlPressed()) - { - _tagHandler.RemoveTag(tag); - } - } - UiShared.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete"); + UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}"); + } + else + { + UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}"); } - private void DrawPairs(string tag, List availablePairsInThisCategory) + var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX; + ImGui.SameLine(buttonDeleteOffset); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) { - // These are all the OtherUIDs that are tagged with this tag - availablePairsInThisCategory - .ForEach(pair => UiShared.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair))); - ImGui.Separator(); + ImGui.OpenPopup("Group Flyout Menu"); } - private void ToggleTagOpen(string tag) + if (ImGui.BeginPopup("Group Flyout Menu")) { - bool open = !_tagHandler.IsTagOpen(tag); - _tagHandler.SetTagOpen(tag, open); - } - - private void PauseRemainingPairs(List availablePairs) - { - foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused())) - { - var perm = pairToPause.UserPair!.OwnPermissions; - perm.SetPaused(paused: true); - _ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm)); - } - } - - private void ResumeAllPairs(List availablePairs) - { - foreach (var pairToPause in availablePairs) - { - var perm = pairToPause.UserPair!.OwnPermissions; - perm.SetPaused(paused: false); - _ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm)); - } + UiSharedService.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag)); + ImGui.EndPopup(); } } + + private void DrawCategory(string tag, List onlineUsers, List allUsers, List? visibleUsers = null) + { + List usersInThisTag; + HashSet? otherUidsTaggedWithTag = null; + bool isSpecialTag = false; + int visibleInThisTag = 0; + if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag or TagHandler.CustomUnpairedTag) + { + usersInThisTag = onlineUsers; + isSpecialTag = true; + } + else + { + otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag); + usersInThisTag = onlineUsers + .Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID)) + .ToList(); + visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0; + } + + if (isSpecialTag && !usersInThisTag.Any()) return; + + DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count); + if (!isSpecialTag) + UiSharedService.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList())); + + if (!_tagHandler.IsTagOpen(tag)) return; + + ImGui.Indent(20); + DrawPairs(tag, usersInThisTag); + ImGui.Unindent(20); + } + + private void DrawGroupMenu(string tag) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag)) + { + _selectGroupForPairUi.Open(tag); + } + UiSharedService.AttachToolTip($"Add more users to Group {tag}"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed()) + { + _tagHandler.RemoveTag(tag); + } + UiSharedService.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete"); + } + + private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total) + { + string displayedName = tag switch + { + TagHandler.CustomUnpairedTag => "Unpaired", + TagHandler.CustomOfflineTag => "Offline", + TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts", + TagHandler.CustomVisibleTag => "Visible", + _ => tag + }; + + string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)"; + + // FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight + var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ToggleTagOpen(tag); + } + ImGui.SameLine(); + UiSharedService.FontText(resultFolderName, UiBuilder.DefaultFont); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ToggleTagOpen(tag); + } + + if (!isSpecialTag && ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted($"Group {tag}"); + ImGui.Separator(); + ImGui.TextUnformatted($"{visible} Pairs visible"); + ImGui.TextUnformatted($"{online} Pairs online/paused"); + ImGui.TextUnformatted($"{total} Pairs total"); + ImGui.EndTooltip(); + } + } + + private void DrawPairs(string tag, List availablePairsInThisCategory) + { + // These are all the OtherUIDs that are tagged with this tag + availablePairsInThisCategory + .ForEach(pair => UiSharedService.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair))); + ImGui.Separator(); + } + + private void PauseRemainingPairs(List availablePairs) + { + foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused())) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: true); + _ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm)); + } + } + + private void ResumeAllPairs(List availablePairs) + { + foreach (var pairToPause in availablePairs) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: false); + _ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm)); + } + } + + private void ToggleTagOpen(string tag) + { + bool open = !_tagHandler.IsTagOpen(tag); + _tagHandler.SetTagOpen(tag, open); + } } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs index 97c6379..8da81b4 100644 --- a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs +++ b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs @@ -3,17 +3,14 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Utility; using ImGuiNET; -using MareSynchronos.Models; +using MareSynchronos.PlayerData.Pairs; using MareSynchronos.UI.Handlers; namespace MareSynchronos.UI.Components; public class SelectGroupForPairUi { - /// - /// Should the panel show, yes/no - /// - private bool _show; + private readonly TagHandler _tagHandler; /// /// The group UI is always open for a specific pair. This defines which pair the UI is open for. @@ -21,13 +18,16 @@ public class SelectGroupForPairUi /// private Pair? _pair; + /// + /// Should the panel show, yes/no + /// + private bool _show; + /// /// For the add category option, this stores the currently typed in tag name /// private string _tagNameToAdd = ""; - private readonly TagHandler _tagHandler; - public SelectGroupForPairUi(TagHandler tagHandler) { _show = false; @@ -35,17 +35,6 @@ public class SelectGroupForPairUi _tagHandler = tagHandler; } - public void Open(Pair pair) - { - _pair = pair; - // Using "_show" here to de-couple the opening of the popup - // The popup name is derived from the name the user currently sees, which is - // based on the showUidForEntry dictionary. - // We'd have to derive the name here to open it popup modal here, when the Open() is called - _show = true; - } - - public void Draw(Dictionary showUidForEntry) { if (_pair == null) @@ -68,34 +57,53 @@ public class SelectGroupForPairUi var childHeight = tags.Count != 0 ? tags.Count * 25 : 1; var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale; - UiShared.FontText($"Select the groups you want {name} to be in.", UiBuilder.DefaultFont); + UiSharedService.FontText($"Select the groups you want {name} to be in.", UiBuilder.DefaultFont); if (ImGui.BeginChild(name + "##listGroups", childSize)) { foreach (var tag in tags) { - UiShared.DrawWithID($"groups-pair-{_pair.UserData.UID}-{tag}", () => DrawGroupName(_pair, tag)); + UiSharedService.DrawWithID($"groups-pair-{_pair.UserData.UID}-{tag}", () => DrawGroupName(_pair, tag)); } ImGui.EndChild(); } ImGui.Separator(); - UiShared.FontText($"Create a new group for {name}.", UiBuilder.DefaultFont); + UiSharedService.FontText($"Create a new group for {name}.", UiBuilder.DefaultFont); if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) { HandleAddTag(); } ImGui.SameLine(); ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40); + if (ImGui.IsKeyDown(ImGuiKey.Enter)) { - if (ImGui.IsKeyDown(ImGuiKey.Enter)) - { - HandleAddTag(); - } + HandleAddTag(); } ImGui.EndPopup(); } } + public void Open(Pair pair) + { + _pair = pair; + // Using "_show" here to de-couple the opening of the popup + // The popup name is derived from the name the user currently sees, which is + // based on the showUidForEntry dictionary. + // We'd have to derive the name here to open it popup modal here, when the Open() is called + _show = true; + } + + private static string PairName(Dictionary showUidForEntry, Pair pair) + { + showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); + var playerText = pair.GetNote(); + if (showUidInsteadOfName || string.IsNullOrEmpty(playerText)) + { + playerText = pair.UserData.AliasOrUID; + } + return playerText; + } + private void DrawGroupName(Pair pair, string name) { var hasTagBefore = _tagHandler.HasTag(pair.UserPair!, name); @@ -129,15 +137,4 @@ public class SelectGroupForPairUi _tagNameToAdd = string.Empty; } } - - private string PairName(Dictionary showUidForEntry, Pair pair) - { - showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); - var playerText = pair.GetNote(); - if (showUidInsteadOfName || string.IsNullOrEmpty(playerText)) - { - playerText = pair.UserData.AliasOrUID; - } - return playerText; - } } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectPairForGroupUi.cs b/MareSynchronos/UI/Components/SelectPairForGroupUi.cs index f16c069..4ecea8e 100644 --- a/MareSynchronos/UI/Components/SelectPairForGroupUi.cs +++ b/MareSynchronos/UI/Components/SelectPairForGroupUi.cs @@ -1,38 +1,31 @@ using System.Numerics; using Dalamud.Interface; using ImGuiNET; -using MareSynchronos.Models; +using MareSynchronos.PlayerData.Pairs; using MareSynchronos.UI.Handlers; namespace MareSynchronos.UI.Components; public class SelectPairForGroupUi { - private bool _show = false; - private bool _opened = false; - private HashSet _peopleInGroup = new(StringComparer.Ordinal); - private string _tag = string.Empty; private readonly TagHandler _tagHandler; private string _filter = string.Empty; + private bool _opened = false; + private HashSet _peopleInGroup = new(StringComparer.Ordinal); + private bool _show = false; + private string _tag = string.Empty; public SelectPairForGroupUi(TagHandler tagHandler) { _tagHandler = tagHandler; } - public void Open(string tag) - { - _peopleInGroup = _tagHandler.GetOtherUidsForTag(tag); - _tag = tag; - _show = true; - } - public void Draw(List pairs, Dictionary showUidForEntry) { var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale; var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale; var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale; - + var popupName = $"Choose Users for Group {_tag}"; if (!_show) @@ -43,7 +36,7 @@ public class SelectPairForGroupUi if (_show && !_opened) { ImGui.SetNextWindowSize(minSize); - UiShared.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always); + UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always); ImGui.OpenPopup(popupName); _opened = true; } @@ -51,10 +44,12 @@ public class SelectPairForGroupUi ImGui.SetNextWindowSizeConstraints(minSize, maxSize); if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal)) { - UiShared.FontText($"Select users for group {_tag}", UiBuilder.DefaultFont); + UiSharedService.FontText($"Select users for group {_tag}", UiBuilder.DefaultFont); ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None); - foreach (var item in pairs.OrderBy(p => PairName(showUidForEntry, p), StringComparer.OrdinalIgnoreCase) - .Where(p => string.IsNullOrEmpty(_filter) || PairName(showUidForEntry, p).Contains(_filter, StringComparison.OrdinalIgnoreCase)).ToList()) + foreach (var item in pairs + .Where(p => string.IsNullOrEmpty(_filter) || PairName(showUidForEntry, p).Contains(_filter, StringComparison.OrdinalIgnoreCase)) + .OrderBy(p => PairName(showUidForEntry, p), StringComparer.OrdinalIgnoreCase) + .ToList()) { var isInGroup = _peopleInGroup.Contains(item.UserData.UID); if (ImGui.Checkbox(PairName(showUidForEntry, item), ref isInGroup)) @@ -80,7 +75,14 @@ public class SelectPairForGroupUi } } - private string PairName(Dictionary showUidForEntry, Pair pair) + public void Open(string tag) + { + _peopleInGroup = _tagHandler.GetOtherUidsForTag(tag); + _tag = tag; + _show = true; + } + + private static string PairName(Dictionary showUidForEntry, Pair pair) { showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); var playerText = pair.GetNote(); @@ -90,4 +92,4 @@ public class SelectPairForGroupUi } return playerText; } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/DownloadUi.cs b/MareSynchronos/UI/DownloadUi.cs index 7b10daa..be92553 100644 --- a/MareSynchronos/UI/DownloadUi.cs +++ b/MareSynchronos/UI/DownloadUi.cs @@ -1,40 +1,38 @@ -using System.Numerics; -using Dalamud.Interface.Windowing; +using Dalamud.Interface.Colors; using ImGuiNET; using MareSynchronos.MareConfiguration; -using MareSynchronos.WebAPI; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Numerics; namespace MareSynchronos.UI; -public class DownloadUi : Window, IDisposable +public class DownloadUi : WindowMediatorSubscriberBase { - private readonly ILogger _logger; - private readonly WindowSystem _windowSystem; private readonly MareConfigService _configService; - private readonly ApiController _apiController; - private readonly UiShared _uiShared; - private bool _wasOpen = false; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly DalamudUtilService _dalamudUtilService; + private readonly FileUploadManager _fileTransferManager; + private readonly UiSharedService _uiShared; + private readonly ConcurrentDictionary _uploadingPlayers = new(); - public void Dispose() + public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, MareConfigService configService, + FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared) : base(logger, mediator, "Mare Synchronos Downloads") { - _logger.LogTrace($"Disposing {GetType()}"); - _windowSystem.RemoveWindow(this); - } - - public DownloadUi(ILogger logger, WindowSystem windowSystem, MareConfigService configService, ApiController apiController, UiShared uiShared) : base("Mare Synchronos Downloads") - { - _logger = logger; - _logger.LogTrace("Creating " + nameof(DownloadUi)); - _windowSystem = windowSystem; + _dalamudUtilService = dalamudUtilService; _configService = configService; - _apiController = apiController; + _fileTransferManager = fileTransferManager; _uiShared = uiShared; SizeConstraints = new WindowSizeConstraints() { - MaximumSize = new Vector2(300, 90), - MinimumSize = new Vector2(300, 90), + MaximumSize = new Vector2(500, 90), + MinimumSize = new Vector2(500, 90), }; Flags |= ImGuiWindowFlags.NoMove; @@ -48,20 +46,175 @@ public class DownloadUi : Window, IDisposable ForceMainWindow = true; - windowSystem.AddWindow(this); IsOpen = true; + + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => IsOpen = true); + Mediator.Subscribe(this, (msg) => + { + if (msg.IsUploading) + { + _uploadingPlayers[msg.Handler] = true; + } + else + { + _uploadingPlayers.TryRemove(msg.Handler, out _); + } + }); + } + + public override void Draw() + { + if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return; + if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return; + if (!IsOpen) return; + + if (_configService.Current.ShowTransferWindow) + { + try + { + if (_fileTransferManager.CurrentUploads.Any()) + { + var currentUploads = _fileTransferManager.CurrentUploads.ToList(); + var totalUploads = currentUploads.Count; + + var doneUploads = currentUploads.Count(c => c.IsTransferred); + var totalUploaded = currentUploads.Sum(c => c.Transferred); + var totalToUpload = currentUploads.Sum(c => c.Total); + + UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.SameLine(); + var xDistance = ImGui.GetCursorPosX(); + UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.NewLine(); + ImGui.SameLine(xDistance); + UiSharedService.DrawOutlinedFont( + $"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + + if (_currentDownloads.Any()) ImGui.Separator(); + } + } + catch + { + // ignore errors thrown from UI + } + + try + { + foreach (var item in _currentDownloads.ToList()) + { + var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); + var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); + var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); + var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); + var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); + var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); + var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); + var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); + + UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.SameLine(); + var xDistance = ImGui.GetCursorPosX(); + UiSharedService.DrawOutlinedFont( + $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + ImGui.NewLine(); + ImGui.SameLine(xDistance); + UiSharedService.DrawOutlinedFont( + $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", + ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); + } + } + catch + { + // ignore errors thrown from UI + } + } + + if (_configService.Current.ShowTransferBars) + { + const int transparency = 100; + const int dlBarBorder = 3; + + foreach (var transfer in _currentDownloads.ToList()) + { + var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GameObjectLazy.Value); + if (screenPos == Vector2.Zero) continue; + + var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); + var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); + + var downloadText = + $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; + var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; + var textSize = ImGui.CalcTextSize(maxDlText); + + int dlBarHeight = (int)textSize.Y + 8; + int dlBarWidth = (int)textSize.X + 150; + + var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f); + var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f); + var drawList = ImGui.GetBackgroundDrawList(); + drawList.AddRectFilled( + dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 }, + dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 }, + UiSharedService.Color(0, 0, 0, transparency), 1); + drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder }, + dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder }, + UiSharedService.Color(220, 220, 220, transparency), 1); + drawList.AddRectFilled(dlBarStart, dlBarEnd, + UiSharedService.Color(0, 0, 0, transparency), 1); + var dlProgressPercent = transferredBytes / (double)totalBytes; + drawList.AddRectFilled(dlBarStart, + dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) }, + UiSharedService.Color(50, 205, 50, transparency), 1); + UiSharedService.DrawOutlinedFont(drawList, downloadText, + screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, + UiSharedService.Color(255, 255, 255, transparency), + UiSharedService.Color(0, 0, 0, transparency), 1); + } + + if (_configService.Current.ShowUploading) + { + foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList()) + { + var screenPos = _dalamudUtilService.WorldToScreen(player.GameObjectLazy.Value); + if (screenPos == Vector2.Zero) continue; + + try + { + if (_uiShared.UidFontBuilt && _configService.Current.ShowUploadingBigText) ImGui.PushFont(_uiShared.UidFont); + var uploadText = "Uploading"; + + var textSize = ImGui.CalcTextSize(uploadText); + + var drawList = ImGui.GetBackgroundDrawList(); + UiSharedService.DrawOutlinedFont(drawList, uploadText, + screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, + UiSharedService.Color(255, 255, 0, transparency), + UiSharedService.Color(0, 0, 0, transparency), 2); + } + catch + { + // ignore errors thrown on UI + } + finally + { + if (_uiShared.UidFontBuilt && _configService.Current.ShowUploadingBigText) ImGui.PopFont(); + } + } + } + } } public override void PreDraw() { - if (_uiShared.IsInGpose) - { - _wasOpen = IsOpen; - IsOpen = false; - } - - base.PreDraw(); + if (_uiShared.EditTrackerPosition) { Flags &= ~ImGuiWindowFlags.NoMove; @@ -76,65 +229,12 @@ public class DownloadUi : Window, IDisposable Flags |= ImGuiWindowFlags.NoInputs; Flags |= ImGuiWindowFlags.NoResize; } - } - public override void Draw() - { - if (!_configService.Current.ShowTransferWindow) return; - if (!_apiController.IsDownloading && !_apiController.IsUploading) return; - - var drawList = ImGui.GetWindowDrawList(); - var yDistance = 20; - var xDistance = 20; - - var basePosition = ImGui.GetWindowPos() + ImGui.GetWindowContentRegionMin(); - - try + var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3); + SizeConstraints = new() { - if (_apiController.CurrentUploads.Any()) - { - var currentUploads = _apiController.CurrentUploads.ToList(); - var totalUploads = currentUploads.Count; - - var doneUploads = currentUploads.Count(c => c.IsTransferred); - var totalUploaded = currentUploads.Sum(c => c.Transferred); - var totalToUpload = currentUploads.Sum(c => c.Total); - - UiShared.DrawOutlinedFont(drawList, "▲", - new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * 0.5)), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - UiShared.DrawOutlinedFont(drawList, $"Compressing+Uploading {doneUploads}/{totalUploads}", - new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * 0), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - UiShared.DrawOutlinedFont(drawList, $"{UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)}", - new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * 1), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - - } - } - catch { } - - try - { - if (_apiController.CurrentDownloads.Any()) - { - var currentDownloads = _apiController.CurrentDownloads.Where(d => d.Value != null && d.Value.Any()).ToList().SelectMany(k => k.Value).ToList(); - var multBase = currentDownloads.Any() ? 0 : 2; - var doneDownloads = currentDownloads.Count(c => c.IsTransferred); - var totalDownloads = currentDownloads.Count; - var totalDownloaded = currentDownloads.Sum(c => c.Transferred); - var totalToDownload = currentDownloads.Sum(c => c.Total); - UiShared.DrawOutlinedFont(drawList, "▼", - new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * multBase + (yDistance * 0.5))), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - UiShared.DrawOutlinedFont(drawList, $"Downloading {doneDownloads}/{totalDownloads}", - new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * multBase), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - UiShared.DrawOutlinedFont(drawList, $"{UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)}", - new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * (1 + multBase)), - UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2); - } - } - catch { } + MinimumSize = new Vector2(300, maxHeight), + MaximumSize = new Vector2(300, maxHeight), + }; } } \ No newline at end of file diff --git a/MareSynchronos/UI/GposeUi.cs b/MareSynchronos/UI/GposeUi.cs index 4759bdc..3bb3702 100644 --- a/MareSynchronos/UI/GposeUi.cs +++ b/MareSynchronos/UI/GposeUi.cs @@ -1,38 +1,73 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using MareSynchronos.Export; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Utils; +using MareSynchronos.PlayerData.Export; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; namespace MareSynchronos.UI; -public class GposeUi : WindowMediatorSubscriberBase, IDisposable +public class GposeUi : WindowMediatorSubscriberBase { - private readonly WindowSystem _windowSystem; - private readonly MareCharaFileManager _mareCharaFileManager; - private readonly DalamudUtil _dalamudUtil; - private readonly FileDialogManager _fileDialogManager; private readonly MareConfigService _configService; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileDialogManager _fileDialogManager; + private readonly MareCharaFileManager _mareCharaFileManager; - public GposeUi(ILogger logger, WindowSystem windowSystem, MareCharaFileManager mareCharaFileManager, - DalamudUtil dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService, + public GposeUi(ILogger logger, MareCharaFileManager mareCharaFileManager, + DalamudUtilService dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService, MareMediator mediator) : base(logger, mediator, "Mare Synchronos Gpose Import UI###MareSynchronosGposeUI") { - _windowSystem = windowSystem; _mareCharaFileManager = mareCharaFileManager; _dalamudUtil = dalamudUtil; _fileDialogManager = fileDialogManager; _configService = configService; + Mediator.Subscribe(this, (_) => StartGpose()); Mediator.Subscribe(this, (_) => EndGpose()); IsOpen = _dalamudUtil.IsInGpose; - Flags = ImGuiWindowFlags.AlwaysAutoResize; - _windowSystem.AddWindow(this); + this.SizeConstraints = new() + { + MinimumSize = new(200, 200), + MaximumSize = new(400, 400) + }; + } + + public override void Draw() + { + if (!_dalamudUtil.IsInGpose) IsOpen = false; + + if (!_mareCharaFileManager.CurrentlyWorking) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF")) + { + _fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, path) => + { + if (!success) return; + + Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); + }); + } + UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor"); + if (_mareCharaFileManager.LoadedCharaFile != null) + { + UiSharedService.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath); + UiSharedService.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF")) + { + Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false)); + } + UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor"); + UiSharedService.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow); + } + } + else + { + UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow); + } + UiSharedService.TextWrapped("Hint: You can disable the automatic loading of this window in the Mare settings and open it manually with /mare gpose"); } private void EndGpose() @@ -45,45 +80,4 @@ public class GposeUi : WindowMediatorSubscriberBase, IDisposable { IsOpen = _configService.Current.OpenGposeImportOnGposeStart; } - - public override void Dispose() - { - base.Dispose(); - _windowSystem.RemoveWindow(this); - } - - public override void Draw() - { - if (!_dalamudUtil.IsInGpose) IsOpen = false; - - if (!_mareCharaFileManager.CurrentlyWorking) - { - if (UiShared.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF")) - { - _fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, path) => - { - if (!success) return; - - Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); - }); - } - UiShared.AttachToolTip("Applies it to the currently selected GPose actor"); - if (_mareCharaFileManager.LoadedCharaFile != null) - { - UiShared.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath); - UiShared.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description); - if (UiShared.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF")) - { - Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false)); - } - UiShared.AttachToolTip("Applies it to the currently selected GPose actor"); - UiShared.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow); - } - } - else - { - UiShared.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow); - } - UiShared.TextWrapped("Hint: You can disable the automatic loading of this window in the Mare settings and open it manually with /mare gpose"); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/GroupPanel.cs b/MareSynchronos/UI/GroupPanel.cs deleted file mode 100644 index ef52168..0000000 --- a/MareSynchronos/UI/GroupPanel.cs +++ /dev/null @@ -1,1038 +0,0 @@ -using Dalamud.Interface.Colors; -using Dalamud.Interface.Components; -using Dalamud.Interface; -using Dalamud.Utility; -using ImGuiNET; -using MareSynchronos.WebAPI; -using System.Numerics; -using System.Globalization; -using MareSynchronos.API.Data; -using MareSynchronos.API.Dto.Group; -using MareSynchronos.API.Dto.User; -using MareSynchronos.API.Data.Enum; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.Managers; -using MareSynchronos.Models; -using MareSynchronos.API.Data.Comparer; -using MareSynchronos.MareConfiguration; - -namespace MareSynchronos.UI -{ - internal class GroupPanel - { - private readonly CompactUi _mainUi; - private readonly UiShared _uiShared; - private ApiController ApiController => _uiShared.ApiController; - private readonly PairManager _pairManager; - private readonly ServerConfigurationManager _serverConfigurationManager; - private readonly MareConfigService _configService; - private readonly Dictionary _showGidForEntry = new(StringComparer.Ordinal); - private string _editGroupEntry = string.Empty; - private string _editGroupComment = string.Empty; - private string _syncShellPassword = string.Empty; - private string _syncShellToJoin = string.Empty; - - private bool _showModalEnterPassword; - private bool _showModalCreateGroup; - private bool _showModalChangePassword; - private bool _showModalBanUser; - private bool _showModalBanList = false; - private bool _showModalBulkOneTimeInvites = false; - private string _newSyncShellPassword = string.Empty; - private string _banReason = string.Empty; - private bool _isPasswordValid; - private bool _errorGroupJoin; - private bool _errorGroupCreate = false; - private GroupPasswordDto? _lastCreatedGroup = null; - private readonly Dictionary _expandedGroupState = new(StringComparer.Ordinal); - private List _bannedUsers = new(); - private List _bulkOneTimeInvites = new(); - private bool _modalBanListOpened; - private bool _modalBulkOneTimeInvitesOpened; - private bool _banUserPopupOpen; - private bool _modalChangePwOpened; - private int _bulkInviteCount = 10; - - public GroupPanel(CompactUi mainUi, UiShared uiShared, PairManager pairManager, ServerConfigurationManager serverConfigurationManager, MareConfigService configurationService) - { - _mainUi = mainUi; - _uiShared = uiShared; - _pairManager = pairManager; - _serverConfigurationManager = serverConfigurationManager; - _configService = configurationService; - } - - public void DrawSyncshells() - { - UiShared.DrawWithID("addsyncshell", DrawAddSyncshell); - UiShared.DrawWithID("syncshelllist", DrawSyncshellList); - _mainUi.TransferPartHeight = ImGui.GetCursorPosY(); - } - - private void DrawAddSyncshell() - { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); - ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - - bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; - bool userCanCreateMoreGroups = _pairManager.GroupPairs.Count(u => string.Equals(u.Key.Owner.UID, ApiController.UID, StringComparison.Ordinal)) < ApiController.ServerInfo.MaxGroupsCreatedByUser; - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) - { - if (_pairManager.GroupPairs.All(w => !string.Equals(w.Key.Group.GID, _syncShellToJoin, StringComparison.Ordinal) && !string.Equals(w.Key.Group.Alias, _syncShellToJoin, StringComparison.Ordinal)) - && !string.IsNullOrEmpty(_syncShellToJoin)) - { - if (userCanJoinMoreGroups) - { - _errorGroupJoin = false; - _showModalEnterPassword = true; - ImGui.OpenPopup("Enter Syncshell Password"); - } - } - else - { - if (userCanCreateMoreGroups) - { - _lastCreatedGroup = null; - _errorGroupCreate = false; - _showModalCreateGroup = true; - ImGui.OpenPopup("Create Syncshell"); - } - } - } - UiShared.AttachToolTip(_syncShellToJoin.IsNullOrEmpty() - ? (userCanCreateMoreGroups ? "Create Syncshell" : $"You cannot create more than {ApiController.ServerInfo.MaxGroupsCreatedByUser} Syncshells") - : (userCanJoinMoreGroups ? "Join Syncshell" + _syncShellToJoin : $"You cannot join more than {ApiController.ServerInfo.MaxGroupsJoinedByUser} Syncshells")); - - if (ImGui.BeginPopupModal("Enter Syncshell Password", ref _showModalEnterPassword, UiShared.PopupWindowFlags)) - { - UiShared.TextWrapped("Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell."); - ImGui.Separator(); - UiShared.TextWrapped("Enter the password for Syncshell " + _syncShellToJoin + ":"); - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint("##password", _syncShellToJoin + " Password", ref _syncShellPassword, 255, ImGuiInputTextFlags.Password); - if (_errorGroupJoin) - { - UiShared.ColorTextWrapped($"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({ApiController.ServerInfo.MaxGroupsJoinedByUser}), " + - $"it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({ApiController.ServerInfo.MaxGroupUserCount} users) or the Syncshell has closed invites.", - new Vector4(1, 0, 0, 1)); - } - if (ImGui.Button("Join " + _syncShellToJoin)) - { - var shell = _syncShellToJoin; - var pw = _syncShellPassword; - _errorGroupJoin = !ApiController.GroupJoin(new(new GroupData(shell), pw)).Result; - if (!_errorGroupJoin) - { - _syncShellToJoin = string.Empty; - _showModalEnterPassword = false; - } - _syncShellPassword = string.Empty; - } - UiShared.SetScaledWindowSize(290); - ImGui.EndPopup(); - } - - if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiShared.PopupWindowFlags)) - { - UiShared.TextWrapped("Press the button below to create a new Syncshell."); - ImGui.SetNextItemWidth(200); - if (ImGui.Button("Create Syncshell")) - { - try - { - _lastCreatedGroup = ApiController.GroupCreate().Result; - } - catch - { - _lastCreatedGroup = null; - _errorGroupCreate = true; - } - } - - if (_lastCreatedGroup != null) - { - ImGui.Separator(); - _errorGroupCreate = false; - ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) - { - ImGui.SetClipboardText(_lastCreatedGroup.Password); - } - UiShared.TextWrapped("You can change the Syncshell password later at any time."); - } - - if (_errorGroupCreate) - { - UiShared.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.", - new Vector4(1, 0, 0, 1)); - } - - UiShared.SetScaledWindowSize(350); - ImGui.EndPopup(); - } - - ImGuiHelpers.ScaledDummy(2); - } - - private void DrawSyncshellList() - { - var ySize = _mainUi.TransferPartHeight == 0 - ? 1 - : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY(); - ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false); - foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList()) - { - UiShared.DrawWithID(entry.Key.Group.GID, () => DrawSyncshell(entry.Key, entry.Value)); - } - ImGui.EndChild(); - } - - private void DrawSyncshell(GroupFullInfoDto groupDto, List pairsInGroup) - { - var name = groupDto.Group.Alias ?? groupDto.GID; - if (!_expandedGroupState.TryGetValue(groupDto.GID, out bool isExpanded)) - { - isExpanded = false; - _expandedGroupState.Add(groupDto.GID, isExpanded); - } - var icon = isExpanded ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; - var collapseButton = UiShared.GetIconButtonSize(icon); - ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0)); - if (ImGuiComponents.IconButton(icon)) - { - _expandedGroupState[groupDto.GID] = !_expandedGroupState[groupDto.GID]; - } - ImGui.PopStyleColor(2); - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + collapseButton.X); - var pauseIcon = groupDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - if (ImGuiComponents.IconButton(pauseIcon)) - { - var userPerm = groupDto.GroupUserPermissions ^ GroupUserPermissions.Paused; - _ = ApiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupDto.Group, new UserData(ApiController.UID), userPerm)); - } - UiShared.AttachToolTip((groupDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " pairing with all users in this Syncshell"); - ImGui.SameLine(); - - var textIsGid = true; - string groupName = groupDto.GroupAliasOrGID; - - if (string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal)) - { - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Crown.ToIconString()); - ImGui.PopFont(); - UiShared.AttachToolTip("You are the owner of Syncshell " + groupName); - ImGui.SameLine(); - } - else if (groupDto.GroupUserInfo.IsModerator()) - { - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.UserShield.ToIconString()); - ImGui.PopFont(); - UiShared.AttachToolTip("You are a moderator of Syncshell " + groupName); - ImGui.SameLine(); - } - - _showGidForEntry.TryGetValue(groupDto.GID, out var showGidInsteadOfName); - var groupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID); - if (!showGidInsteadOfName && !string.IsNullOrEmpty(groupComment)) - { - groupName = groupComment; - textIsGid = false; - } - - if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) - { - if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(groupName); - if (textIsGid) ImGui.PopFont(); - UiShared.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine + - "Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine - + "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - var prevState = textIsGid; - if (_showGidForEntry.ContainsKey(groupDto.GID)) - { - prevState = _showGidForEntry[groupDto.GID]; - } - - _showGidForEntry[groupDto.GID] = !prevState; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _serverConfigurationManager.SetNoteForGid(_editGroupEntry, _editGroupComment); - _editGroupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID) ?? string.Empty; - _editGroupEntry = groupDto.GID; - } - } - else - { - var buttonSizes = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X + UiShared.GetIconSize(FontAwesomeIcon.LockOpen).X; - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); - if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - _serverConfigurationManager.SetNoteForGid(groupDto.GID, _editGroupComment); - _editGroupEntry = string.Empty; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _editGroupEntry = string.Empty; - } - UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); - } - - UiShared.DrawWithID(groupDto.GID + "settings", () => DrawSyncShellButtons(groupDto, pairsInGroup)); - - if (_showModalBanList && !_modalBanListOpened) - { - _modalBanListOpened = true; - ImGui.OpenPopup("Manage Banlist for " + groupDto.GID); - } - - if (!_showModalBanList) _modalBanListOpened = false; - - if (ImGui.BeginPopupModal("Manage Banlist for " + groupDto.GID, ref _showModalBanList, UiShared.PopupWindowFlags)) - { - if (UiShared.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) - { - _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; - } - - if (ImGui.BeginTable("bannedusertable" + groupDto.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) - { - ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); - ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); - - ImGui.TableHeadersRow(); - - foreach (var bannedUser in _bannedUsers.ToList()) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UID); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedBy); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); - ImGui.TableNextColumn(); - UiShared.TextWrapped(bannedUser.Reason); - ImGui.TableNextColumn(); - if (UiShared.IconTextButton(FontAwesomeIcon.Check, "Unban")) - { - _ = ApiController.GroupUnbanUser(bannedUser); - _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); - } - } - - ImGui.EndTable(); - } - UiShared.SetScaledWindowSize(700, 300); - ImGui.EndPopup(); - } - - if (_showModalChangePassword && !_modalChangePwOpened) - { - _modalChangePwOpened = true; - ImGui.OpenPopup("Change Syncshell Password"); - } - - if (!_showModalChangePassword) _modalChangePwOpened = false; - - if (ImGui.BeginPopupModal("Change Syncshell Password", ref _showModalChangePassword, UiShared.PopupWindowFlags)) - { - UiShared.TextWrapped("Enter the new Syncshell password for Syncshell " + name + " here."); - UiShared.TextWrapped("This action is irreversible"); - ImGui.SetNextItemWidth(-1); - ImGui.InputTextWithHint("##changepw", "New password for " + name, ref _newSyncShellPassword, 255); - if (ImGui.Button("Change password")) - { - var pw = _newSyncShellPassword; - _isPasswordValid = ApiController.GroupChangePassword(new(groupDto.Group, pw)).Result; - _newSyncShellPassword = string.Empty; - if (_isPasswordValid) _showModalChangePassword = false; - } - - if (!_isPasswordValid) - { - UiShared.ColorTextWrapped("The selected password is too short. It must be at least 10 characters.", new Vector4(1, 0, 0, 1)); - } - - UiShared.SetScaledWindowSize(290); - ImGui.EndPopup(); - } - - if (_showModalBulkOneTimeInvites && !_modalBulkOneTimeInvitesOpened) - { - _modalBulkOneTimeInvitesOpened = true; - ImGui.OpenPopup("Create Bulk One-Time Invites"); - } - - if (!_showModalBulkOneTimeInvites) _modalBulkOneTimeInvitesOpened = false; - - if (ImGui.BeginPopupModal("Create Bulk One-Time Invites", ref _showModalBulkOneTimeInvites, UiShared.PopupWindowFlags)) - { - UiShared.TextWrapped("This allows you to create up to 100 one-time invites at once for the Syncshell " + name + "." + Environment.NewLine - + "The invites are valid for 24h after creation and will automatically expire."); - ImGui.Separator(); - if (_bulkOneTimeInvites.Count == 0) - { - ImGui.SetNextItemWidth(-1); - ImGui.SliderInt("Amount##bulkinvites", ref _bulkInviteCount, 1, 100); - if (UiShared.IconTextButton(FontAwesomeIcon.MailBulk, "Create invites")) - { - _bulkOneTimeInvites = ApiController.GroupCreateTempInvite(groupDto, _bulkInviteCount).Result; - } - } - else - { - UiShared.TextWrapped("A total of " + _bulkOneTimeInvites.Count + " invites have been created."); - if (UiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard")) - { - ImGui.SetClipboardText(string.Join(Environment.NewLine, _bulkOneTimeInvites)); - } - } - - UiShared.SetScaledWindowSize(290); - ImGui.EndPopup(); - } - - ImGui.Indent(collapseButton.X); - if (_expandedGroupState[groupDto.GID]) - { - var visibleUsers = pairsInGroup.Where(u => u.IsVisible) - .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) - .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) - .ToList(); - var onlineUsers = pairsInGroup.Where(u => u.IsOnline && !u.IsVisible) - .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) - .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) - .ToList(); - var offlineUsers = pairsInGroup.Where(u => !u.IsOnline && !u.IsVisible) - .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) - .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) - .ThenBy(u => u.GetNote() ?? u.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) - .ToList(); - - if (visibleUsers.Any()) - { - ImGui.Text("Visible"); - ImGui.Separator(); - foreach (var entry in visibleUsers) - { - UiShared.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( - entry, - entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - groupDto.OwnerUID, - string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), - groupDto.GroupUserInfo.IsModerator())); - } - } - - if (onlineUsers.Any()) - { - ImGui.Text("Online"); - ImGui.Separator(); - foreach (var entry in onlineUsers) - { - UiShared.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( - entry, - entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - groupDto.OwnerUID, - string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), - groupDto.GroupUserInfo.IsModerator())); - } - } - - if (offlineUsers.Any()) - { - ImGui.Text("Offline/Unknown"); - ImGui.Separator(); - foreach (var entry in offlineUsers) - { - UiShared.DrawWithID(groupDto.GID + entry.UserData.UID, () => DrawSyncshellPairedClient( - entry, - entry.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - groupDto.OwnerUID, - string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal), - groupDto.GroupUserInfo.IsModerator())); - } - } - - ImGui.Separator(); - ImGui.Unindent(ImGui.GetStyle().ItemSpacing.X / 2); - } - ImGui.Unindent(collapseButton.X); - } - - private void DrawSyncShellButtons(GroupFullInfoDto groupDto, List groupPairs) - { - var infoIcon = FontAwesomeIcon.InfoCircle; - - bool invitesEnabled = !groupDto.GroupPermissions.IsDisableInvites(); - var soundsDisabled = groupDto.GroupPermissions.IsDisableSounds(); - var animDisabled = groupDto.GroupPermissions.IsDisableAnimations(); - - var userSoundsDisabled = groupDto.GroupUserPermissions.IsDisableSounds(); - var userAnimDisabled = groupDto.GroupUserPermissions.IsDisableAnimations(); - - bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || userSoundsDisabled || userAnimDisabled; - - var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; - var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; - var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; - var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; - var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; - - var iconSize = UiShared.GetIconSize(infoIcon); - var diffLockUnlockIcons = showInfoIcon ? (UiShared.GetIconSize(infoIcon).X - iconSize.X) / 2 : 0; - var barbuttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars); - var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal); - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - barbuttonSize.X - (showInfoIcon ? iconSize.X : 0) - diffLockUnlockIcons - (showInfoIcon ? ImGui.GetStyle().ItemSpacing.X : 0)); - if (showInfoIcon) - { - UiShared.FontText(infoIcon.ToIconString(), UiBuilder.IconFont); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - if (!invitesEnabled || soundsDisabled || animDisabled) - { - ImGui.Text("Syncshell permissions"); - - if (!invitesEnabled) - { - var lockedText = "Syncshell is closed for joining"; - UiShared.FontText(lockedIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(lockedText); - } - - if (soundsDisabled) - { - var soundsText = "Sound sync disabled through owner"; - UiShared.FontText(soundsIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(soundsText); - } - - if (animDisabled) - { - var animText = "Animation sync disabled through owner"; - UiShared.FontText(animIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(animText); - } - } - - if (userSoundsDisabled || userAnimDisabled) - { - if (!invitesEnabled || soundsDisabled || animDisabled) - ImGui.Separator(); - - ImGui.Text("Your permissions"); - - if (userSoundsDisabled) - { - var userSoundsText = "Sound sync disabled through you"; - UiShared.FontText(userSoundsIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userSoundsText); - } - - if (userAnimDisabled) - { - var userAnimText = "Animation sync disabled through you"; - UiShared.FontText(userAnimIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userAnimText); - } - - if (!invitesEnabled || soundsDisabled || animDisabled) - UiShared.TextWrapped("Note that syncshell permissions for disabling take precedence over your own set permissions"); - } - ImGui.EndTooltip(); - } - ImGui.SameLine(); - } - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + diffLockUnlockIcons); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) - { - ImGui.OpenPopup("ShellPopup"); - } - - if (ImGui.BeginPopup("ShellPopup")) - { - if (UiShared.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell")) - { - if (UiShared.CtrlPressed()) - { - _ = ApiController.GroupLeave(groupDto); - } - } - UiShared.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine - + "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")); - - if (UiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy ID")) - { - ImGui.CloseCurrentPopup(); - ImGui.SetClipboardText(groupDto.GroupAliasOrGID); - } - UiShared.AttachToolTip("Copy Syncshell ID to Clipboard"); - - if (UiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes")) - { - ImGui.CloseCurrentPopup(); - ImGui.SetClipboardText(UiShared.GetNotes(groupPairs)); - } - UiShared.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> Privacy -> Import Notes from Clipboard"); - - - var soundsText = userSoundsDisabled ? "Enable sound sync" : "Disable sound sync"; - if (UiShared.IconTextButton(userSoundsIcon, soundsText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupUserPermissions; - perm.SetDisableSounds(!perm.IsDisableSounds()); - _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); - } - UiShared.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." - + Environment.NewLine + "Disabling the synchronization will stop applying sound modifications for users of this syncshell." - + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." - + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); - - var animText = userAnimDisabled ? "Enable animations sync" : "Disable animations sync"; - if (UiShared.IconTextButton(userAnimIcon, animText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupUserPermissions; - perm.SetDisableAnimations(!perm.IsDisableAnimations()); - _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); - } - UiShared.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." - + Environment.NewLine + "Disabling the synchronization will stop applying animations modifications for users of this syncshell." - + Environment.NewLine + "Note: this setting might also affect sound synchronization" - + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." - + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); - - if (isOwner || groupDto.GroupUserInfo.IsModerator()) - { - ImGui.Separator(); - - var changedToIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; - if (UiShared.IconTextButton(changedToIcon, invitesEnabled ? "Lock Syncshell" : "Unlock Syncshell")) - { - ImGui.CloseCurrentPopup(); - var groupPerm = groupDto.GroupPermissions; - groupPerm.SetDisableInvites(invitesEnabled); - _ = ApiController.GroupChangeGroupPermissionState(new GroupPermissionDto(groupDto.Group, groupPerm)); - } - UiShared.AttachToolTip("Change Syncshell joining permissions" + Environment.NewLine + "Syncshell is currently " + (invitesEnabled ? "open" : "closed") + " for people to join"); - - if (isOwner) - { - if (UiShared.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) - { - ImGui.CloseCurrentPopup(); - _isPasswordValid = true; - _showModalChangePassword = true; - } - UiShared.AttachToolTip("Change Syncshell Password"); - } - - if (UiShared.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) - { - if (UiShared.CtrlPressed()) - { - ImGui.CloseCurrentPopup(); - _ = ApiController.GroupClear(groupDto); - } - } - UiShared.AttachToolTip("Hold CTRL and click to clear this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible." + Environment.NewLine - + "Clearing the Syncshell will remove all not pinned users from it."); - - var groupSoundsText = soundsDisabled ? "Enable syncshell sound sync" : "Disable syncshell sound sync"; - if (UiShared.IconTextButton(soundsIcon, groupSoundsText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupPermissions; - perm.SetDisableSounds(!perm.IsDisableSounds()); - _ = ApiController.GroupChangeGroupPermissionState(new(groupDto.Group, perm)); - } - UiShared.AttachToolTip("Sets syncshell-wide allowance for sound synchronization for all users." + Environment.NewLine - + "Note: users that are individually paired with others in the syncshell will ignore this setting." + Environment.NewLine - + "Note: if the synchronization is enabled, users can individually override this setting to disabled."); - - var groupAnimText = animDisabled ? "Enable syncshell animations sync" : "Disable syncshell animations sync"; - if (UiShared.IconTextButton(animIcon, groupAnimText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupPermissions; - perm.SetDisableAnimations(!perm.IsDisableAnimations()); - _ = ApiController.GroupChangeGroupPermissionState(new(groupDto.Group, perm)); - } - UiShared.AttachToolTip("Sets syncshell-wide allowance for animations synchronization for all users." + Environment.NewLine - + "Note: users that are individually paired with others in the syncshell will ignore this setting." + Environment.NewLine - + "Note: if the synchronization is enabled, users can individually override this setting to disabled."); - - if (UiShared.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) - { - ImGui.CloseCurrentPopup(); - ImGui.SetClipboardText(ApiController.GroupCreateTempInvite(groupDto, 1).Result.FirstOrDefault() ?? string.Empty); - } - UiShared.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); - - if (UiShared.IconTextButton(FontAwesomeIcon.MailBulk, "Bulk one-time invites")) - { - ImGui.CloseCurrentPopup(); - _showModalBulkOneTimeInvites = true; - _bulkOneTimeInvites.Clear(); - } - UiShared.AttachToolTip("Opens a dialog to create up to 100 single-use passwords for joining the syncshell."); - - if (UiShared.IconTextButton(FontAwesomeIcon.Ban, "Manage Banlist")) - { - ImGui.CloseCurrentPopup(); - _showModalBanList = true; - _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; - } - - if (isOwner) - { - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell")) - { - if (UiShared.CtrlPressed() && UiShared.ShiftPressed()) - { - ImGui.CloseCurrentPopup(); - _ = ApiController.GroupDelete(groupDto); - } - } - UiShared.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); - } - } - - ImGui.EndPopup(); - } - } - - private void DrawSyncshellPairedClient(Pair pair, GroupPairFullInfoDto entry, string ownerUid, bool userIsOwner, bool userIsModerator) - { - var plusButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); - var barButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars); - var entryUID = entry.UserAliasOrUID; - var textSize = ImGui.CalcTextSize(entryUID); - var originalY = ImGui.GetCursorPosY(); - var entryIsMod = entry.GroupPairStatusInfo.IsModerator(); - var entryIsOwner = string.Equals(pair.UserData.UID, ownerUid, StringComparison.Ordinal); - var entryIsPinned = entry.GroupPairStatusInfo.IsPinned(); - var isPaused = pair.IsPaused; - var presenceIcon = pair.IsVisible ? FontAwesomeIcon.Eye : (pair.IsOnline ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink); - var presenceColor = (pair.IsOnline || pair.IsVisible) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var presenceText = entryUID + " is offline"; - - var soundsDisabled = entry.GroupUserPermissions.IsDisableSounds(); - var animDisabled = entry.GroupUserPermissions.IsDisableAnimations(); - - var textPos = originalY + barButtonSize.Y / 2 - textSize.Y / 2; - ImGui.SetCursorPosY(textPos); - if (pair.IsPaused) - { - presenceIcon = FontAwesomeIcon.Question; - presenceColor = ImGuiColors.DalamudGrey; - presenceText = entryUID + " online status is unknown (paused)"; - - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); - ImGui.PopFont(); - - UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); - } - else - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); - ImGui.PopFont(); - - UiShared.AttachToolTip("You are paired with " + entryUID); - } - - if (pair.IsOnline && !pair.IsVisible) presenceText = entryUID + " is online"; - else if (pair.IsOnline && pair.IsVisible) presenceText = entryUID + " is visible: " + pair.PlayerName; - - ImGui.SameLine(); - ImGui.SetCursorPosY(textPos); - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(presenceIcon.ToIconString(), presenceColor); - ImGui.PopFont(); - UiShared.AttachToolTip(presenceText); - - if (entryIsOwner) - { - ImGui.SameLine(); - ImGui.SetCursorPosY(textPos); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); - ImGui.PopFont(); - UiShared.AttachToolTip("User is owner of this Syncshell"); - } - else if (entryIsMod) - { - ImGui.SameLine(); - ImGui.SetCursorPosY(textPos); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); - ImGui.PopFont(); - UiShared.AttachToolTip("User is moderator of this Syncshell"); - } - else if (entryIsPinned) - { - ImGui.SameLine(); - ImGui.SetCursorPosY(textPos); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); - ImGui.PopFont(); - UiShared.AttachToolTip("User is pinned in this Syncshell"); - } - - var textIsUid = true; - _mainUi.ShowUidForEntry.TryGetValue(entry.UID, out var showUidInsteadOfName); - var playerText = _serverConfigurationManager.GetNoteForUid(entry.UID); - if (showUidInsteadOfName || string.IsNullOrEmpty(playerText)) - { - playerText = entryUID; - } - else - { - textIsUid = false; - } - - if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && pair.IsVisible && !showUidInsteadOfName) - { - playerText = pair.PlayerName; - textIsUid = false; - } - - bool plusButtonShown = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, entry.UID, StringComparison.Ordinal)); - - ImGui.SameLine(); - if (!string.Equals(_mainUi.EditNickEntry, entry.UID, StringComparison.Ordinal)) - { - ImGui.SetCursorPosY(textPos); - if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(playerText); - if (textIsUid) ImGui.PopFont(); - UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + - "Right click to change nick for " + entryUID); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - var prevState = textIsUid; - if (_mainUi.ShowUidForEntry.ContainsKey(entry.UID)) - { - prevState = _mainUi.ShowUidForEntry[entry.UID]; - } - - _mainUi.ShowUidForEntry[entry.UID] = !prevState; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _serverConfigurationManager.SetNoteForUid(_mainUi.EditNickEntry, _mainUi.EditUserComment); - _mainUi.EditUserComment = _serverConfigurationManager.GetNoteForUid(entry.UID) ?? string.Empty; - _mainUi.EditNickEntry = entry.UID; - } - } - else - { - ImGui.SetCursorPosY(originalY); - var buttonSizes = (plusButtonShown ? plusButtonSize.X : 0) + barButtonSize.X; - var buttons = plusButtonShown ? 2 : 1; - - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * buttons); - if (ImGui.InputTextWithHint("", "Nick/Notes", ref _mainUi.EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - _serverConfigurationManager.SetNoteForUid(entry.UID, _mainUi.EditUserComment); - _mainUi.EditNickEntry = string.Empty; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _mainUi.EditNickEntry = string.Empty; - } - UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); - } - - if (plusButtonShown) - { - var barWidth = userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner) - ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X - : 0; - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - plusButtonSize.X - barWidth); - ImGui.SetCursorPosY(originalY); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) - { - _ = ApiController.UserAddPair(new UserDto(entry.User)); - } - UiShared.AttachToolTip("Pair with " + entryUID + " individually"); - } - - if (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) - { - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - barButtonSize.X); - ImGui.SetCursorPosY(originalY); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) - { - ImGui.OpenPopup("Popup"); - - } - } - - if ((animDisabled || soundsDisabled) && pair.UserPair == null) - { - var infoIconPosDist = (plusButtonShown ? plusButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0) - + ((userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0); - var icon = FontAwesomeIcon.InfoCircle; - var iconwidth = UiShared.GetIconSize(icon); - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - infoIconPosDist - iconwidth.X); - ImGui.SetCursorPosY(originalY); - - UiShared.FontText(icon.ToIconString(), UiBuilder.IconFont); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - - - ImGui.Text("User permissions"); - - if (soundsDisabled) - { - var userSoundsText = "Sound sync disabled by " + pair.UserData.AliasOrUID; - UiShared.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userSoundsText); - } - - if (animDisabled) - { - var userAnimText = "Animation sync disabled by " + pair.UserData.AliasOrUID; - UiShared.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userAnimText); - } - - ImGui.EndTooltip(); - } - } - - if (!plusButtonShown && !(userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner))) - { - ImGui.SameLine(); - ImGui.Dummy(barButtonSize with { X = 0 }); - } - - if (ImGui.BeginPopup("Popup")) - { - if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner)) - { - var pinText = entryIsPinned ? "Unpin user" : "Pin user"; - if (UiShared.IconTextButton(FontAwesomeIcon.Thumbtack, pinText)) - { - ImGui.CloseCurrentPopup(); - var userInfo = entry.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; - _ = ApiController.GroupSetUserInfo(new GroupPairUserInfoDto(entry.Group, entry.User, userInfo)); - } - UiShared.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); - - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Remove user")) - { - if (UiShared.CtrlPressed()) - { - ImGui.CloseCurrentPopup(); - _ = ApiController.GroupRemoveUser(entry); - } - } - - UiShared.AttachToolTip("Hold CTRL and click to remove user " + (entry.UserAliasOrUID) + " from Syncshell"); - if (UiShared.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) - { - _showModalBanUser = true; - ImGui.CloseCurrentPopup(); - } - UiShared.AttachToolTip("Ban user from this Syncshell"); - } - - if (userIsOwner) - { - string modText = entryIsMod ? "Demod user" : "Mod user"; - if (UiShared.IconTextButton(FontAwesomeIcon.UserShield, modText)) - { - if (UiShared.CtrlPressed()) - { - ImGui.CloseCurrentPopup(); - var userInfo = entry.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; - _ = ApiController.GroupSetUserInfo(new GroupPairUserInfoDto(entry.Group, entry.User, userInfo)); - } - } - UiShared.AttachToolTip("Hold CTRL to change the moderator status for " + (entry.UserAliasOrUID) + Environment.NewLine + - "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); - if (UiShared.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership")) - { - if (UiShared.CtrlPressed() && UiShared.ShiftPressed()) - { - ImGui.CloseCurrentPopup(); - _ = ApiController.GroupChangeOwnership(entry); - } - } - UiShared.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (entry.UserAliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); - } - ImGui.EndPopup(); - } - - if (_showModalBanUser && !_banUserPopupOpen) - { - ImGui.OpenPopup("Ban User"); - _banUserPopupOpen = true; - } - - if (!_showModalBanUser) _banUserPopupOpen = false; - - if (ImGui.BeginPopupModal("Ban User", ref _showModalBanUser, UiShared.PopupWindowFlags)) - { - UiShared.TextWrapped("User " + (entry.UserAliasOrUID) + " will be banned and removed from this Syncshell."); - ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255); - if (ImGui.Button("Ban User")) - { - ImGui.CloseCurrentPopup(); - var reason = _banReason; - _ = ApiController.GroupBanUser(entry, reason); - _banReason = string.Empty; - } - UiShared.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."); - UiShared.SetScaledWindowSize(300); - ImGui.EndPopup(); - } - } - } -} diff --git a/MareSynchronos/UI/Handlers/TagHandler.cs b/MareSynchronos/UI/Handlers/TagHandler.cs index 5575303..f3fe01a 100644 --- a/MareSynchronos/UI/Handlers/TagHandler.cs +++ b/MareSynchronos/UI/Handlers/TagHandler.cs @@ -1,83 +1,83 @@ using MareSynchronos.API.Dto.User; -using MareSynchronos.Managers; +using MareSynchronos.Services.ServerConfiguration; -namespace MareSynchronos.UI.Handlers +namespace MareSynchronos.UI.Handlers; + +public class TagHandler { - public class TagHandler + public const string CustomOfflineTag = "Mare_Offline"; + public const string CustomOnlineTag = "Mare_Online"; + public const string CustomUnpairedTag = "Mare_Unpaired"; + public const string CustomVisibleTag = "Mare_Visible"; + private readonly ServerConfigurationManager _serverConfigurationManager; + + public TagHandler(ServerConfigurationManager serverConfigurationManager) { - private readonly ServerConfigurationManager _serverConfigurationManager; - public const string CustomVisibleTag = "Mare_Visible"; - public const string CustomOnlineTag = "Mare_Online"; - public const string CustomOfflineTag = "Mare_Offline"; + _serverConfigurationManager = serverConfigurationManager; + } - public TagHandler(ServerConfigurationManager serverConfigurationManager) + public void AddTag(string tag) + { + _serverConfigurationManager.AddTag(tag); + } + + public void AddTagToPairedUid(UserPairDto pair, string tagName) + { + _serverConfigurationManager.AddTagForUid(pair.User.UID, tagName); + } + + public List GetAllTagsSorted() + { + return _serverConfigurationManager.GetServerAvailablePairTags() + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + public HashSet GetOtherUidsForTag(string tag) + { + return _serverConfigurationManager.GetUidsForTag(tag); + } + + public bool HasAnyTag(UserPairDto pair) + { + return _serverConfigurationManager.HasTags(pair.User.UID); + } + + public bool HasTag(UserPairDto pair, string tagName) + { + return _serverConfigurationManager.ContainsTag(pair.User.UID, tagName); + } + + /// + /// Is this tag opened in the paired clients UI? + /// + /// the tag + /// open true/false + public bool IsTagOpen(string tag) + { + return _serverConfigurationManager.ContainsOpenPairTag(tag); + } + + public void RemoveTag(string tag) + { + // First remove the tag from teh available pair tags + _serverConfigurationManager.RemoveTag(tag); + } + + public void RemoveTagFromPairedUid(UserPairDto pair, string tagName) + { + _serverConfigurationManager.RemoveTagForUid(pair.User.UID, tagName); + } + + public void SetTagOpen(string tag, bool open) + { + if (open) { - _serverConfigurationManager = serverConfigurationManager; + _serverConfigurationManager.AddOpenPairTag(tag); } - - public void AddTag(string tag) + else { - _serverConfigurationManager.AddTag(tag); - } - - public void RemoveTag(string tag) - { - // First remove the tag from teh available pair tags - _serverConfigurationManager.RemoveTag(tag); - } - - public void SetTagOpen(string tag, bool open) - { - if (open) - { - _serverConfigurationManager.AddOpenPairTag(tag); - } - else - { - _serverConfigurationManager.RemoveOpenPairTag(tag); - } - } - - /// - /// Is this tag opened in the paired clients UI? - /// - /// the tag - /// open true/false - public bool IsTagOpen(string tag) - { - return _serverConfigurationManager.ContainsOpenPairTag(tag); - } - - public List GetAllTagsSorted() - { - return _serverConfigurationManager.GetServerAvailablePairTags() - .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) - .ToList(); - } - - public HashSet GetOtherUidsForTag(string tag) - { - return _serverConfigurationManager.GetUidsForTag(tag); - } - - public void AddTagToPairedUid(UserPairDto pair, string tagName) - { - _serverConfigurationManager.AddTagForUid(pair.User.UID, tagName); - } - - public void RemoveTagFromPairedUid(UserPairDto pair, string tagName) - { - _serverConfigurationManager.RemoveTagForUid(pair.User.UID, tagName); - } - - public bool HasTag(UserPairDto pair, string tagName) - { - return _serverConfigurationManager.ContainsTag(pair.User.UID, tagName); - } - - public bool HasAnyTag(UserPairDto pair) - { - return _serverConfigurationManager.HasTags(pair.User.UID); + _serverConfigurationManager.RemoveOpenPairTag(tag); } } } \ No newline at end of file diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 20074e0..5cec40e 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -1,52 +1,41 @@ -using System.Numerics; +using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using MareSynchronos.Localization; using Dalamud.Utility; +using ImGuiNET; using MareSynchronos.FileCache; -using Dalamud.Interface; -using MareSynchronos.Managers; +using MareSynchronos.Localization; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; using Microsoft.Extensions.Logging; +using System.Numerics; namespace MareSynchronos.UI; -internal class IntroUi : WindowMediatorSubscriberBase, IDisposable +public class IntroUi : WindowMediatorSubscriberBase { - private readonly UiShared _uiShared; private readonly MareConfigService _configService; private readonly PeriodicFileScanner _fileCacheManager; + private readonly Dictionary _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; private readonly ServerConfigurationManager _serverConfigurationManager; - private readonly WindowSystem _windowSystem; + private readonly UiSharedService _uiShared; + private int _currentLanguage; private bool _readFirstPage; + private string _secretKey = string.Empty; + private string _timeoutLabel = string.Empty; + private Task? _timeoutTask; private string[]? _tosParagraphs; - private Task? _timeoutTask; - private string _timeoutLabel = string.Empty; - - private readonly Dictionary _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; - private int _currentLanguage; - - public override void Dispose() - { - base.Dispose(); - _windowSystem.RemoveWindow(this); - } - - public IntroUi(ILogger logger, WindowSystem windowSystem, UiShared uiShared, MareConfigService configService, + public IntroUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, PeriodicFileScanner fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator) : base(logger, mareMediator, "Mare Synchronos Setup") { - _logger.LogTrace("Creating " + nameof(IntroUi)); - _uiShared = uiShared; _configService = configService; _fileCacheManager = fileCacheManager; _serverConfigurationManager = serverConfigurationManager; - _windowSystem = windowSystem; + IsOpen = false; SizeConstraints = new WindowSizeConstraints() @@ -59,8 +48,6 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => IsOpen = true); - - _windowSystem.AddWindow(this); } public override void Draw() @@ -73,11 +60,11 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable ImGui.TextUnformatted("Welcome to Mare Synchronos"); if (_uiShared.UidFontBuilt) ImGui.PopFont(); ImGui.Separator(); - UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + + UiSharedService.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); - UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); + UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); - UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + + UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + "might look broken because of this or others players mods might not apply on your end altogether. " + "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); if (!_uiShared.DrawOtherPluginState()) return; @@ -121,17 +108,16 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable string readThis = Strings.ToS.ReadLabel; textSize = ImGui.CalcTextSize(readThis); ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); - UiShared.ColorText(readThis, ImGuiColors.DalamudRed); + UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed); ImGui.SetWindowFontScale(1.0f); ImGui.Separator(); - - UiShared.TextWrapped(_tosParagraphs![0]); - UiShared.TextWrapped(_tosParagraphs![1]); - UiShared.TextWrapped(_tosParagraphs![2]); - UiShared.TextWrapped(_tosParagraphs![3]); - UiShared.TextWrapped(_tosParagraphs![4]); - UiShared.TextWrapped(_tosParagraphs![5]); + UiSharedService.TextWrapped(_tosParagraphs![0]); + UiSharedService.TextWrapped(_tosParagraphs![1]); + UiSharedService.TextWrapped(_tosParagraphs![2]); + UiSharedService.TextWrapped(_tosParagraphs![3]); + UiSharedService.TextWrapped(_tosParagraphs![4]); + UiSharedService.TextWrapped(_tosParagraphs![5]); ImGui.Separator(); if (_timeoutTask?.IsCompleted ?? true) @@ -144,12 +130,12 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable } else { - UiShared.TextWrapped(_timeoutLabel); + UiSharedService.TextWrapped(_timeoutLabel); } } else if (_configService.Current.AcceptedAgreement && (string.IsNullOrEmpty(_configService.Current.CacheFolder) - || _configService.Current.InitialScanComplete == false + || !_configService.Current.InitialScanComplete || !Directory.Exists(_configService.Current.CacheFolder))) { if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); @@ -159,17 +145,17 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable if (!_uiShared.HasValidPenumbraModPath) { - UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); } else { - UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + + UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + "Additionally, a local storage folder must be set where Mare Synchronos will download other character files to. " + "Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."); - UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); - UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + + UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); + UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); - UiShared.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow); + UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow); _uiShared.DrawCacheDirectorySetting(); } @@ -191,22 +177,22 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable ImGui.TextUnformatted("Service Registration"); if (_uiShared.UidFontBuilt) ImGui.PopFont(); ImGui.Separator(); - UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); - UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); - UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #mare-commands."); + UiSharedService.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); + UiSharedService.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); + UiSharedService.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #mare-commands."); if (ImGui.Button("Join the Mare Synchronos Discord")) { Util.OpenLink("https://discord.gg/mpNdkrTRjW"); } - UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); + UiSharedService.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); ImGui.Separator(); - UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); + UiSharedService.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); - var idx = _uiShared.DrawServiceSelection(selectOnChange: true); + _ = _uiShared.DrawServiceSelection(selectOnChange: true); var text = "Enter Secret Key"; var buttonText = "Save"; @@ -215,11 +201,11 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable ImGui.AlignTextToFramePadding(); ImGui.Text(text); ImGui.SameLine(); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); ImGui.InputText("", ref _secretKey, 64); if (_secretKey.Length > 0 && _secretKey.Length != 64) { - UiShared.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); } else if (_secretKey.Length == 64) { @@ -245,8 +231,6 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable } } - private string _secretKey = string.Empty; - private void GetToSLocalization(int changeLanguageTo = -1) { if (changeLanguageTo != -1) @@ -256,4 +240,4 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable _tosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 }; } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 32fc7f9..8f7deb6 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -1,48 +1,65 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Interface.Windowing; using ImGuiNET; using MareSynchronos.WebAPI; using System.Numerics; -using MareSynchronos.WebAPI.Utils; using Dalamud.Utility; -using Newtonsoft.Json; -using MareSynchronos.Export; using MareSynchronos.API.Data; -using MareSynchronos.Managers; using MareSynchronos.API.Data.Comparer; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; using MareSynchronos.MareConfiguration.Models; using Microsoft.Extensions.Logging; -using MareSynchronos.Utils; +using MareSynchronos.WebAPI.SignalR.Utils; +using MareSynchronos.PlayerData.Pairs; +using System.Text.Json; +using MareSynchronos.PlayerData.Export; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Services; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.PlayerData.Handlers; +using System.Collections.Concurrent; namespace MareSynchronos.UI; -public class SettingsUi : WindowMediatorSubscriberBase, IDisposable +public class SettingsUi : WindowMediatorSubscriberBase { private readonly MareConfigService _configService; - private readonly WindowSystem _windowSystem; - private ApiController ApiController => _uiShared.ApiController; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly FileUploadManager _fileTransferManager; + private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly MareCharaFileManager _mareCharaFileManager; private readonly PairManager _pairManager; + private readonly PerformanceCollectorService _performanceCollector; private readonly ServerConfigurationManager _serverConfigurationManager; - private readonly PerformanceCollector _performanceCollector; - private readonly UiShared _uiShared; - public CharacterData? LastCreatedCharacterData { private get; set; } - - private bool _overwriteExistingLabels = false; - private bool? _notesSuccessfullyApplied = null; + private readonly UiSharedService _uiShared; + private bool _deleteAccountPopupModalShown = false; + private bool _deleteFilesPopupModalShown = false; + private string _exportDescription = string.Empty; private string _lastTab = string.Empty; + private bool? _notesSuccessfullyApplied = null; + private bool _overwriteExistingLabels = false; + private bool _readClearCache = false; + private bool _readExport = false; private bool _wasOpen = false; - public SettingsUi(ILogger logger, WindowSystem windowSystem, - UiShared uiShared, MareConfigService configService, + public SettingsUi(ILogger logger, + UiSharedService uiShared, MareConfigService configService, MareCharaFileManager mareCharaFileManager, PairManager pairManager, ServerConfigurationManager serverConfigurationManager, - MareMediator mediator, PerformanceCollector performanceCollector) : base(logger, mediator, "Mare Synchronos Settings") + MareMediator mediator, PerformanceCollectorService performanceCollector, + FileUploadManager fileTransferManager, + FileTransferOrchestrator fileTransferOrchestrator) : base(logger, mediator, "Mare Synchronos Settings") { - _logger.LogTrace("Creating " + nameof(SettingsUi)); + _configService = configService; + _mareCharaFileManager = mareCharaFileManager; + _pairManager = pairManager; + _serverConfigurationManager = serverConfigurationManager; + _performanceCollector = performanceCollector; + _fileTransferManager = fileTransferManager; + _fileTransferOrchestrator = fileTransferOrchestrator; + _uiShared = uiShared; SizeConstraints = new WindowSizeConstraints() { @@ -50,39 +67,17 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable MaximumSize = new Vector2(800, 2000), }; - _configService = configService; - _windowSystem = windowSystem; - _mareCharaFileManager = mareCharaFileManager; - _pairManager = pairManager; - _serverConfigurationManager = serverConfigurationManager; - _performanceCollector = performanceCollector; - _uiShared = uiShared; - Mediator.Subscribe(this, (_) => Toggle()); Mediator.Subscribe(this, (_) => IsOpen = false); - Mediator.Subscribe(this, (_) => UiShared_GposeStart()); - Mediator.Subscribe(this, (_) => UiShared_GposeEnd()); - Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = ((CharacterDataCreatedMessage)msg).CharacterData); - - windowSystem.AddWindow(this); + Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); + Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); + Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = msg.CharacterData); + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); } - private void UiShared_GposeEnd() - { - IsOpen = _wasOpen; - } - - private void UiShared_GposeStart() - { - _wasOpen = IsOpen; - IsOpen = false; - } - - public override void Dispose() - { - base.Dispose(); - _windowSystem.RemoveWindow(this); - } + public CharacterData? LastCreatedCharacterData { private get; set; } + private ApiController ApiController => _uiShared.ApiController; public override void Draw() { @@ -91,38 +86,177 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable DrawSettingsContent(); } - private void DrawSettingsContent() + public override void OnClose() { - _uiShared.PrintServerState(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("Community and Support:"); - ImGui.SameLine(); - if (ImGui.Button("Mare Synchronos Discord")) - { - Util.OpenLink("https://discord.gg/mpNdkrTRjW"); - } - ImGui.Separator(); - if (ImGui.BeginTabBar("mainTabBar")) - { - if (ImGui.BeginTabItem("General")) - { - DrawGeneral(); - ImGui.EndTabItem(); - } + _uiShared.EditTrackerPosition = false; + base.OnClose(); + } - if (ImGui.BeginTabItem("Export & Storage")) - { - DrawFileStorageSettings(); - ImGui.EndTabItem(); - } + private void DrawBlockedTransfers() + { + _lastTab = "BlockedTransfers"; + UiSharedService.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + + "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + + "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", + ImGuiColors.DalamudGrey); - if (ApiController.ServerState is ServerState.Connected) + if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) + { + ImGui.TableSetupColumn( + $"Hash/Filename"); + ImGui.TableSetupColumn($"Forbidden by"); + + ImGui.TableHeadersRow(); + + foreach (var item in _fileTransferOrchestrator.ForbiddenTransfers) { - if (ImGui.BeginTabItem("Transfers")) + ImGui.TableNextColumn(); + if (item is UploadFileTransfer transfer) { - DrawCurrentTransfers(); - ImGui.EndTabItem(); + ImGui.Text(transfer.LocalFile); } + else + { + ImGui.Text(item.Hash); + } + ImGui.TableNextColumn(); + ImGui.Text(item.ForbiddenBy); + } + ImGui.EndTable(); + } + } + + private void DrawCurrentTransfers() + { + _lastTab = "Transfers"; + UiSharedService.FontText("Transfer Settings", _uiShared.UidFont); + + int maxParallelDownloads = _configService.Current.ParallelDownloads; + if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10)) + { + _configService.Current.ParallelDownloads = maxParallelDownloads; + _configService.Save(); + } + + ImGui.Separator(); + UiSharedService.FontText("Transfer UI", _uiShared.UidFont); + + bool showTransferWindow = _configService.Current.ShowTransferWindow; + if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow)) + { + _configService.Current.ShowTransferWindow = showTransferWindow; + _configService.Save(); + } + + if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled(); + ImGui.Indent(); + bool editTransferWindowPosition = _uiShared.EditTrackerPosition; + if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) + { + _uiShared.EditTrackerPosition = editTransferWindowPosition; + } + ImGui.Unindent(); + if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled(); + + bool showTransferBars = _configService.Current.ShowTransferBars; + if (ImGui.Checkbox("Show transfer bars rendered below players", ref showTransferBars)) + { + _configService.Current.ShowTransferBars = showTransferBars; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will render a progress bar during the download at the feet of the player you are downloading from."); + + if (!showTransferBars) ImGui.BeginDisabled(); + ImGui.Indent(); + bool showUploading = _configService.Current.ShowUploading; + if (ImGui.Checkbox("Show 'Uploading' text below players that are currently uploading", ref showUploading)) + { + _configService.Current.ShowUploading = showUploading; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will render an 'Uploading' text at the feet of the player that is in progress of uploading data."); + + ImGui.Unindent(); + if (!showUploading) ImGui.BeginDisabled(); + ImGui.Indent(); + bool showUploadingBigText = _configService.Current.ShowUploadingBigText; + if (ImGui.Checkbox("Large font for 'Uploading' text", ref showUploadingBigText)) + { + _configService.Current.ShowUploadingBigText = showUploadingBigText; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will render an 'Uploading' text in a larger font."); + + ImGui.Unindent(); + + if (!showUploading) ImGui.EndDisabled(); + if (!showTransferBars) ImGui.EndDisabled(); + + ImGui.Separator(); + UiSharedService.FontText("Current Transfers", _uiShared.UidFont); + + if (ImGui.BeginTabBar("TransfersTabBar")) + { + if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers")) + { + ImGui.TextUnformatted("Uploads"); + if (ImGui.BeginTable("UploadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray()) + { + var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.Text(UiSharedService.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.Text(UiSharedService.ByteToString(transfer.Total)); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + + ImGui.EndTable(); + } + ImGui.Separator(); + ImGui.TextUnformatted("Downloads"); + if (ImGui.BeginTable("DownloadsTable", 4)) + { + ImGui.TableSetupColumn("User"); + ImGui.TableSetupColumn("Server"); + ImGui.TableSetupColumn("Files"); + ImGui.TableSetupColumn("Download"); + ImGui.TableHeadersRow(); + + foreach (var transfer in _currentDownloads.ToArray()) + { + var userName = transfer.Key.Name; + foreach (var entry in transfer.Value) + { + var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + ImGui.Text(userName); + ImGui.TableNextColumn(); + ImGui.Text(entry.Key); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); + ImGui.TableNextColumn(); + ImGui.Text(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + UiSharedService.ByteToString(entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + } + + ImGui.EndTable(); + } + + ImGui.EndTabItem(); } if (ImGui.BeginTabItem("Blocked Transfers")) @@ -131,28 +265,311 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Service Settings")) - { - DrawServerConfiguration(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Debug")) - { - DrawDebug(); - ImGui.EndTabItem(); - } - ImGui.EndTabBar(); } } + private void DrawDebug() + { + _lastTab = "Debug"; + + UiSharedService.FontText("Debug", _uiShared.UidFont); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard")) + { + if (LastCreatedCharacterData != null) + { + ImGui.SetClipboardText(JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true })); + } + else + { + ImGui.SetClipboardText("ERROR: No created character data, cannot copy."); + } + } + UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server."); + + _uiShared.DrawCombo("Log Level", Enum.GetValues(), (l) => l.ToString(), (l) => + { + _configService.Current.LogLevel = l; + _configService.Save(); + }, _configService.Current.LogLevel); + + bool logPerformance = _configService.Current.LogPerformance; + if (ImGui.Checkbox("Log Performance Counters", ref logPerformance)) + { + _configService.Current.LogPerformance = logPerformance; + _configService.Save(); + } + UiSharedService.DrawHelpText("Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); + + if (!logPerformance) ImGui.BeginDisabled(); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog")) + { + _performanceCollector.PrintPerformanceStats(); + } + ImGui.SameLine(); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) + { + _performanceCollector.PrintPerformanceStats(60); + } + if (!logPerformance) ImGui.EndDisabled(); + } + + private void DrawFileStorageSettings() + { + _lastTab = "FileCache"; + + UiSharedService.FontText("Export MCDF", _uiShared.UidFont); + + UiSharedService.TextWrapped("This feature allows you to pack your character into a MCDF file and manually send it to other people. MCDF files can officially only be imported during GPose through Mare. " + + "Be aware that the possibility exists that people write unoffocial custom exporters to extract the containing data."); + + ImGui.Checkbox("##readExport", ref _readExport); + ImGui.SameLine(); + UiSharedService.TextWrapped("I understand that by exporting my character data and sending it to other people I am giving away my current character appearance irrevocably. People I am sharing my data with have the ability to share it with other people without limitations."); + + if (_readExport) + { + ImGui.Indent(); + + if (!_mareCharaFileManager.CurrentlyWorking) + { + ImGui.InputTextWithHint("Export Descriptor", "This description will be shown on loading the data", ref _exportDescription, 255); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF")) + { + _uiShared.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", "export.mcdf", ".mcdf", (success, path) => + { + if (!success) return; + + Task.Run(() => + { + try + { + _mareCharaFileManager.SaveMareCharaFile(LastCreatedCharacterData, _exportDescription, path); + _exportDescription = string.Empty; + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Error saving data"); + } + }); + }); + } + UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" + + " equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow); + } + else + { + UiSharedService.ColorTextWrapped("Export in progress", ImGuiColors.DalamudYellow); + } + + ImGui.Unindent(); + } + bool openInGpose = _configService.Current.OpenGposeImportOnGposeStart; + if (ImGui.Checkbox("Open MCDF import window when GPose loads", ref openInGpose)) + { + _configService.Current.OpenGposeImportOnGposeStart = openInGpose; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will automatically open the import menu when loading into Gpose. If unchecked you can open the menu manually with /mare gpose"); + + ImGui.Separator(); + + UiSharedService.FontText("Storage", _uiShared.UidFont); + + UiSharedService.TextWrapped("Mare stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + + "The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage."); + + _uiShared.DrawFileScanState(); + _uiShared.DrawTimeSpanBetweenScansSetting(); + _uiShared.DrawCacheDirectorySetting(); + ImGui.Text($"Currently utilized local storage: {UiSharedService.ByteToString(_uiShared.FileCacheSize)}"); + ImGui.Dummy(new Vector2(10, 10)); + ImGui.Text("To clear the local storage accept the following disclaimer"); + ImGui.Indent(); + ImGui.Checkbox("##readClearCache", ref _readClearCache); + ImGui.SameLine(); + UiSharedService.TextWrapped("I understand that: " + Environment.NewLine + "- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data." + + Environment.NewLine + "- This is not a step to try to fix sync issues." + + Environment.NewLine + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); + if (!_readClearCache) + ImGui.BeginDisabled(); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && UiSharedService.CtrlPressed() && _readClearCache) + { + Task.Run(() => + { + foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder)) + { + File.Delete(file); + } + + _uiShared.RecalculateFileCacheSize(); + }); + } + UiSharedService.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine + + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine + + "Mares storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine + + "If you still think you need to do this hold CTRL while pressing the button."); + if (!_readClearCache) + ImGui.EndDisabled(); + ImGui.Unindent(); + } + + private void DrawGeneral() + { + if (!string.Equals(_lastTab, "General", StringComparison.OrdinalIgnoreCase)) + { + _notesSuccessfullyApplied = null; + } + + _lastTab = "General"; + UiSharedService.FontText("Notes", _uiShared.UidFont); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + { + ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, UserDataComparer.Instance).ToList())); + } + if (UiSharedService.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) + { + _notesSuccessfullyApplied = null; + var notes = ImGui.GetClipboardText(); + _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); + } + + ImGui.SameLine(); + ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); + UiSharedService.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); + if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped("User Notes successfully imported", ImGuiColors.HealerGreen); + } + else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed); + } + + var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; + + if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) + { + _configService.Current.OpenPopupOnAdd = openPopupOnAddition; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); + + ImGui.Separator(); + UiSharedService.FontText("UI", _uiShared.UidFont); + var showNameInsteadOfNotes = _configService.Current.ShowCharacterNameInsteadOfNotesForVisible; + var reverseUserSort = _configService.Current.ReverseUserSort; + var showVisibleSeparate = _configService.Current.ShowVisibleUsersSeparately; + var showOfflineSeparate = _configService.Current.ShowOfflineUsersSeparately; + + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) + { + _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will show all currently visible users in a special 'Visible' group in the main UI."); + + if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) + { + _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will show all currently offline users in a special 'Offline' group in the main UI."); + + if (ImGui.Checkbox("Show player name instead of note for visible players", ref showNameInsteadOfNotes)) + { + _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; + _configService.Save(); + } + UiSharedService.DrawHelpText("This will show the character name instead of custom set note when a character is visible"); + + if (ImGui.Checkbox("Reverse user sort", ref reverseUserSort)) + { + _configService.Current.ReverseUserSort = reverseUserSort; + _configService.Save(); + } + UiSharedService.DrawHelpText("This reverses the user sort from A->Z to Z->A"); + + ImGui.Separator(); + + var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; + var onlineNotifs = _configService.Current.ShowOnlineNotifications; + var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs; + var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs; + UiSharedService.FontText("Notifications", _uiShared.UidFont); + + _uiShared.DrawCombo("Info Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.InfoNotification = i; + _configService.Save(); + }, _configService.Current.InfoNotification); + UiSharedService.DrawHelpText("The location where \"Info\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Info notifications" + + Environment.NewLine + "'Chat' will print Info notifications in chat" + + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + _uiShared.DrawCombo("Warning Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.WarningNotification = i; + _configService.Save(); + }, _configService.Current.WarningNotification); + UiSharedService.DrawHelpText("The location where \"Warning\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Warning notifications" + + Environment.NewLine + "'Chat' will print Warning notifications in chat" + + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + _uiShared.DrawCombo("Error Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.ErrorNotification = i; + _configService.Save(); + }, _configService.Current.ErrorNotification); + UiSharedService.DrawHelpText("The location where \"Error\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Error notifications" + + Environment.NewLine + "'Chat' will print Error notifications in chat" + + Environment.NewLine + "'Toast' will show Error toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings)) + { + _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; + _configService.Save(); + } + UiSharedService.DrawHelpText("Enabling this will not show any \"Warning\" labeled messages for missing optional plugins."); + if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs)) + { + _configService.Current.ShowOnlineNotifications = onlineNotifs; + _configService.Save(); + } + UiSharedService.DrawHelpText("Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online."); + + if (!onlineNotifs) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; + _configService.Save(); + } + UiSharedService.DrawHelpText("Enabling this will only show online notifications (type: Info) for individual pairs."); + if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; + _configService.Save(); + } + UiSharedService.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note."); + if (!onlineNotifs) ImGui.EndDisabled(); + } + private void DrawServerConfiguration() { _lastTab = "Service Settings"; if (ApiController.ServerAlive) { - UiShared.FontText("Service Actions", _uiShared.UidFont); + UiSharedService.FontText("Service Actions", _uiShared.UidFont); if (ImGui.Button("Delete all my files")) { @@ -160,11 +577,11 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable ImGui.OpenPopup("Delete all your files?"); } - UiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); + UiSharedService.DrawHelpText("Completely deletes all your uploaded files on the service."); - if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, UiShared.PopupWindowFlags)) + if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, UiSharedService.PopupWindowFlags)) { - UiShared.TextWrapped( + UiSharedService.TextWrapped( "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); ImGui.Text("Are you sure you want to continue?"); ImGui.Separator(); @@ -175,7 +592,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0))) { - Task.Run(() => ApiController.FilesDeleteAll()); + Task.Run(_fileTransferManager.DeleteAllFiles); _deleteFilesPopupModalShown = false; } @@ -186,7 +603,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable _deleteFilesPopupModalShown = false; } - UiShared.SetScaledWindowSize(325); + UiSharedService.SetScaledWindowSize(325); ImGui.EndPopup(); } ImGui.SameLine(); @@ -196,13 +613,13 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable ImGui.OpenPopup("Delete your account?"); } - UiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); + UiSharedService.DrawHelpText("Completely deletes your account and all uploaded files to the service."); - if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiShared.PopupWindowFlags)) + if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiSharedService.PopupWindowFlags)) { - UiShared.TextWrapped( + UiSharedService.TextWrapped( "Your account and all associated files and data on the service will be deleted."); - UiShared.TextWrapped("Your UID will be removed from all pairing lists."); + UiSharedService.TextWrapped("Your UID will be removed from all pairing lists."); ImGui.Text("Are you sure you want to continue?"); ImGui.Separator(); ImGui.Spacing(); @@ -212,7 +629,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable if (ImGui.Button("Delete account", new Vector2(buttonSize, 0))) { - Task.Run(() => ApiController.UserDelete()); + Task.Run(ApiController.UserDelete); _deleteAccountPopupModalShown = false; Mediator.Publish(new SwitchToIntroUiMessage()); } @@ -224,14 +641,13 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable _deleteAccountPopupModalShown = false; } - UiShared.SetScaledWindowSize(325); + UiSharedService.SetScaledWindowSize(325); ImGui.EndPopup(); } ImGui.Separator(); - } - UiShared.FontText("Service & Character Settings", _uiShared.UidFont); + UiSharedService.FontText("Service & Character Settings", _uiShared.UidFont); var idx = _uiShared.DrawServiceSelection(); @@ -240,20 +656,19 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable var selectedServer = _serverConfigurationManager.GetServerByIndex(idx); if (selectedServer == _serverConfigurationManager.CurrentServer) { - UiShared.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", ImGuiColors.DalamudYellow); + UiSharedService.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", ImGuiColors.DalamudYellow); } - if (ImGui.BeginTabBar("serverTabBar")) { if (ImGui.BeginTabItem("Character Management")) { - UiShared.ColorTextWrapped("Characters listed here will automatically connect to the selected Mare service with the settings as provided below." + + UiSharedService.ColorTextWrapped("Characters listed here will automatically connect to the selected Mare service with the settings as provided below." + " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", ImGuiColors.DalamudYellow); int i = 0; foreach (var item in selectedServer.Authentications.ToList()) { - UiShared.DrawWithID("selectedChara" + i, () => + UiSharedService.DrawWithID("selectedChara" + i, () => { var worldIdx = (ushort)item.WorldId; var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value); @@ -299,33 +714,29 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable } }, EqualityComparer>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx)); - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character")) - { - if (UiShared.CtrlPressed()) - _serverConfigurationManager.RemoveCharacterFromServer(idx, item); - } - UiShared.AttachToolTip("Hold CTRL to delete this entry."); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed()) + _serverConfigurationManager.RemoveCharacterFromServer(idx, item); + UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); ImGui.TreePop(); } }); i++; - } ImGui.Separator(); if (!selectedServer.Authentications.Any(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal) && c.WorldId == _uiShared.WorldId)) { - if (UiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Add current character")) { _serverConfigurationManager.AddCurrentCharacterToServer(idx); } ImGui.SameLine(); } - if (UiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) { _serverConfigurationManager.AddEmptyCharacterToServer(idx); } @@ -337,7 +748,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable { foreach (var item in selectedServer.SecretKeys.ToList()) { - UiShared.DrawWithID("key" + item.Key, () => + UiSharedService.DrawWithID("key" + item.Key, () => { var friendlyName = item.Value.FriendlyName; if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) @@ -351,15 +762,12 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable item.Value.Key = key; _serverConfigurationManager.Save(); } - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) { - if (UiShared.CtrlPressed()) - { - selectedServer.SecretKeys.Remove(item.Key); - _serverConfigurationManager.Save(); - } + selectedServer.SecretKeys.Remove(item.Key); + _serverConfigurationManager.Save(); } - UiShared.AttachToolTip("Hold CTRL to delete this secret key entry"); + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); }); if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) @@ -367,7 +775,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable } ImGui.Separator(); - if (UiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) { selectedServer.SecretKeys.Add(selectedServer.SecretKeys.LastOrDefault().Key + 1, new SecretKey() { @@ -383,7 +791,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable { var serverUri = selectedServer.ServerUri; ImGui.InputText("Service URI", ref serverUri, 255, ImGuiInputTextFlags.ReadOnly); - UiShared.DrawHelpText("You cannot edit the service URI. Add a new service if you need to edit the URI."); + UiSharedService.DrawHelpText("You cannot edit the service URI. Add a new service if you need to edit the URI."); var serverName = selectedServer.ServerName; var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase); var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; @@ -394,18 +802,15 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable } if (isMain) { - UiShared.DrawHelpText("You cannot edit the name of the main service."); + UiSharedService.DrawHelpText("You cannot edit the name of the main service."); } - if (!isMain) + if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) { - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed()) { - if (UiShared.CtrlPressed()) - { - _serverConfigurationManager.DeleteServer(selectedServer); - } + _serverConfigurationManager.DeleteServer(selectedServer); } - UiShared.DrawHelpText("Hold CTRL to delete this service"); + UiSharedService.DrawHelpText("Hold CTRL to delete this service"); } ImGui.EndTabItem(); } @@ -413,415 +818,61 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable } } - private void DrawGeneral() + private void DrawSettingsContent() { - if (!string.Equals(_lastTab, "General", StringComparison.OrdinalIgnoreCase)) - { - _notesSuccessfullyApplied = null; - } - - _lastTab = "General"; - UiShared.FontText("Notes", _uiShared.UidFont); - if (UiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) - { - ImGui.SetClipboardText(UiShared.GetNotes(_pairManager.DirectPairs.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, UserDataComparer.Instance).ToList())); - } - if (UiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) - { - _notesSuccessfullyApplied = null; - var notes = ImGui.GetClipboardText(); - _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); - } - + _uiShared.PrintServerState(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Community and Support:"); ImGui.SameLine(); - ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); - UiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); - if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) + if (ImGui.Button("Mare Synchronos Discord")) { - UiShared.ColorTextWrapped("User Notes successfully imported", ImGuiColors.HealerGreen); + Util.OpenLink("https://discord.gg/mpNdkrTRjW"); } - else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) - { - UiShared.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed); - } - - var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; - - if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) - { - _configService.Current.OpenPopupOnAdd = openPopupOnAddition; - _configService.Save(); - } - UiShared.DrawHelpText("This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); - ImGui.Separator(); - UiShared.FontText("UI", _uiShared.UidFont); - var showNameInsteadOfNotes = _configService.Current.ShowCharacterNameInsteadOfNotesForVisible; - var reverseUserSort = _configService.Current.ReverseUserSort; - if (ImGui.Checkbox("Show player name instead of note for visible players", ref showNameInsteadOfNotes)) + if (ImGui.BeginTabBar("mainTabBar")) { - _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; - _configService.Save(); + if (ImGui.BeginTabItem("General")) + { + DrawGeneral(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Export & Storage")) + { + DrawFileStorageSettings(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Transfers")) + { + DrawCurrentTransfers(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Service Settings")) + { + DrawServerConfiguration(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Debug")) + { + DrawDebug(); + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); } - UiShared.DrawHelpText("This will show the character name instead of custom set note when a character is visible"); - - if (ImGui.Checkbox("Reverse user sort", ref reverseUserSort)) - { - _configService.Current.ReverseUserSort = reverseUserSort; - _configService.Save(); - } - UiShared.DrawHelpText("This reverses the user sort from A->Z to Z->A"); - - ImGui.Separator(); - - var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; - var onlineNotifs = _configService.Current.ShowOnlineNotifications; - var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs; - var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs; - var warnNotifLocation = _configService.Current.WarningNotification; - var errorNotifLocation = _configService.Current.ErrorNotification; - UiShared.FontText("Notifications", _uiShared.UidFont); - - _uiShared.DrawCombo("Info Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.InfoNotification = i; - _configService.Save(); - }, _configService.Current.InfoNotification); - UiShared.DrawHelpText("The location where \"Info\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Info notifications" - + Environment.NewLine + "'Chat' will print Info notifications in chat" - + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); - - _uiShared.DrawCombo("Warning Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.WarningNotification = i; - _configService.Save(); - }, _configService.Current.WarningNotification); - UiShared.DrawHelpText("The location where \"Warning\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Warning notifications" - + Environment.NewLine + "'Chat' will print Warning notifications in chat" - + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); - - _uiShared.DrawCombo("Error Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), - (i) => - { - _configService.Current.ErrorNotification = i; - _configService.Save(); - }, _configService.Current.ErrorNotification); - UiShared.DrawHelpText("The location where \"Error\" notifications will display." - + Environment.NewLine + "'Nowhere' will not show any Error notifications" - + Environment.NewLine + "'Chat' will print Error notifications in chat" - + Environment.NewLine + "'Toast' will show Error toast notifications in the bottom right corner" - + Environment.NewLine + "'Both' will show chat as well as the toast notification"); - - if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings)) - { - _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; - _configService.Save(); - } - UiShared.DrawHelpText("Enabling this will not show any \"Warning\" labeled messages for missing optional plugins."); - if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs)) - { - _configService.Current.ShowOnlineNotifications = onlineNotifs; - _configService.Save(); - } - UiShared.DrawHelpText("Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online."); - - if (!onlineNotifs) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) - { - _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; - _configService.Save(); - } - UiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for individual pairs."); - if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly)) - { - _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; - _configService.Save(); - } - UiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note."); - if (!onlineNotifs) ImGui.EndDisabled(); } - private bool _deleteFilesPopupModalShown = false; - private bool _deleteAccountPopupModalShown = false; - private void DrawDebug() + private void UiSharedService_GposeEnd() { - _lastTab = "Debug"; - - UiShared.FontText("Debug", _uiShared.UidFont); - - if (UiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard")) - { - if (LastCreatedCharacterData != null) - { - ImGui.SetClipboardText(JsonConvert.SerializeObject(LastCreatedCharacterData, Formatting.Indented)); - } - else - { - ImGui.SetClipboardText("ERROR: No created character data, cannot copy."); - } - } - UiShared.AttachToolTip("Use this when reporting mods being rejected from the server."); - - _uiShared.DrawCombo("Log Level", Enum.GetValues(), (l) => l.ToString(), (l) => - { - _configService.Current.LogLevel = l; - _configService.Save(); - }, _configService.Current.LogLevel); - - bool logPerformance = _configService.Current.LogPerformance; - if (ImGui.Checkbox("Log Performance Counters", ref logPerformance)) - { - _configService.Current.LogPerformance = logPerformance; - _configService.Save(); - } - UiShared.DrawHelpText("Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); - - if (!logPerformance) ImGui.BeginDisabled(); - if (UiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog")) - { - _performanceCollector.PrintPerformanceStats(); - } - ImGui.SameLine(); - if (UiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) - { - _performanceCollector.PrintPerformanceStats(60); - } - if (!logPerformance) ImGui.EndDisabled(); + IsOpen = _wasOpen; } - private void DrawBlockedTransfers() + private void UiSharedService_GposeStart() { - _lastTab = "BlockedTransfers"; - UiShared.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + - "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + - "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", - ImGuiColors.DalamudGrey); - - if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) - { - ImGui.TableSetupColumn( - $"Hash/Filename"); - ImGui.TableSetupColumn($"Forbidden by"); - - ImGui.TableHeadersRow(); - - foreach (var item in ApiController.ForbiddenTransfers) - { - ImGui.TableNextColumn(); - if (item is UploadFileTransfer transfer) - { - ImGui.Text(transfer.LocalFile); - } - else - { - ImGui.Text(item.Hash); - } - ImGui.TableNextColumn(); - ImGui.Text(item.ForbiddenBy); - } - ImGui.EndTable(); - } + _wasOpen = IsOpen; + IsOpen = false; } - - private void DrawCurrentTransfers() - { - _lastTab = "Transfers"; - bool showTransferWindow = _configService.Current.ShowTransferWindow; - if (ImGui.Checkbox("Show separate Transfer window while transfers are active", ref showTransferWindow)) - { - _configService.Current.ShowTransferWindow = showTransferWindow; - _configService.Save(); - } - - if (_configService.Current.ShowTransferWindow) - { - ImGui.Indent(); - bool editTransferWindowPosition = _uiShared.EditTrackerPosition; - if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) - { - _uiShared.EditTrackerPosition = editTransferWindowPosition; - } - ImGui.Unindent(); - } - - if (ImGui.BeginTable("TransfersTable", 2)) - { - ImGui.TableSetupColumn( - $"Uploads ({UiShared.ByteToString(ApiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(ApiController.CurrentUploads.Sum(a => a.Total))})"); - ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(ApiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Transferred))} / {UiShared.ByteToString(ApiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Total))})"); - - ImGui.TableHeadersRow(); - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("UploadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Uploaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in ApiController.CurrentUploads.ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); - } - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("DownloadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Downloaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in ApiController.CurrentDownloads.SelectMany(k => k.Value).ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); - } - - ImGui.EndTable(); - } - } - - private bool _readExport = false; - private string _exportDescription = string.Empty; - - private void DrawFileStorageSettings() - { - _lastTab = "FileCache"; - - UiShared.FontText("Export MCDF", _uiShared.UidFont); - - UiShared.TextWrapped("This feature allows you to pack your character into a MCDF file and manually send it to other people. MCDF files can officially only be imported during GPose through Mare. " + - "Be aware that the possibility exists that people write unoffocial custom exporters to extract the containing data."); - - ImGui.Checkbox("##readExport", ref _readExport); - ImGui.SameLine(); - UiShared.TextWrapped("I understand that by exporting my character data and sending it to other people I am giving away my current character appearance irrevocably. People I am sharing my data with have the ability to share it with other people without limitations."); - - if (_readExport) - { - ImGui.Indent(); - - if (!_mareCharaFileManager.CurrentlyWorking) - { - ImGui.InputTextWithHint("Export Descriptor", "This description will be shown on loading the data", ref _exportDescription, 255); - if (UiShared.IconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF")) - { - _uiShared.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", "export.mcdf", ".mcdf", (success, path) => - { - if (!success) return; - - Task.Run(() => - { - try - { - _mareCharaFileManager.SaveMareCharaFile(LastCreatedCharacterData, _exportDescription, path); - _exportDescription = string.Empty; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error saving data"); - } - }); - }); - } - UiShared.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" + - " equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow); - } - else - { - UiShared.ColorTextWrapped("Export in progress", ImGuiColors.DalamudYellow); - } - - ImGui.Unindent(); - } - bool openInGpose = _configService.Current.OpenGposeImportOnGposeStart; - if (ImGui.Checkbox("Open MCDF import window when GPose loads", ref openInGpose)) - { - _configService.Current.OpenGposeImportOnGposeStart = openInGpose; - _configService.Save(); - } - UiShared.DrawHelpText("This will automatically open the import menu when loading into Gpose. If unchecked you can open the menu manually with /mare gpose"); - - - ImGui.Separator(); - - UiShared.FontText("Storage", _uiShared.UidFont); - - UiShared.TextWrapped("Mare stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + - "The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage."); - - _uiShared.DrawFileScanState(); - _uiShared.DrawTimeSpanBetweenScansSetting(); - _uiShared.DrawCacheDirectorySetting(); - ImGui.Text($"Currently utilized local storage: {UiShared.ByteToString(_uiShared.FileCacheSize)}"); - ImGui.Dummy(new Vector2(10, 10)); - ImGui.Text("To clear the local storage accept the following disclaimer"); - ImGui.Indent(); - ImGui.Checkbox("##readClearCache", ref _readClearCache); - ImGui.SameLine(); - UiShared.TextWrapped("I understand that: " + Environment.NewLine + "- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data." - + Environment.NewLine + "- This is not a step to try to fix sync issues." - + Environment.NewLine + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); - if (!_readClearCache) - ImGui.BeginDisabled(); - if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage")) - { - if (UiShared.CtrlPressed() && _readClearCache) - { - Task.Run(() => - { - foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder)) - { - File.Delete(file); - } - - _uiShared.RecalculateFileCacheSize(); - }); - } - } - UiShared.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine - + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine - + "Mares storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine - + "If you still think you need to do this hold CTRL while pressing the button."); - if (!_readClearCache) - ImGui.EndDisabled(); - ImGui.Unindent(); - } - - private bool _readClearCache = false; - - public override void OnClose() - { - _uiShared.EditTrackerPosition = false; - base.OnClose(); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UISharedService.cs similarity index 87% rename from MareSynchronos/UI/UIShared.cs rename to MareSynchronos/UI/UISharedService.cs index 6ea3def..7fd61eb 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -1,68 +1,85 @@ -using System.Globalization; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Plugin; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.FileCache; +using MareSynchronos.Interop; using MareSynchronos.Localization; -using MareSynchronos.Managers; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.Mediator; -using MareSynchronos.Models; -using MareSynchronos.Utils; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; namespace MareSynchronos.UI; -public partial class UiShared : MediatorSubscriberBase +public partial class UiSharedService : DisposableMediatorSubscriberBase { - [LibraryImport("user32")] - internal static partial short GetKeyState(int nVirtKey); - - private readonly IpcManager _ipcManager; - private readonly ApiController _apiController; - private readonly PeriodicFileScanner _cacheScanner; - public readonly FileDialogManager FileDialogManager; - private readonly MareConfigService _configService; - private readonly DalamudUtil _dalamudUtil; - private readonly DalamudPluginInterface _pluginInterface; - private readonly Dalamud.Localization _localization; - private readonly ServerConfigurationManager _serverConfigurationManager; - - public long FileCacheSize => _cacheScanner.FileCacheSize; - public string PlayerName => _dalamudUtil.PlayerName; - public uint WorldId => _dalamudUtil.WorldId; - public Dictionary WorldData => _dalamudUtil.WorldData.Value; - private readonly Dictionary _selectedComboItems = new(StringComparer.Ordinal); - public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory); - public bool EditTrackerPosition { get; set; } - public ImFontPtr UidFont { get; private set; } - public bool UidFontBuilt { get; private set; } - public bool IsInGpose => _dalamudUtil.IsInCutscene; - public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; - public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; - public static readonly ImGuiWindowFlags PopupWindowFlags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; - public ApiController ApiController => _apiController; - private bool _penumbraExists = false; - private bool _glamourerExists = false; + public readonly FileDialogManager FileDialogManager; + + private const string _notesEnd = "##MARE_SYNCHRONOS_USER_NOTES_END##"; + + private const string _notesStart = "##MARE_SYNCHRONOS_USER_NOTES_START##"; + + private readonly ApiController _apiController; + + private readonly PeriodicFileScanner _cacheScanner; + + private readonly MareConfigService _configService; + + private readonly DalamudUtilService _dalamudUtil; + + private readonly IpcManager _ipcManager; + + private readonly Dalamud.Localization _localization; + + private readonly DalamudPluginInterface _pluginInterface; + + private readonly Dictionary _selectedComboItems = new(StringComparer.Ordinal); + + private readonly ServerConfigurationManager _serverConfigurationManager; + + private bool _cacheDirectoryHasOtherFilesThanCache = false; + + private bool _cacheDirectoryIsValidPath = true; + private bool _customizePlusExists = false; + + private string _customServerName = ""; + + private string _customServerUri = ""; + + private bool _glamourerExists = false; + private bool _heelsExists = false; + + private bool _isDirectoryWritable = false; + + private bool _isPenumbraDirectory = false; + private bool _palettePlusExists = false; - public UiShared(ILogger logger, IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, - MareConfigService configService, DalamudUtil dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization, + private bool _penumbraExists = false; + + private int _serverSelectionIndex = -1; + + public UiSharedService(ILogger logger, IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, + MareConfigService configService, DalamudUtilService dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) { _ipcManager = ipcManager; @@ -74,6 +91,9 @@ public partial class UiShared : MediatorSubscriberBase _pluginInterface = pluginInterface; _localization = localization; _serverConfigurationManager = serverManager; + + _localization.SetupWithLangCode("en"); + _isDirectoryWritable = IsDirectoryWritable(_configService.Current.CacheFolder); _pluginInterface.UiBuilder.BuildFonts += BuildFont; @@ -82,18 +102,182 @@ public partial class UiShared : MediatorSubscriberBase Mediator.Subscribe(this, (_) => { _penumbraExists = _ipcManager.CheckPenumbraApi(); - _glamourerExists = ipcManager.CheckGlamourerApi(); + _glamourerExists = _ipcManager.CheckGlamourerApi(); _customizePlusExists = _ipcManager.CheckCustomizePlusApi(); _heelsExists = _ipcManager.CheckHeelsApi(); _palettePlusExists = _ipcManager.CheckPalettePlusApi(); }); } - public static float GetWindowContentRegionWidth() + public ApiController ApiController => _apiController; + + public bool EditTrackerPosition { get; set; } + + public long FileCacheSize => _cacheScanner.FileCacheSize; + + public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory); + + public bool IsInGpose => _dalamudUtil.IsInCutscene; + + public string PlayerName => _dalamudUtil.PlayerName; + + public ImFontPtr UidFont { get; private set; } + + public bool UidFontBuilt { get; private set; } + + public Dictionary WorldData => _dalamudUtil.WorldData.Value; + + public uint WorldId => _dalamudUtil.WorldId; + + public static void AttachToolTip(string text) { - return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(text); + } } + public static string ByteToString(long bytes, bool addSuffix = true) + { + string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; + int i; + double dblSByte = bytes; + for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; + } + + public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) + { + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); + } + + public static uint Color(byte r, byte g, byte b, byte a) + { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } + + public static uint Color(Vector4 color) + { + uint ret = (byte)(color.W); + ret <<= 8; + ret += (byte)(color.X); + ret <<= 8; + ret += (byte)(color.Y); + ret <<= 8; + ret += (byte)(color.Z); + return ret; + } + + public static void ColorText(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(text); + ImGui.PopStyleColor(); + } + + public static void ColorTextWrapped(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + TextWrapped(text); + ImGui.PopStyleColor(); + } + + public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + + public static void DrawHelpText(string helpText) + { + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.SetWindowFontScale(0.8f); + ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); + ImGui.SetWindowFontScale(1.0f); + ImGui.PopFont(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(helpText); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + public static void DrawOutlinedFont(string text, Vector4 fontColor, Vector4 outlineColor, int thickness) + { + var original = ImGui.GetCursorPos(); + + ImGui.PushStyleColor(ImGuiCol.Text, outlineColor); + ImGui.SetCursorPos(original with { Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.PopStyleColor(); + + ImGui.PushStyleColor(ImGuiCol.Text, fontColor); + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + ImGui.PopStyleColor(); + } + + public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) + { + drawList.AddText(textPos with { Y = textPos.Y - thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X - thickness }, + outlineColor, text); + drawList.AddText(textPos with { Y = textPos.Y + thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X + thickness }, + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), + outlineColor, text); + + drawList.AddText(textPos, fontColor, text); + drawList.AddText(textPos, fontColor, text); + } + + public static void DrawWithID(string id, Action drawSubSection) + { + ImGui.PushID(id); + drawSubSection.Invoke(); + ImGui.PopID(); + } + + public static void FontText(string text, ImFontPtr font) + { + ImGui.PushFont(font); + ImGui.TextUnformatted(text); + ImGui.PopFont(); + } + + public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + + public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : + input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; + public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) { ImGui.PushFont(UiBuilder.IconFont); @@ -102,67 +286,101 @@ public partial class UiShared : MediatorSubscriberBase return buttonSize; } - - public T? DrawCombo(string comboName, IEnumerable comboItems, Func toName, - Action? onSelected = null, T? initialSelectedItem = default) + public static Vector2 GetIconSize(FontAwesomeIcon icon) { - if (!comboItems.Any()) return default; - - if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null) - { - if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) - { - selectedItem = initialSelectedItem; - _selectedComboItems[comboName] = selectedItem!; - if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) - onSelected?.Invoke(initialSelectedItem); - } - else - { - selectedItem = comboItems.First(); - _selectedComboItems[comboName] = selectedItem!; - } - } - - if (ImGui.BeginCombo(comboName, toName((T)selectedItem!))) - { - foreach (var item in comboItems) - { - bool isSelected = EqualityComparer.Default.Equals(item, (T)selectedItem); - if (ImGui.Selectable(toName(item), isSelected)) - { - _selectedComboItems[comboName] = item!; - onSelected?.Invoke(item!); - } - } - - ImGui.EndCombo(); - } - - return (T)_selectedComboItems[comboName]; + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + ImGui.PopFont(); + return iconSize; } - private void BuildFont() + public static string GetNotes(List pairs) { - var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - UidFontBuilt = false; + StringBuilder sb = new(); + sb.AppendLine(_notesStart); + foreach (var entry in pairs) + { + var note = entry.GetNote(); + if (note.IsNullOrEmpty()) continue; - if (File.Exists(fontFile)) - { - try - { - UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); - UidFontBuilt = true; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Font failed to load. {fontFile}", fontFile); - } + sb.Append(entry.UserData.UID).Append(":\"").Append(entry.GetNote()).AppendLine("\""); } - else + sb.AppendLine(_notesEnd); + + return sb.ToString(); + } + + public static float GetWindowContentRegionWidth() + { + return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + } + + public static bool IconTextButton(FontAwesomeIcon icon, string text) + { + var buttonClicked = false; + + var iconSize = GetIconSize(icon); + var textSize = ImGui.CalcTextSize(text); + var padding = ImGui.GetStyle().FramePadding; + var spacing = ImGui.GetStyle().ItemSpacing; + + var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; + var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; + var buttonSize = new Vector2(buttonSizeX, buttonSizeY); + + if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) { - _logger.LogDebug("Font doesn't exist. {fontFile}", fontFile); + buttonClicked = true; } + + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() - buttonSize.X - padding.X); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(icon.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(); + ImGui.Text(text); + + return buttonClicked; + } + + public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + { + try + { + using FileStream fs = File.Create( + Path.Combine( + dirPath, + Path.GetRandomFileName() + ), + 1, + FileOptions.DeleteOnClose); + return true; + } + catch + { + if (throwIfFails) + throw; + + return false; + } + } + + public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) + { + var cursorPos = ImGui.GetCursorPos(); + ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); + ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); + ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); + ColorTextWrapped(text, outlineColor); + + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + ColorTextWrapped(text, textcolor); + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + ColorTextWrapped(text, textcolor); } public static void SetScaledWindowSize(float width, bool centerWindow = true) @@ -189,138 +407,7 @@ public partial class UiShared : MediatorSubscriberBase ImGui.SetWindowSize(new Vector2(x, y)); } - private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) - { - var center = ImGui.GetMainViewport().GetCenter(); - ImGui.SetWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); - } - - public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) - { - var center = ImGui.GetMainViewport().GetCenter(); - ImGui.SetNextWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); - } - - public static void DrawWithID(string id, Action drawSubSection) - { - ImGui.PushID(id); - drawSubSection.Invoke(); - ImGui.PopID(); - } - - public static void AttachToolTip(string text) - { - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(text); - } - } - - public bool DrawOtherPluginState() - { - var penumbraColor = _penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var glamourerColor = _glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var heelsColor = _heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var customizeColor = _customizePlusExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var paletteColor = _palettePlusExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - ImGui.Text("Penumbra:"); - ImGui.SameLine(); - ImGui.TextColored(penumbraColor, _penumbraExists ? "Available" : "Unavailable"); - ImGui.SameLine(); - ImGui.Text("Glamourer:"); - ImGui.SameLine(); - ImGui.TextColored(glamourerColor, _glamourerExists ? "Available" : "Unavailable"); - ImGui.Text("Optional Addons"); - ImGui.SameLine(); - ImGui.Text("Heels:"); - ImGui.SameLine(); - ImGui.TextColored(heelsColor, _heelsExists ? "Available" : "Unavailable"); - ImGui.SameLine(); - ImGui.Text("Customize+:"); - ImGui.SameLine(); - ImGui.TextColored(customizeColor, _customizePlusExists ? "Available" : "Unavailable"); - ImGui.SameLine(); - ImGui.Text("Palette+:"); - ImGui.SameLine(); - ImGui.TextColored(paletteColor, _palettePlusExists ? "Available" : "Unavailable"); - - if (!_penumbraExists || !_glamourerExists) - { - ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); - return false; - } - - return true; - } - - public void DrawFileScanState() - { - ImGui.Text("File Scanner Status"); - ImGui.SameLine(); - if (_cacheScanner.IsScanRunning) - { - ImGui.Text("Scan is running"); - ImGui.Text("Current Progress:"); - ImGui.SameLine(); - ImGui.Text(_cacheScanner.TotalFiles == 1 - ? "Collecting files" - : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); - } - else if (_configService.Current.FileScanPaused) - { - ImGui.Text("File scanner is paused"); - ImGui.SameLine(); - if (ImGui.Button("Force Rescan##forcedrescan")) - { - _cacheScanner.InvokeScan(forced: true); - } - } - else if (_cacheScanner.haltScanLocks.Any(f => f.Value > 0)) - { - ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.haltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); - ImGui.SameLine(); - if (ImGui.Button("Reset halt requests##clearlocks")) - { - _cacheScanner.ResetLocks(); - } - } - else - { - ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); - } - } - - public void PrintServerState() - { - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); - ImGui.SameLine(); - ImGui.TextUnformatted("("); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); - ImGui.SameLine(); - ImGui.Text("Users Online"); - ImGui.SameLine(); - ImGui.Text(")"); - } - } - - public static void ColorText(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); - } - - public static void ColorTextWrapped(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - TextWrapped(text); - ImGui.PopStyleColor(); - } + public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; public static void TextWrapped(string text) { @@ -329,179 +416,40 @@ public partial class UiShared : MediatorSubscriberBase ImGui.PopTextWrapPos(); } - public static void FontText(string text, ImFontPtr font) - { - ImGui.PushFont(font); - ImGui.TextUnformatted(text); - ImGui.PopFont(); - } - - public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : - input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; - - public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; - public void LoadLocalization(string languageCode) + public bool ApplyNotesFromClipboard(string notes, bool overwrite) { - _localization.SetupWithLangCode(languageCode); - Strings.ToS = new Strings.ToSStrings(); - } - - public static uint Color(byte r, byte g, byte b, byte a) - { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } - - public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) - { - drawList.AddText(textPos with { Y = textPos.Y - thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X - thickness }, - outlineColor, text); - drawList.AddText(textPos with { Y = textPos.Y + thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X + thickness }, - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), - outlineColor, text); - - drawList.AddText(textPos, fontColor, text); - drawList.AddText(textPos, fontColor, text); - } - - public static string ByteToString(long bytes) - { - string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; - int i; - double dblSByte = bytes; - for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + var splitNotes = notes.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList(); + var splitNotesStart = splitNotes.FirstOrDefault(); + var splitNotesEnd = splitNotes.LastOrDefault(); + if (!string.Equals(splitNotesStart, _notesStart, StringComparison.Ordinal) || !string.Equals(splitNotesEnd, _notesEnd, StringComparison.Ordinal)) { - dblSByte = bytes / 1024.0; + return false; } - return $"{dblSByte:0.00} {suffix[i]}"; - } + splitNotes.RemoveAll(n => string.Equals(n, _notesStart, StringComparison.Ordinal) || string.Equals(n, _notesEnd, StringComparison.Ordinal)); - private int _serverSelectionIndex = -1; - private string _customServerName = ""; - private string _customServerUri = ""; - private bool _cacheDirectoryHasOtherFilesThanCache = false; - private bool _cacheDirectoryIsValidPath = true; - - public int DrawServiceSelection(bool selectOnChange = false) - { - string[] comboEntries = _serverConfigurationManager.GetServerNames(); - - if (_serverSelectionIndex == -1) - _serverSelectionIndex = Array.IndexOf(_serverConfigurationManager.GetServerApiUrls(), _serverConfigurationManager.CurrentApiUrl); - for (int i = 0; i < comboEntries.Length; i++) + foreach (var note in splitNotes) { - if (string.Equals(_serverConfigurationManager.CurrentServer?.ServerName, comboEntries[i], StringComparison.OrdinalIgnoreCase)) - comboEntries[i] += " [Current]"; - } - if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) - { - for (int i = 0; i < comboEntries.Length; i++) + try { - bool isSelected = _serverSelectionIndex == i; - if (ImGui.Selectable(comboEntries[i], isSelected)) - { - _serverSelectionIndex = i; - if (selectOnChange) - { - _serverConfigurationManager.SelectServer(i); - } - } - - if (isSelected) - { - ImGui.SetItemDefaultFocus(); - } + var splittedEntry = note.Split(":", 2, StringSplitOptions.RemoveEmptyEntries); + var uid = splittedEntry[0]; + var comment = splittedEntry[1].Trim('"'); + if (_serverConfigurationManager.GetNoteForUid(uid) != null && !overwrite) continue; + _serverConfigurationManager.SetNoteForUid(uid, comment); } - - ImGui.EndCombo(); - } - - if (_serverConfigurationManager.GetSecretKey(_serverSelectionIndex) != null) - { - ImGui.SameLine(); - var text = "Connect"; - if (_serverSelectionIndex == _serverConfigurationManager.GetCurrentServerIndex()) text = "Reconnect"; - if (IconTextButton(FontAwesomeIcon.Link, text)) + catch { - _serverConfigurationManager.SelectServer(_serverSelectionIndex); - _ = _apiController.CreateConnections(); + Logger.LogWarning("Could not parse {note}", note); } } - if (ImGui.TreeNode("Add Custom Service")) - { - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service URI", ref _customServerUri, 255); - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service Name", ref _customServerName, 255); - if (UiShared.IconTextButton(FontAwesomeIcon.Plus, "Add Custom Service")) - { - if (!string.IsNullOrEmpty(_customServerUri) - && !string.IsNullOrEmpty(_customServerName)) - { - _serverConfigurationManager.AddServer(new ServerStorage() - { - ServerName = _customServerName, - ServerUri = _customServerUri, - }); - _customServerName = string.Empty; - _customServerUri = string.Empty; - _configService.Save(); - } - } - ImGui.TreePop(); - } + _serverConfigurationManager.SaveNotes(); - return _serverSelectionIndex; - } - - - public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) - { - var cursorPos = ImGui.GetCursorPos(); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); - ColorTextWrapped(text, outlineColor); - - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - ColorTextWrapped(text, textcolor); - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - ColorTextWrapped(text, textcolor); - } - - public static void DrawHelpText(string helpText) - { - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SetWindowFontScale(0.8f); - ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); - ImGui.SetWindowFontScale(1.0f); - ImGui.PopFont(); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); - ImGui.TextUnformatted(helpText); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } + return true; } public void DrawCacheDirectorySetting() @@ -566,34 +514,192 @@ public partial class UiShared : MediatorSubscriberBase DrawHelpText("The storage is automatically governed by Mare. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); } - private bool _isDirectoryWritable = false; - private bool _isPenumbraDirectory = false; - - public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + public T? DrawCombo(string comboName, IEnumerable comboItems, Func toName, + Action? onSelected = null, T? initialSelectedItem = default) { - try - { - using FileStream fs = File.Create( - Path.Combine( - dirPath, - Path.GetRandomFileName() - ), - 1, - FileOptions.DeleteOnClose); - return true; - } - catch - { - if (throwIfFails) - throw; + if (!comboItems.Any()) return default; - return false; + if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null) + { + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + { + selectedItem = initialSelectedItem; + _selectedComboItems[comboName] = selectedItem!; + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + onSelected?.Invoke(initialSelectedItem); + } + else + { + selectedItem = comboItems.First(); + _selectedComboItems[comboName] = selectedItem!; + } + } + + if (ImGui.BeginCombo(comboName, toName((T)selectedItem!))) + { + foreach (var item in comboItems) + { + bool isSelected = EqualityComparer.Default.Equals(item, (T)selectedItem); + if (ImGui.Selectable(toName(item), isSelected)) + { + _selectedComboItems[comboName] = item!; + onSelected?.Invoke(item!); + } + } + + ImGui.EndCombo(); + } + + return (T)_selectedComboItems[comboName]; + } + + public void DrawFileScanState() + { + ImGui.Text("File Scanner Status"); + ImGui.SameLine(); + if (_cacheScanner.IsScanRunning) + { + ImGui.Text("Scan is running"); + ImGui.Text("Current Progress:"); + ImGui.SameLine(); + ImGui.Text(_cacheScanner.TotalFiles == 1 + ? "Collecting files" + : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); + } + else if (_configService.Current.FileScanPaused) + { + ImGui.Text("File scanner is paused"); + ImGui.SameLine(); + if (ImGui.Button("Force Rescan##forcedrescan")) + { + _cacheScanner.InvokeScan(forced: true); + } + } + else if (_cacheScanner.HaltScanLocks.Any(f => f.Value > 0)) + { + ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.HaltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); + ImGui.SameLine(); + if (ImGui.Button("Reset halt requests##clearlocks")) + { + _cacheScanner.ResetLocks(); + } + } + else + { + ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); } } - public void RecalculateFileCacheSize() + public bool DrawOtherPluginState() { - _cacheScanner.InvokeScan(forced: true); + var penumbraColor = _penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var glamourerColor = _glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var heelsColor = _heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var customizeColor = _customizePlusExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var paletteColor = _palettePlusExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + ImGui.Text("Penumbra:"); + ImGui.SameLine(); + ImGui.TextColored(penumbraColor, _penumbraExists ? "Available" : "Unavailable"); + ImGui.SameLine(); + ImGui.Text("Glamourer:"); + ImGui.SameLine(); + ImGui.TextColored(glamourerColor, _glamourerExists ? "Available" : "Unavailable"); + ImGui.Text("Optional Addons"); + ImGui.SameLine(); + ImGui.Text("Heels:"); + ImGui.SameLine(); + ImGui.TextColored(heelsColor, _heelsExists ? "Available" : "Unavailable"); + ImGui.SameLine(); + ImGui.Text("Customize+:"); + ImGui.SameLine(); + ImGui.TextColored(customizeColor, _customizePlusExists ? "Available" : "Unavailable"); + ImGui.SameLine(); + ImGui.Text("Palette+:"); + ImGui.SameLine(); + ImGui.TextColored(paletteColor, _palettePlusExists ? "Available" : "Unavailable"); + + if (!_penumbraExists || !_glamourerExists) + { + ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); + return false; + } + + return true; + } + + public int DrawServiceSelection(bool selectOnChange = false) + { + string[] comboEntries = _serverConfigurationManager.GetServerNames(); + + if (_serverSelectionIndex == -1) + _serverSelectionIndex = Array.IndexOf(_serverConfigurationManager.GetServerApiUrls(), _serverConfigurationManager.CurrentApiUrl); + if (_serverSelectionIndex == -1) + { + _serverSelectionIndex = 0; + } + for (int i = 0; i < comboEntries.Length; i++) + { + if (string.Equals(_serverConfigurationManager.CurrentServer?.ServerName, comboEntries[i], StringComparison.OrdinalIgnoreCase)) + comboEntries[i] += " [Current]"; + } + if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) + { + for (int i = 0; i < comboEntries.Length; i++) + { + bool isSelected = _serverSelectionIndex == i; + if (ImGui.Selectable(comboEntries[i], isSelected)) + { + _serverSelectionIndex = i; + if (selectOnChange) + { + _serverConfigurationManager.SelectServer(i); + } + } + + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + if (_serverConfigurationManager.GetSecretKey(_serverSelectionIndex) != null) + { + ImGui.SameLine(); + var text = "Connect"; + if (_serverSelectionIndex == _serverConfigurationManager.GetCurrentServerIndex()) text = "Reconnect"; + if (IconTextButton(FontAwesomeIcon.Link, text)) + { + _serverConfigurationManager.SelectServer(_serverSelectionIndex); + _ = _apiController.CreateConnections(); + } + } + + if (ImGui.TreeNode("Add Custom Service")) + { + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service URI", ref _customServerUri, 255); + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service Name", ref _customServerName, 255); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add Custom Service") + && !string.IsNullOrEmpty(_customServerUri) + && !string.IsNullOrEmpty(_customServerName)) + { + _serverConfigurationManager.AddServer(new ServerStorage() + { + ServerName = _customServerName, + ServerUri = _customServerUri, + }); + _customServerName = string.Empty; + _customServerUri = string.Empty; + _configService.Save(); + } + ImGui.TreePop(); + } + + return _serverSelectionIndex; } public void DrawTimeSpanBetweenScansSetting() @@ -614,101 +720,74 @@ public partial class UiShared : MediatorSubscriberBase DrawHelpText("This allows you to stop the periodic scans of your Penumbra and Mare cache directories. Use this to move the Mare cache and Penumbra mod folders around. If you enable this permanently, run a Force rescan after adding mods to Penumbra."); } - public static Vector2 GetIconSize(FontAwesomeIcon icon) + public void LoadLocalization(string languageCode) { - ImGui.PushFont(UiBuilder.IconFont); - var iconSize = ImGui.CalcTextSize(icon.ToIconString()); - ImGui.PopFont(); - return iconSize; + _localization.SetupWithLangCode(languageCode); + Strings.ToS = new Strings.ToSStrings(); } - public static bool IconTextButton(FontAwesomeIcon icon, string text) + public void PrintServerState() { - var buttonClicked = false; - - var iconSize = GetIconSize(icon); - var textSize = ImGui.CalcTextSize(text); - var padding = ImGui.GetStyle().FramePadding; - var spacing = ImGui.GetStyle().ItemSpacing; - - var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; - var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; - var buttonSize = new Vector2(buttonSizeX, buttonSizeY); - - if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) + if (_apiController.ServerState is ServerState.Connected) { - buttonClicked = true; + ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.SameLine(); + ImGui.Text("Users Online"); + ImGui.SameLine(); + ImGui.Text(")"); } - - ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() - buttonSize.X - padding.X); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(icon.ToIconString()); - ImGui.PopFont(); - ImGui.SameLine(); - ImGui.Text(text); - - return buttonClicked; } - private const string _notesStart = "##MARE_SYNCHRONOS_USER_NOTES_START##"; - private const string _notesEnd = "##MARE_SYNCHRONOS_USER_NOTES_END##"; - - public static string GetNotes(List pairs) + public void RecalculateFileCacheSize() { - StringBuilder sb = new(); - sb.AppendLine(_notesStart); - foreach (var entry in pairs) - { - var note = entry.GetNote(); - if (note.IsNullOrEmpty()) continue; - - sb.Append(entry.UserData.UID).Append(":\"").Append(entry.GetNote()).AppendLine("\""); - } - sb.AppendLine(_notesEnd); - - return sb.ToString(); + _cacheScanner.InvokeScan(forced: true); } - public bool ApplyNotesFromClipboard(string notes, bool overwrite) + [LibraryImport("user32")] + internal static partial short GetKeyState(int nVirtKey); + + protected override void Dispose(bool disposing) { - var splitNotes = notes.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList(); - var splitNotesStart = splitNotes.FirstOrDefault(); - var splitNotesEnd = splitNotes.LastOrDefault(); - if (!string.Equals(splitNotesStart, _notesStart, StringComparison.Ordinal) || !string.Equals(splitNotesEnd, _notesEnd, StringComparison.Ordinal)) - { - return false; - } + base.Dispose(disposing); - splitNotes.RemoveAll(n => string.Equals(n, _notesStart, StringComparison.Ordinal) || string.Equals(n, _notesEnd, StringComparison.Ordinal)); - - foreach (var note in splitNotes) - { - try - { - var splittedEntry = note.Split(":", 2, StringSplitOptions.RemoveEmptyEntries); - var uid = splittedEntry[0]; - var comment = splittedEntry[1].Trim('"'); - if (_serverConfigurationManager.GetNoteForUid(uid) != null && !overwrite) continue; - _serverConfigurationManager.SetNoteForUid(uid, comment); - } - catch - { - _logger.LogWarning("Could not parse {note}", note); - } - } - - _serverConfigurationManager.SaveNotes(); - - return true; - } - - public override void Dispose() - { - base.Dispose(); _pluginInterface.UiBuilder.BuildFonts -= BuildFont; } + private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) + { + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); + } + [GeneratedRegex(@"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript)] private static partial Regex PathRegex(); -} + + private void BuildFont() + { + var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + UidFontBuilt = false; + + if (File.Exists(fontFile)) + { + try + { + UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); + UidFontBuilt = true; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Font failed to load. {fontFile}", fontFile); + } + } + else + { + Logger.LogDebug("Font doesn't exist. {fontFile}", fontFile); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs index 1d699d1..a0cf4d6 100644 --- a/MareSynchronos/Utils/Crypto.cs +++ b/MareSynchronos/Utils/Crypto.cs @@ -7,6 +7,7 @@ namespace MareSynchronos.Utils; public static class Crypto { #pragma warning disable SYSLIB0021 // Type or member is obsolete + public static string GetFileHash(this string filePath) { using SHA1CryptoServiceProvider cryptoProvider = new(); @@ -30,5 +31,6 @@ public static class Crypto using SHA256CryptoServiceProvider cryptoProvider = new(); return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", "", StringComparison.Ordinal); } + #pragma warning restore SYSLIB0021 // Type or member is obsolete -} +} \ No newline at end of file diff --git a/MareSynchronos/Utils/RollingList.cs b/MareSynchronos/Utils/RollingList.cs index d97c122..28cb5ca 100644 --- a/MareSynchronos/Utils/RollingList.cs +++ b/MareSynchronos/Utils/RollingList.cs @@ -4,8 +4,8 @@ namespace MareSynchronos.Utils; public class RollingList : IEnumerable { - private readonly LinkedList _list = new(); private readonly object _addLock = new(); + private readonly LinkedList _list = new(); public RollingList(int maximumCount) { @@ -15,8 +15,19 @@ public class RollingList : IEnumerable MaximumCount = maximumCount; } - public int MaximumCount { get; } public int Count => _list.Count; + public int MaximumCount { get; } + + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return _list.Skip(index).First(); + } + } public void Add(T value) { @@ -30,17 +41,7 @@ public class RollingList : IEnumerable } } - public T this[int index] - { - get - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException(nameof(index)); - - return _list.Skip(index).First(); - } - } - public IEnumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} +} \ No newline at end of file diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs index bc3e91a..3724023 100644 --- a/MareSynchronos/Utils/VariousExtensions.cs +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -1,32 +1,11 @@ -using System.Globalization; -using System.Reflection; -using Newtonsoft.Json; +using System.Text.Json; namespace MareSynchronos.Utils; public static class VariousExtensions { - public static DateTime GetLinkerTime(Assembly assembly) - { - const string BuildVersionMetadataPrefix = "+build"; - - var attribute = assembly.GetCustomAttribute(); - if (attribute?.InformationalVersion != null) - { - var value = attribute.InformationalVersion; - var index = value.IndexOf(BuildVersionMetadataPrefix, StringComparison.Ordinal); - if (index > 0) - { - value = value[(index + BuildVersionMetadataPrefix.Length)..]; - return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture); - } - } - - return default; - } - public static T DeepClone(this T obj) { - return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj))!; + return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; } } diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs deleted file mode 100644 index d1e5540..0000000 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ /dev/null @@ -1,540 +0,0 @@ -using System.Collections.Concurrent; -using System.Net; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Runtime.CompilerServices; -using System.Text; -using Dalamud.Utility; -using LZ4; -using MareSynchronos.API.Data; -using MareSynchronos.API.Dto.Files; -using MareSynchronos.API.Routes; -using MareSynchronos.Mediator; -using MareSynchronos.UI; -using MareSynchronos.WebAPI.Utils; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.WebAPI; - -public partial class ApiController -{ - private readonly Dictionary _verifiedUploadedHashes; - private readonly ConcurrentDictionary _downloadReady = new(); - private bool _currentUploadCancelled = false; - - private int _downloadId = 0; - public async Task CancelUpload() - { - if (CurrentUploads.Any()) - { - _logger.LogDebug("Cancelling current upload"); - _uploadCancellationTokenSource?.Cancel(); - _uploadCancellationTokenSource?.Dispose(); - _uploadCancellationTokenSource = null; - CurrentUploads.Clear(); - await FilesAbortUpload().ConfigureAwait(false); - return true; - } - - return false; - } - - public async Task FilesAbortUpload() - { - await _mareHub!.SendAsync(nameof(FilesAbortUpload)).ConfigureAwait(false); - } - - public async Task FilesDeleteAll() - { - _verifiedUploadedHashes.Clear(); - await _mareHub!.SendAsync(nameof(FilesDeleteAll)).ConfigureAwait(false); - } - - private async Task GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct) - { - var response = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false); - var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - var requestId = Guid.Parse(responseString.Trim('"')); - if (!_downloadReady.ContainsKey(requestId)) - { - _downloadReady[requestId] = false; - } - return requestId; - } - - private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt) - { - bool alreadyCancelled = false; - try - { - CancellationTokenSource localTimeoutCts = new(); - localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); - CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); - - while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady) - { - try - { - await Task.Delay(250, composite.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - if (downloadCt.IsCancellationRequested) throw; - - var req = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false); - try - { - req.EnsureSuccessStatusCode(); - localTimeoutCts.Dispose(); - composite.Dispose(); - localTimeoutCts = new(); - localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); - composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); - } - catch (HttpRequestException) - { - throw; - } - } - } - - localTimeoutCts.Dispose(); - composite.Dispose(); - - _logger.LogDebug($"Download {requestId} ready"); - } - catch (TaskCanceledException) - { - try - { - await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false); - alreadyCancelled = true; - } - catch { } - - throw; - } - finally - { - if (downloadCt.IsCancellationRequested && !alreadyCancelled) - { - try - { - await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false); - } - catch { } - } - _downloadReady.Remove(requestId, out _); - } - } - - private async Task DownloadFileHttpClient(DownloadFileTransfer fileTransfer, string tempPath, IProgress progress, CancellationToken ct) - { - var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false); - - _logger.LogDebug($"GUID {requestId} for file {fileTransfer.Hash} on server {fileTransfer.DownloadUri}"); - - await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false); - - HttpResponseMessage response = null!; - var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId); - - _logger.LogDebug($"Downloading {requestUrl} for file {fileTransfer.Hash}"); - try - { - response = await SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - _logger.LogWarning(ex, $"Error during download of {requestUrl}, HttpStatusCode: {ex.StatusCode}"); - if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized) - { - throw new Exception($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex); - } - } - - try - { - var fileStream = File.Create(tempPath); - await using (fileStream.ConfigureAwait(false)) - { - var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024; - var buffer = new byte[bufferSize]; - - var bytesRead = 0; - while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) - { - ct.ThrowIfCancellationRequested(); - - await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false); - - progress.Report(bytesRead); - } - - _logger.LogDebug($"{requestUrl} downloaded to {tempPath}"); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, $"Error during file download of {requestUrl}"); - try - { - if (!tempPath.IsNullOrEmpty()) - File.Delete(tempPath); - } - catch { } - throw; - } - } - - public int GetDownloadId() => _downloadId++; - - public async Task DownloadFiles(int currentDownloadId, List fileReplacementDto, CancellationToken ct) - { - Mediator.Publish(new HaltScanMessage("Download")); - try - { - await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct).ConfigureAwait(false); - } - catch - { - CancelDownload(currentDownloadId); - } - finally - { - Mediator.Publish(new ResumeScanMessage("Download")); - } - } - - private async Task SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null) - { - using var requestMessage = new HttpRequestMessage(method, uri); - return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); - } - - private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null) - { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._serverManager.GetToken()); - - if (requestMessage.Content != null) - { - _logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri + " (Content: " + await (((JsonContent)requestMessage.Content).ReadAsStringAsync()) + ")"); - } - else - { - _logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri); - } - - try - { - if (ct != null) - return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false); - return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error during SendRequestInternal for " + requestMessage.RequestUri); - throw; - } - } - - private async Task SendRequestAsync(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class - { - using var requestMessage = new HttpRequestMessage(method, uri); - requestMessage.Content = JsonContent.Create(content); - return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); - } - - private async Task DownloadFilesInternal(int currentDownloadId, List fileReplacement, CancellationToken ct) - { - _logger.LogDebug("Downloading files (Download ID " + currentDownloadId + ")"); - - List downloadFileInfoFromService = new(); - downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList()).ConfigureAwait(false)); - - _logger.LogDebug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); - - CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) - .Where(d => d.CanBeTransferred).ToList(); - - foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) - { - if (!ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) - { - ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); - } - } - - var downloadGroups = CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + f.DownloadUri.Port, StringComparer.Ordinal); - - await Parallel.ForEachAsync(downloadGroups, new ParallelOptions() - { - MaxDegreeOfParallelism = downloadGroups.Count(), - CancellationToken = ct, - }, - async (fileGroup, token) => - { - // let server predownload files - await SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri), - fileGroup.Select(c => c.Hash), token).ConfigureAwait(false); - - foreach (var file in fileGroup) - { - var hash = file.Hash; - Progress progress = new((bytesDownloaded) => - { - file.Transferred += bytesDownloaded; - }); - - var tempPath = Path.Combine(_configService.Current.CacheFolder, file.Hash + ".tmp"); - try - { - await DownloadFileHttpClient(file, tempPath, progress, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - File.Delete(tempPath); - _logger.LogDebug("Detected cancellation, removing " + currentDownloadId); - CancelDownload(currentDownloadId); - return; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during download of " + file.Hash); - return; - } - - var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false); - var extratokenedFile = LZ4Codec.Unwrap(tempFileData); - File.Delete(tempPath); - var filePath = Path.Combine(_configService.Current.CacheFolder, file.Hash); - await File.WriteAllBytesAsync(filePath, extratokenedFile, token).ConfigureAwait(false); - var fi = new FileInfo(filePath); - Func RandomDayInThePast() - { - DateTime start = new(1995, 1, 1); - Random gen = new(); - int range = (DateTime.Today - start).Days; - return () => start.AddDays(gen.Next(range)); - } - - fi.CreationTime = RandomDayInThePast().Invoke(); - fi.LastAccessTime = DateTime.Today; - fi.LastWriteTime = RandomDayInThePast().Invoke(); - try - { - _ = _fileDbManager.CreateCacheEntry(filePath); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Issue creating cache entry"); - } - } - }).ConfigureAwait(false); - - _logger.LogDebug("Download complete, removing " + currentDownloadId); - CancelDownload(currentDownloadId); - } - - public async Task PushCharacterData(CharacterData data, List visibleCharacters) - { - if (!IsConnected) return; - - try - { - _currentUploadCancelled = await CancelUpload().ConfigureAwait(false); - - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; - _logger.LogDebug($"Sending Character data {data.DataHash.Value} to service {_serverManager.CurrentApiUrl}"); - - HashSet unverifiedUploads = VerifyFiles(data); - if (unverifiedUploads.Any()) - { - await UploadMissingFiles(unverifiedUploads, uploadToken).ConfigureAwait(false); - _logger.LogInformation("Upload complete for " + data.DataHash.Value); - } - await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Upload operation was cancelled"); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error during upload of files"); - } - finally - { - if (!_currentUploadCancelled) - _currentUploadCancelled = await CancelUpload().ConfigureAwait(false); - } - } - - private HashSet VerifyFiles(CharacterData data) - { - HashSet unverifiedUploadHashes = new(StringComparer.Ordinal); - foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList()) - { - if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime)) - { - verifiedTime = DateTime.MinValue; - } - - if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10))) - { - _logger.LogTrace("Verifying " + item + ", last verified: " + verifiedTime); - unverifiedUploadHashes.Add(item); - } - } - - return unverifiedUploadHashes; - } - - private async Task UploadMissingFiles(HashSet unverifiedUploadHashes, CancellationToken uploadToken) - { - unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal); - - _logger.LogDebug("Verifying " + unverifiedUploadHashes.Count + " files"); - var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList()).ConfigureAwait(false); - - foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) - { - try - { - CurrentUploads.Add(new UploadFileTransfer(file) - { - Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length, - }); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Tried to request file " + file.Hash + " but file was not present"); - } - } - - foreach (var file in filesToUpload.Where(c => c.IsForbidden)) - { - if (ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) - { - ForbiddenTransfers.Add(new UploadFileTransfer(file) - { - LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty, - }); - } - - _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; - } - - var totalSize = CurrentUploads.Sum(c => c.Total); - _logger.LogDebug("Compressing and uploading files"); - foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) - { - _logger.LogDebug("Compressing and uploading " + file); - var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false); - CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length; - await UploadFile(data.Item2, file.Hash, uploadToken).ConfigureAwait(false); - _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; - uploadToken.ThrowIfCancellationRequested(); - } - - if (CurrentUploads.Any()) - { - var compressedSize = CurrentUploads.Sum(c => c.Total); - _logger.LogDebug($"Compressed {UiShared.ByteToString(totalSize)} to {UiShared.ByteToString(compressedSize)} ({(compressedSize / (double)totalSize):P2})"); - - _logger.LogDebug("Upload tasks complete, waiting for server to confirm"); - _logger.LogDebug("Uploads open: " + CurrentUploads.Any(c => c.IsInTransfer)); - const double waitStep = 1.0d; - while (CurrentUploads.Any(c => c.IsInTransfer) && !uploadToken.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromSeconds(waitStep), uploadToken).ConfigureAwait(false); - _logger.LogDebug("Waiting for uploads to finish"); - } - } - - foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal)))) - { - _verifiedUploadedHashes[file] = DateTime.UtcNow; - } - - CurrentUploads.Clear(); - } - - private async Task PushCharacterDataInternal(CharacterData character, List visibleCharacters) - { - _logger.LogInformation("Pushing character data for " + character.DataHash.Value + " to " + string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); - StringBuilder sb = new(); - foreach (var kvp in character.FileReplacements.ToList()) - { - sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}"); - character.FileReplacements[kvp.Key].RemoveAll(i => ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase))); - } - foreach (var item in character.GlamourerData) - { - sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); - } - _logger.LogDebug("Chara data contained: " + Environment.NewLine + sb.ToString()); - await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false); - } - - private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, - (int)new FileInfo(fileCache).Length)); - } - - private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) - { - if (uploadToken.IsCancellationRequested) return; - - async IAsyncEnumerable AsyncFileData([EnumeratorCancellation] CancellationToken token) - { - var chunkSize = 1024 * 512; // 512kb - using var ms = new MemoryStream(compressedFile); - var buffer = new byte[chunkSize]; - int bytesRead; - while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token).ConfigureAwait(false)) > 0 && !token.IsCancellationRequested) - { - CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred += bytesRead; - token.ThrowIfCancellationRequested(); - yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); - } - } - - await FilesUploadStreamAsync(fileHash, AsyncFileData(uploadToken)).ConfigureAwait(false); - } - - public async Task FilesUploadStreamAsync(string hash, IAsyncEnumerable fileContent) - { - await _mareHub!.InvokeAsync(nameof(FilesUploadStreamAsync), hash, fileContent).ConfigureAwait(false); - } - - public async Task FilesIsUploadFinished() - { - return await _mareHub!.InvokeAsync(nameof(FilesIsUploadFinished)).ConfigureAwait(false); - } - - public async Task> FilesGetSizes(List hashes) - { - return await _mareHub!.InvokeAsync>(nameof(FilesGetSizes), hashes).ConfigureAwait(false); - } - - public async Task> FilesSend(List fileListHashes) - { - return await _mareHub!.InvokeAsync>(nameof(FilesSend), fileListHashes).ConfigureAwait(false); - } - - - public void CancelDownload(int downloadId) - { - while (CurrentDownloads.ContainsKey(downloadId)) - { - CurrentDownloads.TryRemove(downloadId, out _); - } - } -} - diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs deleted file mode 100644 index 5f52044..0000000 --- a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs +++ /dev/null @@ -1,55 +0,0 @@ -using MareSynchronos.API.Dto.User; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.WebAPI; - -public partial class ApiController -{ - public async Task UserDelete() - { - CheckConnection(); - await FilesDeleteAll().ConfigureAwait(false); - await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false); - await CreateConnections().ConfigureAwait(false); - } - - public async Task UserPushData(UserCharaDataMessageDto dto) - { - try - { - await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to Push character data"); - } - } - - public async Task> UserGetPairedClients() - { - return await _mareHub!.InvokeAsync>(nameof(UserGetPairedClients)).ConfigureAwait(false); - } - - public async Task> UserGetOnlinePairs() - { - return await _mareHub!.InvokeAsync>(nameof(UserGetOnlinePairs)).ConfigureAwait(false); - } - - public async Task UserSetPairPermissions(UserPermissionsDto dto) - { - await _mareHub!.SendAsync(nameof(UserSetPairPermissions), dto).ConfigureAwait(false); - } - - public async Task UserAddPair(UserDto dto) - { - if (!IsConnected) return; - await _mareHub!.SendAsync(nameof(UserAddPair), dto).ConfigureAwait(false); - } - - public async Task UserRemovePair(UserDto dto) - { - if (!IsConnected) return; - await _mareHub!.SendAsync(nameof(UserRemovePair), dto).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs new file mode 100644 index 0000000..03b2a0b --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -0,0 +1,341 @@ +using Dalamud.Utility; +using LZ4; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Files; +using MareSynchronos.API.Routes; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http.Json; + +namespace MareSynchronos.WebAPI.Files; + +public partial class FileDownloadManager : DisposableMediatorSubscriberBase +{ + private readonly ConcurrentDictionary _downloadReady = new(); + private readonly Dictionary _downloadStatus; + private readonly FileCacheManager _fileDbManager; + private readonly FileTransferOrchestrator _orchestrator; + + public FileDownloadManager(ILogger logger, MareMediator mediator, + FileTransferOrchestrator orchestrator, + FileCacheManager fileCacheManager) : base(logger, mediator) + { + _downloadStatus = new Dictionary(StringComparer.Ordinal); + _orchestrator = orchestrator; + _fileDbManager = fileCacheManager; + + Mediator.Subscribe(this, (msg) => + { + if (_downloadReady.ContainsKey(msg.RequestId)) + { + _downloadReady[msg.RequestId] = true; + } + }); + } + + public List CurrentDownloads { get; private set; } = new(); + public List ForbiddenTransfers => _orchestrator.ForbiddenTransfers; + public bool IsDownloading => !CurrentDownloads.Any(); + + public void CancelDownload() + { + CurrentDownloads.Clear(); + _downloadStatus.Clear(); + } + + public async Task DownloadFiles(GameObjectHandler gameObject, List fileReplacementDto, CancellationToken ct) + { + Mediator.Publish(new HaltScanMessage("Download")); + try + { + await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false); + } + catch + { + CancelDownload(); + } + finally + { + Mediator.Publish(new DownloadFinishedMessage(gameObject)); + Mediator.Publish(new ResumeScanMessage("Download")); + } + } + + protected override void Dispose(bool disposing) + { + CancelDownload(); + base.Dispose(disposing); + } + + private async Task DownloadFileHttpClient(string downloadGroup, DownloadFileTransfer fileTransfer, string tempPath, IProgress progress, CancellationToken ct) + { + var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false); + + Logger.LogDebug("GUID {requestId} for file {hash} on server {uri}", requestId, fileTransfer.Hash, fileTransfer.DownloadUri); + + await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false); + + _downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading; + + HttpResponseMessage response = null!; + var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId); + + Logger.LogDebug("Downloading {requestUrl} for file {hash}", requestUrl, fileTransfer.Hash); + try + { + response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode); + if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized) + { + throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex); + } + } + + try + { + var fileStream = File.Create(tempPath); + await using (fileStream.ConfigureAwait(false)) + { + var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024; + var buffer = new byte[bufferSize]; + + var bytesRead = 0; + while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) + { + ct.ThrowIfCancellationRequested(); + + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false); + + progress.Report(bytesRead); + } + + Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during file download of {requestUrl}", requestUrl); + try + { + if (!tempPath.IsNullOrEmpty()) + File.Delete(tempPath); + } + catch + { + // ignore if file deletion fails + } + throw; + } + } + + private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + { + Logger.LogDebug("Downloading files for {id}", gameObjectHandler.Name); + + // force create lazy + _ = gameObjectHandler.GameObjectLazy.Value; + + List downloadFileInfoFromService = new(); + downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList(), ct).ConfigureAwait(false)); + + Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); + + CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) + .Where(d => d.CanBeTransferred).ToList(); + + foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) + { + if (!_orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) + { + _orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); + } + } + + var downloadGroups = CurrentDownloads.Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal); + + foreach (var downloadGroup in downloadGroups) + { + _downloadStatus[downloadGroup.Key] = new FileDownloadStatus() + { + DownloadStatus = DownloadStatus.Initializing, + TotalBytes = downloadGroup.Sum(c => c.Total), + TotalFiles = downloadGroup.Count(), + TransferredBytes = 0, + TransferredFiles = 0 + }; + } + + Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); + + await Parallel.ForEachAsync(downloadGroups, new ParallelOptions() + { + MaxDegreeOfParallelism = downloadGroups.Count(), + CancellationToken = ct, + }, + async (fileGroup, token) => + { + // let server predownload files + await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri), + fileGroup.Select(c => c.Hash), token).ConfigureAwait(false); + + foreach (var file in fileGroup) + { + var tempPath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: true); + Progress progress = new((bytesDownloaded) => + { + if (!_downloadStatus.ContainsKey(fileGroup.Key)) return; + _downloadStatus[fileGroup.Key].TransferredBytes += bytesDownloaded; + file.Transferred += bytesDownloaded; + }); + + try + { + _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot; + await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false); + _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue; + await DownloadFileHttpClient(fileGroup.Key, file, tempPath, progress, token).ConfigureAwait(false); + _downloadStatus[fileGroup.Key].TransferredFiles += 1; + } + catch (OperationCanceledException) + { + File.Delete(tempPath); + Logger.LogDebug("Detected cancellation, removing {id}", gameObjectHandler); + CancelDownload(); + return; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error during download of {hash}", file.Hash); + continue; + } + finally + { + _orchestrator.ReleaseDownloadSlot(); + } + + _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.Decompressing; + var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false); + var extractedFile = LZ4Codec.Unwrap(tempFileData); + File.Delete(tempPath); + var filePath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: false); + await File.WriteAllBytesAsync(filePath, extractedFile, token).ConfigureAwait(false); + var fi = new FileInfo(filePath); + Func RandomDayInThePast() + { + DateTime start = new(1995, 1, 1); + Random gen = new(); + int range = (DateTime.Today - start).Days; + return () => start.AddDays(gen.Next(range)); + } + + fi.CreationTime = RandomDayInThePast().Invoke(); + fi.LastAccessTime = DateTime.Today; + fi.LastWriteTime = RandomDayInThePast().Invoke(); + try + { + _ = _fileDbManager.CreateCacheEntry(filePath); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Issue creating cache entry"); + } + } + }).ConfigureAwait(false); + + Logger.LogDebug("Download for {id} complete", gameObjectHandler); + CancelDownload(); + } + + private async Task> FilesGetSizes(List hashes, CancellationToken ct) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), hashes, ct).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? new List(); + } + + private async Task GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct) + { + var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + var requestId = Guid.Parse(responseString.Trim('"')); + if (!_downloadReady.ContainsKey(requestId)) + { + _downloadReady[requestId] = false; + } + return requestId; + } + + private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt) + { + bool alreadyCancelled = false; + try + { + CancellationTokenSource localTimeoutCts = new(); + localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); + + while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady) + { + try + { + await Task.Delay(250, composite.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + if (downloadCt.IsCancellationRequested) throw; + + var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false); + req.EnsureSuccessStatusCode(); + localTimeoutCts.Dispose(); + composite.Dispose(); + localTimeoutCts = new(); + localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); + } + } + + localTimeoutCts.Dispose(); + composite.Dispose(); + + Logger.LogDebug("Download {requestId} ready", requestId); + } + catch (TaskCanceledException) + { + try + { + await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false); + alreadyCancelled = true; + } + catch + { + // ignore whatever happens here + } + + throw; + } + finally + { + if (downloadCt.IsCancellationRequested && !alreadyCancelled) + { + try + { + await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false); + } + catch + { + // ignore whatever happens here + } + } + _downloadReady.Remove(requestId, out _); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs new file mode 100644 index 0000000..e169267 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs @@ -0,0 +1,109 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace MareSynchronos.WebAPI.Files; + +public class FileTransferOrchestrator : DisposableMediatorSubscriberBase +{ + private readonly HttpClient _httpClient; + private readonly MareConfigService _mareConfig; + private readonly object _semaphoreModificationLock = new(); + private readonly ServerConfigurationManager _serverManager; + private int _availableDownloadSlots; + private SemaphoreSlim _downloadSemaphore; + + public FileTransferOrchestrator(ILogger logger, MareConfigService mareConfig, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) + { + _mareConfig = mareConfig; + _serverManager = serverManager; + _httpClient = new(); + + _availableDownloadSlots = mareConfig.Current.ParallelDownloads; + _downloadSemaphore = new(_availableDownloadSlots); + + Mediator.Subscribe(this, (msg) => + { + FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress; + }); + + Mediator.Subscribe(this, (msg) => + { + FilesCdnUri = null; + }); + } + + public Uri? FilesCdnUri { private set; get; } + public List ForbiddenTransfers { get; } = new(); + public bool IsInitialized => FilesCdnUri != null; + + public void ReleaseDownloadSlot() + { + _downloadSemaphore.Release(); + } + + public async Task SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null) + { + using var requestMessage = new HttpRequestMessage(method, uri); + return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); + } + + public async Task SendRequestAsync(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class + { + using var requestMessage = new HttpRequestMessage(method, uri); + requestMessage.Content = JsonContent.Create(content); + return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); + } + + public async Task SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct) + { + using var requestMessage = new HttpRequestMessage(method, uri); + requestMessage.Content = content; + return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); + } + + public async Task WaitForDownloadSlotAsync(CancellationToken token) + { + lock (_semaphoreModificationLock) + { + if (_availableDownloadSlots != _mareConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount) + { + _availableDownloadSlots = _mareConfig.Current.ParallelDownloads; + _downloadSemaphore = new(_availableDownloadSlots); + } + } + + await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false); + } + + private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null) + { + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverManager.GetToken()); + + if (requestMessage.Content != null && requestMessage.Content is not StreamContent) + { + var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false); + Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content); + } + else + { + Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri); + } + + try + { + if (ct != null) + return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false); + return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogCritical(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri); + throw; + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs new file mode 100644 index 0000000..dc4b1b1 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -0,0 +1,226 @@ +using LZ4; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Files; +using MareSynchronos.API.Routes; +using MareSynchronos.FileCache; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace MareSynchronos.WebAPI.Files; + +public sealed class FileUploadManager : DisposableMediatorSubscriberBase +{ + private readonly FileCacheManager _fileDbManager; + private readonly FileTransferOrchestrator _orchestrator; + private readonly ServerConfigurationManager _serverManager; + private readonly Dictionary _verifiedUploadedHashes = new(StringComparer.Ordinal); + private CancellationTokenSource? _uploadCancellationTokenSource = new(); + + public FileUploadManager(ILogger logger, MareMediator mediator, + FileTransferOrchestrator orchestrator, + FileCacheManager fileDbManager, + ServerConfigurationManager serverManager) : base(logger, mediator) + { + _orchestrator = orchestrator; + _fileDbManager = fileDbManager; + _serverManager = serverManager; + + Mediator.Subscribe(this, (msg) => + { + Reset(); + }); + } + + public List CurrentUploads { get; } = new(); + public bool IsUploading => CurrentUploads.Count > 0; + + public bool CancelUpload() + { + if (CurrentUploads.Any()) + { + Logger.LogDebug("Cancelling current upload"); + _uploadCancellationTokenSource?.Cancel(); + _uploadCancellationTokenSource?.Dispose(); + _uploadCancellationTokenSource = null; + CurrentUploads.Clear(); + return true; + } + + return false; + } + + public async Task DeleteAllFiles() + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + + await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false); + } + + public async Task UploadFiles(CharacterData data, List visiblePlayers) + { + CancelUpload(); + + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentApiUrl); + + HashSet unverifiedUploads = GetUnverifiedFiles(data); + if (unverifiedUploads.Any()) + { + await UploadUnverifiedFiles(unverifiedUploads, visiblePlayers, uploadToken).ConfigureAwait(false); + Logger.LogInformation("Upload complete for {hash}", data.DataHash.Value); + } + + foreach (var kvp in data.FileReplacements) + { + data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase))); + } + + return data; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Reset(); + } + + private async Task> FilesSend(List hashes, List uids, CancellationToken ct) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + FilesSendDto filesSendDto = new() + { + FileHashes = hashes, + UIDs = uids + }; + var response = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesFilesSendFullPath(_orchestrator.FilesCdnUri!), filesSendDto, ct).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? new List(); + } + + private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, + (int)new FileInfo(fileCache).Length)); + } + + private HashSet GetUnverifiedFiles(CharacterData data) + { + HashSet unverifiedUploadHashes = new(StringComparer.Ordinal); + foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList()) + { + if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime)) + { + verifiedTime = DateTime.MinValue; + } + + if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10))) + { + Logger.LogTrace("Verifying {item}, last verified: {date}", item, verifiedTime); + unverifiedUploadHashes.Add(item); + } + } + + return unverifiedUploadHashes; + } + + private void Reset() + { + _uploadCancellationTokenSource?.Cancel(); + _uploadCancellationTokenSource?.Dispose(); + _uploadCancellationTokenSource = null; + CurrentUploads.Clear(); + _verifiedUploadedHashes.Clear(); + } + + private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + + Logger.LogInformation("Uploading {file}, {size}", fileHash, UiSharedService.ByteToString(compressedFile.Length)); + + if (uploadToken.IsCancellationRequested) return; + + using var ms = new MemoryStream(compressedFile); + + Progress prog = new((prog) => + { + CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded; + }); + var streamContent = new ProgressableStreamContent(ms, prog); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + + var response = await _orchestrator.SendRequestStreamAsync(HttpMethod.Post, MareFiles.ServerFilesUploadFullPath(_orchestrator.FilesCdnUri!, fileHash), streamContent, uploadToken).ConfigureAwait(false); + Logger.LogDebug("Upload Status: {status}", response.StatusCode); + } + + private async Task UploadUnverifiedFiles(HashSet unverifiedUploadHashes, List visiblePlayers, CancellationToken uploadToken) + { + unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal); + + Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count); + var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList(), visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false); + + foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) + { + try + { + CurrentUploads.Add(new UploadFileTransfer(file) + { + Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length, + }); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Tried to request file {hash} but file was not present", file.Hash); + } + } + + foreach (var file in filesToUpload.Where(c => c.IsForbidden)) + { + if (_orchestrator.ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) + { + _orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file) + { + LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty, + }); + } + + _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; + } + + var totalSize = CurrentUploads.Sum(c => c.Total); + Logger.LogDebug("Compressing and uploading files"); + Task uploadTask = Task.CompletedTask; + foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) + { + Logger.LogDebug("Compressing {file}", file); + var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false); + CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length; + await uploadTask.ConfigureAwait(false); + uploadTask = UploadFile(data.Item2, file.Hash, uploadToken); + uploadToken.ThrowIfCancellationRequested(); + _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; + } + + if (CurrentUploads.Any()) + { + await uploadTask.ConfigureAwait(false); + + var compressedSize = CurrentUploads.Sum(c => c.Total); + Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize)); + } + + foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal)))) + { + _verifiedUploadedHashes[file] = DateTime.UtcNow; + } + + CurrentUploads.Clear(); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Utils/DownloadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs similarity index 80% rename from MareSynchronos/WebAPI/Utils/DownloadFileTransfer.cs rename to MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs index 0c64b3a..55e6c42 100644 --- a/MareSynchronos/WebAPI/Utils/DownloadFileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs @@ -1,6 +1,6 @@ using MareSynchronos.API.Dto.Files; -namespace MareSynchronos.WebAPI.Utils; +namespace MareSynchronos.WebAPI.Files.Models; public class DownloadFileTransfer : FileTransfer { @@ -9,7 +9,10 @@ public class DownloadFileTransfer : FileTransfer public Uri DownloadUri => new(Dto.Url); public override long Total { - set { } + set + { + // nothing to set + } get => Dto.Size; } diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs new file mode 100644 index 0000000..2de1432 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public enum DownloadStatus +{ + Initializing, + WaitingForSlot, + WaitingForQueue, + Downloading, + Decompressing +} diff --git a/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs new file mode 100644 index 0000000..d018192 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public class FileDownloadStatus +{ + public DownloadStatus DownloadStatus { get; set; } + public int TotalFiles { get; set; } + public int TransferredFiles { get; set; } + public long TotalBytes { get; set; } + public long TransferredBytes { get; set; } +} diff --git a/MareSynchronos/WebAPI/Utils/FileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs similarity index 94% rename from MareSynchronos/WebAPI/Utils/FileTransfer.cs rename to MareSynchronos/WebAPI/Files/Models/FileTransfer.cs index a4861c6..a778022 100644 --- a/MareSynchronos/WebAPI/Utils/FileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs @@ -1,6 +1,6 @@ using MareSynchronos.API.Dto.Files; -namespace MareSynchronos.WebAPI.Utils; +namespace MareSynchronos.WebAPI.Files.Models; public abstract class FileTransfer { diff --git a/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs b/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs new file mode 100644 index 0000000..10ffbd7 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs @@ -0,0 +1,93 @@ +using System.Net; + +namespace MareSynchronos.WebAPI.Files.Models; + +public class ProgressableStreamContent : StreamContent +{ + private const int _defaultBufferSize = 4096; + private readonly int _bufferSize; + private readonly IProgress _progress; + private readonly Stream _streamToWrite; + private bool _contentConsumed; + + public ProgressableStreamContent(Stream streamToWrite, IProgress downloader) + : this(streamToWrite, _defaultBufferSize, downloader) + { + } + + public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress progress) + : base(streamToWrite, bufferSize) + { + if (streamToWrite == null) + { + throw new ArgumentNullException(nameof(streamToWrite)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + _streamToWrite = streamToWrite; + _bufferSize = bufferSize; + _progress = progress; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _streamToWrite.Dispose(); + } + + base.Dispose(disposing); + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + PrepareContent(); + + var buffer = new byte[_bufferSize]; + var size = _streamToWrite.Length; + var uploaded = 0; + + using (_streamToWrite) + { + while (true) + { + var length = _streamToWrite.Read(buffer, 0, buffer.Length); + if (length <= 0) + { + break; + } + + uploaded += length; + _progress.Report(new UploadProgress(uploaded, size)); + await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false); + } + } + } + + protected override bool TryComputeLength(out long length) + { + length = _streamToWrite.Length; + return true; + } + + private void PrepareContent() + { + if (_contentConsumed) + { + if (_streamToWrite.CanSeek) + { + _streamToWrite.Position = 0; + } + else + { + throw new InvalidOperationException("The stream has already been read."); + } + } + + _contentConsumed = true; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Utils/UploadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs similarity index 84% rename from MareSynchronos/WebAPI/Utils/UploadFileTransfer.cs rename to MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs index 75dcb5a..47a3838 100644 --- a/MareSynchronos/WebAPI/Utils/UploadFileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs @@ -1,6 +1,6 @@ using MareSynchronos.API.Dto.Files; -namespace MareSynchronos.WebAPI.Utils; +namespace MareSynchronos.WebAPI.Files.Models; public class UploadFileTransfer : FileTransfer { diff --git a/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs new file mode 100644 index 0000000..6014c5a --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs @@ -0,0 +1,3 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public record UploadProgress(long Uploaded, long Size); diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs new file mode 100644 index 0000000..0a77cd1 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -0,0 +1,90 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.User; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace MareSynchronos.WebAPI; + +public partial class ApiController +{ + public async Task PushCharacterData(CharacterData data, List visibleCharacters) + { + if (!IsConnected) return; + + try + { + await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogDebug("Upload operation was cancelled"); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during upload of files"); + } + } + + public async Task UserAddPair(UserDto user) + { + if (!IsConnected) return; + await _mareHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); + } + + public async Task UserDelete() + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false); + await CreateConnections().ConfigureAwait(false); + } + + public async Task> UserGetOnlinePairs() + { + return await _mareHub!.InvokeAsync>(nameof(UserGetOnlinePairs)).ConfigureAwait(false); + } + + public async Task> UserGetPairedClients() + { + return await _mareHub!.InvokeAsync>(nameof(UserGetPairedClients)).ConfigureAwait(false); + } + + public async Task UserPushData(UserCharaDataMessageDto dto) + { + try + { + await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to Push character data"); + } + } + + public async Task UserRemovePair(UserDto userDto) + { + if (!IsConnected) return; + await _mareHub!.SendAsync(nameof(UserRemovePair), userDto).ConfigureAwait(false); + } + + public async Task UserSetPairPermissions(UserPermissionsDto userPermissions) + { + await _mareHub!.SendAsync(nameof(UserSetPairPermissions), userPermissions).ConfigureAwait(false); + } + + private async Task PushCharacterDataInternal(CharacterData character, List visibleCharacters) + { + Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); + StringBuilder sb = new(); + foreach (var kvp in character.FileReplacements.ToList()) + { + sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}"); + } + foreach (var item in character.GlamourerData) + { + sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); + } + Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString()); + await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs similarity index 67% rename from MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs rename to MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index fc6abcb..09f2360 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -3,7 +3,7 @@ using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Dto; using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.User; -using MareSynchronos.Mediator; +using MareSynchronos.Services.Mediator; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; @@ -11,255 +11,89 @@ namespace MareSynchronos.WebAPI; public partial class ApiController { - private void ExecuteSafely(Action act) + public Task Client_DownloadReady(Guid requestId) { - try - { - act(); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error on executing safely"); - } - } - - public void OnUpdateSystemInfo(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_UpdateSystemInfo), act); - } - - public void OnUserReceiveCharacterData(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserReceiveCharacterData), act); - } - - public void OnReceiveServerMessage(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_ReceiveServerMessage), act); - } - - public void OnDownloadReady(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_DownloadReady), act); - } - - public void OnGroupSendFullInfo(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupSendFullInfo), act); - } - - public Task Client_GroupSendFullInfo(GroupFullInfoDto dto) - { - _logger.LogTrace("Client_GroupSendFullInfo: " + dto); - ExecuteSafely(() => _pairManager.AddGroup(dto)); + Logger.LogDebug("Server sent {requestId} ready", requestId); + Mediator.Publish(new DownloadReadyMessage(requestId)); return Task.CompletedTask; } - public void OnGroupSendInfo(Action act) + public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupSendInfo), act); - } - - public Task Client_GroupSendInfo(GroupInfoDto dto) - { - _logger.LogTrace("Client_GroupSendInfo: " + dto); - ExecuteSafely(() => _pairManager.SetGroupInfo(dto)); + Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission); + ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission)); return Task.CompletedTask; } - public void OnGroupDelete(Action act) + public Task Client_GroupDelete(GroupDto groupDto) { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupDelete), act); - } - - public Task Client_GroupDelete(GroupDto dto) - { - _logger.LogTrace("Client_GroupDelete: " + dto); - ExecuteSafely(() => _pairManager.RemoveGroup(dto.Group)); + Logger.LogTrace("Client_GroupDelete: {dto}", groupDto); + ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group)); return Task.CompletedTask; } - public void OnGroupPairJoined(Action act) + public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto) { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupPairJoined), act); - } - - public Task Client_GroupPairJoined(GroupPairFullInfoDto dto) - { - _logger.LogTrace("Client_GroupPairJoined: " + dto); - ExecuteSafely(() => _pairManager.AddGroupPair(dto)); - return Task.CompletedTask; - } - - public void OnGroupPairLeft(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupPairLeft), act); - } - - public Task Client_GroupPairLeft(GroupPairDto dto) - { - _logger.LogTrace("Client_GroupPairLeft: " + dto); - ExecuteSafely(() => _pairManager.RemoveGroupPair(dto)); - return Task.CompletedTask; - } - - public void OnGroupChangePermissions(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupChangePermissions), act); - } - - public Task Client_GroupChangePermissions(GroupPermissionDto dto) - { - _logger.LogTrace("Client_GroupChangePermissions: " + dto); - ExecuteSafely(() => _pairManager.SetGroupPermissions(dto)); - return Task.CompletedTask; - } - - public void OnGroupPairChangePermissions(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupPairChangePermissions), act); - } - - public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto dto) - { - _logger.LogTrace("Client_GroupPairChangePermissions: " + dto); + Logger.LogTrace("Client_GroupPairChangePermissions: {perm}", permissionDto); ExecuteSafely(() => { - if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(dto); - else _pairManager.SetGroupPairUserPermissions(dto); + if (string.Equals(permissionDto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(permissionDto); + else _pairManager.SetGroupPairUserPermissions(permissionDto); }); return Task.CompletedTask; } - public void OnGroupPairChangeUserInfo(Action act) + public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo) { - if (_initialized) return; - _mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act); - } - - public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) - { - _logger.LogTrace("Client_GroupPairChangeUserInfo: " + dto); + Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo); ExecuteSafely(() => { - if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(dto); - else _pairManager.SetGroupPairStatusInfo(dto); + if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo); + else _pairManager.SetGroupPairStatusInfo(userInfo); }); return Task.CompletedTask; } - public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dto) + public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) { - _logger.LogTrace("Client_UserReceiveCharacterData: " + dto.User); - ExecuteSafely(() => _pairManager.ReceiveCharaData(dto)); + Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto); + ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto)); return Task.CompletedTask; } - public void OnUserAddClientPair(Action act) + public Task Client_GroupPairLeft(GroupPairDto groupPairDto) { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserAddClientPair), act); - } - - public Task Client_UserAddClientPair(UserPairDto dto) - { - _logger.LogDebug($"Client_UserAddClientPair: " + dto); - ExecuteSafely(() => _pairManager.AddUserPair(dto)); + Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto); + ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto)); return Task.CompletedTask; } - public void OnUserRemoveClientPair(Action act) + public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserRemoveClientPair), act); - } - - public Task Client_UserRemoveClientPair(UserDto dto) - { - _logger.LogDebug($"Client_UserRemoveClientPair: " + dto); - ExecuteSafely(() => _pairManager.RemoveUserPair(dto)); + Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo); + ExecuteSafely(() => _pairManager.AddGroup(groupInfo)); return Task.CompletedTask; } - public void OnUserSendOffline(Action act) + public Task Client_GroupSendInfo(GroupInfoDto groupInfo) { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserSendOffline), act); - } - - public Task Client_UserSendOffline(UserDto dto) - { - _logger.LogDebug($"Client_UserSendOffline: {dto}"); - ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User)); + Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo); + ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo)); return Task.CompletedTask; } - public void OnUserSendOnline(Action act) + public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserSendOnline), act); - } - - public Task Client_UserSendOnline(OnlineUserIdentDto dto) - { - _logger.LogDebug($"Client_UserSendOnline: {dto}"); - ExecuteSafely(() => _pairManager.MarkPairOnline(dto, this)); - return Task.CompletedTask; - } - - public void OnUserUpdateOtherPairPermissions(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act); - } - - public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) - { - _logger.LogDebug($"Client_UserUpdateOtherPairPermissions: {dto}"); - ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto)); - return Task.CompletedTask; - } - - public void OnUserUpdateSelfPairPermissions(Action act) - { - if (_initialized) return; - _mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act); - } - - public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) - { - _logger.LogDebug($"Client_UserUpdateSelfPairPermissions: {dto}"); - ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto)); - return Task.CompletedTask; - } - - public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) - { - SystemInfoDto = systemInfo; - return Task.CompletedTask; - } - - public Task Client_ReceiveServerMessage(MessageSeverity severity, string message) - { - switch (severity) + switch (messageSeverity) { case MessageSeverity.Error: Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Error, 7500)); break; + case MessageSeverity.Warning: Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Warning, 7500)); break; + case MessageSeverity.Information: if (_doNotNotifyOnNextInfo) { @@ -273,10 +107,191 @@ public partial class ApiController return Task.CompletedTask; } - public Task Client_DownloadReady(Guid requestId) + public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) { - _logger.LogDebug($"Server sent {requestId} ready"); - _downloadReady[requestId] = true; + SystemInfoDto = systemInfo; return Task.CompletedTask; } -} + + public Task Client_UserAddClientPair(UserPairDto dto) + { + Logger.LogDebug("Client_UserAddClientPair: {dto}", dto); + ExecuteSafely(() => _pairManager.AddUserPair(dto)); + return Task.CompletedTask; + } + + public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) + { + Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); + ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto)); + return Task.CompletedTask; + } + + public Task Client_UserReceiveUploadStatus(UserDto dto) + { + Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto); + ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto)); + return Task.CompletedTask; + } + + public Task Client_UserRemoveClientPair(UserDto dto) + { + Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto); + ExecuteSafely(() => _pairManager.RemoveUserPair(dto)); + return Task.CompletedTask; + } + + public Task Client_UserSendOffline(UserDto dto) + { + Logger.LogDebug("Client_UserSendOffline: {dto}", dto); + ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User)); + return Task.CompletedTask; + } + + public Task Client_UserSendOnline(OnlineUserIdentDto dto) + { + Logger.LogDebug("Client_UserSendOnline: {dto}", dto); + ExecuteSafely(() => _pairManager.MarkPairOnline(dto)); + return Task.CompletedTask; + } + + public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) + { + Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto)); + return Task.CompletedTask; + } + + public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) + { + Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto)); + return Task.CompletedTask; + } + + public void OnDownloadReady(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_DownloadReady), act); + } + + public void OnGroupChangePermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupChangePermissions), act); + } + + public void OnGroupDelete(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupDelete), act); + } + + public void OnGroupPairChangePermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairChangePermissions), act); + } + + public void OnGroupPairChangeUserInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act); + } + + public void OnGroupPairJoined(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairJoined), act); + } + + public void OnGroupPairLeft(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairLeft), act); + } + + public void OnGroupSendFullInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupSendFullInfo), act); + } + + public void OnGroupSendInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupSendInfo), act); + } + + public void OnReceiveServerMessage(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_ReceiveServerMessage), act); + } + + public void OnUpdateSystemInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UpdateSystemInfo), act); + } + + public void OnUserAddClientPair(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserAddClientPair), act); + } + + public void OnUserReceiveCharacterData(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserReceiveCharacterData), act); + } + + public void OnUserReceiveUploadStatus(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserReceiveUploadStatus), act); + } + + public void OnUserRemoveClientPair(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserRemoveClientPair), act); + } + + public void OnUserSendOffline(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserSendOffline), act); + } + + public void OnUserSendOnline(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserSendOnline), act); + } + + public void OnUserUpdateOtherPairPermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act); + } + + public void OnUserUpdateSelfPairPermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act); + } + + private void ExecuteSafely(Action act) + { + try + { + act(); + } + catch (Exception ex) + { + Logger.LogCritical(ex, "Error on executing safely"); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs similarity index 92% rename from MareSynchronos/WebAPI/ApiController.Functions.Groups.cs rename to MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs index d10e17c..ec796b5 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -1,56 +1,11 @@ using MareSynchronos.API.Dto.Group; +using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.AspNetCore.SignalR.Client; namespace MareSynchronos.WebAPI; + public partial class ApiController { - private void CheckConnection() - { - if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new System.Exception("Not connected"); - } - - public async Task> GroupGetBannedUsers(GroupDto group) - { - CheckConnection(); - return await _mareHub!.InvokeAsync>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false); - } - - public async Task GroupClear(GroupDto group) - { - CheckConnection(); - await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); - } - - public async Task GroupChangeOwnership(GroupPairDto groupPair) - { - CheckConnection(); - await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false); - } - - public async Task GroupChangePassword(GroupPasswordDto groupPassword) - { - CheckConnection(); - return await _mareHub!.InvokeAsync(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false); - } - - public async Task GroupCreate() - { - CheckConnection(); - return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); - } - - public async Task> GroupsGetAll() - { - CheckConnection(); - return await _mareHub!.InvokeAsync>(nameof(GroupsGetAll)).ConfigureAwait(false); - } - - public async Task> GroupsGetUsersInGroup(GroupDto group) - { - CheckConnection(); - return await _mareHub!.InvokeAsync>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false); - } - public async Task GroupBanUser(GroupPairDto dto, string reason) { CheckConnection(); @@ -69,12 +24,48 @@ public partial class ApiController await _mareHub!.SendAsync(nameof(GroupChangeIndividualPermissionState), dto).ConfigureAwait(false); } + public async Task GroupChangeOwnership(GroupPairDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false); + } + + public async Task GroupChangePassword(GroupPasswordDto groupPassword) + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false); + } + + public async Task GroupClear(GroupDto group) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); + } + + public async Task GroupCreate() + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); + } + + public async Task> GroupCreateTempInvite(GroupDto group, int amount) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false); + } + public async Task GroupDelete(GroupDto group) { CheckConnection(); await _mareHub!.SendAsync(nameof(GroupDelete), group).ConfigureAwait(false); } + public async Task> GroupGetBannedUsers(GroupDto group) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false); + } + public async Task GroupJoin(GroupPasswordDto passwordedGroup) { CheckConnection(); @@ -93,21 +84,32 @@ public partial class ApiController await _mareHub!.SendAsync(nameof(GroupRemoveUser), groupPair).ConfigureAwait(false); } + public async Task GroupSetUserInfo(GroupPairUserInfoDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupSetUserInfo), groupPair).ConfigureAwait(false); + } + + public async Task> GroupsGetAll() + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupsGetAll)).ConfigureAwait(false); + } + + public async Task> GroupsGetUsersInGroup(GroupDto group) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false); + } + public async Task GroupUnbanUser(GroupPairDto groupPair) { CheckConnection(); await _mareHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false); } - public async Task GroupSetUserInfo(GroupPairUserInfoDto userInfo) + private void CheckConnection() { - CheckConnection(); - await _mareHub!.SendAsync(nameof(GroupSetUserInfo), userInfo).ConfigureAwait(false); - } - - public async Task> GroupCreateTempInvite(GroupDto group, int amount) - { - CheckConnection(); - return await _mareHub!.InvokeAsync>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false); + if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected"); } } \ No newline at end of file diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs similarity index 73% rename from MareSynchronos/WebAPI/ApiController.cs rename to MareSynchronos/WebAPI/SignalR/ApiController.cs index 5f97fc6..80d7408 100644 --- a/MareSynchronos/WebAPI/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -1,68 +1,53 @@ -using Dalamud.Interface.Internal.Notifications; -using System.Collections.Concurrent; -using MareSynchronos.API.Routes; -using MareSynchronos.FileCache; +using MareSynchronos.API.Routes; using MareSynchronos.Utils; -using MareSynchronos.WebAPI.Utils; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using MareSynchronos.API.Dto; using MareSynchronos.API.SignalR; -using MareSynchronos.Managers; using Dalamud.Utility; -using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Factories; +using System.Reflection; +using MareSynchronos.WebAPI.SignalR.Utils; +using MareSynchronos.WebAPI.SignalR; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Services; namespace MareSynchronos.WebAPI; -public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareHubClient + +public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient { public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; public const string MainServiceUri = "wss://maresynchronos.com"; + private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; - private readonly MareConfigService _configService; - private readonly DalamudUtil _dalamudUtil; - private readonly FileCacheManager _fileDbManager; private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; private CancellationTokenSource _connectionCancellationTokenSource; - private HubConnection? _mareHub; - - private CancellationTokenSource? _uploadCancellationTokenSource = new(); - private CancellationTokenSource? _healthCheckTokenSource = new(); - private bool _doNotNotifyOnNextInfo = false; - private ConnectionDto? _connectionDto; - public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo(); - public string AuthFailureMessage { get; private set; } = string.Empty; + private bool _doNotNotifyOnNextInfo = false; + private CancellationTokenSource? _healthCheckTokenSource = new(); + private bool _initialized; + private HubConnection? _mareHub; + private ServerState _serverState; - public SystemInfoDto SystemInfoDto { get; private set; } = new(); - - private HttpClient _httpClient; - - public ApiController(ILogger logger, HubFactory hubFactory, MareConfigService configService, DalamudUtil dalamudUtil, FileCacheManager fileDbManager, + public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) { - _logger.LogTrace("Creating " + nameof(ApiController)); - _hubFactory = hubFactory; - _configService = configService; _dalamudUtil = dalamudUtil; - _fileDbManager = fileDbManager; _pairManager = pairManager; _serverManager = serverManager; _connectionCancellationTokenSource = new CancellationTokenSource(); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); - Mediator.Subscribe(this, (msg) => MareHubOnClosed(((HubClosedMessage)msg).Exception)); - Mediator.Subscribe(this, (msg) => MareHubOnReconnected(((HubReconnectedMessage)msg).Arg)); - Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(((HubReconnectingMessage)msg).Exception)); + Mediator.Subscribe(this, (msg) => MareHubOnClosed(msg.Exception)); + Mediator.Subscribe(this, (msg) => _ = Task.Run(MareHubOnReconnected)); + Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); ServerState = ServerState.Offline; - _verifiedUploadedHashes = new(StringComparer.Ordinal); - _httpClient = new(); if (_dalamudUtil.IsLoggedIn) { @@ -70,56 +55,40 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH } } - private void DalamudUtilOnLogOut() - { - Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); - ServerState = ServerState.Offline; - } - - private void DalamudUtilOnLogIn() - { - Task.Run(() => CreateConnections(forceGetToken: true)); - } - - public ConcurrentDictionary> CurrentDownloads { get; } = new(); - - public List CurrentUploads { get; } = new(); - - public List ForbiddenTransfers { get; } = new(); - - public bool IsConnected => ServerState == ServerState.Connected; - public bool IsDownloading => !CurrentDownloads.IsEmpty; - public bool IsUploading => CurrentUploads.Count > 0; - - public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; - - public string UID => _connectionDto?.User.UID ?? string.Empty; + public string AuthFailureMessage { get; private set; } = string.Empty; + public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0); public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty; + public bool IsConnected => ServerState == ServerState.Connected; + public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0)); public int OnlineUsers => SystemInfoDto.OnlineUsers; - - private ServerState _serverState; - private bool _initialized; + public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; + public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo(); public ServerState ServerState { get => _serverState; private set { - _logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState); + Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState); _serverState = value; } } + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + public string UID => _connectionDto?.User.UID ?? string.Empty; + + public async Task CheckClientHealth() + { + return await _mareHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); + } + public async Task CreateConnections(bool forceGetToken = false) { - _logger.LogDebug("CreateConnections called"); - - _httpClient?.Dispose(); - _httpClient = new(); + Logger.LogDebug("CreateConnections called"); if (_serverManager.CurrentServer?.FullPause ?? true) { - _logger.LogInformation("Not recreating Connection, paused"); + Logger.LogInformation("Not recreating Connection, paused"); _connectionDto = null; await StopConnection(ServerState.Disconnected).ConfigureAwait(false); _connectionCancellationTokenSource.Cancel(); @@ -129,7 +98,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH var secretKey = _serverManager.GetSecretKey(); if (secretKey.IsNullOrEmpty()) { - _logger.LogWarning("No secret key set for current character"); + Logger.LogWarning("No secret key set for current character"); _connectionDto = null; await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false); _connectionCancellationTokenSource.Cancel(); @@ -138,12 +107,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH await StopConnection(ServerState.Disconnected).ConfigureAwait(false); - _logger.LogInformation("Recreating Connection"); + Logger.LogInformation("Recreating Connection"); _connectionCancellationTokenSource.Cancel(); _connectionCancellationTokenSource = new CancellationTokenSource(); var token = _connectionCancellationTokenSource.Token; - _verifiedUploadedHashes.Clear(); while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) { AuthFailureMessage = string.Empty; @@ -153,11 +121,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH try { - _logger.LogDebug("Building connection"); + Logger.LogDebug("Building connection"); if (_serverManager.GetToken() == null || forceGetToken) { - _logger.LogDebug("Requesting new JWT"); + Logger.LogDebug("Requesting new JWT"); using HttpClient httpClient = new(); var postUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) @@ -171,12 +139,12 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH AuthFailureMessage = await result.Content.ReadAsStringAsync().ConfigureAwait(false); result.EnsureSuccessStatusCode(); _serverManager.SaveToken(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); - _logger.LogDebug("JWT Success"); + Logger.LogDebug("JWT Success"); } while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) { - _logger.LogDebug("Player not loaded in yet, waiting"); + Logger.LogDebug("Player not loaded in yet, waiting"); await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); } @@ -200,7 +168,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH } catch (HttpRequestException ex) { - _logger.LogWarning(ex, "HttpRequestException on Connection"); + Logger.LogWarning(ex, "HttpRequestException on Connection"); if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { @@ -209,34 +177,61 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH } ServerState = ServerState.Reconnecting; - _logger.LogInformation("Failed to establish connection, retrying"); + Logger.LogInformation("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); } catch (Exception ex) { - _logger.LogWarning(ex, "Exception on Connection"); + Logger.LogWarning(ex, "Exception on Connection"); - _logger.LogInformation("Failed to establish connection, retrying"); + Logger.LogInformation("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); } } } + public async Task GetConnectionDto() + { + var dto = await _mareHub!.InvokeAsync(nameof(GetConnectionDto)).ConfigureAwait(false); + Mediator.Publish(new ConnectedMessage(dto)); + return dto; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _healthCheckTokenSource?.Cancel(); + Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + _connectionCancellationTokenSource?.Cancel(); + } + private async Task ClientHealthCheck(CancellationToken ct) { while (!ct.IsCancellationRequested && _mareHub != null) { await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); _ = await CheckClientHealth().ConfigureAwait(false); - _logger.LogDebug("Checked Client Health State"); + Logger.LogDebug("Checked Client Health State"); } } + private void DalamudUtilOnLogIn() + { + Task.Run(() => CreateConnections(forceGetToken: true)); + } + + private void DalamudUtilOnLogOut() + { + Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + ServerState = ServerState.Offline; + } + private async Task InitializeData() { if (_mareHub == null) return; - _logger.LogDebug("Initializing data"); + Logger.LogDebug("Initializing data"); OnDownloadReady((guid) => Client_DownloadReady(guid)); OnReceiveServerMessage((sev, msg) => Client_ReceiveServerMessage(sev, msg)); OnUpdateSystemInfo((dto) => Client_UpdateSystemInfo(dto)); @@ -248,6 +243,7 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH OnUserSendOnline(dto => Client_UserSendOnline(dto)); OnUserUpdateOtherPairPermissions(dto => Client_UserUpdateOtherPairPermissions(dto)); OnUserUpdateSelfPairPermissions(dto => Client_UserUpdateSelfPairPermissions(dto)); + OnUserReceiveUploadStatus(dto => Client_UserReceiveUploadStatus(dto)); OnGroupChangePermissions((dto) => Client_GroupChangePermissions(dto)); OnGroupDelete((dto) => Client_GroupDelete(dto)); @@ -260,12 +256,12 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) { - _logger.LogDebug("Individual Pair: {userPair}", userPair); + Logger.LogDebug("Individual Pair: {userPair}", userPair); _pairManager.AddUserPair(userPair, addToLastAddedUser: false); } foreach (var entry in await GroupsGetAll().ConfigureAwait(false)) { - _logger.LogDebug("Group: {entry}", entry); + Logger.LogDebug("Group: {entry}", entry); _pairManager.AddGroup(entry); } foreach (var group in _pairManager.GroupPairs.Keys) @@ -273,14 +269,14 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false); foreach (var user in users) { - _logger.LogDebug("Group Pair: {user}", user); + Logger.LogDebug("Group Pair: {user}", user); _pairManager.AddGroupPair(user); } } foreach (var entry in await UserGetOnlinePairs().ConfigureAwait(false)) { - _pairManager.MarkPairOnline(entry, this, sendNotif: false); + _pairManager.MarkPairOnline(entry, sendNotif: false); } _healthCheckTokenSource?.Cancel(); @@ -289,47 +285,25 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH _ = ClientHealthCheck(_healthCheckTokenSource.Token); _initialized = true; - Mediator.Publish(new ConnectedMessage()); - } - - public override void Dispose() - { - base.Dispose(); - _healthCheckTokenSource?.Cancel(); - _uploadCancellationTokenSource?.Cancel(); - Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); - _connectionCancellationTokenSource?.Cancel(); } private void MareHubOnClosed(Exception? arg) { - CurrentUploads.Clear(); - CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); _healthCheckTokenSource?.Cancel(); Mediator.Publish(new DisconnectedMessage()); _pairManager.ClearPairs(); ServerState = ServerState.Offline; if (arg != null) { - _logger.LogWarning(arg, "Connection closed"); + Logger.LogWarning(arg, "Connection closed"); } else { - _logger.LogInformation("Connection closed"); + Logger.LogInformation("Connection closed"); } } - private void MareHubOnReconnecting(Exception? arg) - { - _doNotNotifyOnNextInfo = true; - _healthCheckTokenSource?.Cancel(); - ServerState = ServerState.Reconnecting; - Mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to " + _serverManager.CurrentServer!.ServerName, NotificationType.Warning, 5000)); - _logger.LogWarning(arg, "Connection closed... Reconnecting"); - } - - private async void MareHubOnReconnected(string? arg) + private async Task MareHubOnReconnected() { ServerState = ServerState.Connecting; try @@ -345,10 +319,17 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH } catch (Exception ex) { - _logger.LogCritical(ex, "Failure to obtain data after reconnection"); + Logger.LogCritical(ex, "Failure to obtain data after reconnection"); await StopConnection(ServerState.Disconnected).ConfigureAwait(false); } + } + private void MareHubOnReconnecting(Exception? arg) + { + _doNotNotifyOnNextInfo = true; + _healthCheckTokenSource?.Cancel(); + ServerState = ServerState.Reconnecting; + Logger.LogWarning(arg, "Connection closed... Reconnecting"); } private async Task StopConnection(ServerState state) @@ -359,24 +340,11 @@ public partial class ApiController : MediatorSubscriberBase, IDisposable, IMareH { _initialized = false; _healthCheckTokenSource?.Cancel(); - _uploadCancellationTokenSource?.Cancel(); - _logger.LogInformation("Stopping existing connection"); + Logger.LogInformation("Stopping existing connection"); await _hubFactory.DisposeHubAsync().ConfigureAwait(false); - CurrentUploads.Clear(); - CurrentDownloads.Clear(); Mediator.Publish(new DisconnectedMessage()); _mareHub = null; _connectionDto = null; } } - - public async Task GetConnectionDto() - { - return await _mareHub!.InvokeAsync(nameof(GetConnectionDto)).ConfigureAwait(false); - } - - public async Task CheckClientHealth() - { - return await _mareHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/Factories/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs similarity index 92% rename from MareSynchronos/Factories/HubFactory.cs rename to MareSynchronos/WebAPI/SignalR/HubFactory.cs index 3bece37..c09bc31 100644 --- a/MareSynchronos/Factories/HubFactory.cs +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -1,9 +1,9 @@ using MareSynchronos.API.SignalR; -using MareSynchronos.Managers; +using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; -using MareSynchronos.Mediator; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI.Utils; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI.SignalR.Utils; using MessagePack; using MessagePack.Resolvers; using Microsoft.AspNetCore.Http.Connections; @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Factories; +namespace MareSynchronos.WebAPI.SignalR; public class HubFactory : MediatorSubscriberBase { @@ -28,7 +28,7 @@ public class HubFactory : MediatorSubscriberBase private HubConnection BuildHubConnection() { - _logger.LogDebug("Building new HubConnection"); + Logger.LogDebug("Building new HubConnection"); _instance = new HubConnectionBuilder() .WithUrl(_serverConfigurationManager.CurrentApiUrl + IMareHub.Path, options => @@ -101,7 +101,7 @@ public class HubFactory : MediatorSubscriberBase { if (_instance == null || _isDisposed) return; - _logger.LogDebug("Disposing current HubConnection"); + Logger.LogDebug("Disposing current HubConnection"); _isDisposed = true; diff --git a/MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs b/MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs similarity index 67% rename from MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs rename to MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs index f22fb70..bebadce 100644 --- a/MareSynchronos/WebAPI/Utils/ForeverRetryPolicy.cs +++ b/MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs @@ -1,7 +1,8 @@ -using MareSynchronos.Mediator; +using Dalamud.Interface.Internal.Notifications; +using MareSynchronos.Services.Mediator; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI.Utils; +namespace MareSynchronos.WebAPI.SignalR.Utils; public class ForeverRetryPolicy : IRetryPolicy { @@ -19,14 +20,17 @@ public class ForeverRetryPolicy : IRetryPolicy if (retryContext.PreviousRetryCount == 0) { _sentDisconnected = false; - timeToWait = TimeSpan.FromSeconds(1); + timeToWait = TimeSpan.FromSeconds(3); } - else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(2); - else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(3); + else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(5); + else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(10); else { if (!_sentDisconnected) + { + _mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to server", NotificationType.Warning, 5000)); _mediator.Publish(new DisconnectedMessage()); + } _sentDisconnected = true; } diff --git a/MareSynchronos/WebAPI/ServerState.cs b/MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs similarity index 78% rename from MareSynchronos/WebAPI/ServerState.cs rename to MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs index e162e77..f37f992 100644 --- a/MareSynchronos/WebAPI/ServerState.cs +++ b/MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs @@ -1,4 +1,4 @@ -namespace MareSynchronos.WebAPI; +namespace MareSynchronos.WebAPI.SignalR.Utils; public enum ServerState {