From 14575a4a6b5e87bed792ffff3c84baf21b29161c Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Tue, 17 Oct 2023 21:36:44 +0200 Subject: [PATCH] Mare 0.9 (#65) * add jwt expiry * start of 0.9 api impl * some stuff idk * some more impl * some cleanup * remove grouppair, add configuration, rework some pair drawing stuff * do some stuff * rework some ui * I don't even know anymore * add cancellationtoken * token bla * ui fixes etc * probably individual adding/removing now working fully as expected * add working report popup * I guess it's more syncshell shit or so * popup shit idk * work out most of the syncshell bullshit I guess * delete some old crap * are we actually getting closer to the end * update pair info stuff * more fixes/adjustments, idk * refactor some things * some rework * some more cleanup * cleanup * make menu buttons w i d e * better icon text buttons * add all syncshell folder and ordering fixes --------- Co-authored-by: rootdarkarchon --- MareAPI | 2 +- MareSynchronos/FileCache/FileCacheEntity.cs | 4 +- MareSynchronos/FileCache/FileCacheManager.cs | 86 +- MareSynchronos/FileCache/FileCompactor.cs | 12 +- MareSynchronos/FileCache/FileState.cs | 3 +- .../FileCache/PeriodicFileScanner.cs | 32 +- .../FileCache/TransientResourceManager.cs | 45 +- MareSynchronos/Interop/DalamudLogger.cs | 2 +- .../Interop/DalamudLoggingProvider.cs | 7 +- MareSynchronos/Interop/IpcManager.cs | 227 +++--- MareSynchronos/Interop/Structs.cs | 4 +- MareSynchronos/Localization/Strings.cs | 13 +- .../ConfigurationMigrator.cs | 133 +-- .../ConfigurationServiceBase.cs | 4 +- .../Configurations/MareConfig.cs | 8 +- .../Configurations/Obsolete/Configuration.cs | 124 --- .../Configurations/Obsolete/MareConfigV0.cs | 57 -- .../Configurations/Obsolete/ServerConfigV0.cs | 18 - .../Configurations/ServerTagConfig.cs | 4 +- .../Configurations/TransientConfig.cs | 4 +- .../Configurations/UidNotesConfig.cs | 4 +- .../Models/Authentication.cs | 2 +- .../Models/Obsolete/ServerStorageV0.cs | 22 +- .../MareConfiguration/Models/SecretKey.cs | 4 +- .../Models/ServerNotesStorage.cs | 4 +- .../MareConfiguration/Models/ServerStorage.cs | 10 +- .../Models/ServerTagStorage.cs | 6 +- MareSynchronos/MarePlugin.cs | 4 +- MareSynchronos/MareSynchronos.csproj | 3 +- .../PlayerData/Data/CharacterData.cs | 12 +- .../PlayerData/Data/FileReplacement.cs | 7 +- .../PlayerData/Data/PlayerChanges.cs | 2 +- .../PlayerData/Export/MareCharaFileData.cs | 4 +- .../PlayerData/Export/MareCharaFileManager.cs | 41 +- .../Factories/FileDownloadManagerFactory.cs | 2 +- .../PlayerData/Factories/PairFactory.cs | 13 +- .../PlayerData/Factories/PlayerDataFactory.cs | 42 +- .../PlayerData/Handlers/GameObjectHandler.cs | 54 +- .../PlayerData/Handlers/PairHandler.cs | 52 +- .../PlayerData/Pairs/OnlinePlayerManager.cs | 2 +- .../PlayerData/Pairs/OptionalPluginWarning.cs | 2 +- MareSynchronos/PlayerData/Pairs/Pair.cs | 87 +- .../PlayerData/Pairs/PairManager.cs | 177 ++-- .../Services/CacheCreationService.cs | 19 +- MareSynchronos/Plugin.cs | 25 +- MareSynchronos/Services/CharacterAnalyzer.cs | 26 +- .../Services/CommandManagerService.cs | 9 +- MareSynchronos/Services/DalamudUtilService.cs | 37 +- MareSynchronos/Services/MareProfileData.cs | 7 + MareSynchronos/Services/MareProfileManager.cs | 22 +- .../DisposableMediatorSubscriberBase.cs | 2 +- .../Services/Mediator/MareMediator.cs | 22 +- .../Services/Mediator/MessageBase.cs | 4 +- MareSynchronos/Services/Mediator/Messages.cs | 9 + .../Mediator/WindowMediatorSubscriberBase.cs | 2 +- .../Services/PerformanceCollectorService.cs | 2 +- .../PluginWarningNotificationService.cs | 2 +- .../Services/ServerConfiguration/JwtCache.cs | 3 - .../ServerConfigurationManager.cs | 58 +- MareSynchronos/Services/UiService.cs | 32 +- MareSynchronos/UI/CompactUI.cs | 394 +++++---- .../UI/Components/DrawFolderBase.cs | 116 +++ .../UI/Components/DrawFolderGroup.cs | 236 ++++++ MareSynchronos/UI/Components/DrawFolderTag.cs | 156 ++++ MareSynchronos/UI/Components/DrawGroupPair.cs | 336 -------- .../UI/Components/DrawGroupedGroupFolder.cs | 49 ++ MareSynchronos/UI/Components/DrawPairBase.cs | 84 -- MareSynchronos/UI/Components/DrawUserPair.cs | 539 ++++++++---- MareSynchronos/UI/Components/GroupPanel.cs | 765 ------------------ MareSynchronos/UI/Components/IDrawFolder.cs | 6 + MareSynchronos/UI/Components/PairGroupsUi.cs | 249 ------ .../Components/Popup/BanUserPopupHandler.cs | 46 ++ .../Popup/CreateSyncshellPopupHandler.cs | 100 +++ .../UI/Components/Popup/IPopupHandler.cs | 10 + .../Popup/JoinSyncshellPopupHandler.cs | 163 ++++ .../UI/Components/Popup/PopupHandler.cs | 95 +++ .../UI/Components/Popup/ReportPopupHandler.cs | 57 ++ .../Popup/SyncshellAdminPopupHandler.cs | 234 ++++++ ...airForGroupUi.cs => SelectPairForTagUi.cs} | 11 +- ...roupForPairUi.cs => SelectTagForPairUi.cs} | 11 +- MareSynchronos/UI/DataAnalysisUi.cs | 97 +-- MareSynchronos/UI/DrawEntityFactory.cs | 61 ++ MareSynchronos/UI/DtrEntry.cs | 24 +- MareSynchronos/UI/EditProfileUi.cs | 14 +- MareSynchronos/UI/GposeUi.cs | 4 +- ...dDisplayHandler.cs => IdDisplayHandler.cs} | 150 +++- MareSynchronos/UI/Handlers/TagHandler.cs | 11 +- MareSynchronos/UI/IntroUI.cs | 6 +- MareSynchronos/UI/PopoutProfileUi.cs | 38 +- MareSynchronos/UI/SettingsUi.cs | 165 +++- MareSynchronos/UI/StandaloneProfileUi.cs | 30 +- MareSynchronos/UI/UISharedService.cs | 117 ++- MareSynchronos/Utils/Crypto.cs | 5 +- MareSynchronos/Utils/VariousExtensions.cs | 4 +- .../WebAPI/Files/FileDownloadManager.cs | 54 +- .../WebAPI/Files/FileTransferOrchestrator.cs | 33 +- .../WebAPI/Files/FileUploadManager.cs | 14 +- .../Files/Models/DownloadFileTransfer.cs | 10 +- .../WebAPI/Files/Models/DownloadStatus.cs | 2 +- .../WebAPI/Files/Models/FileDownloadStatus.cs | 6 +- .../WebAPI/Files/Models/FileTransfer.cs | 10 +- .../WebAPI/Files/Models/UploadFileTransfer.cs | 9 +- .../WebAPI/Files/Models/UploadProgress.cs | 2 +- .../SignalR/ApIController.Functions.Users.cs | 19 +- .../ApiController.Functions.Callbacks.cs | 58 +- .../SignalR/ApiController.Functions.Groups.cs | 20 +- .../WebAPI/SignalR/ApiController.cs | 171 ++-- MareSynchronos/WebAPI/SignalR/HubFactory.cs | 81 +- .../WebAPI/SignalR/JwtIdentifier.cs | 3 + .../SignalR/MareAuthFailureException.cs | 11 + .../WebAPI/SignalR/TokenProvider.cs | 118 +++ 111 files changed, 3456 insertions(+), 3174 deletions(-) delete mode 100644 MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs delete mode 100644 MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs delete mode 100644 MareSynchronos/MareConfiguration/Configurations/Obsolete/ServerConfigV0.cs create mode 100644 MareSynchronos/Services/MareProfileData.cs delete mode 100644 MareSynchronos/Services/ServerConfiguration/JwtCache.cs create mode 100644 MareSynchronos/UI/Components/DrawFolderBase.cs create mode 100644 MareSynchronos/UI/Components/DrawFolderGroup.cs create mode 100644 MareSynchronos/UI/Components/DrawFolderTag.cs delete mode 100644 MareSynchronos/UI/Components/DrawGroupPair.cs create mode 100644 MareSynchronos/UI/Components/DrawGroupedGroupFolder.cs delete mode 100644 MareSynchronos/UI/Components/DrawPairBase.cs delete mode 100644 MareSynchronos/UI/Components/GroupPanel.cs create mode 100644 MareSynchronos/UI/Components/IDrawFolder.cs delete mode 100644 MareSynchronos/UI/Components/PairGroupsUi.cs create mode 100644 MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/CreateSyncshellPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/IPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/JoinSyncshellPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/PopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs rename MareSynchronos/UI/Components/{SelectPairForGroupUi.cs => SelectPairForTagUi.cs} (92%) rename MareSynchronos/UI/Components/{SelectGroupForPairUi.cs => SelectTagForPairUi.cs} (94%) create mode 100644 MareSynchronos/UI/DrawEntityFactory.cs rename MareSynchronos/UI/Handlers/{UidDisplayHandler.cs => IdDisplayHandler.cs} (51%) create mode 100644 MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs create mode 100644 MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs create mode 100644 MareSynchronos/WebAPI/SignalR/TokenProvider.cs diff --git a/MareAPI b/MareAPI index 820a432..2e0414d 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 820a432ad9ac6c19f9908ec0f51671550e0fa151 +Subproject commit 2e0414de95acf20561960f2dcef2a350963439dc diff --git a/MareSynchronos/FileCache/FileCacheEntity.cs b/MareSynchronos/FileCache/FileCacheEntity.cs index f6a3ea0..2c07b87 100644 --- a/MareSynchronos/FileCache/FileCacheEntity.cs +++ b/MareSynchronos/FileCache/FileCacheEntity.cs @@ -13,14 +13,14 @@ public class FileCacheEntity LastModifiedDateTicks = lastModifiedDateTicks; } - public bool IsCacheEntry => PrefixedFilePath.StartsWith(FileCacheManager.CachePrefix, StringComparison.OrdinalIgnoreCase); + public long? CompressedSize { get; set; } public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}|{Size ?? -1}|{CompressedSize ?? -1}"; public string Hash { get; set; } + public bool IsCacheEntry => PrefixedFilePath.StartsWith(FileCacheManager.CachePrefix, StringComparison.OrdinalIgnoreCase); public string LastModifiedDateTicks { get; set; } public string PrefixedFilePath { get; init; } public string ResolvedFilepath { get; private set; } = string.Empty; public long? Size { get; set; } - public long? CompressedSize { get; set; } public void SetResolvedFilePath(string filePath) { diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 75babc7..9f5aed2 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -11,8 +11,8 @@ namespace MareSynchronos.FileCache; public sealed class FileCacheManager : IDisposable { - public const string CsvSplit = "|"; public const string CachePrefix = "{cache}"; + public const string CsvSplit = "|"; public const string PenumbraPrefix = "{penumbra}"; private readonly MareConfigService _configService; private readonly string _csvPath; @@ -55,7 +55,7 @@ public sealed class FileCacheManager : IDisposable if (File.Exists(_csvPath)) { bool success = false; - string[] entries = Array.Empty(); + string[] entries = []; int attempts = 0; while (!success && attempts < 10) { @@ -94,7 +94,7 @@ public sealed class FileCacheManager : IDisposable continue; } - processedFiles.Add(path, true); + processedFiles.Add(path, value: true); long size = -1; long compressed = -1; @@ -157,11 +157,33 @@ public sealed class FileCacheManager : IDisposable public List GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList(); + public List GetAllFileCachesByHash(string hash) + { + List output = []; + if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + { + foreach (var filecache in fileCacheEntities.ToList()) + { + var validated = GetValidatedFileCache(filecache); + if (validated != null) output.Add(validated); + } + } + + return output; + } + public string GetCacheFilePath(string hash, string extension) { return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension); } + public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, + (int)new FileInfo(fileCache).Length)); + } + public FileCacheEntity? GetFileCacheByHash(string hash) { if (_fileCaches.TryGetValue(hash, out var hashes)) @@ -172,19 +194,20 @@ public sealed class FileCacheManager : IDisposable return null; } - public List GetAllFileCachesByHash(string hash) + public FileCacheEntity? GetFileCacheByPath(string path) { - List output = new(); - if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); + var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)); + + if (entry == null) { - foreach (var filecache in fileCacheEntities.ToList()) - { - var validated = GetValidatedFileCache(filecache); - if (validated != null) output.Add(validated); - } + _logger.LogDebug("Found no entries for {path}", cleanedPath); + return CreateFileEntry(path); } - return output; + var validatedCacheEntry = GetValidatedFileCache(entry); + + return validatedCacheEntry; } public Dictionary GetFileCachesByPaths(string[] paths) @@ -217,29 +240,6 @@ public sealed class FileCacheManager : IDisposable return result; } - public FileCacheEntity? GetFileCacheByPath(string path) - { - var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); - var entry = _fileCaches.SelectMany(v => v.Value).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 async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath; - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, - (int)new FileInfo(fileCache).Length)); - } - public void RemoveHashedFile(string hash, string prefixedFilePath) { if (_fileCaches.TryGetValue(hash, out var caches)) @@ -316,14 +316,12 @@ public sealed class FileCacheManager : IDisposable try { RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); - FileInfo oldCache = new(fileCache.ResolvedFilepath); - var extensionPath = fileCache.ResolvedFilepath.ToUpper() + "." + ext; - File.Move(fileCache.ResolvedFilepath, extensionPath, true); - var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString()); + FileInfo fileInfo = new(fileCache.ResolvedFilepath); + FileInfo oldCache = fileInfo; + var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext; + File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true); + var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); newHashedEntity.SetResolvedFilePath(extensionPath); - FileInfo newCache = new FileInfo(extensionPath); - newCache.LastAccessTime = oldCache.LastAccessTime; - newCache.LastWriteTime = oldCache.LastWriteTime; AddHashedFile(newHashedEntity); _logger.LogDebug("Migrated from {oldPath} to {newPath}", fileCache.ResolvedFilepath, newHashedEntity.ResolvedFilepath); return newHashedEntity; @@ -340,7 +338,7 @@ public sealed class FileCacheManager : IDisposable { if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries)) { - _fileCaches[fileCache.Hash] = entries = new(); + _fileCaches[fileCache.Hash] = entries = []; } if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase))) @@ -394,7 +392,7 @@ public sealed class FileCacheManager : IDisposable return null; } - if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) { UpdateHashedFile(fileCache); } diff --git a/MareSynchronos/FileCache/FileCompactor.cs b/MareSynchronos/FileCache/FileCompactor.cs index 26cf500..0c0efdd 100644 --- a/MareSynchronos/FileCache/FileCompactor.cs +++ b/MareSynchronos/FileCache/FileCompactor.cs @@ -137,8 +137,10 @@ public sealed class FileCompactor { using (var fs = new FileStream(path, FileMode.Open)) { +#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called var hDevice = fs.SafeFileHandle.DangerousGetHandle(); - var ret = DeviceIoControl(hDevice, FSCTL_DELETE_EXTERNAL_BACKING, nint.Zero, 0, nint.Zero, 0, out _, out _); +#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called + _ = DeviceIoControl(hDevice, FSCTL_DELETE_EXTERNAL_BACKING, nint.Zero, 0, nint.Zero, 0, out _, out _); } } catch (Exception ex) @@ -153,7 +155,7 @@ public sealed class FileCompactor if (!fi.Exists) return -1; var root = fi.Directory?.Root.FullName.ToLower() ?? string.Empty; if (string.IsNullOrEmpty(root)) return -1; - if (_clusterSizes.ContainsKey(root)) return _clusterSizes[root]; + if (_clusterSizes.TryGetValue(root, out int value)) return value; _logger.LogDebug("Getting Cluster Size for {path}, root {root}", filePath, root); int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, out uint bytesPerSector, out _, out _); if (result == 0) return -1; @@ -162,7 +164,7 @@ public sealed class FileCompactor return _clusterSizes[root]; } - private bool IsCompactedFile(string filePath) + private static bool IsCompactedFile(string filePath) { uint buf = 8; _ = WofIsExternalFile(filePath, out int isExtFile, out uint _, out var info, ref buf); @@ -173,13 +175,15 @@ public sealed class FileCompactor private void WOFCompressFile(string path) { var efInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_efInfo)); - Marshal.StructureToPtr(_efInfo, efInfoPtr, true); + Marshal.StructureToPtr(_efInfo, efInfoPtr, fDeleteOld: true); ulong length = (ulong)Marshal.SizeOf(_efInfo); try { using (var fs = new FileStream(path, FileMode.Open)) { +#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called var hFile = fs.SafeFileHandle.DangerousGetHandle(); +#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called if (fs.SafeFileHandle.IsInvalid) { _logger.LogWarning("Invalid file handle to {file}", path); diff --git a/MareSynchronos/FileCache/FileState.cs b/MareSynchronos/FileCache/FileState.cs index 8568d5a..7f10e4e 100644 --- a/MareSynchronos/FileCache/FileState.cs +++ b/MareSynchronos/FileCache/FileState.cs @@ -1,9 +1,8 @@ namespace MareSynchronos.FileCache; - public enum FileState { Valid, RequireUpdate, RequireDeletion, -} +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/PeriodicFileScanner.cs b/MareSynchronos/FileCache/PeriodicFileScanner.cs index 7e8a192..36aee0a 100644 --- a/MareSynchronos/FileCache/PeriodicFileScanner.cs +++ b/MareSynchronos/FileCache/PeriodicFileScanner.cs @@ -10,11 +10,11 @@ namespace MareSynchronos.FileCache; public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase { private readonly MareConfigService _configService; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileCompactor _fileCompactor; private readonly FileCacheManager _fileDbManager; private readonly IpcManager _ipcManager; private readonly PerformanceCollectorService _performanceCollector; - private readonly DalamudUtilService _dalamudUtil; - private readonly FileCompactor _fileCompactor; private long _currentFileProgress = 0; private bool _fileScanWasRunning = false; private CancellationTokenSource _scanCancellationTokenSource = new(); @@ -38,15 +38,12 @@ public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase } public long CurrentFileProgress => _currentFileProgress; - public long TotalFilesStorage { get; private set; } public long FileCacheSize { get; set; } public ConcurrentDictionary HaltScanLocks { get; set; } = new(StringComparer.Ordinal); public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0; - public string TimeUntilNextScan => _timeUntilNextScan.ToString(@"mm\:ss"); - public long TotalFiles { get; private set; } - + public long TotalFilesStorage { get; private set; } private int TimeBetweenScans => _configService.Current.TimeSpanBetweenScansInSeconds; public void HaltScan(string source) @@ -213,19 +210,22 @@ public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase var previousThreadPriority = Thread.CurrentThread.Priority; Thread.CurrentThread.Priority = ThreadPriority.Lowest; 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" }; + string[] ext = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk"]; Dictionary penumbraFiles = new(StringComparer.Ordinal); foreach (var folder in Directory.EnumerateDirectories(penumbraDir!)) { try { - penumbraFiles[folder] = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) - .AsParallel() - .Where(f => ext.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) - && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) - && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) - && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)).ToArray(); + penumbraFiles[folder] = + [ + .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) + .AsParallel() + .Where(f => ext.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) + && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) + && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) + && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), + ]; } catch (Exception ex) { @@ -239,7 +239,7 @@ public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase .AsParallel() .Where(f => { - var val = f.Split('\\').Last(); + var val = f.Split('\\')[^1]; return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40; }); @@ -260,8 +260,8 @@ public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase // scan files from database var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8); - List entitiesToRemove = new(); - List entitiesToUpdate = new(); + List entitiesToRemove = []; + List entitiesToUpdate = []; object sync = new(); Thread[] workerThreads = new Thread[threadCount]; diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 5df0873..644f612 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -14,9 +14,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private readonly HashSet _cachedHandledPaths = new(StringComparer.Ordinal); private readonly TransientConfigService _configurationService; private readonly DalamudUtilService _dalamudUtil; - private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" }; - private readonly HashSet _playerRelatedPointers = new(); - private HashSet _cachedFrameAddresses = new(); + private readonly string[] _fileTypesToHandle = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; + private readonly HashSet _playerRelatedPointers = []; + private HashSet _cachedFrameAddresses = []; public TransientResourceManager(ILogger logger, TransientConfigService configurationService, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) @@ -68,17 +68,17 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) { - if (SemiTransientResources.ContainsKey(objectKind)) + if (SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { if (fileReplacement == null) { - SemiTransientResources[objectKind].Clear(); + value.Clear(); return; } foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) { - SemiTransientResources[objectKind].RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase)); + value.RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase)); } } } @@ -97,17 +97,18 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { if (TransientResources.TryGetValue(gameObject, out var result)) { - return result.ToList(); + return [.. result]; } - return new List(); + return []; } public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) { - if (!SemiTransientResources.ContainsKey(objectKind)) + if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { - SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); + value = new HashSet(StringComparer.Ordinal); + SemiTransientResources[objectKind] = value; } if (!TransientResources.TryGetValue(gameObject, out var resources)) @@ -119,7 +120,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); foreach (var gamePath in transientResources) { - SemiTransientResources[objectKind].Add(gamePath); + value.Add(gamePath); } if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) @@ -132,12 +133,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase internal void AddSemiTransientResource(ObjectKind objectKind, string item) { - if (!SemiTransientResources.ContainsKey(objectKind)) + if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { - SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); + value = new HashSet(StringComparer.Ordinal); + SemiTransientResources[objectKind] = value; } - SemiTransientResources[objectKind].Add(item.ToLowerInvariant()); + value.Add(item.ToLowerInvariant()); } internal void ClearTransientPaths(IntPtr ptr, List list) @@ -154,9 +156,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase TransientResources.Clear(); SemiTransientResources.Clear(); - if (SemiTransientResources.ContainsKey(ObjectKind.Player)) + if (SemiTransientResources.TryGetValue(ObjectKind.Player, out HashSet? value)) { - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player]; + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = value; _configurationService.Save(); } } @@ -182,7 +184,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private void Manager_PenumbraModSettingChanged() { - Task.Run(() => + _ = Task.Run(() => { Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); foreach (var item in _playerRelatedPointers) @@ -229,19 +231,20 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase return; } - if (!TransientResources.ContainsKey(gameObject)) + if (!TransientResources.TryGetValue(gameObject, out HashSet? value)) { - TransientResources[gameObject] = new(StringComparer.OrdinalIgnoreCase); + value = new(StringComparer.OrdinalIgnoreCase); + TransientResources[gameObject] = value; } - if (TransientResources[gameObject].Contains(replacedGamePath) || + if (value.Contains(replacedGamePath) || SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))) { Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); } else { - TransientResources[gameObject].Add(replacedGamePath); + value.Add(replacedGamePath); Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath); Mediator.Publish(new TransientResourceChangedMessage(gameObject)); } diff --git a/MareSynchronos/Interop/DalamudLogger.cs b/MareSynchronos/Interop/DalamudLogger.cs index c9571d8..2f1d117 100644 --- a/MareSynchronos/Interop/DalamudLogger.cs +++ b/MareSynchronos/Interop/DalamudLogger.cs @@ -8,8 +8,8 @@ namespace MareSynchronos.Interop; internal sealed class DalamudLogger : ILogger { private readonly MareConfigService _mareConfigService; - private readonly IPluginLog _pluginLog; private readonly string _name; + private readonly IPluginLog _pluginLog; public DalamudLogger(string name, MareConfigService mareConfigService, IPluginLog pluginLog) { diff --git a/MareSynchronos/Interop/DalamudLoggingProvider.cs b/MareSynchronos/Interop/DalamudLoggingProvider.cs index 0a74bb1..5ee0eeb 100644 --- a/MareSynchronos/Interop/DalamudLoggingProvider.cs +++ b/MareSynchronos/Interop/DalamudLoggingProvider.cs @@ -1,8 +1,9 @@ -using System.Collections.Concurrent; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using MareSynchronos.MareConfiguration; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + namespace MareSynchronos.Interop; [ProviderAlias("Dalamud")] @@ -32,7 +33,7 @@ public sealed class DalamudLoggingProvider : ILoggerProvider catName = string.Join("", Enumerable.Range(0, 15 - catName.Length).Select(_ => " ")) + catName; } - return _loggers.GetOrAdd(catName, name => new DalamudLogger(catName, _mareConfigService, _pluginLog)); + return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _mareConfigService, _pluginLog)); } public void Dispose() diff --git a/MareSynchronos/Interop/IpcManager.cs b/MareSynchronos/Interop/IpcManager.cs index 7d23659..64dd28c 100644 --- a/MareSynchronos/Interop/IpcManager.cs +++ b/MareSynchronos/Interop/IpcManager.cs @@ -1,24 +1,22 @@ -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; -using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; -using System.Collections.Concurrent; -using System.Text; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Utility; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; -using Dalamud.Interface.Internal.Notifications; -using Microsoft.Extensions.Logging; -using MareSynchronos.PlayerData.Handlers; -using MareSynchronos.Services.Mediator; -using MareSynchronos.Services; -using Dalamud.Utility; +using System.Collections.Concurrent; +using System.Text; namespace MareSynchronos.Interop; public sealed class IpcManager : DisposableMediatorSubscriberBase { - private readonly uint LockCode = 0x6D617265; - private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion; private readonly ICallGateSubscriber _customizePlusGetBodyScale; private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; @@ -51,6 +49,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; private readonly FuncSubscriber<(int, int)> _penumbraApiVersion; private readonly FuncSubscriber _penumbraAssignTemporaryCollection; + private readonly FuncSubscriber _penumbraConvertTextureFile; private readonly FuncSubscriber _penumbraCreateNamedTemporaryCollection; private readonly EventSubscriber _penumbraDispose; private readonly FuncSubscriber _penumbraEnabled; @@ -58,7 +57,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly FuncSubscriber _penumbraGetMetaManipulations; private readonly EventSubscriber _penumbraInit; private readonly EventSubscriber _penumbraModSettingChanged; - private readonly FuncSubscriber _penumbraConvertTextureFile; private readonly EventSubscriber _penumbraObjectIsRedrawn; private readonly ActionSubscriber _penumbraRedraw; private readonly ActionSubscriber _penumbraRedrawObject; @@ -69,6 +67,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly FuncSubscriber _penumbraResolvePaths; private readonly ParamsFuncSubscriber?[]> _penumbraResourcePaths; private readonly SemaphoreSlim _redrawSemaphore = new(2); + private readonly uint LockCode = 0x6D617265; private bool _customizePlusAvailable = false; private CancellationTokenSource _disposalCts = new(); private bool _glamourerAvailable = false; @@ -253,7 +252,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase { logger.LogWarning("[{appid}] Failed to apply Glamourer data", applicationId); } - }).ConfigureAwait(false); } finally @@ -262,52 +260,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase } } - public async Task GlamourerRevert(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) - { - if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; - try - { - await _redrawSemaphore.WaitAsync(token).ConfigureAwait(false); - await PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => - { - try - { - logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); - _glamourerUnlock.InvokeFunc(handler.Name, LockCode); - logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId); - _glamourerRevert.InvokeAction(chara, LockCode); - logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId); - _penumbraRedrawObject.Invoke(chara, RedrawType.AfterGPose); - } - catch (Exception ex) - { - logger.LogWarning(ex, "[{appid}] Error during GlamourerRevert", applicationId); - } - - }).ConfigureAwait(false); - } - finally - { - _redrawSemaphore.Release(); - } - } - - public void GlamourerRevertByName(ILogger logger, string name, Guid applicationId) - { - if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; - try - { - logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId); - _glamourerRevertByName.InvokeAction(name, LockCode); - logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); - _glamourerUnlock.InvokeFunc(name, LockCode); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Error during Glamourer RevertByName"); - } - } - public async Task GlamourerGetCharacterCustomizationAsync(IntPtr character) { if (!CheckGlamourerApi()) return string.Empty; @@ -334,6 +286,51 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase } } + public async Task GlamourerRevert(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + { + if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; + try + { + await _redrawSemaphore.WaitAsync(token).ConfigureAwait(false); + await PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => + { + try + { + logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); + _glamourerUnlock.InvokeFunc(handler.Name, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId); + _glamourerRevert.InvokeAction(chara, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId); + _penumbraRedrawObject.Invoke(chara, RedrawType.AfterGPose); + } + catch (Exception ex) + { + logger.LogWarning(ex, "[{appid}] Error during GlamourerRevert", applicationId); + } + }).ConfigureAwait(false); + } + finally + { + _redrawSemaphore.Release(); + } + } + + public void GlamourerRevertByName(ILogger logger, string name, Guid applicationId) + { + if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; + try + { + logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId); + _glamourerRevertByName.InvokeAction(name, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); + _glamourerUnlock.InvokeFunc(name, LockCode); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during Glamourer RevertByName"); + } + } + public async Task HeelsRestoreOffsetForPlayerAsync(IntPtr character) { if (!CheckHeelsApi()) return; @@ -469,6 +466,46 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase }).ConfigureAwait(false); } + public async Task PenumbraConvertTextureFiles(ILogger logger, Dictionary textures, IProgress<(string, int)> progress, CancellationToken token) + { + if (!CheckPenumbraApi()) return; + + Mediator.Publish(new HaltScanMessage("TextureConversion")); + int currentTexture = 0; + foreach (var texture in textures) + { + if (token.IsCancellationRequested) break; + + progress.Report((texture.Key, ++currentTexture)); + + logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex); + var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, d: true); + await convertTask.ConfigureAwait(false); + if (convertTask.IsCompletedSuccessfully && texture.Value.Any()) + { + foreach (var duplicatedTexture in texture.Value) + { + logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture); + try + { + File.Copy(texture.Key, duplicatedTexture, overwrite: true); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture); + } + } + } + } + Mediator.Publish(new ResumeScanMessage("TextureConversion")); + + await _dalamudUtil.RunOnFrameworkThread(async () => + { + var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false); + _penumbraRedrawObject.Invoke(gameObject!, RedrawType.Redraw); + }).ConfigureAwait(false); + } + public async Task PenumbraCreateTemporaryCollectionAsync(ILogger logger, string uid) { if (!CheckPenumbraApi()) return string.Empty; @@ -482,6 +519,19 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase }).ConfigureAwait(false); } + public async Task?[]?> PenumbraGetCharacterData(ILogger logger, GameObjectHandler handler) + { + if (!CheckPenumbraApi()) return null; + + return await _dalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); + var idx = handler.GetGameObject()?.ObjectIndex; + if (idx == null) return null; + return _penumbraResourcePaths.Invoke(idx.Value); + }).ConfigureAwait(false); + } + public string PenumbraGetMetaManipulations() { if (!CheckPenumbraApi()) return string.Empty; @@ -529,7 +579,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase await _dalamudUtil.RunOnFrameworkThread(() => { logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData); - var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Meta", collName, new Dictionary(), manipulationData, 0); + var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Meta", collName, [], manipulationData, 0); logger.LogTrace("[{applicationId}] Setting temp meta mod for {collName}, Success: {ret}", applicationId, collName, retAdd); }).ConfigureAwait(false); } @@ -551,59 +601,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase }).ConfigureAwait(false); } - public async Task PenumbraConvertTextureFiles(ILogger logger, Dictionary textures, IProgress<(string, int)> progress, CancellationToken token) - { - if (!CheckPenumbraApi()) return; - - Mediator.Publish(new HaltScanMessage("TextureConversion")); - int currentTexture = 0; - foreach (var texture in textures) - { - if (token.IsCancellationRequested) break; - - progress.Report((texture.Key, ++currentTexture)); - - logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex); - var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, true); - await convertTask.ConfigureAwait(false); - if (convertTask.IsCompletedSuccessfully && texture.Value.Any()) - { - foreach (var duplicatedTexture in texture.Value) - { - logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture); - try - { - File.Copy(texture.Key, duplicatedTexture, true); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture); - } - } - } - } - Mediator.Publish(new ResumeScanMessage("TextureConversion")); - - await _dalamudUtil.RunOnFrameworkThread(async () => - { - var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false); - _penumbraRedrawObject.Invoke(gameObject!, RedrawType.Redraw); - }).ConfigureAwait(false); - } - - public async Task?[]?> PenumbraGetCharacterData(ILogger logger, GameObjectHandler handler) - { - if (!CheckPenumbraApi()) return null; - - return await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); - var idx = handler.GetGameObject()?.ObjectIndex; - if (idx == null) return null; - return _penumbraResourcePaths.Invoke(idx.Value); - }).ConfigureAwait(false); - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/MareSynchronos/Interop/Structs.cs b/MareSynchronos/Interop/Structs.cs index 8f54df6..b3bcc40 100644 --- a/MareSynchronos/Interop/Structs.cs +++ b/MareSynchronos/Interop/Structs.cs @@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; namespace MareSynchronos.Interop; - +#pragma warning disable MA0048 [StructLayout(LayoutKind.Explicit)] public unsafe struct RenderModel { @@ -25,7 +25,6 @@ public unsafe struct RenderModel [FieldOffset(0x60)] public int BoneListCount; - [FieldOffset(0x98)] public void** Materials; @@ -47,3 +46,4 @@ public unsafe struct WeaponDrawObject { [FieldOffset(0x00)] public RenderModel* RenderModel; } +#pragma warning restore MA0048 \ No newline at end of file diff --git a/MareSynchronos/Localization/Strings.cs b/MareSynchronos/Localization/Strings.cs index 675c07f..094178d 100644 --- a/MareSynchronos/Localization/Strings.cs +++ b/MareSynchronos/Localization/Strings.cs @@ -4,11 +4,14 @@ namespace MareSynchronos.Localization; public static class Strings { + public static ToSStrings ToS { get; set; } = new(); + public class ToSStrings { - public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); + public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service"); - public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY"); + public readonly string ButtonWillBeAvailableIn = Loc.Localize("ButtonWillBeAvailableIn", "'I agree' button will be available in"); + public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); public readonly string Paragraph1 = Loc.Localize("Paragraph1", "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " + @@ -36,10 +39,6 @@ public static class Strings public readonly string Paragraph6 = Loc.Localize("Paragraph6", "This service is provided as-is. In case of abuse join the Mare Synchronos Discord."); - public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); - - public readonly string ButtonWillBeAvailableIn = Loc.Localize("ButtonWillBeAvailableIn", "'I agree' button will be available in"); + public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY"); } - - public static ToSStrings ToS { get; set; } = new(); } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs index faf7cf0..4720fa8 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs @@ -1,74 +1,16 @@ using Dalamud.Plugin; 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 -#pragma warning disable CS0612 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones -public class ConfigurationMigrator : IHostedService +public class ConfigurationMigrator(ILogger logger, DalamudPluginInterface pi) : IHostedService { - private readonly ILogger _logger; - private readonly DalamudPluginInterface _pi; - - public ConfigurationMigrator(ILogger logger, DalamudPluginInterface pi) - { - _logger = logger; - _pi = pi; - } - public void Migrate() { - if (_pi.GetPluginConfig() is Configurations.Obsolete.Configuration oldConfig) - { - _logger.LogInformation("Migrating Configuration from old config style to 1"); - - var config = oldConfig.ToMareConfig(_logger); - File.Move(_pi.ConfigFile.FullName, _pi.ConfigFile.FullName + ".old", overwrite: true); - MigrateMareConfigV0ToV1(config); - } - - if (File.Exists(ConfigurationPath(MareConfigService.ConfigName))) - { - try - { - var mareConfig = JsonConvert.DeserializeObject(File.ReadAllText(ConfigurationPath(MareConfigService.ConfigName)))!; - - if (mareConfig.Version == 0) - { - MigrateMareConfigV0ToV1(mareConfig); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to migrate, skipping"); - } - } - - if (File.Exists(ConfigurationPath(ServerConfigService.ConfigName))) - { - try - { - var content = File.ReadAllText(ConfigurationPath(ServerConfigService.ConfigName)); - if (!content.Contains("\"Version\": 1")) - { - var serverConfig = JsonConvert.DeserializeObject(content); - - if (serverConfig != null && serverConfig.Version == 0) - { - MigrateServerConfigV0toV1(serverConfig); - } - } - } - catch (Exception ex) - { - _logger.LogWarning(ex,"Failed to migrate ServerConfig"); - } - } + // currently nothing to migrate } public Task StartAsync(CancellationToken cancellationToken) @@ -87,74 +29,5 @@ public class ConfigurationMigrator : IHostedService 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"); - if (File.Exists(ConfigurationPath(MareConfigService.ConfigName))) - File.Copy(ConfigurationPath(MareConfigService.ConfigName), ConfigurationPath(MareConfigService.ConfigName) + ".migrated." + mareConfigV0.Version + ".bak", overwrite: true); - - MareConfig mareConfigV1 = mareConfigV0.ToV1(); - - var serverConfig = new ServerConfig() - { - ServerStorage = mareConfigV0.ServerStorage.Select(p => p.Value.ToV1()).ToList() - }; - serverConfig.CurrentServer = Array.IndexOf(serverConfig.ServerStorage.Select(s => s.ServerUri).ToArray(), mareConfigV0.CurrentServer); - var transientConfig = new TransientConfig() - { - PlayerPersistentTransientCache = mareConfigV0.PlayerPersistentTransientCache - }; - var tagConfig = new ServerTagConfig() - { - ServerTagStorage = mareConfigV0.ServerStorage.ToDictionary(p => p.Key, p => new ServerTagStorage() - { - UidServerPairedUserTags = p.Value.UidServerPairedUserTags.ToDictionary(p => p.Key, p => p.Value.ToList(), StringComparer.Ordinal), - OpenPairTags = p.Value.OpenPairTags.ToHashSet(StringComparer.Ordinal), - ServerAvailablePairTags = p.Value.ServerAvailablePairTags.ToHashSet(StringComparer.Ordinal) - }, StringComparer.Ordinal) - }; - var notesConfig = new UidNotesConfig() - { - ServerNotes = mareConfigV0.ServerStorage.ToDictionary(p => p.Key, p => new ServerNotesStorage() - { - GidServerComments = p.Value.GidServerComments, - UidServerComments = p.Value.UidServerComments - }, StringComparer.Ordinal) - }; - - SaveConfig(mareConfigV1, ConfigurationPath(MareConfigService.ConfigName)); - SaveConfig(serverConfig, ConfigurationPath(ServerConfigService.ConfigName)); - SaveConfig(transientConfig, ConfigurationPath(TransientConfigService.ConfigName)); - SaveConfig(tagConfig, ConfigurationPath(ServerTagConfigService.ConfigName)); - SaveConfig(notesConfig, ConfigurationPath(NotesConfigService.ConfigName)); - } - - private void MigrateServerConfigV0toV1(ServerConfigV0 serverConfigV0) - { - _logger.LogInformation("Migration Server Configuration from version 0 to 1"); - if (File.Exists(ConfigurationPath(ServerConfigService.ConfigName))) - File.Copy(ConfigurationPath(ServerConfigService.ConfigName), ConfigurationPath(ServerConfigService.ConfigName) + ".migrated." + serverConfigV0.Version + ".bak", overwrite: true); - - ServerConfig migrated = new(); - - var currentServer = serverConfigV0.CurrentServer; - var currentServerIdx = Array.IndexOf(serverConfigV0.ServerStorage.Keys.ToArray(), currentServer); - - if (currentServerIdx == -1) currentServerIdx = 0; - - migrated.CurrentServer = currentServerIdx; - migrated.ServerStorage = new(); - - foreach (var server in serverConfigV0.ServerStorage) - { - migrated.ServerStorage.Add(server.Value); - } - - SaveConfig(migrated, ConfigurationPath(ServerConfigService.ConfigName)); - } + private string ConfigurationPath(string configName) => Path.Combine(pi.ConfigDirectory.FullName, configName); } - -#pragma warning restore CS0612 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones -#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 acb1a01..61c4a0a 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs @@ -14,8 +14,8 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC { ConfigurationDirectory = configurationDirectory; - Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token); - Task.Run(CheckForDirtyConfigInternal, _periodicCheckCts.Token); + _ = Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token); + _ = Task.Run(CheckForDirtyConfigInternal, _periodicCheckCts.Token); _currentConfigInternal = LazyConfig(); } diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 8e4a260..61cd581 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -9,8 +9,8 @@ public class MareConfig : IMareConfiguration public bool AcceptedAgreement { get; set; } = false; public string CacheFolder { get; set; } = string.Empty; public bool DisableOptionalPluginWarnings { get; set; } = false; - public bool EnableRightClickMenus { get; set; } = true; public bool EnableDtrEntry { get; set; } = false; + public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public string ExportFolder { get; set; } = string.Empty; public bool FileScanPaused { get; set; } = false; @@ -22,15 +22,14 @@ public class MareConfig : IMareConfiguration public bool OpenGposeImportOnGposeStart { get; set; } = false; public bool OpenPopupOnAdd { get; set; } = true; public int ParallelDownloads { get; set; } = 10; - public bool UseCompactor { get; set; } = false; + public bool PreferNotesOverNamesForVisible { get; set; } = false; public float ProfileDelay { get; set; } = 1.5f; public bool ProfilePopoutRight { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false; public bool ProfilesShow { get; set; } = true; - public bool ReverseUserSort { get; set; } = false; public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; - public bool PreferNotesOverNamesForVisible { get; set; } = false; public bool ShowOfflineUsersSeparately { get; set; } = true; + public bool GroupUpSyncshells { get; set; } = true; public bool ShowOnlineNotifications { get; set; } = false; public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true; public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; @@ -44,6 +43,7 @@ public class MareConfig : IMareConfiguration public bool TransferBarsShowText { get; set; } = true; public int TransferBarsWidth { get; set; } = 250; public bool UseAlternativeFileUpload { get; set; } = false; + public bool UseCompactor { get; set; } = false; public int Version { get; set; } = 1; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs b/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs deleted file mode 100644 index 53c5bf1..0000000 --- a/MareSynchronos/MareConfiguration/Configurations/Obsolete/Configuration.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Dalamud.Configuration; -using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.MareConfiguration.Models.Obsolete; -using MareSynchronos.WebAPI; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.MareConfiguration.Configurations.Obsolete; - -[Serializable] -[Obsolete("Deprecated, use MareConfig")] -public class Configuration : IPluginConfiguration -{ - public int Version { get; set; } = 6; - public Dictionary ServerStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - { ApiController.MainServiceUri, new ServerStorageV0() { ServerName = ApiController.MainServer, ServerUri = ApiController.MainServiceUri } }, - }; - 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 FullPause { get; set; } = false; - public bool HideInfoMessages { get; set; } = false; - public bool DisableOptionalPluginWarnings { get; set; } = false; - public bool OpenGposeImportOnGposeStart { get; set; } = false; - public bool ShowTransferWindow { get; set; } = true; - public bool OpenPopupOnAdd { get; set; } = true; - public string CurrentServer { get; set; } = string.Empty; - - private string _apiUri = string.Empty; - public string ApiUri - { - get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; - set => _apiUri = value; - } - public Dictionary ClientSecret { get; set; } = new(StringComparer.Ordinal); - public Dictionary CustomServerList { get; set; } = new(StringComparer.Ordinal); - public Dictionary> UidServerComments { get; set; } = new(StringComparer.Ordinal); - public Dictionary> GidServerComments { get; set; } = new(StringComparer.Ordinal); - /// - /// Each paired user can have multiple tags. Each tag will create a category, and the user will - /// be displayed into that category. - /// 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 { 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 { get; set; } = new(StringComparer.Ordinal); - public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); - - public MareConfigV0 ToMareConfig(ILogger logger) - { - - MareConfigV0 newConfig = new(); - logger.LogInformation("Migrating Config to MareConfig"); - - newConfig.AcceptedAgreement = AcceptedAgreement; - newConfig.CacheFolder = CacheFolder; - newConfig.MaxLocalCacheInGiB = MaxLocalCacheInGiB; - newConfig.ReverseUserSort = ReverseUserSort; - newConfig.TimeSpanBetweenScansInSeconds = TimeSpanBetweenScansInSeconds; - newConfig.FileScanPaused = FileScanPaused; - newConfig.InitialScanComplete = InitialScanComplete; - newConfig.DisableOptionalPluginWarnings = DisableOptionalPluginWarnings; - newConfig.OpenGposeImportOnGposeStart = OpenGposeImportOnGposeStart; - newConfig.ShowTransferWindow = ShowTransferWindow; - newConfig.OpenPopupOnAdd = OpenPopupOnAdd; - newConfig.CurrentServer = ApiUri; - - // create all server storage based on current clientsecret - foreach (var secret in ClientSecret) - { - logger.LogDebug("Migrating {key}", secret.Key); - var apiuri = secret.Key; - var secretkey = secret.Value; - ServerStorageV0 toAdd = new(); - if (string.Equals(apiuri, ApiController.MainServiceUri, StringComparison.OrdinalIgnoreCase)) - { - toAdd.ServerUri = ApiController.MainServiceUri; - toAdd.ServerName = ApiController.MainServer; - } - else - { - toAdd.ServerUri = apiuri; - if (!CustomServerList.TryGetValue(apiuri, out var serverName)) serverName = apiuri; - toAdd.ServerName = serverName; - } - - toAdd.SecretKeys[0] = new SecretKey() - { - FriendlyName = "Auto Migrated Secret Key (" + DateTime.Now.ToString("yyyy-MM-dd") + ")", - Key = secretkey, - }; - - if (GidServerComments.TryGetValue(apiuri, out var gids)) - { - toAdd.GidServerComments = gids; - } - if (UidServerComments.TryGetValue(apiuri, out var uids)) - { - toAdd.UidServerComments = uids; - } - if (UidServerPairedUserTags.TryGetValue(apiuri, out var uidtag)) - { - toAdd.UidServerPairedUserTags = uidtag; - } - if (ServerAvailablePairTags.TryGetValue(apiuri, out var servertag)) - { - toAdd.ServerAvailablePairTags = servertag; - } - toAdd.OpenPairTags = OpenPairTags; - toAdd.FullPause = FullPause; - - newConfig.ServerStorage[apiuri] = toAdd; - } - - return newConfig; - } -} diff --git a/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs b/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs deleted file mode 100644 index 9fcb8c5..0000000 --- a/MareSynchronos/MareConfiguration/Configurations/Obsolete/MareConfigV0.cs +++ /dev/null @@ -1,57 +0,0 @@ -using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.MareConfiguration.Models.Obsolete; - -namespace MareSynchronos.MareConfiguration.Configurations.Obsolete; - -[Serializable] -[Obsolete("Deprecated, use MareConfig")] -public class MareConfigV0 : IMareConfiguration -{ - public int Version { get; set; } = 0; - public Dictionary ServerStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase); - public Dictionary> PlayerPersistentTransientCache { get; set; } = new(StringComparer.Ordinal); - 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 bool OpenGposeImportOnGposeStart { get; set; } = false; - public bool ShowTransferWindow { get; set; } = true; - public bool OpenPopupOnAdd { get; set; } = true; - public string CurrentServer { get; set; } = string.Empty; - 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 NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; - public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; - - public MareConfig ToV1() - { - return new MareConfig() - { - AcceptedAgreement = this.AcceptedAgreement, - CacheFolder = this.CacheFolder, - MaxLocalCacheInGiB = this.MaxLocalCacheInGiB, - ReverseUserSort = this.ReverseUserSort, - TimeSpanBetweenScansInSeconds = this.TimeSpanBetweenScansInSeconds, - FileScanPaused = this.FileScanPaused, - InitialScanComplete = this.InitialScanComplete, - DisableOptionalPluginWarnings = this.DisableOptionalPluginWarnings, - OpenGposeImportOnGposeStart = this.OpenGposeImportOnGposeStart, - ShowTransferWindow = this.ShowTransferWindow, - OpenPopupOnAdd = this.OpenPopupOnAdd, - ShowOnlineNotifications = this.ShowOnlineNotifications, - ShowOnlineNotificationsOnlyForIndividualPairs = this.ShowOnlineNotificationsOnlyForIndividualPairs, - ShowCharacterNameInsteadOfNotesForVisible = this.ShowCharacterNameInsteadOfNotesForVisible, - ShowOnlineNotificationsOnlyForNamedPairs = this.ShowOnlineNotificationsOnlyForNamedPairs, - ErrorNotification = this.ErrorNotification, - InfoNotification = this.InfoNotification, - WarningNotification = this.WarningNotification, - }; - } -} diff --git a/MareSynchronos/MareConfiguration/Configurations/Obsolete/ServerConfigV0.cs b/MareSynchronos/MareConfiguration/Configurations/Obsolete/ServerConfigV0.cs deleted file mode 100644 index a46ac69..0000000 --- a/MareSynchronos/MareConfiguration/Configurations/Obsolete/ServerConfigV0.cs +++ /dev/null @@ -1,18 +0,0 @@ -using MareSynchronos.MareConfiguration.Models; -using MareSynchronos.WebAPI; - -namespace MareSynchronos.MareConfiguration.Configurations.Obsolete; - -[Serializable] -[Obsolete("Replaced with ServerConfig")] -public class ServerConfigV0 : IMareConfiguration -{ - public string CurrentServer { get; set; } = string.Empty; - - public Dictionary ServerStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - { ApiController.MainServiceUri, new ServerStorage() { ServerName = ApiController.MainServer, ServerUri = ApiController.MainServiceUri } }, - }; - - public int Version { get; set; } = 0; -} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs b/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs index 7b34d2d..4150f2e 100644 --- a/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs @@ -4,6 +4,6 @@ namespace MareSynchronos.MareConfiguration.Configurations; public class ServerTagConfig : IMareConfiguration { - public int Version { get; set; } = 0; public Dictionary ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase); -} + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs b/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs index 5c7a124..59ed8a5 100644 --- a/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs @@ -2,8 +2,6 @@ public class TransientConfig : IMareConfiguration { - public int Version { get; set; } = 0; - public Dictionary> PlayerPersistentTransientCache { get; set; } = new(StringComparer.Ordinal); - + public int Version { get; set; } = 0; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs index 508301f..0129eb2 100644 --- a/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs @@ -4,6 +4,6 @@ namespace MareSynchronos.MareConfiguration.Configurations; public class UidNotesConfig : IMareConfiguration { - public int Version { get; set; } = 0; public Dictionary ServerNotes { get; set; } = new(StringComparer.Ordinal); -} + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/Authentication.cs b/MareSynchronos/MareConfiguration/Models/Authentication.cs index fcf13a5..fa18fca 100644 --- a/MareSynchronos/MareConfiguration/Models/Authentication.cs +++ b/MareSynchronos/MareConfiguration/Models/Authentication.cs @@ -6,4 +6,4 @@ public record Authentication public string CharacterName { get; set; } = string.Empty; public uint WorldId { get; set; } = 0; public int SecretKeyIdx { get; set; } = -1; -} +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs index 7864d33..8517873 100644 --- a/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs +++ b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs @@ -4,16 +4,16 @@ [Obsolete("Deprecated, use ServerStorage")] public class ServerStorageV0 { - public string ServerUri { get; set; } = string.Empty; - public string ServerName { get; set; } = string.Empty; - 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 { 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(); + public List Authentications { get; set; } = []; public bool FullPause { get; set; } = false; + public Dictionary GidServerComments { get; set; } = new(StringComparer.Ordinal); + public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); + public Dictionary SecretKeys { get; set; } = []; + public HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); + public string ServerName { get; set; } = string.Empty; + public string ServerUri { get; set; } = string.Empty; + public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); + public Dictionary> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); public ServerStorage ToV1() { @@ -21,9 +21,9 @@ public class ServerStorageV0 { ServerUri = ServerUri, ServerName = ServerName, - Authentications = Authentications.ToList(), + Authentications = [.. Authentications], FullPause = FullPause, SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value) }; } -} +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/SecretKey.cs b/MareSynchronos/MareConfiguration/Models/SecretKey.cs index 6b42fc8..04aad1d 100644 --- a/MareSynchronos/MareConfiguration/Models/SecretKey.cs +++ b/MareSynchronos/MareConfiguration/Models/SecretKey.cs @@ -3,6 +3,6 @@ [Serializable] public class SecretKey { - public string Key { get; set; } = string.Empty; public string FriendlyName { get; set; } = string.Empty; -} + public string Key { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs index 82e5508..6d71d91 100644 --- a/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs +++ b/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs @@ -2,6 +2,6 @@ public class ServerNotesStorage { - public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); public Dictionary GidServerComments { get; set; } = new(StringComparer.Ordinal); -} + public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerStorage.cs index 6291315..03d8a25 100644 --- a/MareSynchronos/MareConfiguration/Models/ServerStorage.cs +++ b/MareSynchronos/MareConfiguration/Models/ServerStorage.cs @@ -3,9 +3,9 @@ [Serializable] public class ServerStorage { - public string ServerUri { get; set; } = string.Empty; - public string ServerName { get; set; } = string.Empty; - public List Authentications { get; set; } = new(); - public Dictionary SecretKeys { get; set; } = new(); + public List Authentications { get; set; } = []; public bool FullPause { get; set; } = false; -} + public Dictionary SecretKeys { get; set; } = []; + public string ServerName { get; set; } = string.Empty; + public string ServerUri { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs index 96e985e..eec5ac8 100644 --- a/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs +++ b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs @@ -3,7 +3,7 @@ [Serializable] public class ServerTagStorage { - 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 HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); + public Dictionary> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs index f956f2f..cbd6554 100644 --- a/MareSynchronos/MarePlugin.cs +++ b/MareSynchronos/MarePlugin.cs @@ -90,7 +90,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService var version = Assembly.GetExecutingAssembly().GetName().Version!; Logger.LogInformation("Launching {name} {major}.{minor}.{build}", "Mare Synchronos", version.Major, version.Minor, version.Build); - Mediator.Subscribe(this, (_) => Task.Run(WaitForPlayerAndLaunchCharacterManager)); + Mediator.Subscribe(this, (msg) => _ = Task.Run(WaitForPlayerAndLaunchCharacterManager)); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); @@ -112,7 +112,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService { Logger?.LogDebug("Client login"); - Task.Run(WaitForPlayerAndLaunchCharacterManager); + _ = Task.Run(WaitForPlayerAndLaunchCharacterManager); } private void DalamudUtilOnLogOut() diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index cb46186..a378a2d 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 1.8.0 + 1.9.0 https://github.com/Penumbra-Sync/client @@ -46,6 +46,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MareSynchronos/PlayerData/Data/CharacterData.cs b/MareSynchronos/PlayerData/Data/CharacterData.cs index 12c0bf7..156bd6f 100644 --- a/MareSynchronos/PlayerData/Data/CharacterData.cs +++ b/MareSynchronos/PlayerData/Data/CharacterData.cs @@ -1,15 +1,17 @@ -using System.Text; +using MareSynchronos.API.Data; + using MareSynchronos.API.Data.Enum; -using MareSynchronos.API.Data; + +using System.Text; namespace MareSynchronos.PlayerData.Data; public class CharacterData { - public Dictionary CustomizePlusScale { get; set; } = new(); - public Dictionary> FileReplacements { get; set; } = new(); + public Dictionary CustomizePlusScale { get; set; } = []; + public Dictionary> FileReplacements { get; set; } = []; - public Dictionary GlamourerString { get; set; } = new(); + public Dictionary GlamourerString { get; set; } = []; public string HeelsData { get; set; } = string.Empty; public string HonorificData { get; set; } = string.Empty; diff --git a/MareSynchronos/PlayerData/Data/FileReplacement.cs b/MareSynchronos/PlayerData/Data/FileReplacement.cs index a6c1db8..32c17a3 100644 --- a/MareSynchronos/PlayerData/Data/FileReplacement.cs +++ b/MareSynchronos/PlayerData/Data/FileReplacement.cs @@ -1,5 +1,6 @@ -using System.Text.RegularExpressions; -using MareSynchronos.API.Data; +using MareSynchronos.API.Data; + +using System.Text.RegularExpressions; namespace MareSynchronos.PlayerData.Data; @@ -23,7 +24,7 @@ public partial class FileReplacement { return new FileReplacementData { - GamePaths = GamePaths.ToArray(), + GamePaths = [.. GamePaths], Hash = Hash, FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty, }; diff --git a/MareSynchronos/PlayerData/Data/PlayerChanges.cs b/MareSynchronos/PlayerData/Data/PlayerChanges.cs index 6c7c9d4..f33729c 100644 --- a/MareSynchronos/PlayerData/Data/PlayerChanges.cs +++ b/MareSynchronos/PlayerData/Data/PlayerChanges.cs @@ -9,4 +9,4 @@ public enum PlayerChanges ModFiles = 5, ModManip = 6, Glamourer = 7 -} +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Export/MareCharaFileData.cs b/MareSynchronos/PlayerData/Export/MareCharaFileData.cs index 42e42be..0bb8ea4 100644 --- a/MareSynchronos/PlayerData/Export/MareCharaFileData.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileData.cs @@ -13,8 +13,8 @@ public record MareCharaFileData public string CustomizePlusData { get; set; } = string.Empty; public string PalettePlusData { get; set; } = string.Empty; public string ManipulationData { get; set; } = string.Empty; - public List Files { get; set; } = new(); - public List FileSwaps { get; set; } = new(); + public List Files { get; set; } = []; + public List FileSwaps { get; set; } = []; public MareCharaFileData() { } public MareCharaFileData(FileCacheManager manager, string description, CharacterData dto) diff --git a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs index b27e6f7..1025f72 100644 --- a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs @@ -1,16 +1,16 @@ using Dalamud.Game.ClientState.Objects.Types; using LZ4; -using MareSynchronos.FileCache; using MareSynchronos.API.Data.Enum; -using MareSynchronos.MareConfiguration; -using CharacterData = MareSynchronos.API.Data.CharacterData; -using Microsoft.Extensions.Logging; -using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.FileCache; using MareSynchronos.Interop; -using MareSynchronos.Services; -using MareSynchronos.Utils; +using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using CharacterData = MareSynchronos.API.Data.CharacterData; namespace MareSynchronos.PlayerData.Export; @@ -20,11 +20,11 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly MareCharaFileDataFactory _factory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; + private readonly Dictionary _gposeGameObjects; private readonly IpcManager _ipcManager; private readonly ILogger _logger; private readonly FileCacheManager _manager; private int _globalFileCounter = 0; - private readonly Dictionary _gposeGameObjects; private bool _isInGpose = false; public MareCharaFileManager(ILogger logger, GameObjectHandlerFactory gameObjectHandlerFactory, @@ -38,7 +38,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase _ipcManager = ipcManager; _configService = configService; _dalamudUtil = dalamudUtil; - _gposeGameObjects = new(); + _gposeGameObjects = []; Mediator.Subscribe(this, _ => _isInGpose = true); Mediator.Subscribe(this, async _ => { @@ -46,9 +46,9 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase CancellationTokenSource cts = new(); foreach (var item in _gposeGameObjects) { - if ((await dalamudUtil.RunOnFrameworkThread(() => item.Value.CurrentAddress())) != nint.Zero) + if ((await dalamudUtil.RunOnFrameworkThread(() => item.Value.CurrentAddress()).ConfigureAwait(false)) != nint.Zero) { - await _ipcManager.GlamourerRevert(logger, item.Value, Guid.NewGuid(), cts.Token); + await _ipcManager.GlamourerRevert(logger, item.Value, Guid.NewGuid(), cts.Token).ConfigureAwait(false); } else { @@ -97,9 +97,8 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase await _ipcManager.PenumbraSetTemporaryModsAsync(_logger, applicationId, coll, extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal)).ConfigureAwait(false); await _ipcManager.PenumbraSetManipulationDataAsync(_logger, applicationId, coll, LoadedCharaFile.CharaFileData.ManipulationData).ConfigureAwait(false); - GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, - () => _dalamudUtil.GetGposeCharacterFromObjectTableByName(charaTarget.Name.ToString(), _isInGpose)?.Address ?? IntPtr.Zero, false).ConfigureAwait(false); + () => _dalamudUtil.GetGposeCharacterFromObjectTableByName(charaTarget.Name.ToString(), _isInGpose)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false); if (!_gposeGameObjects.ContainsKey(charaTarget.Name.ToString())) _gposeGameObjects[charaTarget.Name.ToString()] = tempHandler; @@ -152,21 +151,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); using var reader = new BinaryReader(lz4Stream); LoadedCharaFile = MareCharaFileHeader.FromBinaryReader(filePath, reader); - /*using var unwrapped2 = File.OpenRead(filePath); - using var lz4Stream2 = new LZ4Stream(unwrapped2, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); - using var reader2 = new BinaryReader(lz4Stream2); - using var writer = File.OpenWrite(filePath + ".raw"); - using var wr = new BinaryWriter(writer); - var bufferSize = 4 * 1024 * 1024; - var buffer = new byte[bufferSize]; - int chunk = 0; - int length = 0; - while ((length = reader2.Read(buffer)) > 0) - { - if (length < bufferSize) bufferSize = (int)length; - _logger.LogTrace($"Reading chunk {chunk++} {bufferSize}/{length} of {filePath}"); - wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); - }*/ + _logger.LogInformation("Read Mare Chara File"); _logger.LogInformation("Version: {ver}", (LoadedCharaFile?.Version ?? -1)); long expectedLength = 0; diff --git a/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs index 5878316..f208ee9 100644 --- a/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs @@ -27,4 +27,4 @@ public class FileDownloadManagerFactory { return new FileDownloadManager(_loggerFactory.CreateLogger(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor); } -} +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairFactory.cs b/MareSynchronos/PlayerData/Factories/PairFactory.cs index faf1161..31e952a 100644 --- a/MareSynchronos/PlayerData/Factories/PairFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairFactory.cs @@ -1,4 +1,5 @@ -using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.API.Dto.User; +using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using Microsoft.Extensions.Logging; @@ -21,8 +22,14 @@ public class PairFactory _serverConfigurationManager = serverConfigurationManager; } - public Pair Create() + public Pair Create(UserFullPairDto userPairDto) { - return new Pair(_loggerFactory.CreateLogger(), _cachedPlayerFactory, _mareMediator, _serverConfigurationManager); + return new Pair(_loggerFactory.CreateLogger(), userPairDto, _cachedPlayerFactory, _mareMediator, _serverConfigurationManager); + } + + public Pair Create(UserPairDto userPairDto) + { + return new Pair(_loggerFactory.CreateLogger(), new(userPairDto.User, userPairDto.IndividualPairStatus, [], userPairDto.OwnPermissions, userPairDto.OtherPermissions), + _cachedPlayerFactory, _mareMediator, _serverConfigurationManager); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index e0d2a27..30700fb 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -1,24 +1,25 @@ -using System.Diagnostics; -using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.API.Data.Enum; -using MareSynchronos.Interop; -using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using MareSynchronos.FileCache; -using Microsoft.Extensions.Logging; -using System.Globalization; +using MareSynchronos.Interop; using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Globalization; using CharacterData = MareSynchronos.PlayerData.Data.CharacterData; +using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Weapon = MareSynchronos.Interop.Weapon; namespace MareSynchronos.PlayerData.Factories; public class PlayerDataFactory { + private static readonly string[] _allowedExtensionsForGamePaths = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk"]; private readonly DalamudUtilService _dalamudUtil; private readonly FileCacheManager _fileCacheManager; private readonly IpcManager _ipcManager; @@ -26,8 +27,6 @@ public class PlayerDataFactory private readonly PerformanceCollectorService _performanceCollector; private readonly TransientResourceManager _transientResourceManager; - private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk" }; - public PlayerDataFactory(ILogger logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, PerformanceCollectorService performanceCollector) @@ -307,19 +306,16 @@ public class PlayerDataFactory _logger.LogDebug("Building character data for {obj}", playerRelatedObject); - if (!previousData.FileReplacements.ContainsKey(objectKind)) + if (!previousData.FileReplacements.TryGetValue(objectKind, out HashSet? value)) { previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); } else { - previousData.FileReplacements[objectKind].Clear(); + value.Clear(); } - if (previousData.CustomizePlusScale.ContainsKey(objectKind)) - { - previousData.CustomizePlusScale.Remove(objectKind); - } + previousData.CustomizePlusScale.Remove(objectKind); // wait until chara is not drawing and present so nothing spontaneously explodes await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false); @@ -341,9 +337,9 @@ public class PlayerDataFactory var (forwardResolve, reverseResolve) = await _dalamudUtil.RunOnFrameworkThread(() => BuildDataFromModel(objectKind, charaPointer, token)).ConfigureAwait(false); Dictionary> resolvedPaths = await GetFileReplacementsFromPaths(forwardResolve, reverseResolve).ConfigureAwait(false); previousData.FileReplacements[objectKind] = - new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value.ToArray(), c.Key)), FileReplacementComparer.Instance) + new HashSet(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) .Where(p => p.HasFileReplacement).ToHashSet(); - previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !AllowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !_allowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); _logger.LogDebug("== Static Replacements =="); foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) @@ -371,14 +367,14 @@ public class PlayerDataFactory var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)).ConfigureAwait(false); _logger.LogDebug("== Transient Replacements =="); - foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value.ToArray(), c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).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()); + _transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. previousData.FileReplacements[objectKind]]); // make sure we only return data that actually has file replacements foreach (var item in previousData.FileReplacements) @@ -407,16 +403,16 @@ public class PlayerDataFactory previousData.HeelsData = await getHeelsOffset.ConfigureAwait(false); _logger.LogDebug("Heels is now: {heels}", previousData.HeelsData); - if (previousData.FileReplacements.ContainsKey(objectKind)) + if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet? fileReplacements)) { - var toCompute = previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap).ToArray(); + var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray(); _logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray()); foreach (var file in toCompute) { file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty; } - var removed = previousData.FileReplacements[objectKind].RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash)); + var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash)); if (removed > 0) { _logger.LogDebug("Removed {amount} of invalid files", removed); @@ -444,7 +440,7 @@ public class PlayerDataFactory } else { - resolvedPaths[filePath] = new List { forwardPaths[i].ToLowerInvariant() }; + resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; } } diff --git a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs index f6c0ab7..84f979f 100644 --- a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -200,7 +200,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase if (_clearCts != null) { Logger.LogDebug("[{this}] Cancelling Clear Task", this); - _clearCts?.CancelDispose(); + _clearCts.CancelDispose(); _clearCts = null; } var chara = (Character*)Address; @@ -274,32 +274,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase } } - private unsafe bool CompareAndUpdateMainHand(Weapon* weapon) - { - if ((nint)weapon == nint.Zero) return false; - bool hasChanges = false; - hasChanges |= weapon->ModelSetId != MainHandData[0]; - MainHandData[0] = weapon->ModelSetId; - hasChanges |= weapon->Variant != MainHandData[1]; - MainHandData[1] = weapon->Variant; - hasChanges |= weapon->SecondaryId != MainHandData[2]; - MainHandData[2] = weapon->SecondaryId; - return hasChanges; - } - - private unsafe bool CompareAndUpdateOffHand(Weapon* weapon) - { - if ((nint)weapon == nint.Zero) return false; - bool hasChanges = false; - hasChanges |= weapon->ModelSetId != OffHandData[0]; - OffHandData[0] = weapon->ModelSetId; - hasChanges |= weapon->Variant != OffHandData[1]; - OffHandData[1] = weapon->Variant; - hasChanges |= weapon->SecondaryId != OffHandData[2]; - OffHandData[2] = weapon->SecondaryId; - return hasChanges; - } - private async Task ClearAsync(CancellationToken token) { Logger.LogDebug("[{this}] Running Clear Task", this); @@ -342,6 +316,32 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase return hasChanges; } + private unsafe bool CompareAndUpdateMainHand(Weapon* weapon) + { + if ((nint)weapon == nint.Zero) return false; + bool hasChanges = false; + hasChanges |= weapon->ModelSetId != MainHandData[0]; + MainHandData[0] = weapon->ModelSetId; + hasChanges |= weapon->Variant != MainHandData[1]; + MainHandData[1] = weapon->Variant; + hasChanges |= weapon->SecondaryId != MainHandData[2]; + MainHandData[2] = weapon->SecondaryId; + return hasChanges; + } + + private unsafe bool CompareAndUpdateOffHand(Weapon* weapon) + { + if ((nint)weapon == nint.Zero) return false; + bool hasChanges = false; + hasChanges |= weapon->ModelSetId != OffHandData[0]; + OffHandData[0] = weapon->ModelSetId; + hasChanges |= weapon->Variant != OffHandData[1]; + OffHandData[1] = weapon->Variant; + hasChanges |= weapon->SecondaryId != OffHandData[2]; + OffHandData[2] = weapon->SecondaryId; + return hasChanges; + } + private void FrameworkUpdate() { if (!_delayedZoningTask?.IsCompleted ?? false) return; diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index 093d738..8dffb29 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -32,6 +32,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private GameObjectHandler? _charaHandler; private CancellationTokenSource? _downloadCancellationTokenSource = new(); private bool _forceApplyMods = false; + private bool _isVisible; private string _penumbraCollection; public PairHandler(ILogger logger, OnlineUserIdentDto onlineUser, @@ -71,7 +72,18 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase }); } - public bool IsVisible { get; private set; } + public bool IsVisible + { + get => _isVisible; + private set + { + if (_isVisible != value) + { + _isVisible = value; + Mediator.Publish(new RefreshUiMessage()); + } + } + } public OnlineUserIdentDto OnlineUser { get; private set; } public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero; public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero @@ -87,14 +99,14 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}", applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero); var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger, - this, forceApplyCustomization, false).Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); + this, forceApplyCustomization, forceApplyMods: false).Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); _forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null); _cachedData = characterData; Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); return; } - SetUploading(false); + SetUploading(isUploading: false); Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods); Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA"); @@ -146,7 +158,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { base.Dispose(disposing); - SetUploading(false); + SetUploading(isUploading: false); _downloadManager.Dispose(); var name = PlayerName; Logger.LogDebug("Disposing {name} ({user})", name, OnlineUser); @@ -167,7 +179,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); _ipcManager.PenumbraRemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult(); - foreach (KeyValuePair> item in _cachedData?.FileReplacements ?? new()) + foreach (KeyValuePair> item in _cachedData?.FileReplacements ?? []) { try { @@ -201,15 +213,14 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase var handler = changes.Key switch { ObjectKind.Player => _charaHandler!, - ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanion(ptr), false).ConfigureAwait(false), - ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), false).ConfigureAwait(false), - ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), false).ConfigureAwait(false), + ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanion(ptr), isWatched: false).ConfigureAwait(false), + ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false).ConfigureAwait(false), + ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false).ConfigureAwait(false), _ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key) }; try { - bool alreadyRedrawn = false; if (handler.Address == nint.Zero) { return; @@ -262,7 +273,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (changes.Value.Contains(PlayerChanges.ModFiles) || changes.Value.Contains(PlayerChanges.ModManip) || changes.Value.Contains(PlayerChanges.Glamourer)) await _ipcManager.PenumbraRedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); - } finally { @@ -312,7 +322,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - if (toDownloadReplacements.All(c => _downloadManager.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) + if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) { break; } @@ -396,7 +406,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase var pc = _dalamudUtil.FindPlayerByNameHash(OnlineUser.Ident); if (pc == default((string, nint))) return; Logger.LogDebug("One-Time Initializing {this}", this); - Initialize(pc.Name.ToString()); + Initialize(pc.Name); Logger.LogDebug("One-Time Initialized {this}", this); } @@ -411,7 +421,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _ = Task.Run(() => { - ApplyCharacterData(appData, _cachedData!, true); + ApplyCharacterData(appData, _cachedData!, forceApplyCustomization: true); }); } else @@ -432,7 +442,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private void Initialize(string name) { PlayerName = name; - _charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(OnlineUser.Ident), false).GetAwaiter().GetResult(); + _charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(OnlineUser.Ident), isWatched: false).GetAwaiter().GetResult(); Mediator.Subscribe(this, async (_) => { @@ -456,7 +466,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (objectKind == ObjectKind.Player) { - using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, false).ConfigureAwait(false); + using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); @@ -479,7 +489,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (minionOrMount != nint.Zero) { await _ipcManager.CustomizePlusRevertAsync(minionOrMount).ConfigureAwait(false); - using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, false).ConfigureAwait(false); + using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false); await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); } @@ -490,7 +500,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (pet != nint.Zero) { await _ipcManager.CustomizePlusRevertAsync(pet).ConfigureAwait(false); - using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, false).ConfigureAwait(false); + using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false); await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); } @@ -501,7 +511,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (companion != nint.Zero) { await _ipcManager.CustomizePlusRevertAsync(companion).ConfigureAwait(false); - using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, false).ConfigureAwait(false); + using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false); await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); } @@ -513,7 +523,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private List TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary moddedDictionary, CancellationToken token) { Stopwatch st = Stopwatch.StartNew(); - ConcurrentBag missingFiles = new(); + ConcurrentBag missingFiles = []; moddedDictionary = new Dictionary(StringComparer.Ordinal); ConcurrentDictionary outputDict = new(StringComparer.Ordinal); bool hasMigrationChanges = false; @@ -535,7 +545,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension)) { hasMigrationChanges = true; - fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".").Last()); + fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]); } foreach (var gamePath in item.GamePaths) @@ -568,6 +578,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv(); st.Stop(); Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count); - return missingFiles.ToList(); + return [.. missingFiles]; } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs index 23e0610..197eaef 100644 --- a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs +++ b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs @@ -14,7 +14,7 @@ public class OnlinePlayerManager : DisposableMediatorSubscriberBase private readonly ApiController _apiController; private readonly DalamudUtilService _dalamudUtil; private readonly FileUploadManager _fileTransferManager; - private readonly HashSet _newVisiblePlayers = new(); + private readonly HashSet _newVisiblePlayers = []; private readonly PairManager _pairManager; private CharacterData? _lastSentData; diff --git a/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs index 182ee19..2aaa72e 100644 --- a/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs +++ b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs @@ -6,4 +6,4 @@ public record OptionalPluginWarning public bool ShownCustomizePlusWarning { get; set; } = false; public bool ShownPalettePlusWarning { get; set; } = false; public bool ShownHonorificWarning { get; set; } = false; -} +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs index e8e6a2a..cbeb5e5 100644 --- a/MareSynchronos/PlayerData/Pairs/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -1,8 +1,8 @@ using Dalamud.ContextMenu; +using Dalamud.Game.Text.SeStringHandling; using MareSynchronos.API.Data; -using MareSynchronos.API.Data.Comparer; +using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Extensions; -using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.User; using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Handlers; @@ -23,54 +23,55 @@ public class Pair private CancellationTokenSource _applicationCts = new CancellationTokenSource(); private OnlineUserIdentDto? _onlineUserIdentDto = null; - public Pair(ILogger logger, PairHandlerFactory cachedPlayerFactory, + public Pair(ILogger logger, UserFullPairDto userPair, PairHandlerFactory cachedPlayerFactory, MareMediator mediator, ServerConfigurationManager serverConfigurationManager) { _logger = logger; + UserPair = userPair; _cachedPlayerFactory = cachedPlayerFactory; _mediator = mediator; _serverConfigurationManager = serverConfigurationManager; } - public Dictionary GroupPair { get; set; } = new(GroupDtoComparer.Instance); public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null; + public IndividualPairStatus IndividualPairStatus => UserPair.IndividualPairStatus; + public bool IsDirectlyPaired => IndividualPairStatus != IndividualPairStatus.None; + public bool IsOneSidedPair => IndividualPairStatus == IndividualPairStatus.OneSided; public bool IsOnline => CachedPlayer != null; - 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 bool IsPaired => IndividualPairStatus == IndividualPairStatus.Bidirectional || UserPair.Groups.Any(); + public bool IsPaused => UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused(); public bool IsVisible => CachedPlayer?.IsVisible ?? false; public CharacterData? LastReceivedCharacterData { get; set; } public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; - public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; - - public UserPairDto? UserPair { get; set; } + public UserData UserData => UserPair.User; + public UserFullPairDto UserPair { get; set; } private PairHandler? CachedPlayer { get; set; } public void AddContextMenu(GameObjectContextMenuOpenArgs args) { - if (CachedPlayer == null || args.ObjectId != CachedPlayer.PlayerCharacterId) return; + if (CachedPlayer == null || args.ObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return; - if (!IsPaused) + SeStringBuilder seStringBuilder = new(); + SeStringBuilder seStringBuilder2 = new(); + SeStringBuilder seStringBuilder3 = new(); + var openProfileSeString = seStringBuilder.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Open Profile").Build(); + var reapplyDataSeString = seStringBuilder2.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Reapply last data").Build(); + var cyclePauseState = seStringBuilder3.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Cycle pause state").Build(); + args.AddCustomItem(new GameObjectContextMenuItem(openProfileSeString, (a) => { - args.AddCustomItem(new GameObjectContextMenuItem("[Mare] Open Profile", (a) => - { - _mediator.Publish(new ProfileOpenStandaloneMessage(this)); - })); - } - args.AddCustomItem(new GameObjectContextMenuItem("[Mare] Reapply last data", (a) => + _mediator.Publish(new ProfileOpenStandaloneMessage(this)); + })); + args.AddCustomItem(new GameObjectContextMenuItem(reapplyDataSeString, (a) => { - ApplyLastReceivedData(true); - }, false)); - if (UserPair != null && UserPair.OtherPermissions.IsPaired() && UserPair.OwnPermissions.IsPaired()) + ApplyLastReceivedData(forced: true); + }, useDalamudIndicator: false)); + args.AddCustomItem(new GameObjectContextMenuItem(cyclePauseState, (a) => { - args.AddCustomItem(new GameObjectContextMenuItem("[Mare] Cycle pause state", (a) => - { - _mediator.Publish(new CyclePauseMessage(UserData)); - }, false)); - } + _mediator.Publish(new CyclePauseMessage(UserData)); + }, useDalamudIndicator: false)); } public void ApplyData(OnlineUserCharaDataDto data) @@ -152,7 +153,7 @@ public class Pair public bool HasAnyConnection() { - return UserPair != null || GroupPair.Any(); + return UserPair.Groups.Any() || UserPair.IndividualPairStatus != IndividualPairStatus.None; } public void MarkOffline() @@ -191,37 +192,29 @@ public class Pair return data; } - bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations()); - bool disableIndividualVFX = UserPair != null && (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX()); - bool disableGroupAnimations = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations()); + bool disableIndividualAnimations = (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations()); + bool disableIndividualVFX = (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX()); + bool disableIndividualSounds = (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds()); - bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations); + _logger.LogTrace("Disable: Sounds: {disableIndividualSounds}, Anims: {disableIndividualAnims}; " + + "VFX: {disableGroupSounds}", + disableIndividualSounds, disableIndividualAnimations, disableIndividualVFX); - 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 disableGroupVFX = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableVFX() || pair.Key.GroupPermissions.IsDisableVFX() || pair.Key.GroupUserPermissions.IsDisableVFX()); - - bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds); - bool disableVFX = (UserPair != null && disableIndividualVFX) || (UserPair == null && disableGroupVFX); - - _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) + if (disableIndividualAnimations || disableIndividualSounds || disableIndividualVFX) { - _logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}", disableAnimations, disableSounds, disableVFX); + _logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}", + disableIndividualAnimations, disableIndividualSounds, disableIndividualVFX); foreach (var objectKind in data.FileReplacements.Select(k => k.Key)) { - if (disableSounds) + if (disableIndividualSounds) data.FileReplacements[objectKind] = data.FileReplacements[objectKind] .Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase))) .ToList(); - if (disableAnimations) + if (disableIndividualAnimations) data.FileReplacements[objectKind] = data.FileReplacements[objectKind] .Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase))) .ToList(); - if (disableVFX) + if (disableIndividualVFX) data.FileReplacements[objectKind] = data.FileReplacements[objectKind] .Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase))) .ToList(); diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs index 2e2179a..f1ac38d 100644 --- a/MareSynchronos/PlayerData/Pairs/PairManager.cs +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -22,6 +22,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase private readonly PairFactory _pairFactory; private Lazy> _directPairsInternal; private Lazy>> _groupPairsInternal; + private Lazy>> _pairsWithGroupsInternal; public PairManager(ILogger logger, PairFactory pairFactory, MareConfigService configurationService, MareMediator mediator, @@ -34,6 +35,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (_) => ReapplyPairData()); _directPairsInternal = DirectPairsLazy(); _groupPairsInternal = GroupPairsLazy(); + _pairsWithGroupsInternal = PairsWithGroupsLazy(); _dalamudContextMenu.OnOpenGameObjectContextMenu += DalamudContextMenuOnOnOpenGameObjectContextMenu; } @@ -41,8 +43,9 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public List DirectPairs => _directPairsInternal.Value; public Dictionary> GroupPairs => _groupPairsInternal.Value; - + public Dictionary Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value); public Pair? LastAddedUser { get; internal set; } + public Dictionary> PairsWithGroups => _pairsWithGroupsInternal.Value; public void AddGroup(GroupFullInfoDto dto) { @@ -52,10 +55,25 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public void AddGroupPair(GroupPairFullInfoDto dto) { - if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Create(); + if (!_allClientPairs.ContainsKey(dto.User)) + _allClientPairs[dto.User] = _pairFactory.Create(new UserFullPairDto(dto.User, API.Data.Enum.IndividualPairStatus.None, + [dto.Group.GID], dto.SelfToOtherPermissions, dto.OtherToSelfPermissions)); + else _allClientPairs[dto.User].UserPair.Groups.Add(dto.GID); + RecreateLazy(); + } + + public void AddUserPair(UserFullPairDto dto) + { + if (!_allClientPairs.ContainsKey(dto.User)) + { + _allClientPairs[dto.User] = _pairFactory.Create(dto); + } + else + { + _allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus; + _allClientPairs[dto.User].ApplyLastReceivedData(); + } - var group = _allGroups[dto.Group]; - _allClientPairs[dto.User].GroupPair[group] = dto; RecreateLazy(); } @@ -63,14 +81,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { if (!_allClientPairs.ContainsKey(dto.User)) { - _allClientPairs[dto.User] = _pairFactory.Create(); + _allClientPairs[dto.User] = _pairFactory.Create(dto); } else { addToLastAddedUser = false; } - _allClientPairs[dto.User].UserPair = dto; + _allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus; + _allClientPairs[dto.User].UserPair.OwnPermissions = dto.OwnPermissions; + _allClientPairs[dto.User].UserPair.OtherPermissions = dto.OtherPermissions; if (addToLastAddedUser) LastAddedUser = _allClientPairs[dto.User]; _allClientPairs[dto.User].ApplyLastReceivedData(); @@ -88,18 +108,19 @@ public sealed class PairManager : DisposableMediatorSubscriberBase public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList(); - public List GetVisibleUsers() => _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key).ToList(); - public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible); + public List GetVisibleUsers() => _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key).ToList(); + public void MarkPairOffline(UserData user) { if (_allClientPairs.TryGetValue(user, out var pair)) { Mediator.Publish(new ClearProfileDataMessage(pair.UserData)); pair.MarkOffline(); - RecreateLazy(); } + + RecreateLazy(); } public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true) @@ -109,7 +130,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase Mediator.Publish(new ClearProfileDataMessage(dto.User)); var pair = _allClientPairs[dto.User]; - if (pair.HasCachedPlayer) return; + if (pair.HasCachedPlayer) + { + RecreateLazy(); + return; + } if (sendNotif && _configurationService.Current.ShowOnlineNotifications && (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null @@ -125,6 +150,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase } pair.CreateCachedPlayer(dto); + RecreateLazy(); } @@ -138,18 +164,18 @@ public sealed class PairManager : DisposableMediatorSubscriberBase 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); - } + item.Value.UserPair.Groups.Remove(data.GID); - if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair)) + if (!item.Value.HasAnyConnection()) { - pair.MarkOffline(); + item.Value.MarkOffline(); + _allClientPairs.TryRemove(item.Key, out _); } } + RecreateLazy(); } @@ -157,36 +183,32 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { if (_allClientPairs.TryGetValue(dto.User, out var pair)) { - var group = _allGroups[dto.Group]; - pair.GroupPair.Remove(group); + pair.UserPair.Groups.Remove(dto.Group.GID); if (!pair.HasAnyConnection()) { pair.MarkOffline(); _allClientPairs.TryRemove(dto.User, out _); } - - RecreateLazy(); } + + RecreateLazy(); } public void RemoveUserPair(UserDto dto) { if (_allClientPairs.TryGetValue(dto.User, out var pair)) { - pair.UserPair = null; + pair.UserPair.IndividualPairStatus = API.Data.Enum.IndividualPairStatus.None; + if (!pair.HasAnyConnection()) { pair.MarkOffline(); _allClientPairs.TryRemove(dto.User, out _); } - else - { - pair.ApplyLastReceivedData(); - } - - RecreateLazy(); } + + RecreateLazy(); } public void SetGroupInfo(GroupInfoDto dto) @@ -194,6 +216,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase _allGroups[dto.Group].Group = dto.Group; _allGroups[dto.Group].Owner = dto.Owner; _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; + RecreateLazy(); } @@ -206,18 +229,22 @@ public sealed class PairManager : DisposableMediatorSubscriberBase if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); - if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused() - || pair.UserPair.OtherPermissions.IsPaired() != dto.Permissions.IsPaired()) + if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused()) { Mediator.Publish(new ClearProfileDataMessage(dto.User)); } pair.UserPair.OtherPermissions = dto.Permissions; - Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", - pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds(), - pair.UserPair.OwnPermissions.IsDisableVFX()); + Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", + pair.UserPair.OtherPermissions.IsPaused(), + pair.UserPair.OtherPermissions.IsDisableAnimations(), + pair.UserPair.OtherPermissions.IsDisableSounds(), + pair.UserPair.OtherPermissions.IsDisableVFX()); + pair.ApplyLastReceivedData(); + + RecreateLazy(); } public void UpdateSelfPairPermissions(UserPermissionsDto dto) @@ -227,21 +254,22 @@ public sealed class PairManager : DisposableMediatorSubscriberBase throw new InvalidOperationException("No such pair for " + dto); } - if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); - - if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused() - || pair.UserPair.OwnPermissions.IsPaired() != dto.Permissions.IsPaired()) + if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused()) { Mediator.Publish(new ClearProfileDataMessage(dto.User)); } pair.UserPair.OwnPermissions = dto.Permissions; - Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", - pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds(), + Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", + pair.UserPair.OwnPermissions.IsPaused(), + pair.UserPair.OwnPermissions.IsDisableAnimations(), + pair.UserPair.OwnPermissions.IsDisableSounds(), pair.UserPair.OwnPermissions.IsDisableVFX()); pair.ApplyLastReceivedData(); + + RecreateLazy(); } internal void ReceiveUploadStatus(UserDto dto) @@ -254,60 +282,37 @@ public sealed class PairManager : DisposableMediatorSubscriberBase internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) { - var group = _allGroups[dto.Group]; - _allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo; - RecreateLazy(); - } - - internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto 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() - || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) - { - _allClientPairs[dto.User].ApplyLastReceivedData(); - } + _allGroups[dto.Group].GroupPairUserInfos[dto.UID] = dto.GroupUserInfo; RecreateLazy(); } internal void SetGroupPermissions(GroupPermissionDto dto) { - var prevPermissions = _allGroups[dto.Group].GroupPermissions; _allGroups[dto.Group].GroupPermissions = dto.Permissions; - if (prevPermissions.IsDisableAnimations() != dto.Permissions.IsDisableAnimations() - || prevPermissions.IsDisableSounds() != dto.Permissions.IsDisableSounds() - || prevPermissions.IsDisableVFX() != dto.Permissions.IsDisableVFX()) - { - RecreateLazy(); - var group = _allGroups[dto.Group]; - GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); - } RecreateLazy(); } internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) { _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; + RecreateLazy(); } - internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto) + internal void UpdateGroupPairPermissions(GroupPairUserPermissionDto dto) { - var prevPermissions = _allGroups[dto.Group].GroupUserPermissions; _allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions; - if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() - || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds() - || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) - { - RecreateLazy(); - var group = _allGroups[dto.Group]; - GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); - } RecreateLazy(); } + internal void UpdateIndividualPairStatus(UserIndividualPairStatusDto dto) + { + if (_allClientPairs.TryGetValue(dto.User, out var pair)) + { + pair.UserPair.IndividualPairStatus = dto.IndividualPairStatus; + RecreateLazy(); + } + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -328,7 +333,8 @@ public sealed class PairManager : DisposableMediatorSubscriberBase } } - private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value).Where(k => k.UserPair != null).ToList()); + private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value) + .Where(k => k.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None).ToList()); private void DisposePairs() { @@ -345,20 +351,35 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { return new Lazy>>(() => { - Dictionary> outDict = new(); + Dictionary> outDict = []; 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(); + outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.UserPair.Groups.Exists(g => GroupDataComparer.Instance.Equals(group.Key, new(g)))).ToList(); } return outDict; }); } + private Lazy>> PairsWithGroupsLazy() + { + return new Lazy>>(() => + { + Dictionary> outDict = []; + + foreach (var pair in _allClientPairs.Select(k => k.Value)) + { + outDict[pair] = _allGroups.Where(k => pair.UserPair.Groups.Contains(k.Key.GID, StringComparer.Ordinal)).Select(k => k.Value).ToList(); + } + + return outDict; + }); + } + private void ReapplyPairData() { foreach (var pair in _allClientPairs.Select(k => k.Value)) { - pair.ApplyLastReceivedData(true); + pair.ApplyLastReceivedData(forced: true); } } @@ -366,5 +387,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { _directPairsInternal = DirectPairsLazy(); _groupPairsInternal = GroupPairsLazy(); + _pairsWithGroupsInternal = PairsWithGroupsLazy(); + Mediator.Publish(new RefreshUiMessage()); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Services/CacheCreationService.cs b/MareSynchronos/PlayerData/Services/CacheCreationService.cs index 0bc45e6..9a80d18 100644 --- a/MareSynchronos/PlayerData/Services/CacheCreationService.cs +++ b/MareSynchronos/PlayerData/Services/CacheCreationService.cs @@ -8,14 +8,16 @@ using Microsoft.Extensions.Logging; namespace MareSynchronos.PlayerData.Services; +#pragma warning disable MA0040 + public sealed class CacheCreationService : DisposableMediatorSubscriberBase { private readonly SemaphoreSlim _cacheCreateLock = new(1); - private readonly Dictionary _cachesToCreate = new(); + private readonly Dictionary _cachesToCreate = []; private readonly PlayerDataFactory _characterDataFactory; private readonly CancellationTokenSource _cts = new(); private readonly CharacterData _playerData = new(); - private readonly Dictionary _playerRelatedObjects = new(); + private readonly Dictionary _playerRelatedObjects = []; private Task? _cacheCreationTask; private CancellationTokenSource _honorificCts = new(); private bool _isZoning = false; @@ -37,13 +39,13 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (msg) => _isZoning = true); Mediator.Subscribe(this, (msg) => _isZoning = false); - _playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, true) + _playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true) .GetAwaiter().GetResult(); - _playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), true) + _playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true) .GetAwaiter().GetResult(); - _playerRelatedObjects[ObjectKind.Pet] = gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), true) + _playerRelatedObjects[ObjectKind.Pet] = gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), isWatched: true) .GetAwaiter().GetResult(); - _playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), true) + _playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true) .GetAwaiter().GetResult(); Mediator.Subscribe(this, (msg) => @@ -62,7 +64,7 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase { if (_isZoning) return; foreach (var item in _playerRelatedObjects - .Where(item => string.IsNullOrEmpty(msg.ProfileName) + .Where(item => string.IsNullOrEmpty(msg.ProfileName) || string.Equals(item.Value.Name, msg.ProfileName, StringComparison.Ordinal)).Select(k => k.Key)) { Logger.LogDebug("Received CustomizePlus change, updating {obj}", item); @@ -182,4 +184,5 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase Logger.LogDebug("Cache Creation stored until previous creation finished"); } } -} \ No newline at end of file +} +#pragma warning restore MA0040 \ No newline at end of file diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 797ff15..e688f83 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -14,6 +14,8 @@ using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI; +using MareSynchronos.UI.Components; +using MareSynchronos.UI.Components.Popup; using MareSynchronos.UI.Handlers; using MareSynchronos.WebAPI; using MareSynchronos.WebAPI.Files; @@ -59,14 +61,19 @@ public sealed class Plugin : IDalamudPlugin 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((s) => new DalamudContextMenu(pluginInterface)); collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService>(), clientState, objectTable, framework, gameGui, condition, gameData, @@ -83,7 +90,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), pluginInterface)); collection.AddSingleton((s) => new HubFactory(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), pluginLog)); + s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), pluginLog)); // func factory method singletons collection.AddSingleton(s => @@ -92,7 +100,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), pair))); + s.GetRequiredService(), + s.GetRequiredService(), pair))); // add scoped services collection.AddScoped(); @@ -106,6 +115,12 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface.UiBuilder, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -113,7 +128,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService(), s.GetServices(), s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), s.GetRequiredService(), + collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new NotificationService(s.GetRequiredService>(), @@ -133,8 +148,6 @@ public sealed class Plugin : IDalamudPlugin .RunAsync(_pluginCts.Token); } - public string Name => "Mare Synchronos"; - public void Dispose() { _pluginCts.Cancel(); diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index ab5ae32..6ae1b25 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -1,11 +1,11 @@ -using MareSynchronos.API.Data; +using Lumina.Data.Files; +using MareSynchronos.API.Data; using MareSynchronos.API.Data.Enum; using MareSynchronos.FileCache; using MareSynchronos.Services.Mediator; using MareSynchronos.UI; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; -using Lumina.Data.Files; namespace MareSynchronos.Services; @@ -14,7 +14,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable private readonly FileCacheManager _fileCacheManager; private CancellationTokenSource? _analysisCts; private string _lastDataHash = string.Empty; - internal Dictionary> LastAnalysis { get; } = new(); public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager) : base(logger, mediator) { @@ -25,10 +24,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _fileCacheManager = fileCacheManager; } - public bool IsAnalysisRunning => _analysisCts != null; - public int CurrentFile { get; internal set; } + public bool IsAnalysisRunning => _analysisCts != null; public int TotalFiles { get; internal set; } + internal Dictionary> LastAnalysis { get; } = []; public void CancelAnalyze() { @@ -74,6 +73,11 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable if (print) PrintAnalysis(); } + public void Dispose() + { + _analysisCts.CancelDispose(); + } + private void BaseAnalysis(CharacterData charaData) { if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return; @@ -103,7 +107,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable foreach (var entry in fileCacheEntries) { data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, - fileEntry.GamePaths.ToList(), + [.. fileEntry.GamePaths], fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0); } @@ -133,7 +137,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { Logger.LogInformation(" Game Path: {path}", path); } - if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected", entry.Key); + if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected for {key}", entry.Key); foreach (var filePath in entry.Value.FilePaths) { Logger.LogInformation(" File Path: {path}", filePath); @@ -163,11 +167,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); } - public void Dispose() - { - _analysisCts.CancelDispose(); - } - internal sealed record FileDataEntry(string Hash, string FileType, List GamePaths, List FilePaths, long OriginalSize, long CompressedSize) { public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; @@ -205,11 +204,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { return "Unknown"; } - } default: return string.Empty; } }); } -} +} \ No newline at end of file diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs index cb10b60..db9285e 100644 --- a/MareSynchronos/Services/CommandManagerService.cs +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -5,6 +5,7 @@ using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI; using MareSynchronos.WebAPI; +using System.Globalization; namespace MareSynchronos.Services; @@ -18,15 +19,13 @@ public sealed class CommandManagerService : IDisposable private readonly PerformanceCollectorService _performanceCollectorService; private readonly PeriodicFileScanner _periodicFileScanner; private readonly ServerConfigurationManager _serverConfigurationManager; - private readonly UiService _uiService; public CommandManagerService(ICommandManager commandManager, PerformanceCollectorService performanceCollectorService, - UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, + ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, ApiController apiController, MareMediator mediator) { _commandManager = commandManager; _performanceCollectorService = performanceCollectorService; - _uiService = uiService; _serverConfigurationManager = serverConfigurationManager; _periodicFileScanner = periodicFileScanner; _apiController = apiController; @@ -49,7 +48,7 @@ public sealed class CommandManagerService : IDisposable if (splitArgs == null || splitArgs.Length == 0) { // Interpret this as toggling the UI - _uiService.ToggleMainUi(); + _mediator.Publish(new UiToggleMessage(typeof(CompactUi))); return; } @@ -86,7 +85,7 @@ public sealed class CommandManagerService : IDisposable } else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase)) { - if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds)) + if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], CultureInfo.InvariantCulture, out var limitBySeconds)) { _performanceCollectorService.PrintPerformanceStats(limitBySeconds); } diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs index ce0fa8d..9a662ed 100644 --- a/MareSynchronos/Services/DalamudUtilService.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -17,7 +17,7 @@ namespace MareSynchronos.Services; public class DalamudUtilService : IHostedService { - private readonly List _classJobIdsIgnoredForPets = new() { 30 }; + private readonly List _classJobIdsIgnoredForPets = [30]; private readonly IClientState _clientState; private readonly ICondition _condition; private readonly IFramework _framework; @@ -59,6 +59,7 @@ public class DalamudUtilService : IHostedService public bool IsInCutscene { get; private set; } = false; public bool IsInGpose { get; private set; } = false; public bool IsLoggedIn { get; private set; } + public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; public Lazy> WorldData { get; private set; } @@ -74,8 +75,6 @@ public class DalamudUtilService : IHostedService return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false); } - public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; - public void EnsureIsOnFramework() { if (!_framework.IsInFrameworkUpdateThread) throw new InvalidOperationException("Can only be run on Framework"); @@ -89,13 +88,6 @@ public class DalamudUtilService : IHostedService return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; } - public Dalamud.Game.ClientState.Objects.Types.Character? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false) - { - EnsureIsOnFramework(); - return (Dalamud.Game.ClientState.Objects.Types.Character?)_objectTable.Where(i => !onlyGposeCharacters || i.ObjectIndex >= 200) - .FirstOrDefault(f => f.Name.ToString() == name); - } - public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) { EnsureIsOnFramework(); @@ -110,6 +102,13 @@ public class DalamudUtilService : IHostedService return await RunOnFrameworkThread(() => GetCompanion(playerPointer)).ConfigureAwait(false); } + public Dalamud.Game.ClientState.Objects.Types.Character? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false) + { + EnsureIsOnFramework(); + return (Dalamud.Game.ClientState.Objects.Types.Character?)_objectTable + .FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal)); + } + public bool GetIsPlayerPresent() { EnsureIsOnFramework(); @@ -351,19 +350,18 @@ public class DalamudUtilService : IHostedService if (!isDrawing) { isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0; - if (isDrawing) + if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal)) { - if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) && !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded")) - { - _lastGlobalBlockPlayer = playerName; - _lastGlobalBlockReason = "HasModelFilesInSlotLoaded"; - isDrawingChanged = true; - } + _lastGlobalBlockPlayer = playerName; + _lastGlobalBlockReason = "HasModelFilesInSlotLoaded"; + isDrawingChanged = true; } } else { - if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) && !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded")) + if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal)) { _lastGlobalBlockPlayer = playerName; _lastGlobalBlockReason = "HasModelInSlotLoaded"; @@ -373,7 +371,8 @@ public class DalamudUtilService : IHostedService } else { - if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) && !string.Equals(_lastGlobalBlockReason, "RenderFlags")) + if (!string.Equals(_lastGlobalBlockPlayer, playerName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "RenderFlags", StringComparison.Ordinal)) { _lastGlobalBlockPlayer = playerName; _lastGlobalBlockReason = "RenderFlags"; diff --git a/MareSynchronos/Services/MareProfileData.cs b/MareSynchronos/Services/MareProfileData.cs new file mode 100644 index 0000000..6acf9ba --- /dev/null +++ b/MareSynchronos/Services/MareProfileData.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.Services; + +public record MareProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description) +{ + public Lazy ImageData { get; } = new Lazy(Convert.FromBase64String(Base64ProfilePicture)); + public Lazy SupporterImageData { get; } = new Lazy(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture)); +} diff --git a/MareSynchronos/Services/MareProfileManager.cs b/MareSynchronos/Services/MareProfileManager.cs index 516dfeb..191f6d2 100644 --- a/MareSynchronos/Services/MareProfileManager.cs +++ b/MareSynchronos/Services/MareProfileManager.cs @@ -8,12 +8,6 @@ using System.Collections.Concurrent; namespace MareSynchronos.Services; -public record MareProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description) -{ - public Lazy ImageData { get; } = new Lazy(Convert.FromBase64String(Base64ProfilePicture)); - public Lazy SupporterImageData { get; } = new Lazy(string.IsNullOrEmpty(Base64SupporterPicture) ? Array.Empty() : Convert.FromBase64String(Base64SupporterPicture)); -} - public class MareProfileManager : MediatorSubscriberBase { private const string _mareLogo = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAFQGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpDb2xvclNwYWNlPSIxIgogICBleGlmOlBpeGVsWERpbWVuc2lvbj0iMjU2IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjU2IgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjI1NiIKICAgdGlmZjpJbWFnZVdpZHRoPSIyNTYiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjcyLzEiCiAgIHRpZmY6WVJlc29sdXRpb249IjcyLzEiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDMtMThUMjM6MTc6NDIrMDE6MDAiCiAgIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTAzLTE4VDIzOjE3OjQyKzAxOjAwIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHhtcE1NOmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHhtcE1NOnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDEuMTAuNSIKICAgICAgeG1wTU06d2hlbj0iMjAyMi0wNi0xNFQyMzo1MzoxNyswMjowMCIvPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJwcm9kdWNlZCIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWZmaW5pdHkgUGhvdG8gMS4xMC41IgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTAzLTE4VDIzOjE3OjQyKzAxOjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cjw/eHBhY2tldCBlbmQ9InIiPz5Di0U+AAABgWlDQ1BzUkdCIElFQzYxOTY2LTIuMQAAKJF1kc8rRFEUxz8zQ8TIz2RhMWlYIUZNbCxmYigsZkYZbGbe/FLz4/XeTJpsle0UJTZ+LfgL2CprpYiUbFkTG/ScZ9RMMud27vnc773ndO+5YA2mlLReMwTpTE7z+zyOhdCio+4JG2100kprWNHV2cBkkKr2fovFjNcDZq3q5/61xmhMV8BSLzyuqFpOeEp4ZjWnmrwl3KEkw1HhE+F+TS4ofGPqkRI/m5wo8afJWtDvBWuLsCNRwZEKVpJaWlhejjOdyiu/9zFfYo9l5gMSe8S70fHjw4ODaSbw4maYMZndDOBiUFZUyR/6yZ8jK7mKzCoFNFZIkCRHv6h5qR6TGBc9JiNFwez/377q8RFXqbrdA7WPhvHaC3Wb8FU0jI8Dw/g6BNsDnGfK+dl9GH0TvVjWnHvQvA6nF2Utsg1nG9B1r4a18I9kE7fG4/ByDE0haL+ChqVSz373ObqD4Jp81SXs7EKfnG9e/gY/0WfU3EFh5gAAAAlwSFlzAAALEwAACxMBAJqcGAAAIABJREFUeJzsvXuQHFd5N/ycS3dPz31nd7Wr1a4k62ZZDsYQgiHkxdxJIHm/jy+8Sb2VpFKpVOXv5J9QLhJCinCpgqRCCH9AQooqAqSIg00AU4AB35FlI8uyrLusXV33vnPv+znfH9NP7zOtmdmVLFkS2VM1NT3dPadPd5/n9nsuh8Ht21j8yQFABgD0zR3ORttot0VTABDEH/8mj2WjbbSNdjMbu9kDuE7tl+U+NtpGey3ahra80TbaRttoG22jbbSNttE22v/MtmE7/xI1rfV1e5+MMa21ZoyxDXvxl7htMIDbpA0gbtyfJtRrebe0D9ajz86BDabwS9M2GMAt1tYhxdPHb+Q7TBP6FYS/wQxu77bBAG5y60HwKHlZal+6rXV8PccABruEdI9z+m1vMIPbsG0wgNe49ZHw/Yi51zZGQOrUvvX013NIfbbpbz1gu+t/G0zg9mobDOA1aH2kfL/tNKGnz+u3f73bXUPr87vXd/oD0Akr3WACt3HbYAA3oK0h5QcRcj8GkN7X61ivPtPHew63x3Yvgk9/VJ/9nYtuMIHbom0wgOvUBhD9WhIef/Me+/udfz0YQS/pTxlAmsAV2af6HN9gArdZkzd7ALd7G6DeDyL2foSMTIB+9zpnUF90DOnxdA19wHcvQlcAEMXfeC08jmNVqb422i3eNhjANbR12PTr/aQJnPfY32+bxYE6TCnFAIBxzpP9WmvgnEMYhkxrDYZhAABAFEWMMYaBPqCUAiGEZozpMAw1Y0wLIRQAKKWUAgDFOUfC51rriDEWQTdjicg2gpO3WiDR9dR2b5V7etVtgwFcRVuHtF8vsVNCTxP9wH1RFHFC6DwMQ2YYhgYA7nkel1IyzjkAAHMch9m2rQAAWq2WwRjT2Ww2AgAWBAEzDENxzpXv+1wppTOZTAgA2nVdMAwjEkJEAKBc140ymUyoteZBEHAhBIuPAXSIH6U/3t/NIJD1Evi1MIJBQVa3NTPYwADW0VKEvx5pnyb2fsTd96O15lEUcSkli6JIKKW4lBIYYxAEgZRSonRl09PTWcMwYMuWLc78/Hw2m82qfD4fAYD6yU9+MprNZtVb3/rW6iuvvJK9ePGi9ba3va3OOVcPP/zw6ObNm9377rtvCQDUs88+Wx4bG/O2b99eX15eNlzXVePj4x7n3KvX6yybzQZSSg8AlOM4kW3bIQCE0G0eULNAX2cNYD3zda1zGGNrd6N117DXDIjqs++WbxsMYEDrQ/jXIuEHfYTWmgOAYIxxz/OEEIJLKRkAMM/zpGVZGgDA933z9OnTuX379jUOHTo09LOf/Wzze97znvmlpSXr9OnTxVOnTg1/5CMfOfrVr371jgMHDux8y1vecmZ5eTm3uLhYME3T/8AHPnD2oYce2jszM7P5/vvvf2nLli2NJ5544o4wDNkf/dEfvbB///5N09PT5c2bNy9+/OMf/8WXv/zlrceOHSv91m/91pn3ve99lw8fPmx985vfHPn0pz99GgA8x3H0ACaQAILXwATWJOJkg7EuYmWdBlpruj+tmayn/67x92EI62EMt3TbYAA92jUS/kAiT31zrbWIokgIIbjWWgRBIJRSwrbtCABkq9UyL1++nNm1a1fz4Ycf3iqEgHvvvbf1qU996s1SSrVp06Z2o9HIPvvss6+fmpq6WCgU3EcfffS3K5XKBdM0gyiK5MzMzD1TU1NHLMtyT548+dZisXjJtu0G5xwWFha2bt68+YQQIpqbm9sphGjv27fvWdd1rTNnztwzMjJyYufOncfCMFRHjx59/b333vvD3bt3X3jyySd3ZjKZxXe/+90v/fmf//mFdrsN2Ww2hA7x4+dqGUC/edhzPxJ5B6LoejddEZSxqdTVF2MM8vl8Xy2AMQau64LruppzDlEUaSCaTMwIegVFAdl327QNBkDaVRB+2kZPCBuuJHgBMcEzxkQURSKKImGaJgcAppQy4omqAYD9y7/8y57f/M3fXDx//nzuYx/72P/+0Ic+9Oyzzz67e3p6+o4gCEzbtt2VlZWxpaWlrRMTEycvXLhwt+M4Q6ZpNizLajUajXHGmMrn8/PVanWL1poJISLTNFuu6xYBAHK53GKz2RxRSoGUUuVyuYVWqzWqtea2bS9zzv12uz2WyWTmyuXyyUajsdn3/aJpmvO7d+/+lu/7EWOs+qd/+qeP/smf/MlirAkEcHUMYBCQmuzjnCOh9yJyJoRAomRaa8hkMiyTyYBSitm2jYyCxRpBwgTw/ORCnX0avxFEVUqper2uXddFTwjAlQxgUDTlLd02GABcNeH3kvhpghfQUe27vsMwlKZpAgCwRqNhX7hwIXvXXXc1/+3f/m3X888/v/3tb3/7+a985Svvk1JGnHOYm5vbWqvVxoIgyJTL5fnp6elfNU3TkVK6jUZjiDEGUsoIAHQURVIIEcXIvsAJDDHBxESotdZcaw2MMR2rtuhy1BATBedcMcaiKIqM2IPQZozpIAhyhULh4Ojo6JNDQ0NHH3jggZ+8973vrXqepyzL6sUAVNyfBuhIZCK16bNOtolK3/UehBAsHjcUCgX0bDDDMJhSCmLPB0p2ppTq2o/vmZgGjNw7MMZU/EyAMaY55xq/OecqiiLdbDaV67rp6Mfbmgn8j2cAPYh/LcKn0l7AlcQvkeAZYyKW8AmzeOSRRzabpsls22af/exn31OpVJrVanVoenr6zlarNRSj8rmVlZVtlmXVgiDIBkFgSSkDpZRQSnEAAM65ThFKl/rb4zdcw7Eum1lKWSuXywdM05x973vf+++f//znDzUaDVYoFHzo4AAUC9Baa6W1BiEEagIsNeZkO3WMcc4ZYwxyuRwzTZNJKVksyRmeq7VmURQlxI2SnrwzRt4v45xfoTHgMcaYVkpppVQSBxEzQsU511JKJYRQYRiqxcVFBbFZEL+ENBPYYAC3eusj9Xv55dOEfwXB47ZSSjLGBGNMuK5rnjx5MnfPPfd4hw8fLn/ve9/bvmfPnub3v//9e5aWlkYcx8m32+3S0tLSlmq1ujmXy61Uq9XJMAwNKWWA0hdVUrj570pXKpXnc7nczOTk5M+/973vfYdz7imlfCFEAH0YAGMMtm/fDjMzM2npziBG5ON9nDHGLMtihUKBCSFYHLPAlVIsiiKMb+hiBNiP1ppxzlmsZSADSZhA/CzxXBwD5QAAAComaqW1jqIoUvE+JYSIOOfKMAwVhqFaWlqKYEAUJNwmTOB/XBzAAHW/n+uup3oPq2q+DMPQ4JwLIYRoNpsWAEjTNMWFCxdGf/7zn5cOHjx4R7FYjD7xiU+8x/f9fLFYXDlx4sT/ymQydaWUcF0357puGaAzEaMoMuJtTcZ0s5rWWrNsNns5n8/P2ba99Gd/9mfPCiEi13VZJpPpOTZkXl/72tegXq/j7i61nkh9XiqVuGVZjDHGoyjinuexWMLzWFVPNAMhBEci55xzZAgQq/Vxv0wpxZEx4D7o1goAViU5QBz8xBiLGGORYRgRAKgwDKMwDLkQIgQAMAwDhoeH9dLSEhJ+WvO6bdr/KAawhrqftu17SXxJvqVSSnLOpWEYDACsxcXFTBiGhu/71gMPPPDObDarzp8/v/Wll1565+Tk5PFLly693vd9e25uLgQAcBynCAAghNDrSBO+WY1JKb1KpXJGSunfd999z37oQx9adF1XWpYVQg8UHDGGRx99lL300kuA5xAQLpHmmUyG5/N5LoTgYRhypRQLw1DEhMxjLwlnjHHGGI8xDBZvszAM8Rgjx1D6cyL1E22AaiKMsa6wZ8Q/lFIhAIQxHhP4vg9hGCY3mclkdKlUglqthgykl9l0yzOD/xEMYB3q/nokvsRPFEUG51xyznmz2bSfeuqp4fvvv7/1uc997k3Hjh3bOTExUXvuuec+4Pt+3vf9rOd5hRMnTvwvgI5UV0pJ3IYrJdIt10ql0nQmk2mMj4+f+djHPnYEALhhGAECi30+sLi4CFJKaLfbyf0hoQIAGxkZ4YZh8CAIuOd5IooiobXmnHMhpUy8J/itlEqYQGxqJe8u7pPzDjhCmQEFa9FsQC6BNryKw58jpVQURVFommboOI5frVa9oaEhls1mmeM4EEWRjgFRnc1mdbPZRByiC928XdovPQOgIBD57kX8a0r7KIoMIYQUQohTp04V8vm8eOyxx8YPHDiw49vf/nb+6NGjv+J5XuG5557bp7XmsXpfiKUKSolezOiWazGjYrZtV8vl8uVMJtP4vd/7vf0jIyNuu92GOMT4CqJH1f/gwYMwPT0N3/nOd8DzvC6pDzHxCyGE53k8do1KQvgy9mRIiMFUAJBEqqP5lQRQxf0K/Eb7Pz6fs45bgzHGEF9QWmsthNCcc12r1TzGWJjP5zUABIuLi87rXve67PDwsH7ssccWACAaHh6WnudpZAJCCJXP51mtVkNuctsxgV9aBrAOdD9N+GlpT4k/kfgAYP3oRz8a2717d/jQQw9t/dd//df/s3v37ld+8Ytf3L+8vLzdtu2VVqtViUElBL44vEaNINuvqimlmBAiHBkZOZnJZGpvfvObD/zxH//xGQAA0zTV0tISa7fbTErJDMMAIQRYlqUNwwDTNHWj0YBarQYnTpxI3IsQP/vR0VHOORe+72NchDRNUzLGZBRFUmttMMYkYyzRuvDDGEvcqriNGgVhDCzWELhSinmeB0IIns/npe/72nXdaHJyMlOtVv16ve5blqXe8pa3jLuu6x46dGjhox/96P0XL14853le65577pl829veduHAgQMnn3vuuUUppQrDUMXAZDoH4rZQ+2n7pWQAfYg/LfUHqvpaayOKIoMxJoUQ8ujRo8Xp6enSrl27wi9/+cvvs207Onv27M7Lly+/4fLly/c6jlPSWrNmsznC+kSZ3YD7XL1JxoBzDqzjxwbOOQghkt/0g+f36g8/SikwDKPNOc9oreXIyEj18ccfH9q6dWv1jjvucEdGRmoA4AGR/ufOnWMvvPACO3jwoFhaWlIrKytgWRa02+0EhCuVSkJKKVzXTYjf6KQqyjAMTcaYwTk3tNYmY8zQWhsAgPukUkowxgTn3IgJUAghRMxQIAxDns1mDc/zwPM82LRpkz0+Pp6bmZlpDw0N2eVy2XQcJ1xZWXHvu+++4X379m36z//8z4Pvfe9731ypVLLvf//7z42Ojo7u3r37jeVyefL8+fM/0VoH73//+/NhGO5/4oknlorFIo+iiCullG3brNFooPvwtmMCt6wKeq2th8rfS93v58ozAMDQWkvGmBHvs3zft77+9a9vv3z58vCTTz75xtnZ2R2+7+cvXbp0t5TSi6JIxnb9emPNr+W+Oh3HxI0flL64HQf/dBE7/r6appSCMAx1GIas3W57AOBorYNSqbSSz+dPmqZ5qtVqnc/n88fvuOOO0zt37lz61Kc+tUz7eMMb3sCXlpak53nQaDQgDENWqVREEAQyCAKplJJSSgMADKWUCQCmEMISQlhhGCa/oyiSnudJM25aa9loNGB8fDyntRbz8/OhlFIMDw/bhmEY586dc3/3d393l+u6enp6uvn6179+c6lUsqvVatv3ffXud7/7nkajUQ+CILjjjju2t9vtqmEYQkopRkdHd7quO28Yhl0qlYbPnDnznVdeeeVxx3HmvvGNbxyam5trmqYZBEHgm6YZGIYRLiwsoMuQugQBbgNG8EvDAAao/L3Q/V7gnhETscE5NxzHsQ4dOjT01re+1XnggQfue/LJJ9+2adOmpaeeeurDQRDYhmE4vu/nbsB9dP1GQpdSdn2Q6NOEfi0mAIk3AMYYCCHA933wfR+iKIJ2uw2+70M+n4dCoQC1Wg0sy4KVlRXI5XJuoVBYNk1zmnN+JJvNvrywsPBCoVA49eKLL86Sy8ihoSFzaGhINptN4TiO4JybnHMzDENTSmkZhmE3Gg3pOI6sVCoF3/fNZrMpKpVKfuvWrZWTJ086k5OTpampqaFqtRrNzs4G27ZtK//Gb/zGzocffvjEO9/5zp27du2aWFlZaQohzGKxmBsZGSmePXt2/q677tq2adOmkbm5uflisVjMZrNZrXXkum4rl8uVpZQsCIJACGFIKYFzDtVq9dzx48e/7TjO3E9/+tP9jz766KVKpRKEYehFUeQZhhFalhU6jhPVarUIYwhggwG8tm0Nlb+XrY9Ej9smSn3Xda1MJiMef/zxsW9/+9uv830/+8wzz9zvOM5wvV4fdxyngG6k66XqI9Gi2o7bpmmCaZqAE5IS+/Ww9fFaGJ4bq/2gtQbf7ywd77ouAAD4vq9zuRx4ngdSSm2apq7X66xYLPJCoQBhGIKUEqIoglar5UgpFwuFwlHDMB5rNBpPWZZ17PTp07VqtaoBwC4Wi3nLsjJhGBpRFJmZTCZbKpUKmzdvrlQqlZHvfve78x/+8Ifv9n3fmJubC/fs2TNVKpWKy8vL/vLycvA7v/M7v7awsNDwfV/fddddO1gnTTpyXTfcvXv31uXl5RUAYNu2bRtqNBpKx8CAZVnc87xIa61N05QAgMg+ugc1ALAoivzTp09/9+LFi8cuXLhw+t///d9fsm3b01o7WmtkAIFlWZHrumG1Wt1gADejrUPl76XuJ3Y+AJiMMUMpZczPz2c3bdrEXnjhhcoDDzzwu+Pj4/Xnn3/+bZcvX36dbdvVdrs9dB3H3RksIXTDMBICRElMz73eDbUIGp9vGAYEQQAAAGEYQhAEEIZhYlpIKWFkZATm5uaAcw6lUkkLISAMQ10oFMD3fRYEAdNaQz6fB6UUOI7jlcvlE1EU7RdCPNtut184cODAxVhtLu/YsaPy7ne/+87Dhw833/Wud73hzjvvfOvZs2fP2bY9bJpmcdu2bbtmZmYuVSqV4bGxsc1LS0srKMUBAKrVanNoaCgvpZSe5wWWZUnDMJjWGsIw1JxzDB3GWH824JlqIQS7ePHiS4cPH/5Zq9Wae+ihh55bXFysSikdpZQjpXR93/ellEEmkwlc1402GMBNaCniXwvh70KTY6TZAAAziiJTCGH83d/93b31er00PT299bnnnns/Ywxqtdrm2B591RKf2vGmaUImk+mS8PScG9HSxC6lRFsfNm3aBK7rglIKfN8Hz/MgTqZJJDtjDCzLAh2XGLNtG4IgAK01mKYJSikwTRMYY9p1XWi1WnpsbEyLTqRTwtQYY5eUUi8sLi4+Wa1Wn9m3b1/m13/91381l8tNTU5O/kYYhmx4eHiH7/tes9msb926dbvruioMw1BKaZimyXzf10opZRiGoM8tfY9X2bQQgjUajerzzz//o5WVlYvPP//84Z///OfTuVzOVUq1tNaOlNINgsDjnAe2bYcbGsBNaD2In0r+vlIfOkCfCQBGq9WyAMDK5XLq05/+9K8ePHjw7na7Xf7FL37xWwAAURSZvu9n2GpI7rWOFRhjYBgGWJYFlmV1AXY3kugBoAsYBOio+tSMQWBx27ZtMD8/D8vLy1CpVEBrDY7jJP14ngfDw8NQr9ehVCpBEARw/vx52LlzJwAA+L4P2WwWlFKglILFxUUYHR2FXC6nwzDUupOuy5GpxBrPZSnlYSnlmQ9+8IPvvPPOO+9qtVphEARcSsljj4TG3P4YbV9fWZ9rbEeOHNl/4sSJl+fm5s498sgjhxljLQBoMsZaWmuHc+4EQeALIXzbtm9rDeC2cwNehb2ftvMNADCVUkYYhqZpmuKZZ54ZP3LkyOZdu3a5Dz744O82Go1x3/dzjuMUU+6yq55sVCJlMhmwbRtM0+xS62804QNAF2agO5l5yTeV6FEUQaPRgCiKoFwuQzabhUajAYZhQBiGEEVRotJns1mo1WoQlyFL+uacg+u6kM1mIQgCGBkZAdu2wff9JAAqDENtGAb4vq9j7Wez1npzvV73vvvd7+pjx45Fd999t5ycnAQA0EEQMABgyLRuFOHHmAC7cOHCpePHj19ot9vBc889NxeGIbMsKwliovEMa3V5I8Z5vZu42QO4mrYO4keJb8Aq0ePHUkpZnHNTCGG8/PLLw//93/+9r9VqDX3pS1/6v3Nzc3dHUWTWarXNr2aOIVFLKcG2bSiVSpDL5cAwjOsGGl7LmCjDQZPDMIwur0Kr1UpUaMdxwPd9UEpBu92G5eVl2Lx5M0gpE0wgDvpJAEBU8aMoSr4R01BKAeccCoUC45wz27aZYRgsLnemOedGs9mUL7zwAjzzzDNsbm4OcrkcK5VKCZNCJnAjmhCCtdttf//+/S9Vq9XGyZMnL5w+fXrJMIxQa+0DQBB/Qs55qJQKOeeRlFJHUaRc102KicBtFAtw2zCAHsSfjt9PE78Zb5taa4sxZvq+b//85z8fLZfL5uc+97m3PP300++4fPnyxOnTp9/m+34+CAIbrlHVR+IyDCNxl+VyuUTi3+hG1XrqoVBKQRRFYFkWcM4TSY1eBYCOlhCGIQghoNFoJOegaaCUgj179kAmk0lMgricOLC4NBcSuNYaPM9LmAMAdF0PtY3YHAAdZ+sZhqGFEKzZbLJWqwUzMzNw9OhROHfuHAAAjIyMQDabTf57IxjBkSNHZk6cODG7srJSPXjw4AWllAcAPgD4WuuQMeYDQMgYC5VSEedcCSF0GIba8zxFGMCtkL69rnZbMIA+xJ9G+amdb0KH8E3GmAkAFgBkDh8+XJmZmRn9xje+cc+Pf/zj32m32yPnz5+/x/d9O7bzrzpkl0rVQqEAxWIRMplMompf70aBRATgUDoGQZAwgjhxJQH5kEgZY+B5XkLkSimo1+sQRVFyPkpvxA4sy4JCoQALCwvAOU88A0iIVOILIUBKmRA6XhOZQxRFYNs2CCFACEGvx3zfh3K5DLVaLbn2xYsX4fDhw3Dq1CnIZrMwPDwMlmUlAOX1AGYNw4DLly839u/ff8513fahQ4cuxrEEPnQYQMAY8xljYbwdMsaUUioyDENFUaQ8z+tVAfmWZwK3PAMYAPb1AvkSlT8mfsvzPFtKaZw8eXLo7//+79++uLg48vjjj797ZmbmPs/zqNS/JjsfJT4S/vWUTHSCB0GAC32A53mJJPR9HxzHSYpZOo6TuO8whRWPIcofRVEX0h9FEYRhCMViEaIogmazmdwXAEC9Xod2uw2GYSSMgoYe5/P5LmaHIB8Cn5hGi94CrTVks9ku7IB1ypuB53ngum5X/1prWFhYgJdeegkuXboEQggYGRmBTCaT9H2tz10IAc1mM3r66acvLi4uNqenp5dOnz69JKUMtNYBAARaa58xRoueRNCpHRAxxpRlWdpxnDiEYOAaArdcu6UZwAAff6/MPYrwWwBgMcYyUkozDMPsRz7ykXdfvnx5x4kTJ+555ZVX3sIY01EUmXCNhM85h3w+D6VS6boRPlWROefg+34i5TG2HaV3u91OJDHuR4mLtjuVlKghpIN/kEGgNI4TZ6BUKiX3iUSMRCmlhEwmA1HUWRsEmRG6FYUQYNt2ck/4bJApYL+oJWH8A3oQGo1GorHg2PP5PDDGYH5+Ho4fPw7T09MwNDQEo6OjiRmC17uaxjmH2dlZ78iRIyvnzp2rHTt2bElr7cUEj9I/If5YC4g451G8cpKOtRkdawE9X+1VDeo1bLcsA1gn8VOpb8YfK4qiDOfceOSRR7YEQWB/61vf2v3ggw/+QavVGl5YWNjp+3421fd6xwSMMchms1AqlSCbzb4qwk9LeJz0nud1BeG0Wi3QWkMQBAmhR1GUHAeALgKIogiklGBZFrium6jfSOQoWYUQCYPJ5XLQarUgm80mQB8SFhI6EiMuM4YMBe+DSnQASEwCKuVR9UdNQAiRuESVUpDJZCCTySQeCdQMMOcBAcvZ2Vl48cUXodFowPDwMJTL5Svwj7VabCppz/NUoVCQp0+frp4+fXqFMRYIIRINALolfwQAkdZaxcVCFWoB7XZbD7j+LckEbkkGcA02fyL5oyiyhBDmCy+8MHLw4MHJgwcPbv3a1772f2u12mSr1RpxHKd0LX59VF/L5TLk8/nEzr2WRkNv0cWGarnjOIn/HYk9DMPEjkcNAcdEx0CZADIM3I9ESYN7kPhRKwiCICG0IAiSayPhUpsfr4VIP+5H4A+v43leEumIfSDx0+AgChQuLCx03StlYPg/27YhiiKYnp6G48ePQxiGMDEx0aWZrJcReJ6nHccJx8fH7Vwup2ZnZxutVsuVUqLEp58IOtWOlRBCKaWwrkAXSHq7MIFbjgFcJfFTlR/R/gxjLPPJT37y148dO7bnyJEj95w6der+IAiycbLPVdn7OPHy+TyUy+VE+l3DfQHAKpGi9FZKQbPZ7FLnKWiXlr6U6Ok2HgPoVrvpNm2oJcQ19BPXIEpnirZTwI32RX+jZKfeBTQfsI9MJgO5XC4xR7BZlgUAkGgtpmlCo9GAdrudMCf8RjMCt03ThGazCa+88gpcunQJyuUyVCqV5JprMQHOOSsUCiKKIlhZWfGGhoas3bt3FzKZjD537lwtiqJASqm01gnxQ1w8VCmlYgBTa611HB2p2+02BisNnhS3QLvlGMDHP/5xqvqvRfzo5jOVUpkgCDJSSvHXf/3Xb96/f///arVaw8ePH397GIbGtQT0oKsKffnX2lCVRZ86QCfJpt1ud6n6VJ3vRei9Png+EmP6Q5OI8DdAZ3JmMplkG+33OEinS/KiSQCwmrCEgCD9H/aNY8ZzkemhNoAMDt2IFOwzTTOR7qgJUcaGGgaCotQ8mJ2dhRMnToDneTA2Npa4DfG/g95zsViU4+PjuWq16jUaDT+fz8tKpSIajYbTaDQ8KWVSNxBW034BAHQcC6DxWWWzWRjABG4prnBLMYA+iP9A4o+iyOScG+fPny8ePnx4ZG5ubuiLX/zi7y8sLOyuVqsTzWZz+GpVfpy8hUIBSqXSNan7KLEBoEu6I/ru+36ipveS6JTIqXTHY+mWlvp0H/4D0VBqAAAgAElEQVSmhIRERwFHPAfV/jRgRwOIADqhv4j053I5ME0z2YfBQviN3gK8b1T5AVaTktA0CIIg8WZ4ngcA0OV5QOaAQUbIfJDZXLx4Ec6dOweVSgWGh4eT+xrEBGImxjZv3lwsFApWu9326/W6l81mudY6bLfbPnQTv+6UH0wSjHTMbHSaCdBpga+r70Be43bLMIBrIX6ttck5NxcWFgqPP/74Ft/385/5zGc+PD09/WattazVahNXK/m11mBZVhIOe433kkxS13UTBtBut7v87b2IPE3wacJfL/EDQJdExmNUI0ATA5kAJXjsByU1MglkENRDEJcDgzAMwXGchEEgCIgfgFWvA0pyvA56AtBUUErB0tISOI6T/B9zKdKaBTIHahrU63U4ffo0BEEAmzdvTsbXjwlQk2F4eNjeunXrpjAMg/n5+eby8rKbzWZZFEVREAQR76x0pKFTXFTHuQoUn+nFBNLEf0swgVuCAQwg/l7hvYj2m1EUWZxz88EHH9zx7LPP7v3FL36x9/nnn/9Au92uBEGQ0Z3acVczjsS1h9Llav6LE8j3fXBdF6IoSkJqqV2fJv40sfcj/nSjErSf+o+t137sM80oaBQglbiYFdiLiVAJi1Kfuhzxg1oEfkzT7AIosW9U8dGtadt28huvhyAl4jRpRub7PszMzMDly5dhfHwcyuXymkwAoFMfgHPOd+/ePTY1NTUyMzMzf/78+Wa1WvVt22Yx7qdjRgAsXleABEBpAEDgU8dFUXtesudAXsN20xlAH+LH8N5ekt/SWpthGFqGYRg/+9nPJh566KFf8zyvtH///vdVq9UtjJTeXucYEt93oVC4KvCG2uFBEFyhvtLJOsiOX4vo0wyAgmG9gDmUSEiYSJCUcCmh4zHaB9rkaGvjPmR01I+P+1Eyp00IJHZazcg0TSiVSuB5XpI0hd4CrEvQbDYTrIEyGerSRG0CtRm8b9u2McoPzp49C4VCATZt2tT1XHo1Fh8IgkBls9ncm970pj1TU1PZhYWF2iuvvFK3bVswsixY3JdWSukYK0g0AYzFCILglowRuKkMYADxpwN8uiQ/Y8wSQmQAwP7kJz/59lOnTr1+ZmZm7+zs7F2kr/WOAQzDgHK5DLZtX5XURxVUa5346l3XTYJ1eqn61yrxccIiYAfQbRdjQ8Kk29R+p+o9nkOZABInXlMIAY7jJJ4BisanzQMqzaknoFdhE2QGGI2IwVSYnIRJQLVaLRkHlioDgCR3ATUCjIbE6yBDQpPBdV04deoUaK1h69atiTYxiNkzxphSSruu64+Ojo7s3r17FACc8+fPN1zXjeIFYSDGmJAJJGshxpWGIJvNapwft5pn4KYygB6If68iHjTQx9JaW5cuXcoXi0X26U9/+o0/+MEP/l+llH3p0qW7wjC0rgbwQykxNDS0bpWfnpOOxEMNgKqpaxE/3e5F9P3GQGPskQjTUhylD0pvBOwoY6CE2ot4aeAPLVeGQB9lBDTwiF4P+8TfqDngPdCgIBwH4gkYoISqO0Y5ovTHMaKpQrELjGWg+6anp6Fer8OWLVsSb8MgouScMymlaDabbrPZ9Hbs2DGez+f10tJSq91uh+S/Oj5fx89cx3NAo8dlQIzATeMKN40BpKT/Wim96OO3VlZWcnNzc6Wnn3568itf+crv1ev1yWq1OuG6Lubwr5v40d5P28Dr+S+G6WLRzF5+++th5+P+tNSmxIIqOvrFUcVHAqVYATYaf499oR2L18Dr9DIrqK2Ov13XvQJERHchNT3QNEAm1CuaEQG/hYUFqNVqSeASzTCkkh6fETI8bIgRoHkA0Ekwmp2dhS1btqyJCwAASCl5JpOxpJSyWq22HceJJiYmcisrK61WqxXGY07cgjET0KKzmjBorZlhGNq2bXAc55byDNwUBjAA9Esn95jQSeyxGGNWu93O/vCHP9ySyWTML3zhC7959OjR94RhaLmuW4KrfIClUgmKxeJVjRtBJ/Tno9RHVf96qfsA3TYubUjs6YAbCtq5rpswA1pjEKV2nIbbpQVgnxQzSI8FTQDGGNi2ndwvDfNFAk1HLzLGusqMAUDX//E3joWq85xzaDQaAABJDQPUOijGgGOka/ihxhGGIbium5gOCwsLMDMzA2NjYzA8PDxQE4ifFSsUCpnR0dFhz/O848ePzwdBEFmWBc1mky6TFv+lc5NCiEQTMAxD9/EMYHvNmcBrzgDWAP16qf2mUirDOTcfe+yxsenp6fFHH330Vx5//PEPB0GQDYIgq9e58g5OlnK5DLlcbt32PkoWWiAD7XyUMIMIfz3qPt2HEjIt9QFWfeY4YSkBAHSCbSzLgmKxmIwXABLJSNN18T/Ut5+2o9OMBokTCQ73U5MEw5tR8tJ+8bwwDJMkI3qdNFOgCULYD00jTv8HNQWaikxTj9GcMAwjiSAsFoswMTGxpjkQg3x8ampqbHJycvj48eOXX3755RXP8yLbtrmKy5VBxz2oEBzEmgEAoGOGNMgz8Jq215wBrGH3X0H8YRhaUkrj7Nmz5a9//ev31uv14Z/85CcfWFhY2AkATK/T1YcSr1KpXBXYhxPZ87wkx56i+2mpj9daj+SnY8OWJnp6jEpKPE6JE8c0NDQE7XY7UcnRZMEwW5SQ6DbDdF6qnmOfKGWpKUD/S8eWNjG0Top+JH1TjSBdpwAZFB7Ha66srHRpD8hocB/+pmYLrUlAA4fQRYv34XkeTE9PQy6Xg6mpqYGRg6wD/UMYhqpYLJb27t07MT4+blSr1eb09HTLtm0GPQKFIPYUxO5BHa+WpNNMHi9zxYVvYHtNGUAPu7+f6p/Y/UIICwAyn//851+/sLCw+cUXX3z9mTNn3ha7atYt+YUQMDQ0lJSXWk+j6bUYxINStZ/UXy/hr2cMaeKnIF16Gz/UFsZtTNjBcGYKtNm2neTmc87BcZyEwVCcAf9H77FX8FCamdG+qIaTfjZ0vJZlJedIKaFSqSTvABkHXqtfrgQNFsLz8H7QFKC4wLlz58A0Tdi6dWvSRz8JzRhjYRiGvu/riYmJ8bGxMbvZbNbr9boXBEEUM8IkWEhrTd2DmnOuKShIXzleYs3JcZ3aa8YA1mn307ReMwgCSwghv/Wtb93x4x//+C2Li4tbTp48+TbP87LrRft1DGxVKpV1ET8lIEzMwSIVOJkGSf1XQ/gUaOsF+uFxjJjDiY5FOCm6jmi/1p3IxlKplACX+XweDMMA13WhUCh0mRMAqzUDqVsPx54GCwFWGQNVuRG/wGdCXZMA0IUnUJMHx2LbdoIbBEEA9XodOOeJek89Eth/JpOBIAiSfvA+KBCI+zCqkd7b9PQ0GIYB27ZtW5MJCCG4EEJUq9VGs9n0JiYmSu12u12r1TzV4XpJ3gCLYwZozkBcFl4TUPDaUktfZXvNGECs+q/b7o+iyDIMw5yfny995jOfeX+r1do0MzOzb3l5eevVEv/Q0FDialrrfDzHcZzErYf2/nqk/quR+P2kKE7CNLFQ6UwDe6iNbFlWElqLEXpISBSwQwaCRI8SlAJq2DfNiKRjwntIq/DpOAWA1exBfGYYz089BTRcuNFoJFGV+H/6XPD6tPgq9X7gmJD4kDEgNoImwyuvvAKGYcD27dvXZAKGYYhKpVKwbTtz4MCBs77vR0IIFS81jgwgmQJKKYVFRKGDB2itdcK0Uu010QJeEwawhup/BfFrra0wDC0hhPHxj3/8rS+//PKbqtXqxOzs7F7dsfnXRfxo86+H+AGgC7jCCDSKZKelP16H/sZ9vbbXM+ZexJ/+AMAVRJVO60VComG8aGMjsUdxfT5kFpVKpYsokBGgVMbFTOjqQVQrQu2EAof4Huj406YNJWbch/1idWXLshKmjIwM+0lHODqOkyD7juMk6cbUJEANJ405cM5henoabNuGbdu2rQkMaq0hn8/b+/btu6Pdbjd++MMfzvC4WKjurBGQfBhjWDdQxysW6ThISKcYzWumDdxwBrCG6t+rqIcZBIFlmqZ8+OGH7/ja1772/3meV1lcXNzuum5+PcgpTrqrsfmRWJD40UYc5N57Ner+Wo1KNwpw9dpHwTq8Lk4oJDiUqlEUQS6Xg2w2C77vA+ccLMtKioHgPiQwykyoqw2fCboZM5kMFIvF5Hzcj14D6j3Ae6CEh9ei5+NvAICVlZXk/81mM/k/dVGiezCTycCWLVsSpohgIQAkkYEAqzURcrlc0h8WFDl//jxks1mYnJxcFxMwTVOOjo6O3HPPPcMnTpy4PDs768SMp8sUiOMDtNZax6aMpjUEoJv4b7gWcMMZwADV/4pCnhBX9DEMw2w2m4WPfvSj/8/c3NyeVqs1Uq/Xx9frNuGcw9DQUIJAD2o4CTFrD1Hifvb+eoj/1RA+NioxKdHQfWlJm5ai+J90vwCQqPu+73e5yah9Te+HSnlkCDRNenJyMtEK0MVFA5MoWIfjQ4lLsQ9K9IZhQCaTSZhUFEVJBWM8F1V5rGWYz+dhYmICgiCA0dFR2Lp1K+Tz+aSwKQ13RuYO0GEMdG0D3/fh/PnzUCwWYXJycs1goSiKVBiGSkppbtq0KZPL5aKZmZkGY0zF5kAEsXtQKaWEEFqpTlaRZVmDXIM3lAncUAYwQPWnK/ZgwI8FAKbjOFnTNI2//du/ffMTTzzxwSAI8vV6fVwpte6xrjeuH4mGEj0l/l4MAP93owgfWz+1v59ZQKU9ruBD+0mH2RaLRQjDMEHc0wuAYl94T0j0aCJhn3gMA24AOpiC7/uQy+US+zbtDaD/p14MbJhOi//NZrOJOYJx/1SLQGB0cnIyqRiM6dymaSbPhAYUASRJO11MEPv1PA/Onj0L4+PjMDY21s9Wx3tghmEYrVbLXVpaajmOE3DOQ9d1Pdd1Q855VzGR2BxQmDlIawrG56BL8Ya2q66Dfw2NpT5XpPlqrSUAyGazmcnlcuKpp54a/8EPfvDBIAjyrVarEgSBuZ4Laa2hUCgkbq21zkWJh7YlVsLpR/z9pH96+9W2XsSf/t3rflAqpomfAoV4ztLSUlKdiKr61KSgEXZpJkLtdK01VKvV5BpBEIBlWYnLNH0fabUdtRFcPo16JXA7CAJotVpdiU3UHYjeENRKRkdHoVwuQ6lUSoDLsbGxpIowhkmjqUKzN3EOAAA0Gg34zne+AxcuXBiIJcXmCNu6deumffv27bAsK+/7vq21znHOswCQBQAbACzOuamUMjjnBudcBkEglFKiUqnQ9S2hx/d1bzdMA+gj/Xup/iZjzFRKZRYWFvKc88xf/uVf/s7p06ffEoah7bpuGdYJ+uGKPOs5Fzk/DZjBuParCelNb7/atl6pj8foObiNkooi8rQfdBPSKEGaBYhmAB6n6j9Vn6m9j641mn9AUX6KtiPRY3ASlvzGcaP3Ib1sejabBdd1oVqtdrkRse/x8XFgrFO12TTNpJgIrneAXh3MPET3IN4j/VBm2Gg0YH5+Hnbv3p2Apv0YsVJKDw0N5e+6666dw8PD8plnnpl2XTeUUiagoNY6qSnI47wB6AQI6ZiB0tThG6oF3BAG0AP467Vab5fP/+LFi4WhoSHjC1/4wj2PPPLI7/u+n/d9v6DWkdevdSerr1wur2t8KH2CIOiq0nMzpT62fqp9mtipLd8LG8C+sPWKr1dqtd4+TdnFc5EBUJ8/XpNG7VHVHc9HIkLmgq43ZBzIILBvrP2HY8RVlBHYQ4mP/buum4wXoLN0GHp8UN3XurN+gu/7UKlUQKnOKkgo6bHACWV0FPCkz3ZhYQHq9Trs3bu3y4zp8f6YUkr7vh9IKY277757uFarVc+dO9cwTVNrrSO26iLsMgUYY5gwpIgpkHS9rgl0le1GmgBp4C+t+otY9Tfq9XrGdd3MyZMny9/+9rc/2G63h8MwzIRhuKbqr2MkulgsDgRpsGFkH6r+1M13s1V+ut1L3afnAPQu+YUSOb0PJzkAJBIW3WOYuKO1hmKxmOACCORRQkVmgao3xhfgNsBqTEIUlwnHBB68NvaHNQGQmClzowyJplmrOBcDCTCKoqRwK2qBWHeRejbq9TpkMhnYvn07bN26FYrFYpfGgQ3vBYOmAFarDx85cgQef/zxgaZYfI+Md16OaDabbGpqauINb3jDZBRFNmNsoCmgtWaFQoF3HjWjpjPADWAC110DGAD8pd1+FmPM1FpbMzMzpaGhIeNv/uZv3nPo0KEPaK15EARZWMcNI+K/Xl8/RvfhJLnZ9j42OvHXA/pRtR9bOiKvlymB20igqF4DrAJ0qA1hrAD9D0X+MZ8eCYi6+jDvACU43iNKXeyDBifh/hgVT+oZIKFjPIDrugkTl1LC2NgYWJYFhmEkjAD/h4FcWKcAPQtDQ0OgtYZ6vZ4wljQDwnunz2Z2dhaGhoYGega01mCapigUCrkYb2k8+eSTFwAgjNH/kDEWxaZAssYABgj1AARvWLvuDKCH269nmq+O1+6bnZ3NK6Xs73//+zv+4z/+4489zysqpcz1qP4AnbTetRB/lBZYrotqAVdD/DeC8LENAvzSNjz9UHu11/8p0k6vheo4AqZ00uN9Yv/UBMBroHsPj4+OjiYZlkh8+Xw+Cd3FMWDUIT5fuk2ZB9YARPOMcw6VSgWWl5eT4hqu68LmzZshn8+DECLBf9Cbg5mBtJgp9un7PgwNDSXnUIA0bXYBrJpGURTBhQsXYGJiYmAacXw/bMuWLaVisVhQSrXOnj273Gq1vDgCMFljADEBrTueQSGEToUJd02Va5hefdt1ZQADgL8u8C8mftN13czc3Fyx0WjkP/vZz/6fixcv3gsALAzDzDquBblcbl2gH56P2Xx0ddtbgfgBuv32vaR3+njazl/LRUhVcPwvEjwG8KCqjcQHAF14APWbI7ForaFSqUAul4N2u50g9RgtSP38SHwobQFWbX185hRfQCaNgCDiCc1mExYWFpK0Z1yKHAk0k8nA+Pg4OI6TMHyaHozaImYFlsvlxCTE50K1Fgr6GYYB7XYbFhYW4M477+zSUtKNMQaO40Raa7Zjx46JIAhq1Wq15boulhhHHEBprSMRrzQEVwKCN2zy3QgMoJcG0MUIGGMSAIyFhYVsqVQSX/3qV99w5syZtzDG1NXY/esp4IkvEmv1UXv3ViF+eg9pOx+/KeiUPp+ek/4PBfywUXMAF/3A+AcA6AL38DkhKBZFUeKTp0SNax1gdSQ0JXqNF7USpRQUCoWulYHwGF2aDFOYEcE3TROGh4eTNGbsE5dTc103WaIdABJvAmMswQNQY8By4RhyjMwBGQXAalYoHc+5c+fgySefvCJoirZ4ngrbtjNLS0uNcrk88sY3vnG7aZp5xlgG4kVsIfaEAYAhhJBhGIogCEShUBCccxYL1huCBVw3DaCH9EeCx8Afumy3WavVsr7vZ1988cVN//Iv//IHtVptSmvNlVJrrr2Fdv9adfxwQuMCm6hODiL8m0H8adW9l7re67tXH71UWCRerAZk23ZXUAuG/1JJjUUz0O2FBGDbNuTz+QTg45wnEXbIaJCoACDRBPDZo4mBY6SSmdr/afMMoMOYkJFns9kk0hOXCadFTTBpiBYLBegwA2RWSPS5XA7CMITFxcUujwOucEwxEbxHKSVcvnwZKpUKbNmyZWCkoGEYYmRkpOI4TvtHP/rRyUuXLjXi54qLjSqIFxylWoAQQonVVYdvyES8bgygj+1/xTp+jDFLKWXNzc3lh4eH5V/91V994MSJE29njKHqP5C7IdK7VkUfStCtViuJB1/L7sf/0u8b2fqp7PQYJfhBhE+z71Aq0j4Qgac2OWUUmCaMz4YxljALJF60mXHFHuwHbXn8P01KwjJcANC1NBgeQxs/iqLE9Yf9I5OgajkF7VBbQWaEhI++fgxGQiwAQ4ox9JuxTiLV8PAw2LYN7XYb2u12Mn68FjIo+mzDMIRLly7Brl27EtdjPyYghGDDw8OVPXv2VBhj7RMnTiwxxhTnPILOoqOKxcAgX111GJOgVI+5eF20gOvCAAZI/3S4rwkAZq1WsyuVivzSl76077vf/e7vB0GQD8Mwo9YI98UXvR5/P04Wz/MSt9Fa7j68Bv2+ka0fcJfeD7CKA6SPp//bD0tABJ8yCPzgxEUJisSPXhIVJ9SUy2WQUkK73YZ8Pg8A0OUVoM8NJSbdhwAffd4oZRljCZ6DY0KGwhhLgDq8b+qaxPEiIIe4QrVa7XJTUsaHTIaOESMJaSo49crQUGHch4ljd95558B3rZTSjuP4hmGYUkoVhmFrZWWlFQRBwDthwsmqw5xzpTppAkpK2U8LuC4T9HpiAL1CfikTEAAgwzA0bNuWruta//Vf//Xedrs9opTiURStqfozxhLAalBDro3LcVEA61ZQ+9OtF+Gn9+O4Bp2TxgB6mQ29GAwec103Qc+pKoyhvQiUjYyMJNWFUDugDIqaHojI40pJjUbjCs8F2v4YPpyOAsRjyIxQk0NXLrXRkRHUarUE7MWGY8Z7wz6RMXieB/V6HXK5XLKoCMVeKEPC30IIePHFF+H48eMJUNnnHbNSqZTVWsP58+edXC5X0lpnGGO21hrxAIMxZmitpWEYQmstwjDkmUymFxZwa2gA67X9IU74cV03Y9s2/8hHPnL/gQMHflNrLT3PK8AazEjrTpz/elR/Gua7VjEP+j/6faPbWpJ/EMH2MgfwfJyUeIyuCdCLkdBwYfxNbV4kLLSXAQCKxSK0222o1WqJCo39pLMKpZTQarUSBJ/iMHgu56urBqGJoLVOANt0ijC9F2Q+dCkx9PTQUOEgCJLkIrwOrY2gY6+S7/swPz/fdRyvgeYJmlL4vHzfh4WFBdizZ8/AUOFYy8mNjY2Vz507N3v06NGFKIoCIQRiARFjLILYMxBFkdJa6z5aAIProAW8agawXtsfAKwgCKxMJiOfeuqpLV/84hf/wHGc0SAIsmEYWoOugUBPqVRaczwoIXzfT1brSUt+7BO/X2uJD7A+27+fWo/b9JtOSHo+ZQxpaYYTn4b84nnYB6bVDg8PA2OdyjyYZ0/rJaS1DIBVDQD7pkyAEWwAgT+s+UdtfRwrZVLUlEtrHfg/dEWmzQNkLOguxDwQBBExSCiKIlheXk5cjNgPapZ0MRMpJVSrVeCcw65du9bz7vmOHTu2TE1NGU8//fSMaZpKd+ICEiagtY6klEoppRljKg6AwrwBgNWMwVfVXhUDWIftn+T6K6UsxpjFObf+4i/+4rfPnTt3r1LKaLfblbWuwzlP7M9BjU5KLP+Mk6cfE+ilBdzoNojYB0n7tO+f9keR8n5MIs38UNozturywoIZqEVhhF273e5C1PE/VCKm+0ZtDBkBhtamtQ60x9NrCdi2nUhhJDw8RjMTcQx4LxjdmAYikYAxYxQDoVzXBSFEEv9AGQHWH6ABVdTcwH6llLC8vAxTU1NQqVQGFhHxPC9oNBrtEydOXCgWi+A4jtNsNt1YEwhRCwAAhWCAlFJxznthAa+KCVwPDCBtl/RkBmEYGkII9o//+I/7zpw5cy/nXLXb7aG1OkfVbK1QX5xsWuskDnyQ6p/u62ZqAbhN96dbL+2FbqeZQLr/Xq7DdOorACSBLZxzKBQKXcSqtU4AWHTH0XBZbDRgCNF5ZBi9pDmtvoRjQ+ARUX2qhvu+D77vJ0SOjAIZF/rzkVkg42i1WhAEQRLzgLECaBrgtZFRmaYJIyMjkM1mEw8F/geZAI4NoJM6/Mwzz6xZPKRYLGYnJiY27d27d3u9XufVapVLKTNaayyJZyIWIDp+QR5FEY/XIkxjAK9q4l6zBtBD+tNP17JeYWclX/PChQulf/qnf3r/8vLy1larNew4TmnQg0LQZT2JPjRQgwaj3CqIP7a11P1BWkCv83EffmNJK1TjqWqO52BQDIbGUsL0fT/5D/rMMf4eQ67b7XZSMxEZAV1ZCBuq4tQWpy68NCaB40bwD98dEiQWMi0UCol2R0uIo2nCGEuYB2opaMtTdB+rKeN42u12oglhX61WKwk7xlRmNF0ow0XmNT8/D5VKBaampgblCmjLssTo6OhoJpMJFxYWli5fvtyQUiZmAKzGBURxXICKmZq+ntGBr1YD6CX5r4j8i4N72Be+8IV7V1ZWNgMAazabm9YiagBIXDODGpX+WPN9EMHT7Zsh+bGlCTl9LE24+EnfB9UCqPRN/x+Dd1CtRz869ZAArNrrrutCPp8Hx3GSgB6sn8AYSyQxRtdhnj7FLjCrDpkLEigNAML7wPUAUa2n2gfGHjSbTajValCv1xNJjt4AzP/H5cYRW0AtBM0KTGKq1WpdgCd6DNADkMlkYGxsLNEQqJcEMQYA6Lpnxhjs378flpeXu5hE6t0yrTWsrKzUS6XS8Dve8Y67K5VKkXOO3gATYiGqlJJCCBFFkVBKccuyrmtU4PU0AdKlvgUAyCAIDNM04cknnxw7duzYbqWUtbKyMqGUGohioiRYT3UfJHia+DFI6t8sol+L4Nf6DyV+CtZRoC9dDAT7UKqT+z88PJwQDKr7FMRDQsWJXigUktTZZrMJzWazawVkJCCsqNNsNhP8hYKMVPOgtf17pRbTpC1kWNiH7/vQbDa7Apvw+vjuEczEvi5fvpyAjYyxBFegi7wAdFR+1DbQbDAMA+644w7YuXNnV74EPY6uQcQC5ubm4OjRowPDhJVSUC6XCyMjI0OnT5+u+75vZrPZHJoB0AmaM7TWBudcMsZEEATcsiyWcgm+KjDwmkyAAep/z6Qfxpj1D//wD/ctLCxMLSwsTC0tLe2IJ3zfgTPGoFQqJSpZv0bVSgz3pap/PN6bbvdfDbjXT+VP/zeNuiMh9WMaKLEajQb4vp+E9NKJitpDoVBIgDuUmI1GI5n41NePwBgF/vA94LtAgqX1BhF0w77wf4ZhJFKcruBD74lKdoDuun+UmQghoF6vJ+YCwBDKPM8AACAASURBVOp7R7ceZQBIzHT9Ac/zEu8HLRtGvRRpLbXZbMLevXsHJgsJIXg+n8+Vy2Wj0WhUT506tQQAAYkOTABBFhcTjV2CEIOBXVOs98wb3F6NBrAW+CejKJKcc/7ggw9OOo5TAAC5sLCwK37ga0r/tUp6p1X/QcAf/Q/9vtntak0AKtXT59PzqGoN0CFUqsIWCoWubDu6oCdWC0bmgBoB9pPOG+inoSCRYHAPQIcJYf0GmkBEzwFYjftH7wP2R89RSiUZoTTuADUcLDYyMjLSVQEZtZH0/BJCdJULw+eRy+UgXs+vS9ukuFMQBAlzMwwDLly4AAcOHBhovqq4ctDExMSWN73pTbv37t07ZllWlmoB0BGkgjEmtNY8DgxiQggWj/21NQGI9KcXT4OAQiklpJRiZWUle+rUqVEhBJ+enr4zru0/UG1hjEEul1tXxB9VPwep+zdT9aetl3RPH+9lKlDCpx/KJKhLrBdDQJQcQT9ajAP/gxIQpXYURVCtVrsi+DBIBv+L+6lJQlFyDBRCyYnqfRiG0Gw2E9yG4gbof6djS98ragh4Pq4ehNdBfAJt/Fwul5gNWCkIpTMFQDHbEfcZhgFbtmyByclJiN12Xd4Fir3QMR86dAjm5+evCJemLZ/Pm47jeIuLi/7mzZuHhRAZrXUXDqC1NiAWrGEYCq01z+VyDABYTCPXzASupwaQSH+tteCcCwCQP/nJTzaVy2V16dKlidnZ2bvXUv2vRvrjB8NM0/tvBdUfoL99T1s/dZ62tCaD/6G2Mu6jK/vgpKcEC7C6BiAeQ0LDQBcASKrv4LiQsKgKTK8DAF0qL0pH/KYEj3gFDdjB+0bEHZkVrSxEU5gx/ZeaFEKIJGGMZgricuSI6gOs1kTA54maBl0iDd3K+XwehoaGoFAoJOOipg7mECDzrFarcPjw4UH4DgvDELZs2TL627/927+hlDIWFxeZEMJicXowdMKDpdZaUJegaZoMAFiMpQG8xiZAmvApFpCYAHNzc/bw8LCu1+v5F1988e1KKQ7r8FuiX3dQQ+mfLu21FtHfTACQbvfDA/C7ly2f7g/vE21XnHwo2Sjh00VBkcDTRIvMtFAogG3bXWnC9LlSrwF16aWP4wcJFTMyqbSn941qdzab7WIciNBTBoGaQrPZBN/3kyClTCYDmUwGyuUyVCqVLs8CeglwfiHGgaAk3jNNGQfoaDAAkJhDuDwajkep1YrHNBgNAOD48ePQaDT6egQAAJ+7/653veuNf/iHf3iP67oyxs6oFiA554IxJqIo4lJKLqWkdHhNYOBVMYAe6n8/958EALGysmJVKhX1s5/97I31en2Sd7KeBg4S7TB8gL0aHlNKJepYPL41GcHNaINAO4DeQT697H/aHxJP+v5o8U66jdIM7dj0hERphoDf3NxcYlohMo5MCWA1+AVtYPq+aN48VZHT5gZeH9F0+gxQcqP6jt8oyXEpMq114gKkeQP1ej0hPMZWU4q11kmOCNWSkFGge5SOH7UF0zRhcnIyWVkZ75W+Q/r+hBAwNzcHL7/88sDaFfH9WlprceHCBa9QKOSEEJZSKsEBWBwYxDnnURRxpRS3bZuReXRNmsCr0QDwOy39OWNM+L5v7N2713v00Ue3Hjly5B1CCJ91yiH3bfjiB0l/OknwpePvfgR/M6X/WlI8LfVxsQqqmqfBP9yPGXMIfqKkx3Oom4uxThw/1tdHlBsJn+IErVYrYR440fHaVLIjhoD9pDWAdBGQtOaAfaLqjcg+btMKxWhHI4iJCUrIANC8QE0BCR/PBYBkDUSU/Bh+TM1HvD7Nd6C1JAAApqamYGpqKhm31qsuaLw+PnPOOTz33HNQr9cHYgHlcjm3Y8eOyT179mz2fd8IwxA9aAZ0qmgJTcDAKIqYaZocANAliO2qJvm1MIBBwT8cYqDCNE12+fLl/Pe+9723c84Z67gxBkb0cM6TjLN+DR8sgj+4b9DnZjYcQzpKLo2a47mYdgtwZdlvuvwWlfDUf00JnEpslJ5UnUYph9dijCVMlarayHDo/eC10kwNiRtDd3EfXg8JkgZr4bukfnrs37bt5FkZhpFU70G1G8E9dHFiiC99jqj206xGXA8BGRTGLiATzWazkM/nu8qE4f+RSWOgESV4alKhS7NarcKxY8f6egQYY+D7vpJSmu985zvvu//++7dLKTOcc1N3AEBDEyCQMcZjM4Bls1laQhzgNdAABrr/tNZSd+r983/+539+3eLi4nYppb9Wvj9y37Vi/vFhI5K8XmK/mdK/3/XTKj666nAiUYQ6fR5jLPFJY0y81rpL1aQETicm1Spon1S6o12cHj9eizIjbFSlx9/IRNJRhxQ8Q2aB5h8Gf6EmgIwEGcDY2BiMj49DFEWQz+ehXC4nrkWqDQFAVxgxgqWUIXPOkxoTjUYDWq0WAECSSUqLjiBDKZfLyfPDd0Q/1IOC93nixInk3aZbPA6WzWYzs7OzywsLC2rHjh3DUsqEAcBqbI3knIsoirjWGvMDIGYAV80E1h0IpPsv801r/ZthGJqGYcjnnntu0ze/+c13RFFkLy0tTXmeN9D9xzmHUqk00G+KEwxBJXwBa0n/m6UFDAL70jY+QLfdTydWWpLTyYYoNsa8o0RNMx/KFDBRBp8hLfFNUXEq3dJEgwyD9p3GKeiHeiCoWYIEAwAJ8WcymaRmH41SRMZBtRgM7kHzAbfRW4FEj9oCegzS5hZdJAW1FNRU8Bvdjkop2LRpU4L0A6wGWaGWQ58JmlU7duxIUo17mIPMMAyeyWQySqn2kSNHLs7PzzcMwwgBIIg/GByklFIR51xxzrXjOFp3T/J1T/ir1QB6MYFkO+ZIPAxD86c//emUbdtho9EYcRxnKAVUdDVUfddy/aHK1o/4b4c2CA8A6GZW1GamRIhEickxFM0H6F7dFxtNhEFpiKAXEgc+07S20EtjoEg3HT/AKpNKuwmxT8ZWw4Kp/e26buJKw0VCASCJO8DIUIxJwEg+AEgWOkXio+5PyhxQy6TmCEYdUsZEA6Tw/pDpRFGU1A3ctm1bkpyE18b3hmOI6/rB6dOnB8a2BEGg8vl8ft++fTvf+MY3br3zzjtHgiCQiAMAKbaDZoBhGIx4A66aCK4nBiCiKBIAwF566aXChQsXNnHO+crKykQURQMHxxhLlnIe1BDwQWJIS6BbTfrjd78Plezp7359UPUSiYRmo2Gjfn8A6ErIQftbSgm5XC5hDpjuSsN68fqIlKf3p7/TzIJ6PqhpgMdwG9+t4zhJ6C66LLF/dMGVy+Wu6yB2lF5clIYsO46TaBcY6IMAJ7pQMZoPf9NS8mgCoAbBWKeGIa41SD0stAgqRhYKIeDYsWNJHkOvJqXkAKDPnTu3xBiz2CoISD9Sx3E2sTeAZbNZBgDAOb9qd+C6TACi/iOxM0gV/tBam0IIo16v51566aURADAeffTR9ywsLOzinPcdFL5oBGUGNQSL0EalvtpbTQNIS/peaj7dT4/1MhvwON2PUgvtfir5qURDhkElfLvdBs47q+3UarWumHXq1sP+UEugLtg0JpHWYigDSps2KN2p1KXeBdd1E1DQdd0kMhTDfVEdB+gQMM1pQK0A1xBA6Tw0NJTce7PZTJ4X3js1gZCZ0LoCeC7VTBCLoedQ0wmfjRCdBU3GxsYGlhHnnLNt27ZNBEHQeuaZZ043Go22lDLUWvsQmwAQ5wjouIColFKRpcSSqbWeeXo1GkCvGIBeHgA5Pj4eHT58ePvc3NzemPgHNlTxBjVUeQFWM7V6SftbhRGkpXsvlb+XOUCJh6rx9F5ppB0t45WugAQAV2gGGNmGnoJarZZIQyQ+unR4OtuPjo/eI+7Da6YZAhINPU7vl5oWaF40Go2EENHjg0yFFuCkbkxspmlCpVLpqoRMcx8oM6FzCSU4NY1wvmGcBDIJpRRUKhUYGRnp8rLQeYhgIgZrnTx5cmDBkPhdeFLKzAc/+MFfKZfLuTAM0QxIIm0ZY1x33IFcCMGFEGkAcF2EcC0YQE8TgDHGgyCQlUol2rRpU3To0KFfC8Mwyzq+/4HcCAGYQY3aX6jOYksT/q2gFVCVmTZqR9PflAH2InpkBkiQlHmkEXXsm7r9cLFOlEqWZUGhUEjq71MgkC4a0s+kSo+fIv0Aq25HPAdtbrwG9QQg46GxA0iM1ByhHg2lVFflH+otQM8I1i7AY1gshrHOuoWIgSBgSN8Zhhu3Wi3IZDJJUZFqtZq4EjFC0PO8LjMJ3aP4PGiq8sWLFwfWCog1Ymmapjk7O9tutVqMmAESOm5AoZQSnHOuOqHALJ/PM0icAVcxT6/q7AH+f621UJ26/voTn/jEr9dqtc25XG5BDaj1j6rleuL+8ZsSfz/CvxUaJcy0GUDHmSYknDjp8/BcOsmxUTUb1V+cyGkVnXOe2NBUPc1ms122ML1++jlT04JeH0N3qfaS1hjSuA1lkLgPK/YgISFzAlhF+LE/NANopiB1Y+LqQZh5iP3atp1oTwjoCSFgdHQUCoVCIrlxXIiP0BJnGHOxfft2KJfLXRWW0HuADApxgeXlZbhw4UJfHEAppW3bljt37ty2ZcuW0YmJiZIQwtAd1zp+MB5AKKWY1jrJCYifE3UJDmxrMgDd7f6jnXcxgSiKhGVZ+pFHHtkyOzs7kc1m647jDCzjq/XakX8AqyojXbO9n+p/q0h/qvZSVb6fKUB/p4/TyYKMBVVyzIWg6ajUlkXbmjGWrKSMAS2FQiE5DxOqKFCH3zS+PT0exBeopKb2PrXvUVLS50HDhil4iIAgPkeUrii1qdaEhIz3TyP98JpoZkZRBLlcDvL5fGIOITPD64m4hgDiK2gW0IAj1E7wHWitk5L1yIgo46W4xZkzZ/rOT8YYi6IIVlZW6pOTk+Pvete79sQmQGIGoPrPOwF2PAxDlkoOun4MID0+8p18dCdTiQOA+dhjj+0Iw9BSSvEgCAaG9XHOEw7dr+GEwAmP+24FQu/V0sTcCwdI7+uFB1AVEgC6gCU8jpMNJxiGo9LkFSklZLPZxP2FFXjRzkdVFFNnaYowwCoDSAfv4DH8TpsqVMOg46b2P70O7QcAkrLuGJiD94KNc57k6QNAl0TH54OqPWoAWOK8Uqkk16J4gmVZMDs7C4uLiwlDAICu8GSci8gY0FTAdQmwdgIyKIwcpHkH58+fB9d1B7oER0ZGhmzbzjzxxBOX2u0255wbsTadBNpB7HpXSnHDMBjBAa57HADlKF2+f+iAEpwxxh599NHhsbH/n7p3i5Ekq+5+V0Tk/V637qrununuaTMDAwPzGYPHBj4MgzzYGJCNpSNLPCDLlu0nkKzzhmXJz/jJ8pGfzpt1ZMmyDP7sMTbwDQgYGAz2gZ4eN9P0NH2p7qxLZuX9FhH7PGT8dq6Iysyqvs302VKpqvISlx17rfVf/3XZJ4e1Wq2zt7d3IXoYcy8Gjb0sP1oPHmTyGPx+VBTBPOFe9Lr2hxlaUPg/SQhqAdKf0VYSC0rDDN1lJ5PJ2K289bmT0J9z6cgFSoD39N8ci6gECkb7u5ozQKC0IuB1XtMRH5APwoUA4kISsuM8nItmMYQ6qSIk+lEsFu17/X5f2u22RUpEnZhDwnpYfMdxZHV1VZ588slDz5hr0hGGVCol+/v7cuPGjYUFQsYYKRQK2ZWVldrv/u7vvvfd7373+mAwcD3P0+nAXhiGLjyAMUbzAMcmAu4VAehwoFUE5XLZZDIZ8+qrr/6P4zT+mJdOeuiECvprv3IeAngUUEESASSFaZ6PP0/AtdDpkJ7+nBYmIgCw5+1227bwrlQqVgFwHH7jpiT33kv66vPuAcivP6MFV7sD+pj6OCgvlAHHoBs0bctwcfCnq9WqrK+vW0OiCUUQES4AcxWGoWxvb0uj0bBzoTsRMRfNZjPmPjE/EJSTycQ2JQVtdbtdKRaLUqlUYrkZoDTNXQyHQ3n99dcPrQ89h8PhcDIajfx8Pp/3PC8rans9x3FsTYDjOFYB4K5HCuBY+QBHbcc1L/R3yP93HMcdjUapX/7lX27evHlzJYr9H8n+H9XvT2RG7ugJVdd35PffzLFI4EXiJJ8WDi38GhlASukYfpIL4Ps02BAR2/pqbW1NMpmMLXLBPxaZhc004Zf07ZPhQ/2ZJKzX94ePzECxaIUy79h6aPcBVl27QggSff65Nt23XwurdjU7nc6hkmjNn4BY9vf37T12Oh3bFanValk0SkYi/RVQTvq+GfoZZrNZuXLliu1PkByRO5I+d+7cmb29ve6lS5e66XQaItALw5CqQM+ZDjcIAod9A8Lp7kFJ7m7+XC99ErOxSBG4IuL6vu9ls1nzyiuvbHz1q1/99Ww220un04OlJ460/FFD151rWPwoKIJlPn4ShSWRgV7k894XmVWUJZUKDL9OWAGiAk1zuZxl09vtdiz27Lqz7bw1wSciMUXD3xqpaD4giRI0ocf1o/CSiUUiEkuz1degm4DApvd6PalUKrZOoNlsWuISFl8TicwDXAdz3m63D5GLrEWs/Pr6ui34wQ2AXyFfwnGm2avNZtO6D6urq7awiGOJiHU7uO9cLif9fl/q9fpCF9h1Xen3+/0nn3zy/Oc///lfdV2XsmBKg93I9bb5ACrsrF32pcJxHAUwj/iz/4dhSGeS1N/93d89XSqVeo7jhOPxuLjogNofWia8etEkw3/8nvfamzW0ECMo2kIykrA5yQHozyRRAufRvQE06+66rs1wwxoiPGT7iYj1j5PQXMN1DXvnwXp9H0llB7egEY/ITInp13hdC2tSEcIfoOTS6bQ0m01pNBrWL2cesPaDwcAm+5CyS5PQg4MD6fV6dvNO9jigkzRuE+cjhx84r0OSWHtdPci1E07Vc5ZEBTzDnZ2dQ4aCYYyRdDqd9jzPu3nzZnswGDgSL7zzIvhvewKEYaj9//t3AdRIKgHbBgwC8MqVK4VGo1HNZrOj8XhcOIoA1C2dF31GZDqpeguqR8HPZySZcRathtvzrH1yaPg/D+aLiCW7RGZ5/ZVKxZbA9vv9WPcbndGHvwtXoBNjNBkrIrK2thbryAQs1opOXx+Dz9A/UCvFRVxC8j618OvqO2A57b+woJ1OxwosMXdNLGLh6Ru4u7trBRpF0ev1bOdgx5k2TYFU1PdAs5HRaGSjE5VKRc6fPy+9Xs/2C2w2m3ZHIu3a6bAs+RJcz6I1USgU0uvr6yvVarX4C7/wCysy4wFcY4xnjHFFxPU8z0X475YIvJdUYKsEjDGO53lOr9dL/+QnP6n96q/+6s/39vY2u93uqrOEAATGHnWNejFra/EowH/OnRQMbcEXcQDJhYEfqO9Pk2NJaExRiy4N1j3tGVi/5I48KBA4GBQE5yJawDFE4qRg0ofnOjURmeQI+Fxy3pJrwBhj9yRE2IlquG68gs+YWUNYchsI3+mGoplMRgaDgT0u502lUrb12MHBgbRaLduNGNju+76srq7aRqK9Xk8ODg7sfWpXodfrSaFQkPX19VitCnPL+uD127dv2wzEeWMymZhGo9F+6qmnnnjve997JspijBGAEsmhIgG1nB4pHAsVwBICUP9vScC3v/3tgx//+Men79y58/Zlll1ELBGyTHhZQEC56JqS1zj395s5tMAjLNpqzRN6BosCaM5nIK1QEBpZRNDwUAos1k/vjqMXIEJPTXy/34/xC9oi6jJarhM/Xvv3XCvPW99D0k2Zh4i0MuF1vsNcFotF2xtAl/5CfuoW3exaxPVOJhPZ2dmx0J/uQpqgC8NQ1tfXZW9vz84rm6GQ0be3tye9Xk/29/ctb0Uk4I033pAwDKXRaNh2ZSBWbay4fx3NarVa0uv1YjyLHq7rOqdOndoIgiD42te+dis1nYCUqHqA6cdcx3Ech2iAzIT/yISgu+EA9P8WZvi+7xYKBbOyshJcvHjx7SLiua7rLzoxi3DRTeuBT8vC0MLzKLgASUXHA0/eXxLe8hnCUFg5lAiCgYBhyRAOFpdu+6XnBuFLxvXJkdcZg9Tel0olG9LiPCC1JOei0YtGFvN8fc1j8D9+sb4ukVn68u7urrRaLXEcR/b392P8AoqC1mXj8dj67xyT0uAgCGxsH4Wor0N3oNKZkbgB29vbsr+/L3t7eyIyVZI7OzsWyhtjLO+ws7MjhULBZgPqFmUoHQbKZzAYSKPRWIgAIpSSKhQKxd/7vd979sSJE8XJZOI6jkM2oGeMcSJF4ARB4Kg9DY6VEXhfxUAwkSJi/uZv/ubtBwcHm7Va7dZRxwWqLRuarNLadJ4/+VaOecU5SUERifu8OvTnutOMNhGJNTrhbyyGzmUnAUXHr1mI+M34mfr8CIHnebbARfcA4FyO48ReL5VKsSq3pJui5wHllrxnUNI8PgQFoPM9RKbbkzFPpDyPRiOrDOAKCAcieCiyfr9ve/jT+yCZnZfL5SwRyBwcHBzI66+/btEFz1ErrF6vZ0OKKG5yDthQNFnyDIKiJ0G/35dGo7GQDHccR/r9/jAIAuN5HpZft973XNeNuQBzXIGl4ygFMC8CYH/CaQqi+dGPflQLwzB1/vz511ut1skgCJb2/z9O6a/2SbXGfZSEX/v5GsKKHI6r69c0LyAiNsSUVBJ60SBYmvTSacAi8QIhXVbLOXQ5LyE2lAupsvjSzDtwuVKp2BBX0ponowYaXmu+QLsr2m3AZYDIy+Vysrq6Got+gJBQbtwDc8X9ZDIZOXHihJRKJXtewqKe58lgMLCch3YfSODpdDp2g1PdGkw3AQ3Daavwg4MDSw5SQDQcDqXdbku73bZzo+eReYIzuX37dqyCU48gCEytVis88cQTZ27evNna29vzIzfAk4iIjxSCm0qlbEJQ1Fj3WFuI30sqsPX/I6bRbTabWdd1ZX9/f2M4HFajm1maAbj0ZM4s1VN3/k2OR4EM1MSY9onnwX+GVmaO49jccToia2JRKxheI49czyPXgCBoQo5FzHwSW09yJ/AKCFgYhrbrEOy5vsbk/OOezEsX1kpB7+GorSSCLjJVYK1Wy/rtmuxDYLCc/E2//mazaa1xv9+3jUWYM76vuxAbY6xV1xuEEg7UaBTj5Pu+3WKcPAx2YGaXIp03wX0RpkylpjsJL6oL4Jnv7+8fnD9/fus3fuM3npDI/5coHVihcCdyBxy2EHfmaZXEOC4HYNTf9sd1XWc0GqWef/753c3Nzd61a9feHT34hdLIhB9naL/pKALwrRpYZhYuVmueby4yEwgy1HQ3Hw2dNXTXRCPWDv8fwdaWRp9TcyeanNMLk3NrcpH7YNcdwl+4D1hfEYn583rM40jgPYD7yXkir4G/IdT4bBAEsru7a3sAonBJ+tHx+1KpJIVCwUZDuFc4F1wvkFW/37fXoyMjIjNeRaMOnZFYr9etQhmNRlIulw89S82x0Jeg1WrZisV5a9wYI8ViseB5nnfr1q1+pCyswMMAKvivk7oeSh4Av50wDJ1o4WdffPHFX3RdN8zn8x1ZYv1ZWEcNrJheRI+K0OvheZ6cPHnSCjWCrfkAETn0P8KvXQd9X0nCk//1xpqcEyHXCzZJmibDlEBfFozud48SQFkjOGyssbq6KidOnJByuWytI9Bc+/laQfE+PxxbKziuW2/qyWvaj4b/0L36ut2uJQO3t7ctH8LGpMyL3nQUS8wg2gCCwbWCf9GFRhpJDQYD2d/ft0iuVCrZ/7Wrw3qGqyDUub+/P1cmHMdxgiAwJ0+erJw+fXo9m81mTpw4kQ+CwHGm4cBDIUAVCbDLR5bI47KNOpJEwiE+IAxDx3Vd+epXv7rR6/XKruv6vu8vTPCfB5HnDSwT6ZlJOPmoDGOMrKysWDYYtle36E6SgPzWtQ1Jn5q/tZughZbj8IPvq6Ml2mXg81opIAAsvFwuZ+E2hBvhOMeZbhgCrG232/L4449LuVyW7e1tSafT0u12JQgC29wVcky7BgxCjChAnZYMT4FfjUUF5pN5p4WJ77HVeKvVkkwmY0uE+W4qNd0uHH+ehB3Wm440aISEAmTt4ZYy9yiNdrttn2u1WrXuhsiMqHVd16IXirUODg5iqFEP13Wddrs9vnLlys7Zs2fXDw4O6nt7e04ul3PDMCQVmO3CLQqIrs9EygAlcEh4jusCHBJ+Y4yTSqUcY4z30ksvPfb4449vZ7PZ3ng8zs47kT3YHFg8b7CYk77qo6QARMQKR/I1ndyjfyc/N4/c1P8nlR6LHouKhUWYWURa8OehqOQ1lstlWV9ftyw1MLrb7Vp+QvcFhDiknTu78TKy2awUCgXLGWgFqF0Rzs9ckHvPfZH9pxEJAioiNq+BfBHfn+3+22637S4/hPyKxaLtB1AoFKyLUCgULMmn0Rzn0TyKfnbchzHGIiSUD4oryQOQh0G2ZrfbXbi+IkTlvPe9733ScRzn4sWL7Vwul4qMr83FkUhGjTEyZ+fghWOZAkiu2BgaiAgGp9vtun/yJ39yud/v5+v1+juOEu6jMgA1RNY+on7vURrdbjcm5PihyRRPLXgMba21kPJ/ErbrYyXdCxa/9u+1BUsqIk1eYjnZNJPXaYsN061Ds91uV3Z3dy05p5teZLNZyefzNuwmMn3utVrN+sn49bqfnnZlRMTuEUBiFWW6oB1d34CwEias1WqW1Z9MZjsjF4tFy3kkIwtkHKJYcR20e5J0rRis18FgYKMQuEPMp454aH4LZTDvuMYYk0qlUul0OtXr9YInnniiEll9K/iu61rrH4ahE63BY4UC5yqAJeRBDA1MJhO3XC6H9Xo9e+3atQvZbLbjum6w6MTAwWUKgMWO1l+UL5C0mG/FYBGSh08Kqa4ES/q4SYHXQqzzA/SC0ShBHxeIrv3HefOSdCW0NeK1g4MD2d3dFRGxvisQGquIYJMFR94718qiRqgRCI61vr5u3QgRiT1fTSqiAHSoEoHkflBsNwqIowAAIABJREFUSfehVqtJtVq14cbJZGITi0h44r5pL0bPAZKLdHkw59WhWJEZIuCZwSc4jiO7u7vSbDZjnYf08+c+xuOxncdFisVxHMfzPGdvb681Ho/DcrmcCYLAieL/uieHRebROTVxL7IAld9zSzC10My3v/3tzX6/X3VddyJHaJ2jEoBE4tly2heOXcgjwgfw8HXffB37Fjns22ulkBTYZNKIVgjzwl/6ePOUQfKHod0rFjHlrlr5Uo+P5YZUQ+CwwpPJxLLcCCbZeQhXq9WKCTpQO7mrrraSEG7azZpXGco9aYvN8wnDaapuvV6XMAxlZWVFzp07J+VyWTY3NyWfz1s3BrcFNKOfCcfTz4fUY9KGuY+1tTX7viZEI4hujUe325Vut3tozTCisO/45MmT6x/84Aefunnz5tCo7D8RuzswOwQZ9VpSCRw+/qI3El869HcwrT8Of/CDH9SuXbu2/ra3ve2S7/u5cFqeOFcyj+v/68lIsuMijxYXgEbXi4LXteAloblWCghjchEkCUEsvobbyWvQJKtevMyXdge0e0DCjU500eXFWnGnUikplUqyuroaa/9lIjIU/1uz5r7vS6PRkMFgEPObdRow94+bqOdFK0aUCkpC33O73Y7Nn45K9Pt9uX79uqysrMjm5qZ43nSTEKy1zjzkHCh4fTyEXPMxKAHQA9maevMQ/YxExCpQvSNzchhjJEr+kddff/3OaDQyuVwuHarmn2YWBRD+TnIvi8Y9VwNGN+GdP39+9PTTT+/cvn371HA4rEQnnHtWSJxlwsti1PXti6z9cZTJwxxagPl/nmViJK0JSkALsn6dQTjKcWbNQAjZ6fJX3fiTnH99PK0UOD6Lm8WJDwwK4HmE4WwHIgplCKlRbkvoDsHWkF0Lr54rjYa0IKOMuD4ISK4JopD74LsIVLLgB9TSbrfl4sWLcvXqVct9HBwc2PPo1l+QftyPPp6ODuiwYSqVksFgIHt7ezYUqTM9iSBol4lU5nnrPEIMrud5biqVSq2vr+eia41Zf5akmeYGSJQM9EAUwNwjRJPtrq+vj9bW1oatVmvD87yJPIAkIBaihsnziMC3EgXo69G++zxoz0jmB2gWnFAZlkez3NrXD8MwFq7SKapacCiH1dluXINe0FowEVSsKpEFTSgSvjPGSL1el4ODA5vUAxOPYOr7Y+gqSf2erojkda2kIAO1EkXoEUBdCUkoE/KP/42ZVuHBT6B4qPjj2XJc7mUeZzOvjBuOpFqtytramhV4NgjV+QMoSVyoRcLq+77U6/XmhQsXtp555pkTnU4niCIARAJElK+fQMhLheSeE4HCMHQymYwREffv//7v/0cYhulisdiQBQrDnvAIDoCFk7SCj9pI+oSLfFK9iETi969dIhY/AqwXFd/X8XNIKix+klzlb604eJ1rQID0dXJ+QmYoIFpxgczwZXu9nuUfkuy2JjY106/vdR4SSBYTaSWlCVbmBnQAk6+TmeiXqPMOUBggl2q1atEA7zmOY7P5QFT6GWq3iOhCsVi0fAK9GfU862fNPRIJ0dzIvOX2xBNPnPF933/ppZe2M5mMa8yhcn3WnubnjowE3IsLIJzMcRx58cUXN5599tmfnzlz5qfdbnf9OAdaZrlZIDSr0PD1URjz2Fy9eJPviRwm95LcAItaW2buObnggcWcS1tehi73BSXwXa1IdCaeRggUA4EiWMDlctkKgzHTOLruU5AMN6KcEDp9HwzNoovM/H/d7lsjCaC/DpNqt4aEHN/3bV9EUAHPBfeAWop8Pi/V6nQPG83ykxrMfWkuhnPyPKvVqqyursZSj/H7dYUipCrH0FGURcPzPGcymUwODg76v/Irv7JVLpfTQRBYsi+aQ1wCUb/FHJEOfLdRAA7ueJ7nhNNMJJlMJt7t27fPBtPtwe9LUpmwZDnrozI0YaYtmB7a2vJbL6Dk8fiOhtraKorEiThtUfmf68E3Z2GyyLGKCKVO+9VuA+chfKcVRbFYtEksQFbdOQchTDLxIjPXQ98Xx9aEplaA+M1J1IhQ6nx9+I5er2fvmddS0eYdWFqODSqoVCpSKBQsCajRyrzoDvfCcwOFcB+uO03x3djYiDVnDcNZDYB+xjoKsmjNjcfjieu6biqVcjUHEH1EtwS/K2N5LwpAWyfn4x//+O1ms5lvNBpnjyOsR31GC4vmCx4lFJAU/Hl+v9boWpBFDicFJZWDPpZm5lnoxWIxJsxcD58VEWu9QQu4DfAFfE9DY34KhYKcOHEiFm3IZDIyHA5tT318VxQ2C18rRp0/r+9Xk5hwFCgTBtCY7yWVCoKHItLW+M6dO9JqtWySkiYMSQDSZcHMNaXIGiEllayIxI7Fb23VQUugM5QJG5ImkVfSTdQjMrBBKpXy3vGOd5zb29sbtNttPwr5iURKwDlG6e+8cU8KgEnIZrPB1atXK1euXHns9OnTl3K5XOsoyHHUMGZWpDKP+HurxzxLJnKY0ddwcdGD5XP6tz7PPEsfLYhDhTQaOWmUgtDrratRCCxaSls5h+9PdxNm4XIsY4zdUw8lBXTX7dB0+E9byiRXotGKPjcCq2F2khfSClIXIfGdXq8nN27ckNu3b8ve3p615lxzq9WSwWBgMyBpqVYoFGzGIkKp54Zr0YVcbBoK/EfwkxWbuVzO9h1wXdfWXiwjxqNn6aZSqfTrr79+fXt7u7+xsZH1fd8A+4+xbBeOe1YA0UW7nueZ06dP14fDYXE8HpfuF7Kz4CBxHkUycJHQMrRgasusXQe+r5UEi0F/RrsRuEUQbTpGjkUUkRijLzLjBHRsOwoVxUizpHDpc1Mpl81m7SLX98Cx9WvJv/Ht4T0Gg4HdHIOdizSPIHJ4/8B5a4E5026F6856Bvb7fetOTCYT6fV6MhgMbJ0DyoE0Ytd1bRckkbh7xhwwz5osXVlZkVqtJul02nYL4rjaQIThtN4CtKFdinnD8zwnCALfdV33woUL1clkgvCL3K/LfS9fim7GdDodt9/vex/60IeuhdHW4PcKRRia3NJhoUdlzCNskhCf1zRS0Oy3JujmEYkaKuvFgRBrQlFEYj5okkjUAgQi0NYaggzmGgHSeQCaIwA5MBc6nKiVWVKI+b7OBdDlwbpJCYKsuQF93EURD6xxNpu17glCv729bcuEcR0iFGsTl7i/crksxWLRCqs+vsiMg0ilUrY1u+tOqyRbrZZ4nierq6ty8uRJqzTgIbh+KgVxgY5CuXt7ewelUqlw7ty5lVarNYmu5b6h8f1wAE4mkzHveMc7ul/+8pfffXBwcDqTyXTvF65rK3SUZnyrxjxLqRelFnCtBPiOFhhttZLuhP6b7+k4PrBeQ1+sqA4X6v53un0YWXnaj+WznF8n/GiFpJFNMqyXRDBaAcBb6ExAzeInlaJ2IURm/SST5yD/H7RijLEpvZrjgDzkPllnwHqsvu/7curUKVswxLnZNLRYLMqZM2dkc3NTCoWCDIdDm1+Qy+WkXC5Lo9GwoUnmU98nz2mRm8gIw1BOnTp1Ip/PZ771rW9dM8aYaG6M3KcSWNYPYNEwImKCIDDZbNb/9re/fXJ1dbX9xBNP/PDSpUsfkfv0SfTi1v6kyKNDBM5zS/S1JS05gsvQ1kB/H6uiz0FSDq/rzT6A4wgXvr0WUqA959QCipXTVhbBxKfVG47o7jzcHxBXJN7rMenC6KpAjsW8JOcuOU+4hRqK6/Nw3xyX+SsUCnazjlwuJ8Vi0eYt5PN5K3y6ph+exPM8K+wMfHhjZoVRzWZTqtWqGGNiiUej0chyD6wBFJ/mTyBYl41oTszt27f3zp07VxuNRrt7e3uhIgLNHLk4lhzejQKInSF6wE4mkwk2NjY6/X6/FATB0bt9HjFYtEkG+VEQfEYSxoscTgDSQriMEEwuDIg8rUQ05ByNRrFcAOrPPW/aLRjFonkUHYpC0QD9eY+FjsJA8YxGI1sAJDITZASN+UiOJLGl54L7nBf71sfSqeB6vjQBi9LqdDr2b0KTWOxWq2WVCfCb4+trImLgeZ6thVhbW7OKEPQCkaf5DfolGGOk2WzK/v6+FAqFWEk77o9eH3AxyVCnmjfjeZ7TaDRaBwcHA8/zjO/7YWLOHbXOnGhujiUwx3EBNMywv13XNcPh0H3/+9+/v7m52T44ONhMfGbRDR19QjMLA2pY/aiOeaRZ0gfWr2uWnN/AcJF49p6G+MyJ9ss1vwCppRUNCSl6hyAREfrHc14gM6QgPi2WNVmtp++JnXe5V9BDUmloZaaZfZ36rN0MHaLjOrXLpPcsYB5RfHQ48rxpD0PCfiJirbPeBIXIB9+hbLnX61k0AZTXHAyCTzp0t9uVnZ0dG65lsxKRePhQP3eQwDzZcF3XGY/HoTHGPPvss0/4vh/s7OwMo52AjTPt+gMCMI7jJHm4B5IKfGhEliUUkcx3vvOdXzh58uQbpVJp18TLEO/52Jp80Zr2URlJ4dWvJYVQJB4+1J/heyx8XaKr21DpmDyv6aIV+hLoxTQejy3ZpL+jw3uc/+TJk5YQ0/eE5UzGqVEaEIG4A8n7wlJruK0jEXo+UHzJEOc8joV7YJ1wHSiG9fV1MWZaHci9TiYTG/YbDAbSbDZlZ2fH7jGYbM0O6uJ+QBRYcRQuaIaQJJ+b5x7xXB3HObQH4aJ15nmeWyqVCtevX7/9yiuv1HO5nBtJfJgQfpGE7B1lOO+mI1Ds9WjSzWQykXe+8503RCQcDocVzrvspMsuigmhnDHpby/6+60a2ipqmK8X9jxuIEma8b5+DYFAOFzXjVlx7Z8mFWRS4ejwqr5mXmdh69JXSEOuN5ncVCgUDt03gkkxl/bddWfeeeSXjvwkFQGRDu1D6xRhUATH7vV6sru7a5uo6r0IdNEVLcS5ZtCSiMTWoIjYSkMq/thwtNPpSKlUkmq1Kp1ORzKZjOzv78v169elVCrFXAAQllYcRFbmGbnIBRDHcaTVag2eeeaZ1Uwm44TRzSP8Ml9ej7Sax0UASTfAOI5jotRG9zOf+cy1TCYzEhFxXXfpSef5fcmBD6rJs3nC/lajAhYnQ8NgLRSLlEDSQiLoLAZe1y6B9qX1dYjEeyckowqE9mD6tQuRzWal1+vZVlqgEceZ9eMXEcvNcG88Sw3NRWb5CVr4kzkLyTnT86aPlRxkD/K3Rh10/CFdWURsiJH/UWo6GiIy7YJEfgKKEi6Ae4E3gSfY3Ny0OQWe58nOzo4tRR6Px9YNom1coVCw96+zDLn2eevZmQ65cePGnX6/Pzp16lTB9/0ggvlWFpPW/7jG8W5dgJhvMR6PpVqt9v72b//2/M2bN58sFAoNx3EWSrjW2ouGhnnad0xa/kfB+oscJreS5J+25Hw+SWJpiKlDZcn3RQ5XH2pBTkYQ9OscF2vDxhRcG8KE5c3n87HrFJm1+sK9AAJrC6fdDa2QtELkPvit7xO+geQZ7g+BgQAlkUZfdyqVsgk4fBe+hc/l83lZW1uzrdtERBqNhuU4fN+PbToKL0AUAcYecnBzc1PW1tbkzp07cufOHUtAkm7c7/ctutKCru9f71GYXNeO40iv1xvXarXKk08+ufnDH/7wtu/7AXkAKAJjjFEG0wyHQ8O8yxIkcK8KwIRhaAqFQnD79u3C2bNnW88///w/B0GQWVYQBAF2lPACi7QPPO9Yb4USWKZ85hE5ML9JoUzGzjXZByusXYGk4hCZKVQWMkIUhqHtd6/P5TiODf0hIOl0WlZXV23IVcfCRWaEZPL8b3/722V9fd26AoTR9P3rpKV57o7OetSIhXvJ5/OxpieEPTleUqnwmj4uxGYul5NqtSqj0ciiCPgQx3Gk0+lYaw9M51ngHug5wJKzoajmH8gOHA6HNv8f5QjJyP8os0VrPbofL5VKeVeuXLm1s7MzyOVyjjEmFIUAZCqTRAeSCmChoMxVAM78EELMBUCzNBqNVK1WG9br9Zrv+xnP85Y2BT0ObGfBLdsZ6K3iArQAzkMlepGwQEVmwqktXfI1YDILUVt5Taxpq6p9ZrL5dDwfYQBREAevVCq2hn40GlkijONC1AF7OYcxszJfnTCkUYrmEeZxE/rveYw/yp/njyIBlmsXUaMEvWY0eQqUF5FYxIJrJT24Xq9bV0FHargfFFyhULDNPYggMKcnTpyQdDot9XpdXNeNbRJqjIlVUg4GA+n1elIul5eGAQuFgjcej0c3b95sFgoFJwzD0BgTRmjbGGMs86/XjIjdC+CeEIDONU4ewETxSHnnO9/Ze9e73tWeTCZpfQFzD5iAr8sGGj/pK7+VIyngvMaPJqUQOs3KayWhWWHHcWKLBLgIpCNRRW90kUzN1f42PfmxTMPh0CoGFi2DunfCXKAvEYkdD0IMN+NnP/uZZc01RE9yNhrlaFJTpxAnlRvf0ZwBFlLzFyISi6vTj1+jKt6HuMvn8zGEhL9OH4F2u20jKjQMCYLAQnQalE4mEzvvJK0ZY2z3Ic/z5NSpU5aE5LlC4uLmgMpQYMnhuq4zHA7NlStXbp45c6Z2/vz5Uq/X8yOuzUwfUxhGPIBxXTdJDtrlO29NHxUFOCT4vGaMMdFFhF/96ldPvO1tb7t+6tSpV8OoJmDRQY9DAkbHjy2eR0EBzIPgyWvU1l8/VP09IGayIk/7/RohsPDZ/47vIFRaAel4uracQM5UKmWTXBBmEbHhQl0mizJAWPSOR/q6mQvgtlYwmiDUCkBEYijDdV0L1bkvXZ+AQtA5/igMba1RRvjVnIeNPw8ODizc5z1+PM+TTqdj8wM6nU5sbskpODg4iFUtcp8gKuZcRGzasUYCmkhEASxzAfr9/nBjY6MahuHk2rVr7QjFmCgSYF0AmebnyGAwMGEYmuPIy91wAMkoQBj5AebXfu3X7jiO4+/s7DxxlG+eTIGdNyBGRGa500nL+1aMoxBMEvbrxaqZcBY477FYEXi9iQbknW6Qomv6ReKWE6vHuV13WiBDZRqfT0J2uAQQg17IIA0dSkO5FIvFmC+N4tIhSpEZ9E82O9HIAdYcgcSy448Tq+fcOgeAeyYDEOWUvF6qEJPujv6/0WjY/AHckV6vZ7+/srJiMyix/tlsVjY3N6Xb7Uo+n7cZiHqfQ8KMKFg4l0qlIouGMcaMx+NJLpdLb29vNxqNxiCVShkRCURxAEEQmGhek0lAySS++HNZeObEdSQOaKKHaowx8u53v7uTz+dHlUqlnslkhmZJT4DjwH+RmXUAeorEYaX+/WaOpPXXfi6LH4EE+un/RWapwqTvJhcjyCGZLqstHUKZDJ1pC6tZdZCCVqgIHllsOjzF+zwH7g3lg3ICMnN9pBCj5DRPwdDuD+cBlqPkgmC6z+DGxkaspRnH0wlTGjFphUJGIO+RZo3/jsJLhiYPDg6kXq/Hqga5vtFoJK1Wy3IpOssS0jGVSsm1a9di6cIgARSHyDRLM5fLyerq6rKGIM7Kykrl1q1b9e9973u3c7mchGEYwAHAA4gcCgmKiO0adF9RgHnCb0QkDILAOI4TvPLKK6V6vV557LHHrqTT6eEc/8OO5GQvGiw6Fr1me3k/6Yu/GSPpq807r3YD9Gf0NeP/8Xmd4kuWmVYmWFkN/zk+0F7Ph+u6lrnm3Bo1MEiI0UoA5UKNgohIuVw+REzyjJKlxCIzIk8rIX19yTi4iMTqPwhFFotFWVtbk42NDdna2rL+MkqAhp8oK5APwmaMsaQqCnAe4YYrwf2Px2PZ3t6WVqsl9Xpd7ty5YzdAzWaz0ul0JJ/PS6VSkWazaTMN+a4TEa66/FhHUyACM5mMrK6uzpUJY4xJp9PSarXa169f3/uf//N/bpVKJTfKAwgdxwm1G4DwKxRwpLU9qhjIyHwNYkTEpFKpMAgCef/73986e/bstz/1qU+9r9fr1RQDGRssBG3lFg3IFZ1Cmvy8FoLjIov7HUlrphcUD1GnjyaFn4ePwPb7/diCROgiXy7GBbDwWWx8js4ymkzUrDnfXVlZsYqF0JZOMBIRW1zEAIGxx57jxDPYaKZJhiHbXWnFrav6koKo+R2UBygGdJLL5aRUKonjOLK3tyciYsN4pNtWKhV7TZlMRvr9vr0O/H2uSZOz8xQ11aiTycS2FtMojuy+XC4nnU4npsx935dmsymlUsnu+KPPA1piraytrVliMmlMHMdxJpOJ7OzsNNPptNtut/uDwWDieZ4xxoQmCgU6iaSg6OtG39uicbccwCEUEIahEZHgK1/5yslqtdo4ffr0paMOdBwEgEbWxJH+SY43CwEkOQDOy+LTVpAHoMNi+jp1EQyWTAsyC04X1qAgUASw3toN4LO6/xxKJFlirc/LNSIcIhLzV+ETKJPlfxSG4zj2fVpl6445Gj1w73oDEy0EJ06ckPX1ddtR1/M8aTQa4nmebGxsSKVSsZER7gM3h1RekenmrWTqYUSYP3x/3AXmVj9ffH+dqce24r1eT27fvm0JwNFoJO12W0qlUszC63vTKc3GGDl37pwlaZPDdV3p9/vDdDrtnTlzpnrlypX90Wg0caf7b4bOjPUPwzA0ruuaMAxNp9MxnPOosVABRJokKVUxBWCMMZ7nhSIi9Xo95zhOMB6Pc8tOiAU8ztA7yUbXpK/vWMd40GMRUysy82m1n83QDwOLjBDobjjaMomIdRe2trZiQo6l059HkLDSk2hXXN+fbpm9u7sbEwCtoFAofJ8BLK/Vala5mSgcpuPy3Dv8QD6fl1qtJuVyWYbDoU0xhvFmHhzHibXuJinm1KlT8oEPfECKxaLtGsRmG9Tc12o1WVtbk7W1tVjOfz6fl16vJ+12W4wxdt8+5o/7B53AxWgegc/CSRDhcN3ZrtW7u7vWd89ms3ZHYuYbV4pzQ7DSVyGTyciZM2cW1gGEYSj5fD5bKpXyr7zyyuvb29vdbDZrwjAMIt8fFGAVQCqVMupYS3MARI5GAEmLr19DA5nxeCxf/OIX/9+PfexjPzDR7qSLTqwt4rKhYaHelGGeEngzrb+24pp9x7oSQ0/2yNPH0EKnGXsWJ8oAq8RxiU8TCtRbcXFdWBbmDrJKt//SShWLzT1wHO3/B0Fg/V8RsYKMEsEXRpiy2awUi0UxZpo0s7W1FTuHyMxN4piEAMvlsmxsbIjv+3Lx4sXY7r+FQsEKM805MpmMlMtlu7cfKbgiswIoagc0ektyGaABno2eU9ed9hfkWJChImJ3X6L0t1AoSL1ej+UZwL1o/gu0VC6XF8J0Y4xJpVJOs9ls3L59u3X27NmcMSYQkSD6HRpj8P1DZDFC5ccad9sRKKkQgiAIwkwmExwcHBS++93vvm1lZeX2cDis9nq91UVQ/bj9/njwSYuYPJ724d4MLgCyLnkt+gFrQk9/j89p4k/7/ZrE8zzPEko7OzvWOup++Zr8Sy5cLDVxcb6LtWbOuDaafwBfQRf4zAhxEATWYhoz7RRMijFbZKMIRcRmxunSYhSRFkjdnLTdbstwOLQhSfIRuJ4wDC1x6fu+lEolG3qD20AJasiv+RkYfnIj9HMBoWHFIf+A7yhhogPdbldERPr9vnS7Xesy6GeEEmc+y+WyrK+vL0yP9zzP6ff7kx/96EdXHMcJ0ul0GASB73kelj9wHCeMQoAmmn+bD6DW3sJIwN1WA1rrz9+u64ZhGIblctn/+Mc//l+lUqk5GAzmCj8LTpd1LhuayeW7/H4rXIBFSgjLoi1MUmnpv1lowEcWGt8DZg4GAymVSjaUxUImHq2FVM+nZvsRMgSBcwA7aRgiMtt2m886jmMTWIbDoUQhKIvIKC4CfnNsnhuuAPkDutmIVqLMWblclkKhYI9N1hyfAU6Px2OrcIjB0/SDvnz63pmTRR2mdARAR1o0KYniGAwGNsuy1WpJJpORarVq73Vvb88qBt2dmXXPHImIbG1tWfJy3noOgkCuX7++vba2VnzssccK165da7uuGxpjAsdxLApwVBagG1XjRn7AkdGAewkD8jcMZGCMCT3PC/7oj/7oaqVSaRQKhYbrugsdfRbwkSeOEAAwS+TR6AGQXDz8ZsElXQWRuKXVv2GcsVD6e1gcnTyCxUNRIEjJghksPcIKOiCxCFQgMiMWtT8POci153I5mxYL5OXZ5HI5u+sO2XIiU/eCEGOtVouF6PQ8iUxRAlVzum4BF2EymUin0xHf923kQbsQRB9QYtraY9EhLJmzJB+SVJwoLa6z1+tZa813RqORVKtVKRQKNhyoCUNdYck9+74vhULhUNNRPSIyc5zP57PpdFpeffXVOyLiRwRgECkBIxEPECmBZPTpyOY899IRKIkEwmC6T9n4S1/60vnt7e3HV1ZWbi4TVCzPcYQZgdAwKWlZkxb2zRqLzqWTU/SPZpg1d0DMXteoo/DISNOZcZ7n2Ww2nSarlYr+zQKjCg6lgFUFslMiy+J2HMcy6CsrKzF2HYvn+77NZAT2c1667nS7XWv9OLbOG6AOodPp2PsaDAbWpycSADfhuq5ttQVUb7VaIiLWTdGdgPXaQAi1L45C0FWYXKNGL8YYq5Sz2aycOHHCfpa5qVQqsUpM3BqdNszxtra2YmnTekRh2kk+n8/W6/W9V199dT8iAH2ZZgHagqAwnNYCiIgZjUYQ9EeuX5EjFIBzOKFgriuQSqUCEZHPfvazV3/nd37nf/X7/ZLv++lDB5wdN9aIctnQOfPzBP5RGNr6Y0mSrkAS5iG0WAuEW7sOOqtOH08TVTqHQOcjaOEAGaysrMjq6mqMbdcWTxf1aB+XgiHIPxjxSqViiTH8ZP35ZrMpvu/bHHytWFA4sOfk/uuQIaiEe+YcxNcJMeK2lEqlmEWHYNWEq95cVCdVsb50KDa5lyLhQnIQ8vm8nDp1ykYpUqlUrAegdvX4X7tIJ0+eXMZZmVqtVmw2m41//Md/vJTP50NjjC8ifvQ7kIgDiOC/McaYfr+fdAGWCso91wK3lYZyAAAgAElEQVTIjAcIHccJJ5OJbG5u9geDgbu+vn5jZWXl1qKb4+EsSn/Ug0nEYiSP82aMeb78Iqi/iJCE3EpCfPx9YsGO49g4uy604Tu4ArDhGg2gVIkarK2t2caeKysrtvRUh/10ohVWkZp5jVCMMRZCr66uisiMS2i1WnJwcGDRAz33yETUyV/49QiTnlsSnBB+FBeptrg/yXTl8XhsSUCtMLkffZ+ci/vSURFd2KOvS+ei6E1MQAeDwcD6+3t7e/Z8ukdCMlejWq3KyZMnFxpBz/OcwWAwvnTp0hunT5/ORc9t4jgOQq8JwDCVSpnBYGD0emVJLlvb95MIFPJjjAnT6bTfbDZTzWYz67qu7/v+0hbhx+UBNGE1L6HmYbsB2rLr1/jRAoKlT2Z/aXJQJB6O0laOQhbgZFLh8HmtZPQOuXTI4ToODg5s1hqL/9atW9Z31tYRZUMpMRwCi18jAja8IKYNRMefDcMw5iJwDvgHPoflHgwG1hrDuudyOevXQ/jh17MOdESEhKKVlRWbPzIvwUgLuUYaXAsuFpaf82hkhOLq9Xq21FpE5MaNG7FnrzcESRK1p0+ftsp40Xq9ffv2zs9//vO9SqXiiEjM+odT7UY9AASgSCIb8Kj1fV/FQBIpANd1A2OMqVar47/+67/+388999z31IJfmA+gfcqlF+nOCjuSvvWbNbQA6/Nr4Uq6Alp4udfk9eutowmT8VkWU/KcesH2+33J5XKWPceSszklAgVKwB/nczpfQYfuCAtyLk2y+b4vrVZLWq2WZd+x/ggkC1v3KwDdoKRIiOHcIBj9fSx0uVyOZRzid5OG2+v1bM4Dbg6KNmk4sNycH6HXbgCwn+9qAhMOgPsVEVtGjKJKQn9NAGazWXnyySdj4Vg9PM+Tdrvdr9frjQsXLtTq9Xqr1WoNvGmzHQ39A2MMzL8Zj8e6F8CxxgNxAab3GAau6/oXL14sX7ly5dSpU6f+O5/Pt82SykDd4XbpiSOB0jFcxsNWAvMY/+R1JRN+NMcBSpiHIFgMIrMKOchOSLN5ZbgiIsVi0f5mUWooikvAgu71enaTzHlNKoDYeittbc1AOvTKo2EGVpmW2zpHAZhMKFBbXI6nXRGsrFYMDEpwIfm4dub84OBA9vb2bN5AspsUn9M1Cbyn0QBCiUJFEYKIQFo6uWg4HMrOzk7MNdHXh/JA8a6srMiFCxfs80kO3/fNYDAYVSqVfL1e37969Wozk8mEEQFokYDrukEYhqE7TQfG/zfK/z9yHKkAnPmbDBxyA6LwRLi1tTV4/vnnfxQEgdPv9yuLBFSTNEcJMROvmds30w3Qx9MsvmaUtUuABdDv6+/xGyE1xtjWXPjKJNok/V023dCMMlBY5/mTKSgi9tgoUU3u6Zg3lprPQtQhMPo9foIgsDvgJBNtUISgG5GZNeR4+no0RAZuY4m73a6k02m7fwH3CCHH/2EYWiUFJyAyC3VybM3h6HAi88xz4TnB12j3hOfTbDatmwpHoZ+9JhxTqZScPn06lkmZXGe+7wee57kiMnn55ZffCIJg7DiOFX78/3Da9CP0PC/p/yOnRwrBvTQFnccDBI7jhL7vm7W1teEf/uEfvn7q1Klrq6urN535/QVFRGKQ8agBDCTG/Fa4AcmhfX+GhntJ10ArJ23xsTBYXd1EEqEmUw2CEL+6WCxa6w1aEBFrlUkXxpohoFpp6dg9bDuuAW4BFWuuO90tJ5m4RHRAN71kfhB0TWpqXkHzKKAGLaAiYlN+tR+uIwcUDGnrr/MINBrj+nhGOtlIu0X63NVqVarVaozYw4jxWSw6zz0ZTcFVe+KJJ2KRIj3CMDSe53mZTCb98ssvv3rlypVGoVAIwzCciMhE4mnAoYgYz/Pm+f8iD5AD0AdLKgGbFhyGYSAiky996UsXXNf119bWbiw7oOM4sSyyowYPM8kez0MD/H2/Q1v+pMJJWvZ5ZJ8+hk5PJWFHcwY6H13X8nOMMMrC05ZMKwOuSSf2lEolC+M9z7PEncisgQeLGvhPnn0ul5MTJ05IqVSK+dFYOZ04tL6+bjkaTZqVy2WbzYiV5DOgHc3+g3BIrdXKdG1tzSoTFE06nZZSqSSrq6sWoXieZ7v1kj+gowekR2PVU6mUNS4QlSjSUqkkZ8+elTNnzti5AHngElF+zH0nE7pQuqlUSlZXV+Xs2bNL/XTP85wbN27ceOONN3Yfe+yx9GQyGctU+IH/geM4QRAEi/z/IxOAGPfcFlwSHICIhBFJIX/wB39w+T3vec9rzWbzRBAECy8GrbnIF4pdqIKpScF6WLBfZL7fr19LknoIdHIg4CxmBJeFyGe0oOvzI/j4ogh8r9eTdDodY5SxpLxGBh9KRVtCkZmLwP1wThZspVKRbDYra2tr4rqu7cm/tbVl22X1er3YXgL46zp8S8ou98Y1AJ1RXoT1dCaiMdNNN/v9vlW8KLfd3V3pdrux8BtWGHTEdWCx9aadzBvJTDpZanNzU7a2tqyC2NjYsK2+WY9sN6YVvTYIPGvXdeXcuXOyvr6+kP33pvn/o0uXLv08DMNxOp0OjDET5QJAAgbOtAAo6f8fe22LHFMBzIEVh1wAmUUDwjNnzgxOnz7d+OhHP/q/VlZWboZhuFAJYO2OGsBGYtE6YSO6xkXXfpxbPHJo8nGe+zHvWrQ/KzJDDCxKhB1EAyIg7x7FgHVGWSAoycw9rUhIsmFhauJN++giYhlzjsVPJpOxIT+UBIIZBIHdhYeQ43g8toLiOI7UajXZ2NiwVXzAYQYKTSMB7Xro5z0ej2V/f9/yJIVCQc6cOWPTgzVCEZklUUGgQtbhHkCcYrV5JtrQgILoeATCwVUglL23t2fXJ8+ROQZJcNwLFy5Yd2rROrty5cq18Xg8OnnyZPrOnTttz/N8Y4xFAJH/H5pp/D8cj8dGJLY34JEJQIy7rQbkBPpEKAJCEkG9XnejSi5PRMTzvMAYs1DZjEYjS3otGlgBklzQqsuUwN1qw2UjiTp4DeFLCpW+7iTJBGTH/yV8ZIyRYrFoFznVbVg1moMinCgCbWFYpLrIR4cTNUTleEBe3avu9OnTMhgM5M6dO5aDWFlZsfxAv9+XW7duxdJcEVx8+JMnT4rv+7YuH6sLI44ywXqLzMJtkJgIHp15dDhxMplYErBarVrislKpWMHUEY15aEsknm0KB0OpLgoEwebZwKfU6/VYKzC4EJ6byCwHoFwuy7lz55YKf6PR6HY6nV46nQ5effXVW77vjzKZDPF/XyLrD/vvum7IJiASD9E/NBdgYUKQ67p+GIbByZMnh1/4whdee+qpp65F1n/hcBwnZhGP+iyWQQv/w0YBSYi/7HPaRdC/k4pDQ0OG67p2h15gO1YJyAnZpy06gguZqJGGyCzphb9RXDrhSL9njJFOp2MtHEp3PB5Lr9ezyT9cp8gsPAmvQPuxVqslp0+ftrkIPGv6/K2urtoyWbIHB4OBzTnodru2ngAfP5WaNiO9ffu2FItFewz2N2y1WrZqD9+c9FttpUEX/NbzDLHJufRmHsYY2djYkIODA1sAhKJH+fK/NgDvete7ZGNjY2n4b39/v5XNZr1Go9G4du3aQSaTCUQkxgE4juOHYRh402Y8ZjAYsEGIyF34/yJ3oQCO4QZod8C/fPly9umnn65/+tOf/tujtg03xtgmDse4DksUaTdgHin4IOC/ZvIRIv6fRwryWxN6XFfSjeA4hMJ0Yg8ChrBrtpyFpbMOsZygCyyZDp/qfH19LSxw0MVwOLSJLRxHt8sCsieheqFQsB2IUGx6sROaKxaLUqvVxBhj05P1xhsQlaPRSHq9nrRarViqsk57TqVSthy31WpZJEQfv1KpZCE5z0tX6IGumBPcy3n7DpL7QJXjnTt3Ytac8+p+BBrZPfXUU0uNVLfbHRQKhdxgMOh8/etfv5JKpSYiMo7gPwogJAswlUqxBVgS/h973Es1ICcSUX0BRJETIhKurKyMstns+PLly49HkK0tC/wS/N7j1AaIzEJVGp4tEvoHRRBqizvveEkrr7+HEtDXCisM4aRDZFSc0QxTIwkdO0YYRWZddLk2BF1bd64fBhtoinLTRBlQXce3uUcUHEILnF9fX7fz0+/3bSrsz3/+cxkMBlKtVqVSqUixWLS78IBYarWaVKvVWCmtTlKC9dcVkxQDcU+ZTEba7bbNo2BOdA4FfAfzxzNi3rTQ4mKlUim7jRdzdPnyZel0OhY1wd9wXN1jUERkc3NTTp8+vTDvJQgC47quM5lMRt/5znde3dvb66TT6YkxRlv/wHEc3xgTuK4bOrO9OUTiwn/sxX6/5cCHUEDknwQnTpwYvvDCC/UPf/jDPzh58uTl4XC4ePcDmT7Q46AAx3FiUPUoFHC/AwHRD5eRjAjo97lOrTCSn9WKQEP00WgkhULB9sNLp9O2+yxWnoWJMGtiCcJNz41OKNLEIb4vCT4ID/cM0YirRlIM72OJNzY2ZGVlJdbEhcIYYDaQWit6XVNAMpMuV+aacR1YI6Cb8XgsnU5HGo2GveednR25c+eODAYD21kZJATxp+cdpIa7o4ua+v2+LXYC8ZB1qNcgSpH51s1HXNeV8+fP22rFeSMIAj+bzWauX79+/bvf/e6NUqmkY/82CoD/73le6DhO2Ov1jFp3LPgH7wKI3FU0wA+CINzb2zNf/OIX/+vZZ5/9YaVSueV53tLkfxjl4wyso7q2h4oCdOht3jlF4sKfRCbJ93VyDD6jbsedzWZtAwqsGSW5WF0sIEqAZBPddktkFkLkb3xeGHmuZzKZ2KSafr9vK/xExEJrPg+rboyJ7SvIecnI09l8wHuRWUouSKXX68XqFJhzhKzdbkur1ZJms2l5EWL72tWq1Wp2V6VcLmf/xqKDajTy0koShEBPf7b7ApGJTIt+IFg1WkHB6GQikoieeeaZZWvbZDKZ9O7u7u6LL774X6urqyaTyUzCMBxJZP0jEjAwURNQ2P8gCGwvAHkTXQCRuPDrv4lV+ltbW4NvfOMblZs3b65tbGxcT6VS40XMPBbmuJmBfAcBSCqAo5TBXd+siaf08tqyz+vv6M9qhYCVY3GiCJgHhIl70am3fJ+tpTY2NkRkloGGv8px/ai776lTp2w5seu6tqMNgkgY0vNm++OxeFE2+M8okXq9LvV6XURmexKsrKyIiMQsP7UI1A2IiO0mBHeh3RbuG8RRqVSs1WWXHo63urpqBR6uYWtrS1ZWVmyEhOal5XLZHhdEiXLU5KyuKahUKnJwcGCRmC584vp0JiZr+l3vepeNiMxbf47jOL7vhxcvXvxpGIbjbDbrDwaDgeM4Y4kIQGMM1j9YwP7bwy1clHPG/XAAyaSgGBHoeZ7v+3740Y9+tP6nf/qn/xoEgRmNRqVlAmjMtMjkuOE7fax5eQHz/k5+77jnQYi4Tu1HIsRJa8+imKc85vVFBE5TA69Tb4Ng2ogT64jlBBZD3tGTHra80+lYcgquBVJNZIq6KCISmcXmSSHmXGTc0YwD4efzdCwmpMl71WrVwm2sK4qAVGU9kuFd5hYUpJ9FJpOxRCE+N/dZKpUsIRmGoSUotSvmuq51PXSYFaXIPYByGo2G7OzsiIjYaIBeCyhDvRZrtZo8/fTTC62/McZ4nifXr1+/denSpZv5fD44ODjoGGPGjuNY8g8XYAn7f1fhP8ZdK4AFbkAyCgAh6ItIUK1WB7/1W7/1rxcuXPjuMuFmgeqdVpYNHhoL6ygEcK9KAAGe9xA1P6CjBLynFYfILHceq68FD9IJwg3rAzHIGA6HUigU7P8IHi21sHZY/V6vZwWv2Wxaf5bj6+5MWHYgLpEDuAegN7CY6zdmVjVXLBalUCjI2traoWzEpP+vfW5SfPUOw6ANhOnMmTOWe8jn85LL5WRlZUUymYx0Op1YZALlmM/nZW1tzfIdNDShFBtiD0tOAhD3Cylbr9el3+/bPQxxxVBGeo5Ye+fPn5cTJ07Mtf4Reei02+3h5cuXf762tpba2dlpNJvNnud544gA9CVSAkEQBI7jLEr+0b+PPe7HBeCEOiIQCwVGfou/s7PjDIdDk81me/l8vuN5nr/geHeFAmBfSc2c53fP+58xz0VYdi6t7ed9b1lokO8AOXU5Ka9zH4T9dO68Pk4SUXDfnufZ+DfWzIvy//GnWaQsdhYu/jghLCIKk8kkpmxoyIkQYm1JDoJtp9SZ9t4a8WgLj3An0RDn4LlqAdJKh0y9bDYrq6ur0mw2pdlsiojYKkK4CMdxpFqtWnIVJKOTudjjANQkMlWK9D8gF0LPOyFafY1eVIfx9NNPxyIPyfsLgkC2t7fr4/F40Gw2965du7afyWRg/y38j7JsA29a+BOORqPYpqAc8vDKXT4eBAegf7D+oYgEqVTKD4Ig/MQnPnHn93//93/s+74Jw9CTJZrKcWY7rh4lnLwP+TWvYcg8NDBvoR2XQ2BovxzrilU4Snmx6PXn8JtRAFht7RfzeZ3Dr0N2QG387KRV16y1SFz54FKAqhBKst9ExAq2iNj7xc0QkVgL8bW1tUPKjO8x59yPzkTUxU0oK1ANFY7cq46WDIdDaTQatv+ALr9FsRKCpJ6AOeB5pNNpKRQKNvyIH9/pdKTZbMaQDAoEtMH16nDr448/vrDuH+N1cHDQuXPnzn4qlZq89tpr26PRaOC6rhV+x3EmTpT4E1n/IAgC0+/39d5/d83+M+5JASxxA5KKwPc8zx+Px+Ha2lr/z//8z//2He94x0vGmPmtUKNBCOY4KEBbEeDXvNAgn12EDI46xzwlonmA5Gt2chKkIeQRITUdg240GlbgEa5liovFpzP+9AYXzKW2/pq9R6Dy+bwMh0Pp9/uxBBmdOQffgI+NwOiGmCgS0oth0nlG3A8cQbJHgE65dV3XknsQnkB9lMloNJLr16/L9evXZTgcSjablVKpJNVq1UYySLKC3IR8RHh1yBTYTzcl5pwEI5GZ7898Q8jqdUdU5H3ve59VvMkRIYxgZ2enkclkzM9+9rMbV69e3c/lchNjzEji5J8fBIHvum6YSqXCbrebtP53zf4z7qUWQI9kEsLcrEAR8SuVyvD69euZVCo12traunjnzp13BEEwt3MwXMB4PLYhnGUDSO04jnS73VjTCo4nMj8lV79/6OYUxF70vj6P/l+fSxOC/I9V18IehqG0223r7+rjaaZaZBruohUXPjtlt1gcjQxEJMb8gwIg8YD9EHRYTwSdslfdZ4AkHNKEdf+/er1uj4Vbp8N+us8jSANfG99aK3TYdfgPWpRhKGjRrS24H3UkpoaA76N4IDLJfdAKzZhp1mKn07Etx1utVkyhQkJqZUvJ8pkzZ+Ts2bMLkWykWLrD4XDYbDb3X3755Tc8zxuJyCipAFzXDRzHCVKpVDCZTMxwOAwdx0mSf3eVAsy4Xw6AMc8NiJGB+Xy+f/bs2cbW1ta1fr9fnkwm6WUXHIahdLvdY3MBImJThCHA+FmULXgU1E++nqw4S1r8pCLQCkSnFCe/g3+cTqel2+3aZJp+vx87r4a2QGCsNouR46E0+DwDQY/gp/0eMJk0XywnQgxSIR4OB7CysiKlUslae3gI7omQGcqE3AeuVUcMyE9AALXbgg+PQJEhGYahbT7KZyFDSasG7ZDlyHVwXuYEl0NkljsBnwDRytpkTilIwo3hmt773vcuXK+O48hgMJj0er1BNpuV733ve6/t7Oy00uk0sX+Ef+K67iRB/mH9ReLy8+YigEgDad8DBOBKXAH46XR6MhqN0r/5m79569lnn/3aZz/72ceHw2G13+8v3UKMEA/975Zci134LCh8VB5okkhLCq+2tnoh8J6Gckm3YtHfSRdBZNYFmGvS8Bz4z+LmflhkhLlg82HLuTaUiL4ODc1Ho5FUKhWbpkssHOuajLvrKkNi+Tr/gFwDsvfwhXFFsO746sBq7qtUKtm99IyZ1vsTLoSk1MLWbDatgPMZ13VtHgTciRZyEbH5EChFEZHV1VUJgtmGIlh05s91XVuBGIah5TlAbhwHhMRot9vy3HPPyblz5xYW/YRhaLrdbt/zPHnttdcuf//7379eKBQmxpihiKAAiAD4YRj6mUwmFJHwXvr+LRsPAgEsSgu2CsAY42ez2fFkMpHvfOc7pc985jMvvfOd7/xWsVhsOtNtjucfOIKOOq1y2WDxiMwYWm39lxGE+vN6IYjMhFnneycfLP+zWJMhw3kRAngLFqcxxna2AQkgULqrMD8krnBcQogiYmG8JglhvwmVlctl2d/ft4saxh3Bwt8tl8sWThP64z7ZNdhEYUssZ1L5IrD5fF5OnDhhtwvXhV3MG1ECvY02CslE4Ubuk2YlzJEu9GHTTrIoyRfg+eoeC5wTBImiMcZYQpZB5yAUC9c3HA5ldXVVnnvuuYXINXJvx77v+81mc++f//mff2KMGTiOM4wUgIb+2voHw+EwjDL/RB6A9Re5TwXgLCcDdb9A3xjjp1KpySc/+cntRqPh1uv1rVwu116WHgzMPG6loMgs/KI34kgK/iJFQIYYSmSeazAv1MjrOlU2mRg0T3FoV4EfnQTE+4VCweZHlEol22ZLh/W0FUI54A/TUJNmlijK4XBoU3+xtJonEJlV3uFXM086aUbzFDwzjcZQdCgdtgFDoVQqFbvVN3NQLpdt8hE9CEglRgmwsebu7q60Wq1Y/0LyImDlyXngOVUqFVlZWZEzZ87I6dOnZW1tzSoBY4zdZJQ8BZ26zbPCjaAsOpVKyXPPPSfr6+tzrX+EUsPRaDQRkeCb3/zmj15//fWdXC43DsNwKNPKv7EzZf5tCnAqlQpFJOx0OuT937flZ9wvCchIkoEogECmSsaPlMAkm82O/+zP/uw/c7mc81d/9Vf/53g8zi0i2USmkwZk1bnyy4bjOLZ6DAsF1NaEXNIFIKTEZ1hsGrbzWc2qa9is4SGf0QUwCAQCol0TIDTCrTsloVRIlsE6kelHyI2EFuB6oVCw54QVz+VyNu/+xIkTsS45WEYIRu0e6BAcO+OKHK5ypFkJ9wBH4bqu9Pt9aTQa9t6Yl1qtZtEJW46BKrD+miylDgLYr/mOfr8v5XI59rxwj0BCGxsbsrm5aTv6iszSfnWxj26qqqMUImIVFkTg2bNn5ZlnnrHoKzmiz04cxzFXrlz52de+9rWfVqtVPwgCoP/IcZxRGIbjpPUfj8cmDMN55N99KYP7dgGWoABbHBT9tuEMEZn89m//9s9eeOGFvzt79uyPPM+bLFNqQTDbD+4Y12N/E9pBAOYhAP4mfszDJpTEsRAu7fslXQeKU5JpvyKz3HCNErDEWHEESu92g2DncjnZ3Ny0VhP2PoKUMb6BhU5HHb3VGMoIn10rOpJfdOdhLD33JjJzb8g5oEGI53m2YzA+dblctiSdyKxVV6FQiG3lRQLO+vq6lEolqdVqsrW1JSdOnLDdf/Sc6QIdMgJbrZatXUCZJZUtDURwCyAGmZPRaCT1et12QWJu4DO0otFGJJvNygc+8AGrlOaty8lkEhhjTLfbbf/bv/3bf45Go57neSNjzEAiBSCq+Ycxxo8Sf4wi/5Jp+Pc1HlQUQOSw9dcogHDgxEybG4y/973vFT3PG66trW3n8/mDZdmB+IPEoI8aLAAtnDpBQ7/G34VCwVpYIKe2eCJihYAFzucZevMLvoeF0zF1En6AyAgPiwuLks/npVAoSCqVkmKxKPv7+zYkp1N1UVQ6aSiTydiwIIgApYjF4z5RDix6IC/ziJCTaOW6rhVq3VsA3ziIyoNPnz4t+XxeRqORzcjTKAyXAJ+b+9UojM/hpxMuZB3oz2HBOR4wXXMN5XJZyuWyJfh4toQNt7e3LeHH82bN8IzgNlDCvu/L+973PnnqqacWhv3CMDS+7/thGAY/+MEPfvzaa69tVyoVfzweDxzHGYkILsDEnVbTBlHcPwiCIEgk/jxaLoCzOCLgyMwNCETET6VSE2OM99nPfvZ6uVx2Ll68+C7XdSfOkv0DRMSSMTpP/Kih24frzK8kO497oX3WdDotxWLRMtRMPrBaZ9pBfCF8RCFEZq27NYLAzxWZZfUB+weDgfVtWfBk2HW7XZt2u7KyIkEQyMHBgWXzsdrlctlaLfx+hIB7QMAHg0GMLdfkHe4A1w8iGI1GNj8D5YSVrdVq1qJThwC/gALXnXZQxHQ/KpfLNlEqDENbt4Drw/dKpZI0m00ZjUYxJEEuAem/WkGRt6BdIZHZDlWNRkMajUYs2sH5dKoyChMX4fz58/KhD30oFjlKjslkEriu69y8efPGv//7v7/qed5wNBr1RGRgZnF/6/9Hvn/geZ5pt9sg6gdq/UUeLAIQOSIaIFNCEIJj8ulPf/rmt7/97b9+9tlnv26OyA6EXKJ91XEHqaBYeCy+9uf03vMIEn6rRhL8BFFJLlBcZ8/B1gNV+bz2jREQrA+oQ2TW15//tRV0HEd2d3dta2oWHASU4zixPHYUBPdH7B3XgHlFOVLJl0RPcAygCU2aQrDR9IP3Go2G3Lx50woX52BuabrJVl8acenrY5DlxxxTpw9igDDM5/NSKpXksccei3XzhUxkzlEOkJ6j0Uhu3bplLTrXrBW7di9R7KVSST7ykY8s3egzCIJQRMxwOBx+7Wtf+4/9/f2miAzG43HPcZxY4o+y/kE6nQ593w8Gg8FDsf4iD44EXIQCkoSgLxEp6Pv+RERSr7zySv5tb3vbVdd1/5+XX37508PhcGHJMP5uEAS2+8xxCFEEiLizXsTAVg3ndWqxSLxXfj6fl263K/l8XlZWVmL5+FgK7StCnmkfvFQqxVJNCd+hcMg4w/ITSw+CQE6ePGnj6hBiGg5rAsqYWZdhYuOkuk4mE5s1yPGZT6w54TysNinDXHcYhrKxsRFDEMBikIaOcujr5Nq1KwOpqRUbboEWRNwVdibbwvcAACAASURBVCIiGYikpUqlYjMGeZa6ToTroc2X67pSr9dtV6Fks1Tun/ngWMPhUD7wgQ/IU089tTDmH81n4DiO8+Mf//jiK6+88kY2mx01m82u4zhDERlGLoAN/xH3dxwnaDQa7MD9wK2/yANUAGroi9RugCNTBeCJyMTzPM9xHO+JJ544WFlZ2d/d3X1fPp9vuq4bDAaD6rITkAZKw4llQ1tuEbEQkeQVLKv25flesViUIAhsb30WDdYZ60Z8W5cx4z9mMplYiWitVou1/Qaqa+5AE3MsfgQhnU5b6KthbRAENllIE42tVsu+ho+fTNrhGlBEsOwQqChM7h3lxveKxaIMBgML8fP5vE0dxqJzTZqD0MiKzEeEjzkvl8s2eQnuod1uW3eE4xEZ0eFLuABNfupITafTseTy7u6u9e95BrhMKCueEyjkySeflI985CNLoT/Cf+fOndtf+cpXfug4Tn84HHbCMOy7rjsyUezfGDPxPG8ymUwmnucFUdzfGGPCh2X9RR6wC3BERCBWG0BEYHNzs/f5z3/+4vPPP/9SJpPpqN6BSysG0d7HIQXx7VmQIrNeApognDdYPBoaI3BYOaAloSYsF1VqLELds79arR6CuggkbaR0Si+fQzCoS9dbd+EbY501OZbMYdcCzDnG43HMb9b77nF/uj056drj8VgajYYNHWr3Cn8fl4eqPCw3Vt0YI7VazaYS6ygIc6cJOJQ3UQCdRVmpVOw86HoGKkZRBijR3d1dq7CZC9ARnAioiDyNYrEoH/vYx2wOxSLiLwgCfzgcDr/5zW/+x82bN3emj7DfdWaJPyNjzNh13XE43f038DwvcF037HQ6bPf1UKy/yMNBACLzi4RiZKBMUYAbBIFXKBRGf/EXf/HdIAgy//AP//B/bG9vPyPTTkkLT+A408IfnYN91CDrCwHRzPi843N+lANKACiIYCPICBXwOpfL2V74rjvdJ488eb6PPykisS2pQANsiImPjFD1+32bvMK2XPv7+9aSo4yApvjqnBv/GfeELEGSaIhvYxFrtZrNnCM5KJvNymg0svnyCAmbYuBycE9kFOqiG+L+zAtzpxl83StSox7NXxD5oPNPu90WEYnxGVqZQuI2Gg1pNptWgaB0eB8EZ8ysxfh4PJYPfvCDcu7cuaVl6+PxeGKMkcuXL//061//+muZTGbYbrfbxph+BP9jab9BEPipVCpIp9NBu90Ow2j334QCeKDjQZOAi1CALhTy1e+J4zhjz/NGV69ezeVyud6v//qv//Pm5uZ/p6Y90ZcOSK5F5Mu8QbgKq8ZCTQ4tgLqWHQadBYPiIaZMgQzxer6r03qpSYdIZNFVq1VrBeE5sO401SDDLZfLWZfDGGN71mPpdWQinU7byjmRWfkt5B2hON2L0IsKZHR5LUoLV4E0bVqW6eabKB6thHC/9vb2bPQBjgLWnQpQ3fKduede6K6rOwlxz3qeKZ5iPsmOnEwmsrOzI51OR9rttp0PUCLn4lgiYouler2e/NIv/ZJ88IMfXOj3R4R1EARB2Gg09v/lX/7lB8PhsD0YDNrj8bjnuu5QpmG/kYiMI+LPd6KKv2Ba74/vjxw9lPHAFYAaR5YIy9QVmBhjJmfPnu08++yzN+r1ej6TyQxqtdot13WXFgEQGWi1WsdCAHocBf15sHrxikgsFx7FQ+tsLKnezkr77/jfIiLlcjnWihtii4SZXC4nq6ursfp1/GtcIGAsYUAEVcN8EbEls6Af8h6034+SMcbYWLkXVe2NRiO7RRjhWBGxzTfhSRBQnRAF0nAcxyoKEAHnw8ITBtSKRofj+IEw1dEX5pz3UV7kXNAwdDKZyBtvvGFTkVGKHE9kVkehCcRSqSSDwUDOnDkjL7zwQiycnByRAA993x9///vf/8+f/OQnN1zX7Q8Gg57ruhb6yyzpZ0zYL5VKmV6vF4rYrb4fSM7/ovFQFIBCAfPCgloJ0PFkIiKTT37yk9e/8IUv/O/Tp0+/OhwOS0EQpJzpRiPLziWDwUC63e6xUEAynHPUZ/E1tbBr/xoSTS8gEl9g9UXEWrZ+v28XKxVyKA+2xcpkMjaFWSscIC4C6ziOtWLVatW2Scc9GgwGtuiG1GiEie+DaFB0mvEnnj4YDKyw029BhyfhSUApxNcR1DAMY4y79v2TOQcILGiAaxCZRWdQqig8FBsw3XGm5cIoY14bj8d2mzEqTZkPEB/XDf+Ca4HL84lPfMIqz0Xrp9/vD40x4dWrV6/+0z/90395ntcbDocdY0xfRAYyZf7Hke8/8X3fh/ibTCbBYDCYl/L7UMZDQwBHEIKxcmGZ8gHj8XhsnnvuuZ2//Mu//PtPf/rT//fm5ualyWRyZNYPgqC3aD7Od44zYJl17zxgLnFxXoNkY8Ei/Lq5BUQa/e2B1rregDr//f39WMKMiNjQ2alTp6x1dV03VjBF8o7eLATFRQ4Fyox7I3zmuq7tnoOLgJAnk5/IyyD6wDkQRv0/SkAjDfgTiDvHmVUA0ucP8o9YveM4tkchuQQ6vMq1ad5Bb+5x9epVW+vAnIE+QCM6koDidRxHPvWpT8ljjz22tMnHeDye9Pv9YafTab344ovfv3379t5kMulGMf+BRNDfGDNyp/n+E8dxSPoJo7CfmeP3PxRF8LBIQD3mEYI6Q9AXEdcY42YyGbfX63mXL19OffjDH/7vZrO54nmeqdfrT/q+n14mtMZMK7jw8e/WJVg0tB9NJxwiAQgtIaxOp2MLWba2tqwAw4BjVUArwEuq8rCs9EAAEdRqNbtxRq/Xs7xAuVyWbrdrjy0yK64BXQC5tZ978uRJOTg4kEajIdVqVSaTiZw6dcpm5GERsZwIC6QYOzlzLvxlUA9CxHcRYNJ5ReIbkaIwuE6sLueAfR8MBtLpdGRzc1Ncd1rRpxUGiItnhUA3m01xXVf29vak1+sdqlYEAYHGuD54kdFoJM8//7w8/fTTNnIwbwRBYJrNZicMw8l//Md//Nc3vvGN1yuVytD3/Zjwi6r6C8PQT6VSYSqVCtrtdhCF/R4q8afHw+QAkq7AoTJhSbgCIjLJ5/OjF1544fZgMJhMJhN/bW3tZrFY3E+n06N551DnkjAMbRnncS38Me7h0GLEqmIJdCebfr8v1WrV7qqDMtIpuviiWEWgLn30gLXr6+siIjYGjrXGH6XHv4hYawohBouPAvI8z7oJhO4gQ/P5vFUW5XLZzqNO6uGasNb4yKTn6gQizR2gVJhLlBPoCIWpSUHIQu5bp03jRpHTQMJUsVi0tQ8oAN+ftfLu9/uyt7cXS8rS59LJP/Ak8AnPPfecPPfcc0vb1buuK51OpzcYDAY3b968/uUvf/lHqVSqHwRB30yLfWLW3/O8ie/7E8/zgnQ6Hfi+r5t9JA///0sScN5IKgEiAoFEe6C5rjsuFArDz33ucz/94z/+4294ntdxHCdwXXcpFyAya6/VbDattX1QA/+bgeWChCSlGJhNowrdkhu4fnBwYPeQp8hmMplIr9eTtbU1yz5ns1lbtkvFHRtrUhtAVZ12RURmm3nUajUbthQRex5q5TXTPxqNZGdnR9rtti06wsoTtYCk000yKE4qFAqxDEDOxf9cI9mca2trcuHCBRujhx/RhUj0CfA8z1bx6foEIie1Wi0Ws+eaO52OjMdju1cgtQMoMUhAeBERiZVPv+c975Hnn39+abJP5IJN7ty5s9fr9Q6+9a1v/ej69es7uVxuGIZhT0T6MhX+YRL6p9PpwPM8m/EnD6Hib9l46ApgASGYRAJEBSYSxUV93w8/9rGP3XnppZf+r1/8xV/8VxFbvLN0Qlh4dxsePM7QNQSlUskK9mQysYKMEjDG2EIiIgDUrdPcAwJOROzCpGIulUrZQh8RsRZ4PB7HOtGS0ISvTGISZBqZbiAZkpaAvnTdpee9znyDUyEzkNbb3CvZcSKzrEidrsvQ24kB90Wm7tX+/n4scxFExDlJLuInlUrJ+vq6FAoFqVarcurUKVvwBHEIutne3ra+P519KR1GiaE0cOfgFCaTibz73e+Wj3/84/Z5LlpLvu+bW7du1QeDQe/SpUuXv/nNb/60WCwOgyDoy1T4BzIV/rHjOBr6B57naeifFP6HPt4MDkCcWZ0Agm/fklmasCNTheRGuQHOaDTyxuNx+nOf+9y3zpz5/9r7tuC2rvPcf619wY0ASfBOkSIlWbJ1ItuVJcWpk8h2fNKMM7HTJH2o3Z66zWSa6Uxn2ulrX9qXdjp9i/vgzPThnHacJp126iZx6kRxXcVRrTi2LMvWnZRIihRBgLgD+7ou5wH7X1yAQIpSbYmO+c9ggI3L3hvA/v7r9/9rovTSSy/9n2q1Oqpn8tc5niKo4Lpw/xNBwGDSCC0WXkAA0JYkREANDAwoIAEAlEollQ/wfV91zqEbjoqEMaY+29fXB6urqyqhVa/X27wRx3FUjItlRkysYUiB5T6Ms6WUymtAC9k5Nw9BhEDEaTy4fz1ux4SeTpoBWFuSXE8M6p2S2HaL8Tv+rxhiYTYeE6FmNLEJ8xD33HNP25JnuqJYXV2FYrGoGJPooWGZUvcQEeAYopVKJTh48CB87Wtfa6uYdIqUUpqmSYrFYnVpaSnneV75+PHj7/m+X4/H405k/fXY3yeEKLpvRPjBZp876vqjbK6v9gOQv/iLv8BfcKP7tscRU4+89dZbfeVyOWkYRlNKKV3X7eecGzdTAvhHI+nndgWz/npWX3drMf7F2jcqHax9NxqNNguOlgZd0L6+PgAANWVHRk0xAC1wDA8PQ6PRgHK5rFa2QWYfJt/0OjkyHFEZYTgQBIFKGvKodVgHsGVZKkGpWz2daYlARBcdt1EBeJ6nSn86RRZ/G/Ra9H2jQsQlxFGEEGoYKXIXUEENDQ2ppChOR6pUKmqB0lwup1qR9XPGZjDkGuC1gkDP5/Nw4MABeOaZZ9pmBXYTwzCI4zjBqVOnzjqOUz19+vS5N99882oymXSFEA1ot/6eYRgB5zwghDDbtpmUktdqtc6k3x1x/VHuWA6gSyiwXklQJQWFEKFt295zzz03u2/fvoVou5lOp/O2bXubOCZ4ngeVSmVdvvYmz125+3osrfPHMaONFlOI1oz/crkMAwMDsG/fPpVEQkWigxP3hcnAgYEBYIxBrVZTZB893MASIJbfMKGFrreeRNOtHpYMO+cfoGXFUEK33DqFGKnXOpsQSUD4XCaTUSBD9xoTqKh89E49/bujxUdlgCFONptVGf7e3l5oNBqwvLyslAAu3QUAavFTzH1gIhGVDio/VEh4ToVCAQ4cOAC/8zu/o8KEja4ZIQTMzs7OLS0t5a5cuXL1rbfeupJIJFyM++XasE8/qveHUkpuWRa3LIuvs8DHHZU7EgKgdIQCADf2ChDtRimlRAhBKKX0G9/4xuWpqSnvW9/61pP1en1QSkkJIVxKSQHWXxMN40rMvmP56TbOXZWGsAlJ56NjjMs5h2KxqGrwlmXBvn37lPtbKpVgaGiozUrmcjmYmJhQlt80TeX6x+Nxtb4eLmqpKxG03Pq6fHiB4xJr+nfGgSKMMZU1R+uMrjJmyXG/WOfHrDgmB5FghGAHaHksxWJR7VNXbjprD9+rNyMBgAKd3qylszaTyaRSgEIIWFpagmQyCYuLi8rTKpVKKuZHJdPJFsX/DsMdx3Hg8OHD8Oyzz6rKwAYxP7csy8jlcoU33njjvTAMq+fOnbtar9ersVhMuf6EEE8I4VNKfdIi/oSmaTLbtlmlUhEdC3zocscUwZ2uAmyWIIQJwZBSGkopA8YY//znP7/0gx/84P/+3u/93v+Lx+OV6PxvuiIKegK4vtvteAJI9Okc+4WuZRiGqtyFiTTk2V++fBmCIFCrxszNzSnaK+4DZ97HYjGoVCrQaDSUJcX2Z8wnoNVG7wAAFIDR7dUbWwghatVdLDnqTDeMk1Fp6ZwC5NN3JvYwUaazFfG3RmWBdXls0EFFolNuAaBtQhCek/4+PVzB42GCsFqtwtWrV2FlZQVqtZpK/GHIhsQsPFf9/8FkarPZhIcffhieffZZNSBkvWuEcy6EEKJWqzWOHz/+y5WVlZWZmZn5K1eu5GzbRuvfFvcDAMb9zLIsHoah0Nh+d8X1R7ljOQBdtHwAih77y45tgiKlNJaWlnoymUzwyCOPvHX27NmRcrk8RjaBaHQ1kZLbrQPwZp8nEZ1Uj//117GshiQaVAqu66rWWUKIKmdhXqJWq0E8HlelONd1oVwut40GGxkZgXQ6DZVKRQFanxosolZYVBDYH4Cf72xZRiusswMx1sdYW68I6OxHdMVxH1hKTKVSbck6QojyADqnOOktwzrDUs8/6D0QGAJVKhWQUkKtVoNz586phUWazabyDADWmI/4eVTUaG3x93NdFz7zmc/cNOGHUq1WG5RSOHfu3IV//ud/foMxVllcXMwxxuqEkCYANAHAQdffNM0gDMMAwW+aJl9dXRUAcFey/p1yR0MAlC6hQOcYVQYt8IewVh0wKKU0kUh4AOAvLCzEHnnkkTeGhoaK77zzzmeDIIjfTA8ggJEBt5l1B3VBQgxe0AgenROA78OwAACUJXRdVw28RH69nqDDMiHAWtkPY1WMp1OplMrCI0MQ3XmcMQAAiljT19fX1vuO54YzCHk0OAOPgUlBQojyQNAFx3kBeL4Yr+ukGYC1/AOWJHU6M4ZJ+F/p1l3vRkR2I1ZcUOliKbJUKqmEHu4HiU+YVASAtk5OrC4gK5BSCk8++SR87nOfU+9Z7xqKPC4nCIIgn89f/+53v/szx3HKtVqt1mw2q4ZhOLAW9/tCCN8wjIAxpqi+lmXxarWqSn5d2H53XBHcFQ8AAOAv//IvOz2BzioA3lOIvAApJU0mkzA9PR3E43E2Ozvbl8/nh3p6euqcc9v3/YRcG0vWVdByY7yLF8itCIIbL0h8DhWCPrU2nU4r97Ovrw9isZiK7TGh19vbGoCE7cbY6IJuPcbBSEPG/nkkC6E7i9+tVCq1dcQhYw4ThgiOTCZzw9BPtMZYlkOrDwDQ29urrCjG16iwsCKBAJdRg5TOYNTLgahkkN2Hc/0IIYpqnEqlVOMNhk6FQgGazSasrq4CwNoKyaggMIwAWBvFrrv+mJxNJpPwzDPPwNGjR9X5bgR+3/fZwsLC9Xq9Xnn99dd/+eqrr14ghDQ45zVCiMr4Y7cfpTTgnIdSytC2bRaBX3f9Ow9zV7yAu+IBoJCNpwkT6MgLEEIYAHDOuTh06FChv7//zIsvvkheeeWVxy3L8vv7+wvlcnlICLFhbgMvbMwJ3GztQf1zePHqbjNaLf11fWFKtE4y4qbjoBAEL4YFONAjk8m0lanQJUYSEbrzmM3H19ArwTKkPuASANRzvu+3xbo6mNEj0K2w3m+A3xE/h89hXkQvl+rrLOjfRfcI8PfBhCIqUgBQCdByuQye58H8/Hzbb4HNWEgC6kzK6sxI9HAajQbs3LkTvva1r8H+/fuVMuwGfl0pzMzMzBdbsvzyyy+fSaVSrpSyIaVsEkLcyPK7GvgDKWVoWRbT6v1bIu7X5a55ACjr5AM6bzS6GdAKBYwwDO3BwUHx6KOP5hhj3uLiYl8ul5sQQhgAQDbjCQBAWzPJZrgCGNeG4dqSZXouQC8F4nJV2CtACFFjp5Fxhu29neQZvJillNDf368uZswt6NYZWW3YP49Wn5C1ZcUMw1Bdful0Gvbs2aNIQJgsQwWAXYIYv+slUN1bAFgbmIplSD2uxwSizhNgjKkBJHqnZX9/vwopcCRYqVSCubk5cF0XlpeXFVjxXHHOASo5HfAA7V1+mD944IEH4Pd///dh586dbS3Q3f5nIYSklJJ8Pl88derU+VKplDtx4sR7uVyuYFlWU0rZgLWY35VS+oQQnxAScM5Dy7KYbds6+LvV+++q3FUPAOCGfADe695A5z0HAG5ZFhNCSNd17W9+85une3t74a/+6q925HK5HZzzW/peGEv29vaqC2YjwbIW9sfjBYQWA62nlGtr2OldaENDQ22ZdVzEEt1kxhikUikol8sK8Hr3HAIWXWx8jC42WkQEB4Yqundw/fr1NkCjBUWSESos/D2azWYbmDspv3ge+Bi/K5YlEejo2uNviJWPQqGg9j07OwuZTAbK5TLk8/m23xete+f6iVjL1xN/qMBQUX/+85+Hp556SjU/rafwo9+QRZ6ePHbs2MlisVio1+uFs2fPXtOIPljuc4UQHpb7MONvmiZnrbHe61l+gLusCO66AuiQ9foG1ruJVCoVzs/PD/zrv/7rg6lUqhmPx13f92O3UuqLYjwolUqQTqdVU8t6gu4yUnz1mBMvSiyh4So02OqKDUFI1y2VSgrUaMHr9bpKUqI7jgk4zCcQQmB6ehoWFxeVosFBpJjwI4RAsVhU3g2CG5txsCKiswfRs6jVam3AQ/CiokCg6Sw7DBPQsiP1VgelbdtQr9dVSzNSn7HBCfsk5ufnVWil1/P1EATDHlSI+g0VUq1Wg4GBAXjqqafgyJEjKk+zkbcnhIBms+kxxsKzZ8+ePX369GXOeW1hYWE5Ho87UsomADTR7RdCeJHl9yPwh9jkE2X8pZSyc2EPgC3gBWw1BYDSjR3V9hz+0QAg/v7v//7epaWlsWazGW82m+uuK7CRYJkQ23hxEYpuigCtCxJGMPYGWFs/HgWXNENQJJNJyOfzasimPqCSc66OWygUlBXcsWMHWFZrHDjO5kd6M2bwMVmIxB+MtXXaLX4XHGGGPQX4GL0FDGHwN9ZLprgPvU6P7j4+r4cwGKagksUwA5t2CCEwNzenFGehUFBKDhWNHg7pwEVlg+/Dc0FFIqWEAwcOwJe//GWYnJzcMN7Hr2cYBsnlcuVarVZzHKf64osv/rxUKq3atu24rluhlKICcCilnhDCJYT4lNI28Hdk/NH1bzvWpi7MD1m2hALoSAa2vaQ9bvvBohIR+4//+I/Jn//85w8wxmixWBxhjJm3owCi8wAAUP3yaIXx4tIFQaLzAnRXGl1ULH0BgLrw0UIjW21wcBAqlYqyhq7rguM4qhW2UChAf3+/iveHhoZgZGQEVlZW1DJl2P2HigBbd3FMF3bZoULQuRDoueh8fb381wl2LPvhBGH8bpjRR6WB5ThCiFJs169fB9u2oVarQS6Xg0QioUCPGEEPR0/s6c/hPSYbdcWAHsTAwAA89thjcPToUaU0N7L6kQdDGo2Gd/Xq1UXXdWsnT558d2FhYdk0zabneU1KaVO2xno5uuVfD/yO43SC/64n/TplSyiATYpCdZTckY7jWP/wD/9wpNFo9FYqlYFms5m5XfC3HUjjCyDBRbegAKDq3npnHH5WRiQWPWGIyTSci4cJLELWlj9HTj2lFCYnJ9VxpJSwsrIC+/fvh+XlZVU+1EeM46x9pO/qlQ1UHFj2xLyD3sCk018RKJinQFYfuuB67V/nHuh8f5ywk81mIZfLqdWUsC8foKXEMQTCz3UqIvy/9fyK/n48XxwSIoSA+++/H770pS/B7t27lYe2UbzPORe+74fJZDJ2+vTpc6dOnbqYSqWCkydPXjIMox5ZfQc6Yn5M+G0Aft3wbznwA2yBKgAAgGb9O7P+KvMPLWVlAoAVhqFlGAb5m7/5m187efLkr3uely4UCrtu1iF4K4IXGgJFt4L6ewDWeuFREFDJZBIymdY6J5g115OH6MLrjSnY9AIAqn+/r69PNbjoi3Kiq6yTgLBEifE+WnHsQETrqDPxMOOP31FnKWLogt8PS5D4/XE/2NyjL7mNk3iwLDozM4P/t+Ix4G+I+9d/Sx3oeDxUEPg5VKCu60I2m4Xf+I3fgKeffhpGR0fV73Gza8JxHN/3/fDChQsXf/KTn7wVBEHl5MmTF5vNZtkwDBXzQwR+2Wru8SilQTTVh60D/i1T7ltPtpIHoJf9oMtjAAAIw5DYts3feOON4VdfffWTQRAkyuXyWBiGNmlRKz+4CSDQPqQCR0/puQE9K95ZDpyYmIBKpQLlclmxAU3TVG6z4zjQbDZhYmJCLbulJxDxM6urqzA0NASu6yrw1ut1sCwLstks+L6vFgFFBYMuOQCo5KOeGUfQ43dwXVdlx3Wl1KmMMP8A0OrnxxWUC4WCSjYuLy+D67rQ09MDy8vLiiyFngvAWh8Anp9+vjq4AdamG2l5H7W/er0OsVgMjhw5Al/4whdgcnJShVobAV9KKS3LIuVy2Zmfn1+KxWLw8ssv//eJEydmh4eHWb1eLxut9nPV2Uejpbw2afm3PPgBtpYCQPDqwAf9OSkl1vfl888/f6RUKo03m83eZrM5FP3ZHyj41QlElqder4PneTKVSkEikSCRu41uHgFYs1rYW46WF0d72batCDKe58Hq6ir09PSoOjgAqJr56uoqjI2Nqem72BuAcXWhUIBKpaJ6CzBxiO4/jv3CZcaCIFAjwur1OoRhCMPDw8pllxHVWa8i6HG8YRiQz+fbKhmFQkEtxDk7O6vWMqxWq4oXoIdJeqONDmj8jfEx9hkAQNu5YOUAy5p79+6Fxx57DA4ePKjCLdzHRv+nlJJ4nscuXrw4W61WK5cuXZo5efLkZdM0nXw+XzcMw4UWuw9jfl+0OvuC2wD/lpWtpADWSwKq513XpclkMvjWt771iXPnzh0Kw9CqVqs7hBDkA7L+nftQ26Q1l1AEQWBxzoXneSKZTJJUKkWllFAul4VstSaDEEICAMnlcpBMJmF0dFTF5lgxwH73RCKhFEU2m1W1bgBQzDhUDkg+0pOOCwsLUC6XIZvNAiEErl27BqZpws6dO1WOAEthQRDAzMwMjI2NKa/l2rVrMDg4qPr6MTmIQ0enpqZgdXVVzSU0TVOtRTAwMADFYhGCIFDNNVjxwJKgbr0xZMGkI3oYeqkQk464VBkCXeczeJ4HIyMj8MQTT8CRI0fUkNXNKRHPDgAAHIJJREFUDoMtFou1RCIRm5+fn//+97//xtDQELz77rtXwjCsxGIxzzRNdPtxhr+ncftxEY/Qtm1umiavVCo3A/+WVQQfisW8VYmsuh7/d8b9dhiGMcuyrNnZ2cxzzz33B8Vi8UC9Xt/ZaDR23c4ho3u9IYlo9eWQc24BgEwkEhXP83oJIcIwDD8ejxc8zxsEAH98fPzHjuM8UK/Xh2q12ogQggshuJTSxgsBackjIyNqcrA+3z6RSKjE3r59+9QMQATKyMgI1Ot1pRRwKCiuZovTgHBeAJYEDcNQAzVw4RAs7WH9v6enR7nuGNpgZh7r9VjCQ6ve39+vymwYCumxud7XD7DWRYkg18dq6/wD5OPr+Qe9vRpzMf39/fDQQw/Bo48+CuPj40rJ3Mzio5TL5XqpVKo2Go3KD3/4wxOVSqW8srJSKBQKJdM0XczyRzV+fYpvwDkPRGuWH7NtmwkhRKPR4Fpf/0cK/AAfEQUgpbQ9z4slEgny7LPP/u9Tp059NQiCgUql8qAQwoZ2y72RFZeEEIHJQsMwGOfcjOivfjwerzmOk81kMtcTiUSt2Wz2CiHMwcHBy7FYrJbP53c9/PDDL+Xz+d5yudw3ODg4k81m5+655565f/qnf3oqn88/GYZhjxAiK6VMYbKOcy445zSVSkF/f7/K9u/atQsWFxcBoJUP2LdvX1urLa5CizE3EnQwuYbPIX0XLaWUa7P3MGGmgw6z9XgsLGViJQAFcwTIE0A3XqfY4jkgE1EfdAoAbWENKgpUCDjME88HgY7njYlO13VhcHAQDh8+DA8//DBMTk6qcCL6X9e9tiL+QWgYhsE556+99tqbsVhMXLhw4fJ3v/vdd0dGRrjv+3UhBI7vxjFeuHS3b5pmyBgLhRDMsqzQtm3OGOPFYlG19X4UwQ+wBRTAOhWANgXQbDbtVCpFn3/++ekXXnjhm77v76jVageCIBiGdgqxsuKUUi6lpJxzkkqlqowxi3OeME3TSyaTxSAIkp7n9e/du/fnlUplgBACU1NTF8MwpLVaLZNKpWpf/OIXj7/55pu7h4eHC7/927995vLly/GrV68mpqenC0ePHl35zne+M/TEE09cO3r0aAEA2F//9V/3f/vb3z6az+e/yBi7RwiRJoTsQuBwzoVt22RgYIDoNeze3l4F6JGREdW1h40/mBMwTROuX78OtVpNeROY0EO3mnPe1nasZ+rxeHhs3WpjDgCz7hif630CumeBHgnG5NherI9JQ3ITHlf3GDorKph0ROWAI7z7+vrgyJEjcPToURgbG7sl4EffQVYqlQZjLDx//vzFy5cvLxSLxfyJEydmpJSOEMIhhCCXH11+X7Z4/R4AhNjSG8X7DLn9AIDDPLEE9JECP8DWVwBGEASmbdtms9mMP/bYY39QKBQ+yxgbbDQaD0YXLJdSUikloZSKeDxecl13MJlMltLpdM5xnH4ppdHb27s8Ojo6d/78+U8fOXLkxzt27Mhdvnx5YmJiIjcxMbF677335v/93//9/j/8wz/87127djW+//3vj/7xH//xjG3bwaVLl2KLi4vm5z73uaIQgl+7dg2mpqaa0Jr6QqrVKuecs2w26wKA82d/9meD3/nOd37NcZxx13UPcM4fJYQ8QAgxDcOAvr4+adu2DMOQCCGIXjLUm30Q2GitsQUWJ/ggsQeVBCoDgLUx4wCg6uCoCJCNF/3+bR4CWncULE/qVQ9d2aD1RkHugN6FuN4+sUSJbdAyojRTSmHHjh1w6NAhOHToEIyOjqocAsDGwI++kxRCCMuyjKtXry7lcrmCYRjs+eef/0kymfR936/l8/kypdQFAAS+mt8nW2v26ck+XLizs6sP6b0AH0HwA2xtBUCllIbrulYymaR/+qd/uvdHP/rRHziO80Cz2XyIUurHYrGc4zj3jI2NHY/FYm6hULh/586dbxmGIWKxmFer1Qa+8IUvHH/vvfem4/G4//Wvf/0XV65c6fF9n8ZiMfZHf/RHsy+88MLO8fHxxtNPP700NzeXGh4e9pPJZAAA4erqqk0p5dlsNgAA4fu+iMViDABYGIY8CAIej8eZ0VrlhdfrdbG0tCQOHDjgE0L8r3zlK72vvfbaiOd5A2EYfllK+UXTNKcsy8rgaDHGmJicnIRMJkNrtZrqH0DwRAoDSqUS5HI5FUvjiDAsvSHAsA25s0kHQa4rAzyGHhYg0HVPQI/j9ay+9h8q1x+pyTjvoLNVFwDaPAC9pdi2bZiamoIjR47AfffdB729vZu2+HgqhBDieR5rNBpNznn49ttvv18qlYqnT5+eee+995YgYvFZluVF8T6O7tKX6w6iAZ5tNf5KpSK6dPUBfETBD7AFFABA1xwABQDD930jFosZFy5ciD/33HNPO44zGY/Hw6Wlpa8kk8nlw4cPv3L+/PlHxsbGrg0NDZXuu+++/Pvvvz994MCBxT//8z8/97d/+7f7n3jiieWHHnqoVqlUzAsXLsQPHz5cj5pNrD179rixWCwEAAiCgNi2zQAAwjCUnHMRj8c5AAjOuaSUctIaQiqklJy2li7n0DHZWEoplpeXxczMDBw9ejSILhaSyWQGXdd9CAB+DQD+VyKRONjT03OfEMLKZrMwODgIxWJReJ5HRkZGgDFGkKtvmq1FQnAcGLr5uP4fgh/jfgQuKgi93IbxtW61sbyGSqLzNXTNCSHKO9G9CQQ4AChPBGDNDcf96LRoTPDZtg1DQ0Owd+9euP/++2FqaqptuXXcz81ECCFjsRipVCru3Nzc0sDAQM/LL7/8s1/+8pdXduzYQU6ePDlnGAZaeSeK711ozeoPpJQ+AARRpp8JIUKc4mNEi3d8VDP9G8lWVADKCxBCUEqp6fu++Sd/8ifTV69e3bV79+7K5OSkv7Ky0m+apvlbv/Vb1yuVSnx5eTn59a9/fWllZSXluq4xPT3tA4BwXdd0XZdks1kGADJqwOEAIKK4XBJCpGEYCHSc1da5jJmEdsDjvf68uoVhKK9fvy7+5V/+hbzzzjvw4osvMgAAQkivbdtjlmWNJJPJ/ZTSzzqO8+lsNjuF1jzKystoXXsyMzMDYRhCOp2GYrHYNjCj0WiosAF77RuNRhsvH4Hb6XYjKUi3zPgY8wT4Pqz7o2CSTicMdVr6ztwDvteyLBgeHoa9e/fCrl27YHJyEvr7+9V56grnJteNJC1EQqlUqtq2bV65cmX+/PnzV4MgqP3oRz96v9Fo1Bhjjm3bvpbkUxY/qu8HlNIwcvk5ISS0LIvbts3CMBSlUknIG8d4feTBD7C1FADAjTRgKqU0CCEGAHDHcVLf/va3h3bt2sV/8zd/s76wsJDq6elBcBuO49jJZFIAAGGMkSg7LQFARpZREkKEEC0PzjCMznkDnXMI9O1OhSDWeV5GF6ZYWlqSP/3pT+G1114j586dI6dOnSKcczkwMEATiYTluq4NAH3xePxeQsgjQoiHx8fH9+zevXu82WwmCCFq/j0CDpl0vu8roGCsj2xFrPHrHHo9Uw8AbWEAxuzIakTvAUGIVltP4OleBSYLO1/DHAE2Dg0ODsL4+Djcd999sGvXLjU4VE8WbsbaSymlYRgEuQZLS0s53/f9XC6Xe+WVV94eHx+3Tp48ObOyslKOx+MBby3QiRbfA4CAEBJEfP6AttbqY0IIXLFHjfByHEfP9AP8CoEfYGsqgBuUAAAYpVLJzGazBABIEASWlNKIxWIEWqECJYTQaPgEIYQQSqmM9i2ji6obwEHbhi6vr6cUuj1Wz4nWJBn50ksvwS9+8QsolUrwj//4j8R1XUoIoYODgyZjzArD0KSU2pxzm1KaSqVSQ0KI3YODg3v7+/v3J5PJffV6fffCwkIMQYbuOlpoPeZG4KI1BlibVYhhAQIUE4wYo3PO1chz/Bwm+xDYer1fT+QBgKrjYzIvkUhAX18fjI2NwdTUFExOTsLw8LCahKSDHmBzwI9+W4jFYqRQKNSDIAhM06QvvfTScUKIv7i4mPuv//qvK6lUKoS1xTjR7dfHdPtSypBSGoCW5TcMQ3Rz+aPL6CMf73eTrcQEROkEHQEAns1mJWPM4JwbsViMSSm5EIISQnikCAhAa7km0Ag+ZP3FSTuTOLf6Wrdbm2KJ+P3ke9/7nuqAi8fjREu2UcMwDEKITQgxHcfxAWBhZWWleunSpQvPPvvsp7/0pS8N1Gq1xJUrV3pWV1fJ8vKyGtbBGFOjv8JoRWISNccgHRg9BFQOWLYjZG1+P7r5CPJORQEAyr1Huq2uYHA2QiaTgcHBQRgbG4Pdu3fD8PAwJJNJtQ+9coDntqkLQkpJKSWMMel5XlCpVPzFxcVlz/Ocd99992I+ny/W6/Xq22+/vdjT0xMIIVzQ3HwN/IGMFug0DCOUUjLGGKOUMtu2mWmaIgxDsbq6ejOXH7psfyRlSygAsv5YMIBWfE0BVAlKytaqQIS0rqDOG3TsB2BjQMMtvtb5vhu20VoQQuDv/u7vZLVaJbS1yhHEYjEiW/wESlvmU61+FJUJ7WazSR944IHBxx9//PDQ0BAkEgnj8ccfJ47jQLFYhHq9DoVCAVZXV2FlZQU457C6ugr5fF7V5JFGi26yDmrdk8DzxNgb3XxUGBjH47yBWCymrHs6nYaBgQEYGhqCTCYD2WwWUqmUCjUw5tfXBNgM6PW8gWzNLiCVSsVbWVlZHRgY6H311Vf/e35+Pjc8PGz+27/92+l4PB4ahuEnEokmtuki6CPA+1LKAFoZ/hBaVZyQEMI7En2/8i5/p2wJBdBF0JpiOrrN5Y6Aj6sCdTYO6fvofNwNtBu953Y+q8gwzzzzjMTzRJ2gt7oahkGiHAeuikyllMS2beOrX/3qJ/v7+0ellJZhGEkhBCQSCZiammqzvugFYEcg9hvg6rjIrMO+AKQKo8uOlh27/ZCTgGsAxmIx6O3thcHBQTXMM5FIqNkDerkQww0c6gkAm0rmdUoYhhygtTbc4uLiSl9fX+ratWtLZ86cmTUMw3/99dcv1mq1erFYrJmm6QVB4MkWXdeHaBluiLL7EFl9SmlAKWWMMY6xfhTvc8aYsvoAitzzK+nyd8qWUQDreAGoBPA5FApr4QF0vKbLen/ehgC+yWsbPcZ4HJ/Tv8/aByJlgNdYpACIaZpkdXWV/e7v/u6ee++990HDMEgqlerRE3OoQKLPgWEY0NPTA5lMBkZGRtrAhu/HG7rfaOnxHkHcuYAH3vT34g23deowntN6wzc6BbP4uB2GIYvFYqbv+6Hrum6tVqsWi8XqxYsXy//5n/95JpvN0vPnz19fWVmppNNpTlsTenASL1p5jPOZECIghISGYTAhRBApFmaapljH6reC/TW3v+10N/WlPmKyZRRAh3QqAbxIcAFRfe0AFN3977a/zv1uCtCb2G4dvH3NQ/182rblWktz23sMwyCNRoPv3bs39cQTTxyNx+M9sVgsaVmWpSmKboe+QTF0OTcFan0fnWQePauPCmO9Y+sW/nbFsiwSeTEiCAIWj8ft2dnZhWKxWE6n07EXXnjhWF9fHyGE+CdOnLja19fHI0vu1et1P0ri6bG9DwBh9DiklDJCCIsy/NxYW55LhmHIu1h9gF9xl79TtpQCIDcuFALa407Q3MqVd7M/dD2Qky6vtV64EfCdr7cNr9A/h5/VMszCMAzpeR7/xje+8etDQ0N7KaVGLBbr0c5n3e97C8m0DRVF575ux31fT3T6MVYR8vl8ta+vL10sFsvvvPPOxampqezPfvazM/Pz8wXXdZtLS0vF2dlZNwxDN5vNMs65zzn3oyRegHE9APiUUiaECKC1oCwjhISccy6EYJRSbtu2sCyLcc5FtEKPhBbwRaT8PjZWX5ctpQAA1oC1jiLA7XWBuYHcDsg3RstGB+sCfm2/QAjBcqGwLEu6rss+9alPDUxNTd1vWVbCsqxeEpEWDMOgEXA3VASbkQ8K0BvtHy0p5mk557JWq/npdDq+urpaY4yFlmWZ586dmxVC+IVCoeQ4TvOVV145UalU6n19ffLixYtF0zQDwzACAAg45z60wI7AD6PtUEqJK0kzaLn+TAjBCCEiovEKSqnu7qs4/+MS668nW04BoHR4AwDrewTdRH/vhsrifwLyW5S2c0ZgRLV4kU6nyZNPPnlvqVS64vt+MD09/SQAENd1y6ZpJmzbTpimSXzf50ZL0EVXQNtI6XxoXyo6Plp1xpgghBDbtkkul6skEokYIYTOzc0tJRIJq1wul33f9996662LEfvSP378+JXp6Wl7eXm5Wq1Wm/l8HuN2H+N4iMAP0bLxsAZ8hsBHV59Sipl9YRiG8DyP1+t1KSIt+nEo721WtqwCALjRzd5AIWy0jzsF8I1EeS1RWU1SSkV0QQrOOSOE8MXFxSXbttNXr169trKysjw+Pv6Jvr6+XYVCYcnzvHBsbGzX6Ohof6VSCVzX9TKZTNq2bRL1LkjTNGnED5CtqqIaVS4ppQRgbVqRVmpTCkQIoZJyuL3RfiilJB6PE8dxRLVadXp7e3uq1WqjUqnU4vG46bquf/369cbs7OzixMRE/7Fjx86///771w8fPjz405/+9JLv+342myWe5zXPnDnjx2Ixbpomg5aVR6DrjxkhJJRSMtAsPuecSynR4gvTNLlhGJIxxvP5PMb58HF397vJlmAC/opKW9WCtMwOSSaTRm9vr+H7vhkEgWkYhk0pjTHGYvV6nR48eHD04MGD0/Pz8+y1116rP/PMMw8cPnz4SLVaDRYWFqpHjhw5MDQ0NOH7PisWi7Xp6ekd/f39faVSqQYAkEgk4v39/XalUgkBQMbjcTvi8QtCCMTjcRp14MkWB4lAEAQCANRrYRiKaF+00WiEUkoZj8ftMAwFY4xZlmXatk3n5+dzhmGQRCIRv3LlynXf951YLGZIKeUPfvCDt6emplKe57nf+973Lo6MjNAwDINGo+EBAEskElxKGXLOQ8MwQkIIk1KGkXvPACDUwM4AgEkpQ0IIx+dlq+uXkda0JgS+4JyLZrOJcb7K7kP3ik637Y+NbCuAD1e6UZzJwMCAQQgxgyCwhBCmbds25zxm23a8Wq1atm0nhoaG0s1m0yoUCua99947+IlPfGKiXq+Ts2fPNj/96U9PHzp06L7FxcVqpVIJxsbG+j/5yU8+uLS0lC8UCvV9+/ZNZDKZjO/7wcLCwsqBAwf2SClJs9l0GGNsYGCgn1JKkVhjGIYZBEFYq9Xq4+Pjw/V63cnn88VMJpO0LMv2PM+fmZm5dv/99+/O5XKFy5cvL+7YsaO/WCzWyuVyo1ar1Xbu3Nl/6tSpuTNnzuT279+fPnPmTH5lZaWeTqeJbdvC9/2AUspM0+RSSiaEQIvOdNBH93gLpZSctFaFZpRSLoRgnHMBAJxSyiPgC0qp5JzzdYAPcPNy7sdSthXAhy/4G6MXQNPptJFIJIwgCAzGmEUIsUzTtBhjtm3bcc65jY8JIfFms2kahpEYHx/PcM7Nubk5lslkEp/97Gd3Oo4DJ0+eLH7qU58am56eHrBtO37s2LG5J5988p6JiYmhy5cvF4IgYKOjo7379+/ffezYsXeHh4dTe/bsGWWMwdzc3IoQQjz44IO733777dmenh4zkUjYqVQq8cYbb1zas2dPdmBgIP3jH//4XDabtSilMplM2q+//vrV8fHxeDabtX/4wx9eHR0dtSilvFQquVJKHo/HhWmaQggRCiEQxArQkVvPoR3weEOXnkcDX9DN54QQQSkVpmkK0zQ5AIggCKTv+8JxHAV8WCNkfqzKercq2wrgw5VO8hIBAGIYBh0aGjLCMDQZYwZjzKSUmqZp2pxzixBiU0pjQggLAGxKaUxKaYdhaBqGYZumaUWzDA3bti1CiFUsFtno6GhycnIyMz8/36xWq2JwcDD+yCOP7HjnnXcKi4uL3mc+85lRy7KMUqnknz9/vvLUU0/tnp+fr166dKk6Pj6emJycTK+srDTffffd1UOHDmULhUIzl8t58XicjIyMxIrFoluv14M9e/YkFxcX667rhpRSadu2CMOQEUJ41GHJIxcdZyjg7ASmW3T9OXxMCBGEEBZ9Toho0Kru5kecfRkEgXAcR0YegbwJ8KHL9sdethXAhyudRCWCuYBEImFkMhkjDEODc64rASuaSGwTQkwAsADABgCLEGJKKc3osQEAhpTSAADDNE0zDEMSjQIzKaW0RXcnxDAMKqUklUqFjY6OJizLIqVSKYjWCTCTyaSRy+U827ZpMpmknudx13VZIpEgtm0Tz/OYEEJGU4WF7/thNCQUiVosuucamNtA3fE8vp/JVtMNx1vUq80xQapZe0FaA11lo9EQnucpevgmgA/rPPexl20F8OHLDV6ApgRoFyVgWJZlCSFMIYQJAGakCPCGw1INQggqAOwlMAghNJruQymlxlrlq6UIgiCQstUQhDV61SAUzUmQ2qwAKaUU0WMBAJy0WIVCtMaft81FQBcd1oCuHkfv5dBy0Xm0Lx6V5NDSK9BjbG8YhoiSjzIMQxm5+Qh6oYEeZdvq34JsK4APX27wAqCLEmCMUc65wTk3pJSmYRgGpZRKKVER4GAUBLwJrYEpNHofla0uSVVxgLWwA6DVe0AiEhKNzGVnWbVbwgxvIiIvCRKNV5et8WgiAj3y6UVUTlTAx9cj955HioVH+1Ldd1pCTwCA7IjtN2vtb5co9rGUbQVwZ+SmSqCvr8/gnBP0BkRrQRGcF2AQQqgQAoelItCpEMKI6vg66I3OY0nVoUzWSAAdohHjug1RaVMEkajxaQh0sjYwU0QJPCmEEJRSGZGEVK+9YRgiYkIqdz4IAul5nsSx29ENOw633fwPWLYVwJ2Rbk1LbUogHo/Tnp4eYhgG5ZyrGyoCaCkDCgAELT60gN0WVsiOWQkRYCgqgE2eb5sCiMppaNn12B8VglIKCHYhhBRCcG0fklIqMabH7TAMZeTe65a+7Rw6QN/Num8D/zZlWwHcOenWHagrAQAApQgsy6JCCMI5J6gIhBDoxpPI9SeUUrVoaue2dhxUAN3Oo1PQVVDkGaI1MAGA5JxLiFiNIhKi0Wt1sEf3MmoRFgAgXdeVlFIZBIHcJOi7neO2m/8ByLYCuLOynhKADotNkskksSyLmKZJo2W4CAAQIQSNEndECEGEEPg5srYbtQ2UUoo9/V2Ov55ITBDq/Hl8DdGJiiGiN98AdkKIZIzJaFFR6fv+DYCPzlHKtXbkbdDfQdlWAHdeuv3muiIA0BQBRKCOx+MkFovRaHgn0ZVCZPEBlUE0H4CSteWxOhuFSMc2Hlfq23iTraEhIgK21O7bGmo8z5PRe6Xv+xK3oQPUkeJQS3dpr3UCfBv0H7JsK4C7J+v99gr0pNWE023+gVIK8Xgcx4wpUCcSibZ9t8Ly9ik9sr2x6obGq2geYFsjVUSxVcqCUgqe50nP82R0rgBdSnJ0bULzZgDf7flt+ZBkWwHcfVnPI9DBoBL3pFVn2/Dz2LGn3kAISafTt/Rf1+t15eqjRJ2EnaKstFZcUICHduu/DfgtJtsKYOuIPgClW64AX8Ntorvo+me6xPu3C662fXRUD3U3Ho8hu32uy+NtwG8R2VYAW1e6TUTqLCd2I/N0KzluelAnSoeX0S0u79zeiI23DfgtKtsK4KMn64Fpo1Didv/njZJw653DNtA/QrKtAH615INw++/GvrdlW7ZlW7ZlW7ZlW7ZlW7ZlW7ZlW7ZlW7ZlW7ZlW7blw5D/DxNDjYGwo2tNAAAAAElFTkSuQmCC"; @@ -26,9 +20,9 @@ public class MareProfileManager : MediatorSubscriberBase private readonly MareConfigService _mareConfigService; private readonly ConcurrentDictionary _mareProfiles = new(UserDataComparer.Instance); - private readonly MareProfileData DefaultProfileData = new(false, false, _mareLogo, string.Empty, _noDescription); - private readonly MareProfileData LoadingProfileData = new(false, false, _mareLogoLoading, string.Empty, "Loading Data from server..."); - private readonly MareProfileData NsfwProfileData = new(false, false, _mareLogoNsfw, string.Empty, _nsfw); + private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, _mareLogo, string.Empty, _noDescription); + private readonly MareProfileData _loadingProfileData = new(IsFlagged: false, IsNSFW: false, _mareLogoLoading, string.Empty, "Loading Data from server..."); + private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, _mareLogoNsfw, string.Empty, _nsfw); public MareProfileManager(ILogger logger, MareConfigService mareConfigService, MareMediator mediator, ApiController apiController) : base(logger, mediator) @@ -50,8 +44,8 @@ public class MareProfileManager : MediatorSubscriberBase { if (!_mareProfiles.TryGetValue(data, out var profile)) { - Task.Run(() => GetMareProfileFromService(data)); - return (LoadingProfileData); + _ = Task.Run(() => GetMareProfileFromService(data)); + return (_loadingProfileData); } return (profile); @@ -61,7 +55,7 @@ public class MareProfileManager : MediatorSubscriberBase { try { - _mareProfiles[data] = LoadingProfileData; + _mareProfiles[data] = _loadingProfileData; var profile = await _apiController.UserGetProfile(new API.Dto.User.UserDto(data)).ConfigureAwait(false); MareProfileData profileData = new(profile.Disabled, profile.IsNSFW ?? false, string.IsNullOrEmpty(profile.ProfilePictureBase64) ? _mareLogo : profile.ProfilePictureBase64, @@ -69,7 +63,7 @@ public class MareProfileManager : MediatorSubscriberBase string.IsNullOrEmpty(profile.Description) ? _noDescription : profile.Description); if (profileData.IsNSFW && !_mareConfigService.Current.ProfilesAllowNsfw && !string.Equals(_apiController.UID, data.UID, StringComparison.Ordinal)) { - _mareProfiles[data] = NsfwProfileData; + _mareProfiles[data] = _nsfwProfileData; } else { @@ -80,7 +74,7 @@ public class MareProfileManager : MediatorSubscriberBase { // if fails save DefaultProfileData to dict Logger.LogWarning(ex, "Failed to get Profile from service for user {user}", data); - _mareProfiles[data] = DefaultProfileData; + _mareProfiles[data] = _defaultProfileData; } } } \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs index f76e04e..d97cfaf 100644 --- a/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs +++ b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs @@ -10,7 +10,7 @@ public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, public void Dispose() { - Dispose(true); + Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/MareSynchronos/Services/Mediator/MareMediator.cs b/MareSynchronos/Services/Mediator/MareMediator.cs index 4b421d1..5489aa5 100644 --- a/MareSynchronos/Services/Mediator/MareMediator.cs +++ b/MareSynchronos/Services/Mediator/MareMediator.cs @@ -8,12 +8,12 @@ namespace MareSynchronos.Services.Mediator; public sealed class MareMediator : IHostedService { private readonly object _addRemoveLock = new(); - private readonly Dictionary _lastErrorTime = new(); + private readonly Dictionary _lastErrorTime = []; private readonly ILogger _logger; private readonly CancellationTokenSource _loopCts = new(); private readonly ConcurrentQueue _messageQueue = new(); private readonly PerformanceCollectorService _performanceCollector; - private readonly Dictionary> _subscriberDict = new(); + private readonly Dictionary> _subscriberDict = []; public MareMediator(ILogger logger, PerformanceCollectorService performanceCollector) { @@ -23,13 +23,13 @@ public sealed class MareMediator : IHostedService 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()) + foreach (var subscriber in _subscriberDict.SelectMany(c => c.Value.Select(v => v.Subscriber)) + .DistinctBy(p => p).OrderBy(p => p.GetType().FullName, StringComparer.Ordinal).ToList()) { - _logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().Name, kvp.Subscriber.ToString()); + _logger.LogInformation("Subscriber {type}: {sub}", subscriber.GetType().Name, subscriber.ToString()); StringBuilder sb = new(); sb.Append("=> "); - foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == kvp.Subscriber)).ToList()) + foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == subscriber)).ToList()) { sb.Append(item.Key.Name).Append(", "); } @@ -62,7 +62,7 @@ public sealed class MareMediator : IHostedService { await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); - HashSet processedMessages = new(); + HashSet processedMessages = []; while (_messageQueue.TryDequeue(out var message)) { if (processedMessages.Contains(message)) { continue; } @@ -89,7 +89,7 @@ public sealed class MareMediator : IHostedService { lock (_addRemoveLock) { - _subscriberDict.TryAdd(typeof(T), new HashSet()); + _subscriberDict.TryAdd(typeof(T), []); if (!_subscriberDict[typeof(T)].Add(new(subscriber, action))) { @@ -130,20 +130,22 @@ public sealed class MareMediator : IHostedService { if (!_subscriberDict.TryGetValue(message.GetType(), out HashSet? subscribers) || subscribers == null || !subscribers.Any()) return; - HashSet subscribersCopy = new HashSet(); + HashSet subscribersCopy = []; lock (_addRemoveLock) { - subscribersCopy = subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet(); + subscribersCopy = subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? []; } foreach (SubscriberAction subscriber in subscribersCopy) { try { +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields typeof(MareMediator) .GetMethod(nameof(ExecuteSubscriber), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? .MakeGenericMethod(message.GetType()) .Invoke(this, new object[] { subscriber, message }); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields } catch (Exception ex) { diff --git a/MareSynchronos/Services/Mediator/MessageBase.cs b/MareSynchronos/Services/Mediator/MessageBase.cs index cd1c3d5..e29bf8d 100644 --- a/MareSynchronos/Services/Mediator/MessageBase.cs +++ b/MareSynchronos/Services/Mediator/MessageBase.cs @@ -1,5 +1,6 @@ namespace MareSynchronos.Services.Mediator; +#pragma warning disable MA0048 public abstract record MessageBase { public virtual bool KeepThreadContext => false; @@ -8,4 +9,5 @@ public abstract record MessageBase public record SameThreadMessage : MessageBase { public override bool KeepThreadContext => true; -} \ No newline at end of file +} +#pragma warning restore MA0048 \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index e80f8f3..adbc2d9 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using MareSynchronos.API.Data; using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.Group; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.WebAPI.Files.Models; @@ -10,6 +11,7 @@ using System.Numerics; namespace MareSynchronos.Services.Mediator; #pragma warning disable MA0048 // File name must match type name +#pragma warning disable S2094 public record SwitchToIntroUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase; @@ -68,5 +70,12 @@ public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase; public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase; public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase; public record PairHandlerVisibleMessage(PairHandler Player) : MessageBase; +public record RefreshUiMessage : MessageBase; +public record OpenReportPopupMessage(Pair PairToReport) : MessageBase; +public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase; +public record JoinSyncshellPopupMessage() : MessageBase; +public record OpenCreateSyncshellPopupMessage() : MessageBase; +public record OpenSyncshellAdminPanelPopupMessage(GroupFullInfoDto GroupInfo) : MessageBase; +#pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs index 1d7424d..9552d39 100644 --- a/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs +++ b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs @@ -27,7 +27,7 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber public void Dispose() { - Dispose(true); + Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/MareSynchronos/Services/PerformanceCollectorService.cs b/MareSynchronos/Services/PerformanceCollectorService.cs index 6f2be36..8646628 100644 --- a/MareSynchronos/Services/PerformanceCollectorService.cs +++ b/MareSynchronos/Services/PerformanceCollectorService.cs @@ -121,7 +121,7 @@ public sealed class PerformanceCollectorService : IHostedService DrawSeparator(sb, longestCounterName); } - var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : entry.Value.ToList(); + var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value]; if (pastEntries.Any()) { diff --git a/MareSynchronos/Services/PluginWarningNotificationService.cs b/MareSynchronos/Services/PluginWarningNotificationService.cs index 36bd68d..23925bd 100644 --- a/MareSynchronos/Services/PluginWarningNotificationService.cs +++ b/MareSynchronos/Services/PluginWarningNotificationService.cs @@ -35,7 +35,7 @@ public class PluginWarningNotificationService }; } - List missingPluginsForData = new(); + List missingPluginsForData = []; if (changes.Contains(PlayerChanges.Heels) && !warning.ShownHeelsWarning && !_ipcManager.CheckHeelsApi()) { missingPluginsForData.Add("SimpleHeels"); diff --git a/MareSynchronos/Services/ServerConfiguration/JwtCache.cs b/MareSynchronos/Services/ServerConfiguration/JwtCache.cs deleted file mode 100644 index c28a86f..0000000 --- a/MareSynchronos/Services/ServerConfiguration/JwtCache.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace MareSynchronos.Services.ServerConfiguration; - -public record JwtCache(string ApiUrl, string PlayerName, uint WorldId, string SecretKey); diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index d3adbc8..f89c6a8 100644 --- a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -1,5 +1,6 @@ using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; using System.Diagnostics; @@ -11,18 +12,20 @@ public class ServerConfigurationManager private readonly ServerConfigService _configService; private readonly DalamudUtilService _dalamudUtil; private readonly ILogger _logger; + private readonly MareMediator _mareMediator; private readonly NotesConfigService _notesConfig; private readonly ServerTagConfigService _serverTagConfig; - private readonly Dictionary _tokenDictionary = new(); public ServerConfigurationManager(ILogger logger, ServerConfigService configService, - ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil) + ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil, + MareMediator mareMediator) { _logger = logger; _configService = configService; _serverTagConfig = serverTagConfig; _notesConfig = notesConfig; _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; EnsureMainExists(); } @@ -92,7 +95,7 @@ public class ServerConfigurationManager { try { - return _configService.Current.ServerStorage.ElementAt(idx); + return _configService.Current.ServerStorage[idx]; } catch { @@ -107,20 +110,6 @@ public class ServerConfigurationManager return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray(); } - public string? GetToken() - { - var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetWorldIdAsync().GetAwaiter().GetResult(); - var secretKey = GetSecretKey(); - if (secretKey == null) return null; - if (_tokenDictionary.TryGetValue(new JwtCache(CurrentApiUrl, charaName, worldId, secretKey), out var token)) - { - return token; - } - - return null; - } - public bool HasValidConfig() { return CurrentServer != null; @@ -129,19 +118,10 @@ public class ServerConfigurationManager public void Save() { var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - _logger.LogDebug(caller + " Calling config save"); + _logger.LogDebug("{caller} Calling config save", caller); _configService.Save(); } - public void SaveToken(string token) - { - var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetWorldIdAsync().GetAwaiter().GetResult(); - var secretKey = GetSecretKey(); - if (string.IsNullOrEmpty(secretKey)) throw new InvalidOperationException("No secret key set"); - _tokenDictionary[new JwtCache(CurrentApiUrl, charaName, worldId, secretKey)] = token; - } - public void SelectServer(int idx) { _configService.Current.CurrentServer = idx; @@ -185,6 +165,7 @@ public class ServerConfigurationManager { CurrentServerTagStorage().ServerAvailablePairTags.Add(tag); _serverTagConfig.Save(); + _mareMediator.Publish(new RefreshUiMessage()); } internal void AddTagForUid(string uid, string tagName) @@ -192,10 +173,11 @@ public class ServerConfigurationManager if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { tags.Add(tagName); + _mareMediator.Publish(new RefreshUiMessage()); } else { - CurrentServerTagStorage().UidServerPairedUserTags[uid] = new() { tagName }; + CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName]; } _serverTagConfig.Save(); @@ -295,6 +277,7 @@ public class ServerConfigurationManager RemoveTagForUid(uid, tag, save: false); } _serverTagConfig.Save(); + _mareMediator.Publish(new RefreshUiMessage()); } internal void RemoveTagForUid(string uid, string tagName, bool save = true) @@ -302,8 +285,23 @@ public class ServerConfigurationManager if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) { tags.Remove(tagName); + if (save) + { _serverTagConfig.Save(); + _mareMediator.Publish(new RefreshUiMessage()); + } + } + } + + internal void RenameTag(string oldName, string newName) + { + CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName); + CurrentServerTagStorage().ServerAvailablePairTags.Add(newName); + foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value)) + { + if (existingTags.Remove(oldName)) + existingTags.Add(newName); } } @@ -314,6 +312,8 @@ public class ServerConfigurationManager internal void SetNoteForGid(string gid, string note, bool save = true) { + if (string.IsNullOrEmpty(gid)) return; + CurrentNotesStorage().GidServerComments[gid] = note; if (save) _notesConfig.Save(); @@ -321,6 +321,8 @@ public class ServerConfigurationManager internal void SetNoteForUid(string uid, string note, bool save = true) { + if (string.IsNullOrEmpty(uid)) return; + CurrentNotesStorage().UidServerComments[uid] = note; if (save) _notesConfig.Save(); diff --git a/MareSynchronos/Services/UiService.cs b/MareSynchronos/Services/UiService.cs index 2e21acd..60565a7 100644 --- a/MareSynchronos/Services/UiService.cs +++ b/MareSynchronos/Services/UiService.cs @@ -1,17 +1,17 @@ -using Dalamud.Plugin; -using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; -using MareSynchronos.UI; +using Dalamud.Plugin; using MareSynchronos.MareConfiguration; -using MareSynchronos.Services.Mediator; -using Microsoft.Extensions.Logging; using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using Microsoft.Extensions.Logging; namespace MareSynchronos.Services; public sealed class UiService : DisposableMediatorSubscriberBase { - private readonly List _createdWindows = new(); + private readonly List _createdWindows = []; private readonly DalamudPluginInterface _dalamudPluginInterface; private readonly FileDialogManager _fileDialogManager; private readonly ILogger _logger; @@ -42,8 +42,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (msg) => { - if (!_createdWindows.Any(p => p is StandaloneProfileUi - && string.Equals(((StandaloneProfileUi)p).Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal))) + if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui + && string.Equals(ui.Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal))) { var window = standaloneProfileUiFactory(msg.Pair); _createdWindows.Add(window); @@ -59,14 +59,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase }); } - public void ToggleUi() - { - if (_mareConfigService.Current.HasValidSetup()) - Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - else - Mediator.Publish(new UiToggleMessage(typeof(IntroUi))); - } - public void ToggleMainUi() { if (_mareConfigService.Current.HasValidSetup()) @@ -75,6 +67,14 @@ public sealed class UiService : DisposableMediatorSubscriberBase Mediator.Publish(new UiToggleMessage(typeof(IntroUi))); } + public void ToggleUi() + { + if (_mareConfigService.Current.HasValidSetup()) + Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + else + Mediator.Publish(new UiToggleMessage(typeof(IntroUi))); + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 4455ec8..b6c8737 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -1,16 +1,12 @@ -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Globalization; -using System.Numerics; -using System.Reflection; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.API.Data.Extensions; -using MareSynchronos.API.Dto.User; +using MareSynchronos.API.Dto.Group; using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; @@ -23,6 +19,10 @@ using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files.Models; using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Globalization; +using System.Numerics; +using System.Reflection; namespace MareSynchronos.UI; @@ -33,18 +33,16 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly MareConfigService _configService; private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly DrawEntityFactory _drawEntityFactory; private readonly FileUploadManager _fileTransferManager; - private readonly GroupPanel _groupPanel; - private readonly PairGroupsUi _pairGroupsUi; private readonly PairManager _pairManager; - private readonly SelectGroupForPairUi _selectGroupForPairUi; - private readonly SelectPairForGroupUi _selectPairsForGroupUi; + private readonly SelectTagForPairUi _selectGroupForPairUi; + private readonly SelectPairForTagUi _selectPairsForGroupUi; private readonly ServerConfigurationManager _serverManager; - private readonly Stopwatch _timeout = new(); - private readonly UidDisplayHandler _uidDisplayHandler; + private readonly TagHandler _tagHandler; private readonly UiSharedService _uiShared; - private bool _buttonState; private string _characterOrCommentFilter = string.Empty; + private List _drawFolders; private Pair? _lastAddedUser; private string _lastAddedUserComment = string.Empty; private Vector2 _lastPosition = Vector2.One; @@ -52,11 +50,12 @@ public class CompactUi : WindowMediatorSubscriberBase private string _pairToAdd = string.Empty; private int _secretKeyIdx = -1; private bool _showModalForUserAddition; - private bool _showSyncShells; private bool _wasOpen; public CompactUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, - ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler) : base(logger, mediator, "###MareSynchronosMainUI") + ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, + TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi) + : base(logger, mediator, "###MareSynchronosMainUI") { _uiShared = uiShared; _configService = configService; @@ -64,13 +63,12 @@ public class CompactUi : WindowMediatorSubscriberBase _pairManager = pairManager; _serverManager = serverManager; _fileTransferManager = fileTransferManager; - _uidDisplayHandler = uidDisplayHandler; - var tagHandler = new TagHandler(_serverManager); + _tagHandler = tagHandler; + _drawEntityFactory = drawEntityFactory; + _selectGroupForPairUi = selectTagForPairUi; + _selectPairsForGroupUi = selectPairForTagUi; - _groupPanel = new(this, uiShared, _pairManager, uidDisplayHandler, _serverManager); - _selectGroupForPairUi = new(tagHandler, uidDisplayHandler); - _selectPairsForGroupUi = new(tagHandler, uidDisplayHandler); - _pairGroupsUi = new(configService, tagHandler, apiController, _selectPairsForGroupUi); + _drawFolders = GetDrawFolders().ToList(); #if DEBUG string dev = "Dev Build"; @@ -87,6 +85,7 @@ public class CompactUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + Mediator.Subscribe(this, (msg) => _drawFolders = GetDrawFolders().ToList()); Flags |= ImGuiWindowFlags.NoDocking; @@ -107,8 +106,11 @@ public class CompactUi : WindowMediatorSubscriberBase 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(); + using (ImRaii.PushFont(_uiShared.UidFont, _uiShared.UidFontBuilt)) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, unsupported); + } 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); } @@ -116,55 +118,12 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.DrawWithID("header", DrawUIDHeader); ImGui.Separator(); UiSharedService.DrawWithID("serverstatus", DrawServerStatus); + ImGui.Separator(); if (_apiController.ServerState is ServerState.Connected) { - var hasShownSyncShells = _showSyncShells; + UiSharedService.DrawWithID("pairlist", DrawPairList); - ImGui.PushFont(UiBuilder.IconFont); - if (!hasShownSyncShells) - { - ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); - } - if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) - { - _showSyncShells = false; - } - if (!hasShownSyncShells) - { - ImGui.PopStyleColor(); - } - ImGui.PopFont(); - UiSharedService.AttachToolTip("Individual pairs"); - - ImGui.SameLine(); - - ImGui.PushFont(UiBuilder.IconFont); - if (hasShownSyncShells) - { - ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); - } - if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) - { - _showSyncShells = true; - } - if (hasShownSyncShells) - { - ImGui.PopStyleColor(); - } - ImGui.PopFont(); - - UiSharedService.AttachToolTip("Syncshells"); - - ImGui.Separator(); - if (!hasShownSyncShells) - { - UiSharedService.DrawWithID("pairlist", DrawPairList); - } - else - { - UiSharedService.DrawWithID("syncshells", _groupPanel.DrawSyncshells); - } ImGui.Separator(); UiSharedService.DrawWithID("transfers", DrawTransfers); TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; @@ -213,12 +172,6 @@ public class CompactUi : WindowMediatorSubscriberBase } } - public override void OnClose() - { - _uidDisplayHandler.Clear(); - base.OnClose(); - } - private void DrawAddCharacter() { ImGui.Dummy(new(10)); @@ -237,7 +190,7 @@ public class CompactUi : WindowMediatorSubscriberBase _serverManager.Save(); - _ = _apiController.CreateConnections(forceGetToken: true); + _ = _apiController.CreateConnections(); } _uiShared.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key); @@ -250,18 +203,15 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawAddPair() { - var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); - ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus); + var usersButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Users); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X - ImGui.GetStyle().ItemSpacing.X - usersButtonSize.X); ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20); - 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) + ImGui.SameLine(); + var alreadyExisting = _pairManager.DirectPairs.Exists(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal)); + using (ImRaii.Disabled(alreadyExisting || string.IsNullOrEmpty(_pairToAdd))) { - ImGuiComponents.DisabledButton(FontAwesomeIcon.Plus); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + if (ImGuiComponents.IconButton(FontAwesomeIcon.UserPlus)) { _ = _apiController.UserAddPair(new(new(_pairToAdd))); _pairToAdd = string.Empty; @@ -269,95 +219,48 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); } + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Users)) + { + ImGui.OpenPopup("Syncshell Menu"); + } + UiSharedService.AttachToolTip("Syncshell Menu"); + + if (ImGui.BeginPopup("Syncshell Menu")) + { + using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct() + .Count(g => string.Equals(g.OwnerUID, _apiController.UID, StringComparison.Ordinal)) >= _apiController.ServerInfo.MaxGroupsCreatedByUser)) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create new Syncshell", _syncshellMenuSize, true)) + { + Mediator.Publish(new OpenCreateSyncshellPopupMessage()); + ImGui.CloseCurrentPopup(); + } + } + + using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser)) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Join existing Syncshell", _syncshellMenuSize, true)) + { + Mediator.Publish(new JoinSyncshellPopupMessage()); + ImGui.CloseCurrentPopup(); + } + } + _syncshellMenuSize = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + ImGui.EndPopup(); + } + ImGuiHelpers.ScaledDummy(2); } + private float _syncshellMenuSize = 0; + private void DrawFilter() { - var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowUp); - var playButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Play); - if (!_configService.Current.ReverseUserSort) + ImGui.SetNextItemWidth(WindowContentWidth); + if (ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255)) { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) - { - _configService.Current.ReverseUserSort = true; - _configService.Save(); - } - UiSharedService.AttachToolTip("Sort by name descending"); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowUp)) - { - _configService.Current.ReverseUserSort = false; - _configService.Save(); - } - UiSharedService.AttachToolTip("Sort by name ascending"); - } - ImGui.SameLine(); - - var users = GetFilteredUsers(); - var userCount = users.Count; - - var spacing = userCount > 0 - ? playButtonSize.X + ImGui.GetStyle().ItemSpacing.X * 2 - : ImGui.GetStyle().ItemSpacing.X; - - ImGui.SetNextItemWidth(WindowContentWidth - buttonSize.X - spacing); - ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255); - - if (userCount == 0) return; - - var pausedUsers = users.Where(u => u.UserPair!.OwnPermissions.IsPaused() && u.UserPair.OtherPermissions.IsPaired()).ToList(); - var resumedUsers = users.Where(u => !u.UserPair!.OwnPermissions.IsPaused() && u.UserPair.OtherPermissions.IsPaired()).ToList(); - - if (!pausedUsers.Any() && !resumedUsers.Any()) return; - ImGui.SameLine(); - - switch (_buttonState) - { - 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; - } - - var button = _buttonState ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - - if (!_timeout.IsRunning || _timeout.ElapsedMilliseconds > 15000) - { - _timeout.Reset(); - - if (ImGuiComponents.IconButton(button) && UiSharedService.CtrlPressed()) - { - 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; - } - 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); - UiSharedService.AttachToolTip($"Next execution is available at {availableAt} seconds"); + Mediator.Publish(new RefreshUiMessage()); } } @@ -374,24 +277,13 @@ public class CompactUi : WindowMediatorSubscriberBase var ySize = TransferPartHeight == 0 ? 1 : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY(); - var users = GetFilteredUsers() - .OrderBy( - u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.PlayerName) - ? (_configService.Current.PreferNotesOverNamesForVisible ? u.GetNote() : u.PlayerName) - : (u.GetNote() ?? u.UserData.AliasOrUID), StringComparer.OrdinalIgnoreCase).ToList(); - - if (_configService.Current.ReverseUserSort) - { - users.Reverse(); - } - - var onlineUsers = users.Where(u => u.IsOnline || u.UserPair!.OwnPermissions.IsPaused()).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, _selectGroupForPairUi)).ToList(); - var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, _selectGroupForPairUi)).ToList(); - var offlineUsers = users.Where(u => !u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused()).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, _selectGroupForPairUi)).ToList(); ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false); - _pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers); + foreach (var item in _drawFolders) + { + item.Draw(); + } ImGui.EndChild(); } @@ -417,7 +309,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); ImGui.SameLine(); if (!printShard) ImGui.AlignTextToFramePadding(); - ImGui.Text("Users Online"); + ImGui.TextUnformatted("Users Online"); } else { @@ -474,7 +366,7 @@ public class CompactUi : WindowMediatorSubscriberBase { var currentUploads = _fileTransferManager.CurrentUploads.ToList(); ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); + ImGui.TextUnformatted(FontAwesomeIcon.Upload.ToIconString()); ImGui.PopFont(); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); @@ -486,20 +378,20 @@ public class CompactUi : WindowMediatorSubscriberBase var totalUploaded = currentUploads.Sum(c => c.Transferred); var totalToUpload = currentUploads.Sum(c => c.Total); - ImGui.Text($"{doneUploads}/{totalUploads}"); + ImGui.TextUnformatted($"{doneUploads}/{totalUploads}"); var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})"; var textSize = ImGui.CalcTextSize(uploadText); ImGui.SameLine(WindowContentWidth - textSize.X); - ImGui.Text(uploadText); + ImGui.TextUnformatted(uploadText); } else { - ImGui.Text("No uploads in progress"); + ImGui.TextUnformatted("No uploads in progress"); } var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Download.ToIconString()); + ImGui.TextUnformatted(FontAwesomeIcon.Download.ToIconString()); ImGui.PopFont(); ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); @@ -510,16 +402,16 @@ public class CompactUi : WindowMediatorSubscriberBase var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes); var totalToDownload = currentDownloads.Sum(c => c.TotalBytes); - ImGui.Text($"{doneDownloads}/{totalDownloads}"); + ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}"); var downloadText = $"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})"; var textSize = ImGui.CalcTextSize(downloadText); ImGui.SameLine(WindowContentWidth - textSize.X); - ImGui.Text(downloadText); + ImGui.TextUnformatted(downloadText); } else { - ImGui.Text("No downloads in progress"); + ImGui.TextUnformatted("No downloads in progress"); } if (UiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Mare Character Data Analysis", WindowContentWidth)) @@ -583,15 +475,119 @@ public class CompactUi : WindowMediatorSubscriberBase } } - private List GetFilteredUsers() + private IEnumerable GetDrawFolders() { - return _pairManager.DirectPairs.Where(p => + List drawFolders = []; + + var users = GetFilteredGroupUsers() + .ToDictionary(g => g.Key, g => g.Value); + + if (_configService.Current.ShowVisibleUsersSeparately) + { + var visibleUsers = users.Where(u => u.Key.IsVisible) + .OrderBy( + u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) + ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) + : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID), StringComparer.OrdinalIgnoreCase) + .ToDictionary(k => k.Key, k => k.Value); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, visibleUsers)); + } + + List groupFolders = new(); + foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.Ordinal)) + { + var groupUsers2 = users.Where(v => v.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)) + && (v.Key.IsOnline || (!v.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) + || v.Key.UserPair.OwnPermissions.IsPaused())) + .OrderByDescending(u => u.Key.IsOnline) + .ThenBy(u => + { + if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0; + if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info)) + { + if (info.IsModerator()) return 1; + if (info.IsPinned()) return 2; + } + return u.Key.IsVisible ? 3 : 4; + }) + .ThenBy( + u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) + ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) + : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID), StringComparer.Ordinal) + .ToDictionary(k => k.Key, k => k.Value); + + groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, groupUsers2)); + } + + if (_configService.Current.GroupUpSyncshells) + drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler)); + else + drawFolders.AddRange(groupFolders); + + var tags = _tagHandler.GetAllTagsSorted(); + HashSet alreadyInTags = []; + foreach (var tag in tags) + { + var tagUsers = users.Where(u => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasTag(u.Key.UserData.UID, tag) + && (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) + || u.Key.UserPair.OwnPermissions.IsPaused())) + .OrderByDescending(u => u.Key.IsVisible) + .ThenByDescending(u => u.Key.IsOnline) + .ThenBy( + u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) + ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) + : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID), StringComparer.OrdinalIgnoreCase) + .ToDictionary(u => u.Key, u => u.Value); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, tagUsers.Select(u => + { + alreadyInTags.Add(u.Key); + return (u.Key, u.Value); + }).ToDictionary(u => u.Key, u => u.Value))); + } + + var onlineDirectPairedUsersNotInTags = users.Where(u => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyTag(u.Key.UserData.UID) + && (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) + || u.Key.UserPair.OwnPermissions.IsPaused())) + .OrderByDescending(u => u.Key.IsVisible) + .ThenByDescending(u => u.Key.IsOnline) + .ThenBy( + u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) + ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) + : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID), StringComparer.OrdinalIgnoreCase) + .ToDictionary(u => u.Key, u => u.Value); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), + onlineDirectPairedUsersNotInTags)); + + if (_configService.Current.ShowOfflineUsersSeparately) + { + var offlineUsersEntries = users.Where(u => (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused()).OrderBy( + u => _configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName) + ? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName) + : (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID), StringComparer.OrdinalIgnoreCase) + .ToDictionary(u => u.Key, u => u.Value); + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, offlineUsersEntries)); + } + + drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, users.Where(u => u.Key.IsOneSidedPair).ToDictionary(u => u.Key, u => u.Value))); + + return drawFolders; + } + + private Dictionary> GetFilteredGroupUsers() + { + if (string.IsNullOrEmpty(_characterOrCommentFilter)) return _pairManager.PairsWithGroups; + + return _pairManager.PairsWithGroups.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, StringComparison.OrdinalIgnoreCase) ?? false); - }).ToList(); + return p.Key.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) || + (p.Key.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.Key.PlayerName?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false); + }).ToDictionary(k => k.Key, k => k.Value); } private string GetServerError() diff --git a/MareSynchronos/UI/Components/DrawFolderBase.cs b/MareSynchronos/UI/Components/DrawFolderBase.cs new file mode 100644 index 0000000..7c505a8 --- /dev/null +++ b/MareSynchronos/UI/Components/DrawFolderBase.cs @@ -0,0 +1,116 @@ +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.UI.Handlers; + +namespace MareSynchronos.UI.Components; + +public abstract class DrawFolderBase : IDrawFolder +{ + protected readonly IEnumerable _drawPairs; + protected readonly string _id; + protected readonly TagHandler _tagHandler; + private float _menuWidth = -1; + protected DrawFolderBase(string id, IEnumerable drawPairs, TagHandler tagHandler) + { + _id = id; + _drawPairs = drawPairs; + _tagHandler = tagHandler; + } + + protected abstract bool RenderIfEmpty { get; } + protected abstract bool RenderMenu { get; } + + public void Draw() + { + if (!RenderIfEmpty && !_drawPairs.Any()) return; + + using var id = ImRaii.PushId("folder_" + _id); + var originalY = ImGui.GetCursorPosY(); + var pauseIconSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var textSize = ImGui.CalcTextSize(_id); + var textPosY = originalY + pauseIconSize.Y / 2 - textSize.Y / 2; + + // draw opener + var icon = _tagHandler.IsTagOpen(_id) ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight; + ImGui.SetCursorPosY(textPosY); + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemClicked()) + { + _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + } + + ImGui.SameLine(); + var leftSideEnd = DrawIcon(textPosY, originalY); + + ImGui.SameLine(); + var rightSideStart = DrawRightSide(originalY); + + // draw name + ImGui.SameLine(leftSideEnd); + DrawName(textPosY, rightSideStart - leftSideEnd); + ImGui.Separator(); + + // if opened draw content + if (_tagHandler.IsTagOpen(_id)) + { + using var indent = ImRaii.PushIndent(20f); + if (_drawPairs.Any()) + { + foreach (var item in _drawPairs) + { + item.DrawPairedClient(); + } + } + else + { + ImGui.TextUnformatted("No users (online)"); + } + + ImGui.Separator(); + } + } + + protected abstract float DrawIcon(float textPosY, float originalY); + + protected abstract void DrawMenu(float menuWidth); + + protected abstract void DrawName(float originalY, float width); + + protected abstract float DrawRightSide(float originalY, float currentRightSideX); + + private float DrawRightSide(float originalY) + { + var barButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); + + // Flyout Menu + var rightSideStart = windowEndX - (RenderMenu ? (barButtonSize.X + spacingX) : spacingX); + + if (RenderMenu) + { + ImGui.SameLine(windowEndX - barButtonSize.X); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("User Flyout Menu"); + } + if (ImGui.BeginPopup("User Flyout Menu")) + { + UiSharedService.DrawWithID($"buttons-{_id}", () => + { + DrawMenu(_menuWidth); + }); + _menuWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + ImGui.EndPopup(); + } + else + { + _menuWidth = 0; + } + } + + return DrawRightSide(originalY, rightSideStart); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawFolderGroup.cs b/MareSynchronos/UI/Components/DrawFolderGroup.cs new file mode 100644 index 0000000..c77dc3b --- /dev/null +++ b/MareSynchronos/UI/Components/DrawFolderGroup.cs @@ -0,0 +1,236 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.UI.Components; + +public class DrawFolderGroup : DrawFolderBase +{ + private readonly ApiController _apiController; + private readonly GroupFullInfoDto _groupFullInfoDto; + private readonly IdDisplayHandler _idDisplayHandler; + private readonly MareMediator _mareMediator; + + public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController, + IEnumerable drawPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler, + MareMediator mareMediator) : + base(id, drawPairs, tagHandler) + { + _groupFullInfoDto = groupFullInfoDto; + _apiController = apiController; + _idDisplayHandler = idDisplayHandler; + _mareMediator = mareMediator; + } + + protected override bool RenderIfEmpty => true; + protected override bool RenderMenu => true; + private bool IsModerator => IsOwner || _groupFullInfoDto.GroupUserInfo.IsModerator(); + private bool IsOwner => string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal); + private bool IsPinned => _groupFullInfoDto.GroupUserInfo.IsPinned(); + + protected override float DrawIcon(float textPosY, float originalY) + { + ImGui.SetCursorPosY(textPosY); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(_groupFullInfoDto.GroupPermissions.IsDisableInvites() ? FontAwesomeIcon.Lock.ToIconString() : FontAwesomeIcon.Users.ToIconString()); + if (_groupFullInfoDto.GroupPermissions.IsDisableInvites()) + { + UiSharedService.AttachToolTip("Syncshell " + _groupFullInfoDto.GroupAliasOrGID + " is closed for invites"); + } + if (IsOwner) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); + UiSharedService.AttachToolTip("You are the owner of " + _groupFullInfoDto.GroupAliasOrGID); + } + else if (IsModerator) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); + UiSharedService.AttachToolTip("You are a moderator in " + _groupFullInfoDto.GroupAliasOrGID); + } + else if (IsPinned) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); + UiSharedService.AttachToolTip("You are pinned in " + _groupFullInfoDto.GroupAliasOrGID); + } + ImGui.SameLine(); + return ImGui.GetCursorPosX(); + } + + protected override void DrawMenu(float menuWidth) + { + ImGui.TextUnformatted("Syncshell Menu (" + _groupFullInfoDto.GroupAliasOrGID + ")"); + ImGui.Separator(); + + ImGui.TextUnformatted("General Syncshell Actions"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID", menuWidth, true)) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(_groupFullInfoDto.GroupAliasOrGID); + } + UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes", menuWidth, true)) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(UiSharedService.GetNotes(_drawPairs.Select(k => k.Pair).ToList())); + } + 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"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed()) + { + _ = _apiController.GroupLeave(_groupFullInfoDto); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.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.")); + + ImGui.Separator(); + ImGui.TextUnformatted("Permission Settings"); + var perm = _groupFullInfoDto.GroupUserPermissions; + bool disableSounds = perm.IsDisableSounds(); + bool disableAnims = perm.IsDisableAnimations(); + bool disableVfx = perm.IsDisableVFX(); + + if ((_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != disableAnims + || _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != disableSounds + || _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != disableVfx) + && UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Align with suggested permissions", menuWidth, true)) + { + perm.SetDisableVFX(_groupFullInfoDto.GroupPermissions.IsPreferDisableVFX()); + perm.SetDisableSounds(_groupFullInfoDto.GroupPermissions.IsPreferDisableSounds()); + perm.SetDisableAnimations(_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations()); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } + + if (UiSharedService.IconTextButton(disableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeOff, disableSounds ? "Enable Sound Sync" : "Disable Sound Sync", + menuWidth, true)) + { + perm.SetDisableSounds(!disableSounds); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } + + if (UiSharedService.IconTextButton(disableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, disableAnims ? "Enable Animation Sync" : "Disable Animation Sync", + menuWidth, true)) + { + perm.SetDisableAnimations(!disableAnims); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } + + if (UiSharedService.IconTextButton(disableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, disableVfx ? "Enable VFX Sync" : "Disable VFX Sync", + menuWidth, true)) + { + perm.SetDisableVFX(!disableVfx); + _ = _apiController.GroupChangeIndividualPermissionState(new(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + ImGui.CloseCurrentPopup(); + } + + if (IsModerator || IsOwner) + { + ImGui.Separator(); + ImGui.TextUnformatted("Syncshell Admin Functions"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel", menuWidth, true)) + { + ImGui.CloseCurrentPopup(); + _mareMediator.Publish(new OpenSyncshellAdminPanelPopupMessage(_groupFullInfoDto)); + } + } + } + + protected override void DrawName(float originalY, float width) + { + _idDisplayHandler.DrawGroupText(_id, _groupFullInfoDto, ImGui.GetCursorPosX(), originalY, () => width); + } + + protected override float DrawRightSide(float originalY, float currentRightSideX) + { + var spacingX = ImGui.GetStyle().ItemSpacing.X; + + FontAwesomeIcon pauseIcon = _groupFullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var pauseButtonSize = UiSharedService.GetIconButtonSize(pauseIcon); + + var folderIcon = FontAwesomeIcon.UsersCog; + var userCogButtonSize = UiSharedService.GetIconSize(folderIcon); + + var individualSoundsDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableSounds(); + var individualAnimDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableAnimations(); + var individualVFXDisabled = _groupFullInfoDto.GroupUserPermissions.IsDisableVFX(); + + var infoIconPosDist = currentRightSideX - pauseButtonSize.X - spacingX; + + ImGui.SameLine(infoIconPosDist - userCogButtonSize.X); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, + _groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations() != individualAnimDisabled + || _groupFullInfoDto.GroupPermissions.IsPreferDisableSounds() != individualSoundsDisabled + || _groupFullInfoDto.GroupPermissions.IsPreferDisableVFX() != individualVFXDisabled)) + UiSharedService.FontText(folderIcon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.TextUnformatted("Syncshell Permissions"); + ImGui.Dummy(new(2f)); + + UiSharedService.BooleanToColoredIcon(!individualSoundsDisabled, inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("Sound Sync"); + + UiSharedService.BooleanToColoredIcon(!individualAnimDisabled, inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("Animation Sync"); + + UiSharedService.BooleanToColoredIcon(!individualVFXDisabled, inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("VFX Sync"); + + ImGui.Separator(); + + ImGui.Dummy(new(2f)); + ImGui.TextUnformatted("Suggested Permissions"); + ImGui.Dummy(new(2f)); + + UiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableSounds(), inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("Sound Sync"); + + UiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableAnimations(), inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("Animation Sync"); + + UiSharedService.BooleanToColoredIcon(!_groupFullInfoDto.GroupPermissions.IsPreferDisableVFX(), inline: false); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("VFX Sync"); + + ImGui.EndTooltip(); + } + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(pauseIcon)) + { + var perm = _groupFullInfoDto.GroupUserPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_groupFullInfoDto.Group, new(_apiController.UID), perm)); + } + return currentRightSideX; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawFolderTag.cs b/MareSynchronos/UI/Components/DrawFolderTag.cs new file mode 100644 index 0000000..3db2caa --- /dev/null +++ b/MareSynchronos/UI/Components/DrawFolderTag.cs @@ -0,0 +1,156 @@ +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.UI.Components; + +public class DrawFolderTag : DrawFolderBase +{ + private readonly ApiController _apiController; + private readonly SelectPairForTagUi _selectPairForTagUi; + + public DrawFolderTag(string id, IEnumerable drawPairs, TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi) + : base(id, drawPairs, tagHandler) + { + _apiController = apiController; + _selectPairForTagUi = selectPairForTagUi; + } + + protected override bool RenderIfEmpty => _id switch + { + TagHandler.CustomUnpairedTag => false, + TagHandler.CustomOnlineTag => false, + TagHandler.CustomOfflineTag => false, + TagHandler.CustomVisibleTag => false, + TagHandler.CustomAllTag => true, + _ => true, + }; + + protected override bool RenderMenu => _id switch + { + TagHandler.CustomUnpairedTag => false, + TagHandler.CustomOnlineTag => false, + TagHandler.CustomOfflineTag => false, + TagHandler.CustomVisibleTag => false, + TagHandler.CustomAllTag => false, + _ => true, + }; + + private bool RenderPause => _id switch + { + TagHandler.CustomUnpairedTag => false, + TagHandler.CustomOnlineTag => false, + TagHandler.CustomOfflineTag => false, + TagHandler.CustomVisibleTag => false, + TagHandler.CustomAllTag => false, + _ => true, + } && _drawPairs.Any(); + + protected override float DrawIcon(float textPosY, float originalY) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var icon = _id switch + { + TagHandler.CustomUnpairedTag => FontAwesomeIcon.ArrowsLeftRight.ToIconString(), + TagHandler.CustomOnlineTag => FontAwesomeIcon.Link.ToIconString(), + TagHandler.CustomOfflineTag => FontAwesomeIcon.Unlink.ToIconString(), + TagHandler.CustomVisibleTag => FontAwesomeIcon.Eye.ToIconString(), + TagHandler.CustomAllTag => FontAwesomeIcon.User.ToIconString(), + _ => FontAwesomeIcon.Folder.ToIconString() + }; + + ImGui.SetCursorPosY(textPosY); + ImGui.TextUnformatted(icon); + ImGui.SameLine(); + return ImGui.GetCursorPosX(); + } + + protected override void DrawMenu(float menuWidth) + { + ImGui.TextUnformatted("Group Menu"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true)) + { + _selectPairForTagUi.Open(_id); + } + UiSharedService.AttachToolTip("Select Individual Pairs for this Pair Group"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) && UiSharedService.CtrlPressed()) + { + _tagHandler.RemoveTag(_id); + } + UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine + + "Note: this will not unpair with users in this Group."); + } + + protected override void DrawName(float originalY, float width) + { + ImGui.SetCursorPosY(originalY); + string name = _id switch + { + TagHandler.CustomUnpairedTag => "One-sided Individual Pairs", + TagHandler.CustomOnlineTag => "Online / Paused by you", + TagHandler.CustomOfflineTag => "Offline / Paused by other", + TagHandler.CustomVisibleTag => "Visible", + TagHandler.CustomAllTag => "Users", + _ => _id + }; + + ImGui.TextUnformatted(name); + } + + protected override float DrawRightSide(float originalY, float currentRightSideX) + { + if (!RenderPause) return currentRightSideX; + + var allArePaused = _drawPairs.All(pair => pair.UserPair!.OwnPermissions.IsPaused()); + var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var pauseButtonX = UiSharedService.GetIconButtonSize(pauseButton).X; + + var buttonPauseOffset = currentRightSideX - pauseButtonX; + ImGui.SameLine(buttonPauseOffset); + if (ImGuiComponents.IconButton(pauseButton)) + { + if (allArePaused) + { + ResumeAllPairs(_drawPairs); + } + else + { + PauseRemainingPairs(_drawPairs); + } + } + if (allArePaused) + { + UiSharedService.AttachToolTip($"Resume pairing with all pairs in {_id}"); + } + else + { + UiSharedService.AttachToolTip($"Pause pairing with all pairs in {_id}"); + } + + return currentRightSideX; + } + + private void PauseRemainingPairs(IEnumerable availablePairs) + { + foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused())) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: true); + _ = _apiController.UserSetPairPermissions(new(new(pairToPause.UID), perm)); + } + } + + private void ResumeAllPairs(IEnumerable availablePairs) + { + foreach (var pairToPause in availablePairs) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: false); + _ = _apiController.UserSetPairPermissions(new(new(pairToPause.UID), perm)); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs deleted file mode 100644 index 1597415..0000000 --- a/MareSynchronos/UI/Components/DrawGroupPair.cs +++ /dev/null @@ -1,336 +0,0 @@ -using Dalamud.Interface.Colors; -using Dalamud.Interface.Components; -using Dalamud.Interface; -using ImGuiNET; -using MareSynchronos.PlayerData.Pairs; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.WebAPI; -using MareSynchronos.API.Dto.User; -using MareSynchronos.UI.Handlers; -using MareSynchronos.API.Dto.Group; -using MareSynchronos.API.Data.Enum; -using Dalamud.Interface.Utility; - -namespace MareSynchronos.UI.Components; - -public class DrawGroupPair : DrawPairBase -{ - private static string _banReason = string.Empty; - private static bool _banUserPopupOpen; - private static bool _showModalBanUser; - private readonly GroupPairFullInfoDto _fullInfoDto; - private readonly GroupFullInfoDto _group; - - public DrawGroupPair(string id, Pair entry, ApiController apiController, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto, UidDisplayHandler handler) : base(id, entry, apiController, handler) - { - _group = group; - _fullInfoDto = fullInfoDto; - } - - protected override void DrawLeftSide(float textPosY, float originalY) - { - var entryUID = _pair.UserData.AliasOrUID; - var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); - var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); - var entryIsPinned = _fullInfoDto.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"; - - ImGui.SetCursorPosY(textPosY); - 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(textPosY); - ImGui.PushFont(UiBuilder.IconFont); - UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor); - ImGui.PopFont(); - UiSharedService.AttachToolTip(presenceText); - - if (entryIsOwner) - { - ImGui.SameLine(); - ImGui.SetCursorPosY(textPosY); - 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(textPosY); - 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(textPosY); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); - ImGui.PopFont(); - UiSharedService.AttachToolTip("User is pinned in this Syncshell"); - } - } - - protected override float DrawRightSide(float textPosY, float originalY) - { - var entryUID = _fullInfoDto.UserAliasOrUID; - var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); - var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); - var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); - var userIsOwner = string.Equals(_group.OwnerUID, _apiController.UID, StringComparison.OrdinalIgnoreCase); - var userIsModerator = _group.GroupUserInfo.IsModerator(); - - var soundsDisabled = _fullInfoDto.GroupUserPermissions.IsDisableSounds(); - var animDisabled = _fullInfoDto.GroupUserPermissions.IsDisableAnimations(); - var vfxDisabled = _fullInfoDto.GroupUserPermissions.IsDisableVFX(); - 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 individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); - - bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled); - bool showPlus = _pair.UserPair == null; - bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused; - - var spacing = ImGui.GetStyle().ItemSpacing.X; - var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle - : ((soundsDisabled || animDisabled || vfxDisabled) ? FontAwesomeIcon.InfoCircle : FontAwesomeIcon.None); - var infoIconWidth = UiSharedService.GetIconSize(permIcon).X; - var plusButtonWidth = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X; - var barButtonWidth = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; - - var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing - - (showInfo ? (infoIconWidth + spacing) : 0) - - (showPlus ? (plusButtonWidth + spacing) : 0) - - (showBars ? (barButtonWidth + spacing) : 0); - - ImGui.SameLine(pos); - if (individualAnimDisabled || individualSoundsDisabled) - { - ImGui.SetCursorPosY(textPosY); - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); - UiSharedService.FontText(permIcon.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")); - } - - if (individualVFXDisabled) - { - var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; - UiSharedService.FontText(FontAwesomeIcon.Circle.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userVFXText); - ImGui.NewLine(); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled")); - } - - ImGui.EndTooltip(); - } - ImGui.SameLine(); - } - else if ((animDisabled || soundsDisabled)) - { - ImGui.SetCursorPosY(textPosY); - UiSharedService.FontText(permIcon.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); - } - - if (vfxDisabled) - { - var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID; - UiSharedService.FontText(FontAwesomeIcon.Circle.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userVFXText); - } - - ImGui.EndTooltip(); - } - ImGui.SameLine(); - } - - if (showPlus) - { - ImGui.SetCursorPosY(originalY); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) - { - _ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID))); - } - UiSharedService.AttachToolTip("Pair with " + entryUID + " individually"); - ImGui.SameLine(); - } - - if (showBars) - { - ImGui.SetCursorPosY(originalY); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) - { - ImGui.OpenPopup("Popup"); - } - } - - 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 = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; - _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.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(_fullInfoDto); - } - - UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " 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 = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; - _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.User, userInfo)); - } - UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_fullInfoDto.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(_fullInfoDto); - } - UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); - } - - ImGui.Separator(); - if (!_pair.IsPaused) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) - { - _displayHandler.OpenProfile(_pair); - ImGui.CloseCurrentPopup(); - } - UiSharedService.AttachToolTip("Opens the profile for this user in a new window"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) - { - ImGui.CloseCurrentPopup(); - _showModalReport = true; - } - UiSharedService.AttachToolTip("Report this users Mare Profile to the administrative team"); - } - 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 " + (_fullInfoDto.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(new GroupPairDto(_group.Group, _fullInfoDto.User), 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(); - } - - return pos - spacing; - } -} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawGroupedGroupFolder.cs b/MareSynchronos/UI/Components/DrawGroupedGroupFolder.cs new file mode 100644 index 0000000..ee4432a --- /dev/null +++ b/MareSynchronos/UI/Components/DrawGroupedGroupFolder.cs @@ -0,0 +1,49 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.UI.Handlers; + +namespace MareSynchronos.UI.Components; + +public class DrawGroupedGroupFolder : IDrawFolder +{ + private readonly IEnumerable _groups; + private readonly TagHandler _tagHandler; + + public DrawGroupedGroupFolder(IEnumerable groups, TagHandler tagHandler) + { + _groups = groups; + _tagHandler = tagHandler; + } + + public void Draw() + { + if (!_groups.Any()) return; + + string _id = "__folder_syncshells"; + using var id = ImRaii.PushId(_id); + + var icon = _tagHandler.IsTagOpen(_id) ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight; + UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont); + if (ImGui.IsItemClicked()) + { + _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + } + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.UsersRectangle.ToIconString()); + ImGui.SameLine(); + ImGui.TextUnformatted("All Syncshells"); + ImGui.Separator(); + + if (_tagHandler.IsTagOpen(_id)) + { + using var indent = ImRaii.PushIndent(20f); + foreach (var entry in _groups) + { + entry.Draw(); + } + } + } +} diff --git a/MareSynchronos/UI/Components/DrawPairBase.cs b/MareSynchronos/UI/Components/DrawPairBase.cs deleted file mode 100644 index e9ce17a..0000000 --- a/MareSynchronos/UI/Components/DrawPairBase.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Colors; -using ImGuiNET; -using MareSynchronos.PlayerData.Pairs; -using MareSynchronos.UI.Handlers; -using MareSynchronos.WebAPI; - -namespace MareSynchronos.UI.Components; - -public abstract class DrawPairBase -{ - protected static bool _showModalReport = false; - protected readonly ApiController _apiController; - protected readonly UidDisplayHandler _displayHandler; - protected Pair _pair; - private static bool _reportPopupOpen = false; - private static string _reportReason = string.Empty; - private readonly string _id; - - protected DrawPairBase(string id, Pair entry, ApiController apiController, UidDisplayHandler uIDDisplayHandler) - { - _id = id; - _pair = entry; - _apiController = apiController; - _displayHandler = uIDDisplayHandler; - } - - public string UID => _pair.UserData.UID; - - public void DrawPairedClient() - { - var originalY = ImGui.GetCursorPosY(); - var pauseIconSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Play); - var textSize = ImGui.CalcTextSize(_pair.UserData.AliasOrUID); - - var textPosY = originalY + pauseIconSize.Y / 2 - textSize.Y / 2; - DrawLeftSide(textPosY, originalY); - ImGui.SameLine(); - var posX = ImGui.GetCursorPosX(); - var rightSide = DrawRightSide(textPosY, originalY); - DrawName(originalY, posX, rightSide); - - if (_showModalReport && !_reportPopupOpen) - { - ImGui.OpenPopup("Report Profile"); - _reportPopupOpen = true; - } - - if (!_showModalReport) _reportPopupOpen = false; - - if (ImGui.BeginPopupModal("Report Profile", ref _showModalReport, UiSharedService.PopupWindowFlags)) - { - UiSharedService.TextWrapped("Report " + (_pair.UserData.AliasOrUID) + " Mare Profile"); - ImGui.InputTextMultiline("##reportReason", ref _reportReason, 500, new System.Numerics.Vector2(500 - ImGui.GetStyle().ItemSpacing.X * 2, 200)); - UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" + - $"The report will be sent to the team of your currently connected Mare Synchronos Service.{Environment.NewLine}" + - $"The report will include your user and your contact info (Discord User).{Environment.NewLine}" + - $"Depending on the severity of the offense the users Mare profile or account can be permanently disabled or banned."); - UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", ImGuiColors.DalamudRed); - if (string.IsNullOrEmpty(_reportReason)) ImGui.BeginDisabled(); - if (ImGui.Button("Send Report")) - { - ImGui.CloseCurrentPopup(); - var reason = _reportReason; - _ = _apiController.UserReportProfile(new(_pair.UserData, reason)); - _reportReason = string.Empty; - _showModalReport = false; - _reportPopupOpen = false; - } - if (string.IsNullOrEmpty(_reportReason)) ImGui.EndDisabled(); - UiSharedService.SetScaledWindowSize(500); - ImGui.EndPopup(); - } - } - - protected abstract void DrawLeftSide(float textPosY, float originalY); - - protected abstract float DrawRightSide(float textPosY, float originalY); - - private void DrawName(float originalY, float leftSide, float rightSide) - { - _displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide); - } -} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 04ba261..cb1e215 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -1,73 +1,298 @@ -using Dalamud.Interface.Colors; +using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface; -using ImGuiNET; -using MareSynchronos.PlayerData.Pairs; -using System.Numerics; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.WebAPI; -using MareSynchronos.API.Dto.User; -using MareSynchronos.UI.Handlers; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; namespace MareSynchronos.UI.Components; -public class DrawUserPair : DrawPairBase +public class DrawUserPair { - private readonly SelectGroupForPairUi _selectGroupForPairUi; + protected readonly ApiController _apiController; + protected readonly IdDisplayHandler _displayHandler; + protected readonly MareMediator _mediator; + protected readonly List _syncedGroups; + protected Pair _pair; + private readonly string _id; + private readonly SelectTagForPairUi _selectTagForPairUi; + private readonly ServerConfigurationManager _serverConfigurationManager; + private float _menuRenderWidth = -1; - public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController, SelectGroupForPairUi selectGroupForPairUi) : base(id, entry, apiController, displayHandler) + public DrawUserPair(string id, Pair entry, List syncedGroups, + ApiController apiController, IdDisplayHandler uIDDisplayHandler, + MareMediator mareMediator, SelectTagForPairUi selectTagForPairUi, + ServerConfigurationManager serverConfigurationManager) { - if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry)); + _id = id; _pair = entry; - _selectGroupForPairUi = selectGroupForPairUi; + _syncedGroups = syncedGroups; + _apiController = apiController; + _displayHandler = uIDDisplayHandler; + _mediator = mareMediator; + _selectTagForPairUi = selectTagForPairUi; + _serverConfigurationManager = serverConfigurationManager; } - public bool IsOnline => _pair.IsOnline; - public bool IsVisible => _pair.IsVisible; - public UserPairDto UserPair => _pair.UserPair!; + public Pair Pair => _pair; + public string UID => _pair.UserData.UID; + public UserFullPairDto UserPair => _pair.UserPair!; - protected override void DrawLeftSide(float textPosY, float originalY) + public void DrawPairedClient() { - FontAwesomeIcon connectionIcon; - Vector4 connectionColor; - string connectionText; - if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())) + using var id = ImRaii.PushId(GetType() + _id); + var originalY = ImGui.GetCursorPosY(); + var pauseIconSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var textSize = ImGui.CalcTextSize(_pair.UserData.AliasOrUID); + + var textPosY = originalY + pauseIconSize.Y / 2 - textSize.Y / 2; + DrawLeftSide(textPosY); + ImGui.SameLine(); + var posX = ImGui.GetCursorPosX(); + var rightSide = DrawRightSide(originalY); + DrawName(originalY, posX, rightSide); + } + + private void DrawCommonClientMenu() + { + if (!_pair.IsPaused) { - connectionIcon = FontAwesomeIcon.ArrowUp; - connectionText = _pair.UserData.AliasOrUID + " has not added you back"; - connectionColor = ImGuiColors.DalamudRed; + if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile", _menuRenderWidth, true)) + { + _displayHandler.OpenProfile(_pair); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("Opens the profile for this user in a new window"); } - else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused()) + if (_pair.IsVisible) { - connectionIcon = FontAwesomeIcon.PauseCircle; - connectionText = "Pairing status with " + _pair.UserData.AliasOrUID + " is paused"; - connectionColor = ImGuiColors.DalamudYellow; + if (UiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data", _menuRenderWidth, true)) + { + _pair.ApplyLastReceivedData(forced: true); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); + } + + if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuRenderWidth, true)) + { + _ = _apiController.CyclePause(_pair.UserData); + ImGui.CloseCurrentPopup(); + } + ImGui.Separator(); + + ImGui.TextUnformatted("Pair Permission Functions"); + var isSticky = _pair.UserPair!.OwnPermissions.IsSticky(); + string stickyText = isSticky ? "Disable Preferred Permissions" : "Enable Preferred Permissions"; + var stickyIcon = isSticky ? FontAwesomeIcon.ArrowCircleDown : FontAwesomeIcon.ArrowCircleUp; + if (UiSharedService.IconTextButton(stickyIcon, stickyText, _menuRenderWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetSticky(!isSticky); + _ = _apiController.UserSetPairPermissions(new(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Preferred permissions means that this pair will not" + Environment.NewLine + " be affected by any syncshell permission changes through you."); + + string individualText = Environment.NewLine + Environment.NewLine + "Note: changing this permission will turn the permissions for this" + + Environment.NewLine + "user to preferred permissions. You can change this behavior" + + Environment.NewLine + "in the permission settings."; + bool individual = !_pair.IsDirectlyPaired && _apiController.DefaultPermissions!.IndividualIsSticky; + + var isDisableSounds = _pair.UserPair!.OwnPermissions.IsDisableSounds(); + string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync"; + var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute; + if (UiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText, _menuRenderWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetDisableSounds(!isDisableSounds); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Changes sound sync permissions with this user." + (individual ? individualText : string.Empty)); + + var isDisableAnims = _pair.UserPair!.OwnPermissions.IsDisableAnimations(); + string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync"; + var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop; + if (UiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText, _menuRenderWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetDisableAnimations(!isDisableAnims); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Changes animation sync permissions with this user." + (individual ? individualText : string.Empty)); + + var isDisableVFX = _pair.UserPair!.OwnPermissions.IsDisableVFX(); + string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync"; + var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; + if (UiSharedService.IconTextButton(disableVFXIcon, disableVFXText, _menuRenderWidth, true)) + { + var permissions = _pair.UserPair.OwnPermissions; + permissions.SetDisableVFX(!isDisableVFX); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); + } + UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); + + if (!_pair.IsPaused) + { + ImGui.Separator(); + ImGui.TextUnformatted("Pair reporting"); + if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile", _menuRenderWidth, true)) + { + ImGui.CloseCurrentPopup(); + _mediator.Publish(new OpenReportPopupMessage(_pair)); + } + UiSharedService.AttachToolTip("Report this users Mare Profile to the administrative team."); + } + } + + private void DrawIndividualMenu() + { + ImGui.TextUnformatted("Individual Pair Functions"); + var entryUID = _pair.UserData.AliasOrUID; + + if (_pair.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups", _menuRenderWidth, true)) + { + _selectTagForPairUi.Open(_pair); + } + UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently", _menuRenderWidth, true) && UiSharedService.CtrlPressed()) + { + _ = _apiController.UserRemovePair(new(_pair.UserData)); + } + UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); } else { - connectionIcon = FontAwesomeIcon.Check; - connectionText = "You are paired with " + _pair.UserData.AliasOrUID; - connectionColor = ImGuiColors.ParsedGreen; - } - - ImGui.SetCursorPosY(textPosY); - ImGui.PushFont(UiBuilder.IconFont); - UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor); - ImGui.PopFont(); - UiSharedService.AttachToolTip(connectionText); - if (_pair is { IsOnline: true, IsVisible: true }) - { - ImGui.SameLine(); - ImGui.SetCursorPosY(textPosY); - ImGui.PushFont(UiBuilder.IconFont); - UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); - ImGui.PopFont(); - UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName!); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Pair individually", _menuRenderWidth, true)) + { + _ = _apiController.UserAddPair(new(_pair.UserData)); + } + UiSharedService.AttachToolTip("Pair individually with " + entryUID); } } - protected override float DrawRightSide(float textPosY, float originalY) + private void DrawLeftSide(float textPosY) + { + string userPairText = string.Empty; + + ImGui.SetCursorPosY(textPosY); + + if (_pair.IsPaused) + { + ImGui.SetCursorPosY(textPosY); + using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.PauseCircle.ToIconString()); + userPairText = _pair.UserData.AliasOrUID + " is paused"; + } + else if (!_pair.IsOnline) + { + ImGui.SetCursorPosY(textPosY); + using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional + ? FontAwesomeIcon.User.ToIconString() : FontAwesomeIcon.Users.ToIconString()); + userPairText = _pair.UserData.AliasOrUID + " is offline"; + } + else + { + using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional + ? FontAwesomeIcon.User.ToIconString() : FontAwesomeIcon.Users.ToIconString()); + userPairText = _pair.UserData.AliasOrUID + " is online"; + } + + if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided) + { + userPairText += UiSharedService.TooltipSeparator + "User has not added you back"; + } + else if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional) + { + userPairText += UiSharedService.TooltipSeparator + "You are directly Paired"; + } + + if (_syncedGroups.Any()) + { + userPairText += UiSharedService.TooltipSeparator + string.Join(Environment.NewLine, + _syncedGroups.Select(g => + { + var groupNote = _serverConfigurationManager.GetNoteForGid(g.GID); + var groupString = string.IsNullOrEmpty(groupNote) ? g.GroupAliasOrGID : $"{groupNote} ({g.GroupAliasOrGID})"; + return "Paired through " + groupString; + })); + } + UiSharedService.AttachToolTip(userPairText); + + if (_pair.UserPair.OwnPermissions.IsSticky()) + { + ImGui.SetCursorPosY(textPosY); + + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X * 3 / 4f })) + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.SameLine(); + ImGui.TextUnformatted(FontAwesomeIcon.ArrowCircleUp.ToIconString()); + } + + UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has preferred permissions enabled"); + } + + if (_pair.IsVisible) + { + ImGui.SetCursorPosY(textPosY); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X * 3 / 4f })) + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.SameLine(); + using var _ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedGreen); + ImGui.TextUnformatted(FontAwesomeIcon.Eye.ToIconString()); + } + + UiSharedService.AttachToolTip("User is visible: " + _pair.PlayerName); + } + } + + private void DrawName(float originalY, float leftSide, float rightSide) + { + _displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide); + } + + private void DrawPairedClientMenu() + { + DrawIndividualMenu(); + + if (_syncedGroups.Any()) ImGui.Separator(); + foreach (var entry in _syncedGroups) + { + bool selfIsOwner = string.Equals(_apiController.UID, entry.Owner.UID, StringComparison.Ordinal); + bool selfIsModerator = entry.GroupUserInfo.IsModerator(); + bool userIsModerator = entry.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var modinfo) && modinfo.IsModerator(); + bool userIsPinned = entry.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var info) && info.IsPinned(); + if (selfIsOwner || selfIsModerator) + { + var groupNote = _serverConfigurationManager.GetNoteForGid(entry.GID); + var groupString = string.IsNullOrEmpty(groupNote) ? entry.GroupAliasOrGID : $"{groupNote} ({entry.GroupAliasOrGID})"; + + if (ImGui.BeginMenu(groupString + " Moderation Functions")) + { + DrawSyncshellMenu(entry, selfIsOwner, selfIsModerator, userIsPinned, userIsModerator); + ImGui.EndMenu(); + } + } + } + } + + private float DrawRightSide(float originalY) { var pauseIcon = _pair.UserPair!.OwnPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; var pauseIconSize = UiSharedService.GetIconButtonSize(pauseIcon); @@ -76,8 +301,9 @@ public class DrawUserPair : DrawPairBase var spacingX = ImGui.GetStyle().ItemSpacing.X; var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); var rightSideStart = 0f; + float infoIconDist = 0f; - if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()) + if (_pair.IsPaired) { var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); @@ -86,75 +312,83 @@ public class DrawUserPair : DrawPairBase if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) { var infoIconPosDist = windowEndX - barButtonSize.X - spacingX - pauseIconSize.X - spacingX; - var icon = FontAwesomeIcon.ExclamationTriangle; + var icon = FontAwesomeIcon.InfoCircle; var iconwidth = UiSharedService.GetIconSize(icon); - rightSideStart = infoIconPosDist - iconwidth.X; + infoIconDist = iconwidth.X; 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"); + ImGui.TextUnformatted("Individual User permissions"); + ImGui.Separator(); if (individualSoundsDisabled) { - var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; + var userSoundsText = "Sound sync"; UiSharedService.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userSoundsText); + ImGui.TextUnformatted(userSoundsText); ImGui.NewLine(); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); + ImGui.TextUnformatted("You"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableSounds()); + ImGui.SameLine(); + ImGui.TextUnformatted("They"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableSounds()); } if (individualAnimDisabled) { - var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; + var userAnimText = "Animation sync"; UiSharedService.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userAnimText); + ImGui.TextUnformatted(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.TextUnformatted("You"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableAnimations()); + ImGui.SameLine(); + ImGui.TextUnformatted("They"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableAnimations()); } if (individualVFXDisabled) { - var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; + var userVFXText = "VFX sync"; UiSharedService.FontText(FontAwesomeIcon.Circle.ToIconString(), UiBuilder.IconFont); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userVFXText); + ImGui.TextUnformatted(userVFXText); ImGui.NewLine(); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled")); + ImGui.TextUnformatted("You"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OwnPermissions.IsDisableVFX()); + ImGui.SameLine(); + ImGui.TextUnformatted("They"); + UiSharedService.BooleanToColoredIcon(!_pair.UserPair!.OtherPermissions.IsDisableVFX()); } ImGui.EndTooltip(); } } - - if (rightSideStart == 0f) - { - rightSideStart = windowEndX - barButtonSize.X - spacingX * 2 - pauseIconSize.X; - } - ImGui.SameLine(windowEndX - barButtonSize.X - spacingX - pauseIconSize.X); - ImGui.SetCursorPosY(originalY); - if (ImGuiComponents.IconButton(pauseIcon)) - { - var perm = _pair.UserPair!.OwnPermissions; - perm.SetPaused(!perm.IsPaused()); - _ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm)); - } - UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused() - ? "Pause pairing with " + entryUID - : "Resume pairing with " + entryUID); } + rightSideStart = windowEndX - barButtonSize.X - spacingX * 3 - pauseIconSize.X - infoIconDist; + ImGui.SameLine(windowEndX - barButtonSize.X - spacingX - pauseIconSize.X); + ImGui.SetCursorPosY(originalY); + if (ImGuiComponents.IconButton(pauseIcon)) + { + var perm = _pair.UserPair!.OwnPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm)); + } + UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused() + ? "Pause pairing with " + entryUID + : "Resume pairing with " + entryUID); + // Flyout Menu if (rightSideStart == 0f) { @@ -169,91 +403,90 @@ public class DrawUserPair : DrawPairBase } if (ImGui.BeginPopup("User Flyout Menu")) { - UiSharedService.DrawWithID($"buttons-{_pair.UserData.UID}", () => DrawPairedClientMenu(_pair)); + UiSharedService.DrawWithID($"buttons-{_pair.UserData.UID}", () => + { + ImGui.TextUnformatted("Common Pair Functions"); + DrawCommonClientMenu(); + ImGui.Separator(); + DrawPairedClientMenu(); + if (_menuRenderWidth <= 0) + { + _menuRenderWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + } + }); + ImGui.EndPopup(); } return rightSideStart; } - private void DrawPairedClientMenu(Pair entry) + private void DrawSyncshellMenu(GroupFullInfoDto group, bool selfIsOwner, bool selfIsModerator, bool userIsPinned, bool userIsModerator) { - if (!entry.IsPaused) + if (selfIsOwner || ((selfIsModerator) && (!userIsModerator))) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) - { - _displayHandler.OpenProfile(entry); - ImGui.CloseCurrentPopup(); - } - UiSharedService.AttachToolTip("Opens the profile for this user in a new window"); - } - if (entry.IsVisible) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) - { - entry.ApplyLastReceivedData(forced: true); - ImGui.CloseCurrentPopup(); - } - UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); - } - - if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state")) - { - _ = _apiController.CyclePause(entry.UserData); - ImGui.CloseCurrentPopup(); - } - var entryUID = entry.UserData.AliasOrUID; - if (UiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) - { - _selectGroupForPairUi.Open(entry); - } - UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); - - 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)) - { - var permissions = entry.UserPair.OwnPermissions; - permissions.SetDisableSounds(!isDisableSounds); - _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); - } - - 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)); - } - - var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX(); - string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync"; - var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; - if (UiSharedService.IconTextButton(disableVFXIcon, disableVFXText)) - { - var permissions = entry.UserPair.OwnPermissions; - permissions.SetDisableVFX(!isDisableVFX); - _ = _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); - - ImGui.Separator(); - if (!entry.IsPaused) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) + ImGui.TextUnformatted("Syncshell Moderator Functions"); + var pinText = userIsPinned ? "Unpin user" : "Pin user"; + if (UiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText, _menuRenderWidth, true)) { ImGui.CloseCurrentPopup(); - _showModalReport = true; + if (!group.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var userinfo)) + { + userinfo = API.Data.Enum.GroupPairUserInfo.IsPinned; + } + else + { + userinfo.SetPinned(!userinfo.IsPinned()); + } + _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(group.Group, _pair.UserData, userinfo)); } - UiSharedService.AttachToolTip("Report this users Mare Profile to the administrative team"); + 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", _menuRenderWidth, true) && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + _ = _apiController.GroupRemoveUser(new(group.Group, _pair.UserData)); + } + UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell"); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuRenderWidth, true)) + { + _mediator.Publish(new OpenBanUserPopupMessage(_pair, group)); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("Ban user from this Syncshell"); + + ImGui.Separator(); + } + + if (selfIsOwner) + { + ImGui.TextUnformatted("Syncshell Owner Functions"); + string modText = userIsModerator ? "Demod user" : "Mod user"; + if (UiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText, _menuRenderWidth, true) && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + if (!group.GroupPairUserInfos.TryGetValue(_pair.UserData.UID, out var userinfo)) + { + userinfo = API.Data.Enum.GroupPairUserInfo.IsModerator; + } + else + { + userinfo.SetModerator(!userinfo.IsModerator()); + } + + _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(group.Group, _pair.UserData, userinfo)); + } + UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_pair.UserData.AliasOrUID) + Environment.NewLine + + "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership", _menuRenderWidth, true) && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + ImGui.CloseCurrentPopup(); + _ = _apiController.GroupChangeOwnership(new(group.Group, _pair.UserData)); + } + UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + + (_pair.UserData.AliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); } } } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs deleted file mode 100644 index 30a9e3b..0000000 --- a/MareSynchronos/UI/Components/GroupPanel.cs +++ /dev/null @@ -1,765 +0,0 @@ -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.Data.Enum; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.API.Data.Comparer; -using MareSynchronos.PlayerData.Pairs; -using MareSynchronos.Services.ServerConfiguration; -using MareSynchronos.UI.Components; -using MareSynchronos.UI.Handlers; -using Dalamud.Interface.Utility; - -namespace MareSynchronos.UI; - -internal sealed class GroupPanel -{ - 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 UidDisplayHandler _uidDisplayHandler; - private readonly UiSharedService _uiShared; - private List _bannedUsers = new(); - 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 _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, UidDisplayHandler uidDisplayHandler, ServerConfigurationManager serverConfigurationManager) - { - _mainUi = mainUi; - _uiShared = uiShared; - _pairManager = pairManager; - _uidDisplayHandler = uidDisplayHandler; - _serverConfigurationManager = serverConfigurationManager; - } - - 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; - bool alreadyInGroup = _pairManager.GroupPairs.Select(p => p.Key).Any(p => string.Equals(p.Group.Alias, _syncShellToJoin, StringComparison.Ordinal) - || string.Equals(p.Group.GID, _syncShellToJoin, StringComparison.Ordinal)); - - if (alreadyInGroup) ImGui.BeginDisabled(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) - { - if (!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 (alreadyInGroup) ImGui.EndDisabled(); - - 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#" + bannedUser.UID)) - { - _ = 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) - .Select(c => new DrawGroupPair(groupDto.GID + c.UserData.UID, c, ApiController, groupDto, c.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - _uidDisplayHandler)) - .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) - .Select(c => new DrawGroupPair(groupDto.GID + c.UserData.UID, c, ApiController, groupDto, c.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - _uidDisplayHandler)) - .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) - .Select(c => new DrawGroupPair(groupDto.GID + c.UserData.UID, c, ApiController, groupDto, c.GroupPair.Single(g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group)).Value, - _uidDisplayHandler)) - .ToList(); - - if (visibleUsers.Any()) - { - ImGui.Text("Visible"); - ImGui.Separator(); - foreach (var entry in visibleUsers) - { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); - } - } - - if (onlineUsers.Any()) - { - ImGui.Text("Online"); - ImGui.Separator(); - foreach (var entry in onlineUsers) - { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); - } - } - - if (offlineUsers.Any()) - { - ImGui.Text("Offline/Unknown"); - ImGui.Separator(); - foreach (var entry in offlineUsers) - { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); - } - } - - 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 vfxDisabled = groupDto.GroupPermissions.IsDisableVFX(); - - var userSoundsDisabled = groupDto.GroupUserPermissions.IsDisableSounds(); - var userAnimDisabled = groupDto.GroupUserPermissions.IsDisableAnimations(); - var userVFXDisabled = groupDto.GroupUserPermissions.IsDisableVFX(); - - bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled; - - var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; - var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; - var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; - var vfxIcon = vfxDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; - var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; - var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; - var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; - - 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 || vfxDisabled) - { - 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 (vfxDisabled) - { - var vfxText = "VFX sync disabled through owner"; - UiSharedService.FontText(vfxIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(vfxText); - } - } - - if (userSoundsDisabled || userAnimDisabled || userVFXDisabled) - { - if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled) - 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 (userVFXDisabled) - { - var userVFXText = "VFX sync disabled through you"; - UiSharedService.FontText(userVFXIcon.ToIconString(), UiBuilder.IconFont); - ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); - ImGui.Text(userVFXText); - } - - if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled) - 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."); - - var vfxText = userVFXDisabled ? "Enable VFX sync" : "Disable VFX sync"; - if (UiSharedService.IconTextButton(userVFXIcon, vfxText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupUserPermissions; - perm.SetDisableVFX(!perm.IsDisableVFX()); - _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); - } - UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell." - + Environment.NewLine + "Disabling the synchronization will stop applying VFX modifications for users of this syncshell." - + Environment.NewLine + "Note: this setting might also affect animation synchronization to some degree" - + 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."); - - var groupVFXText = vfxDisabled ? "Enable syncshell VFX sync" : "Disable syncshell VFX sync"; - if (UiSharedService.IconTextButton(vfxIcon, groupVFXText)) - { - ImGui.CloseCurrentPopup(); - var perm = groupDto.GroupPermissions; - perm.SetDisableVFX(!perm.IsDisableVFX()); - _ = ApiController.GroupChangeGroupPermissionState(new(groupDto.Group, perm)); - } - UiSharedService.AttachToolTip("Sets syncshell-wide allowance for VFX 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(); - } -} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/IDrawFolder.cs b/MareSynchronos/UI/Components/IDrawFolder.cs new file mode 100644 index 0000000..ead3bb2 --- /dev/null +++ b/MareSynchronos/UI/Components/IDrawFolder.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.UI.Components; + +public interface IDrawFolder +{ + void Draw(); +} diff --git a/MareSynchronos/UI/Components/PairGroupsUi.cs b/MareSynchronos/UI/Components/PairGroupsUi.cs deleted file mode 100644 index 4527077..0000000 --- a/MareSynchronos/UI/Components/PairGroupsUi.cs +++ /dev/null @@ -1,249 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.MareConfiguration; -using MareSynchronos.UI.Handlers; -using MareSynchronos.WebAPI; - -namespace MareSynchronos.UI.Components; - -public class PairGroupsUi -{ - private readonly ApiController _apiController; - private readonly MareConfigService _mareConfig; - private readonly SelectPairForGroupUi _selectGroupForPairUi; - private readonly TagHandler _tagHandler; - - public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi) - { - _mareConfig = mareConfig; - _tagHandler = tagHandler; - _apiController = apiController; - _selectGroupForPairUi = selectGroupForPairUi; - } - - public void Draw(List visibleUsers, List onlineUsers, List offlineUsers) where T : DrawPairBase - { - // 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 (typeof(T) == typeof(DrawUserPair)) - { - DrawUserPairs(tagsWithPairsInThem, allUsers.Cast().ToList(), visibleUsers.Cast(), onlineUsers.Cast(), offlineUsers.Cast()); - } - } - - 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) - { - // If all are paused => resume all - ResumeAllPairs(availablePairsInThisTag); - } - else - { - // otherwise pause all remaining - PauseRemainingPairs(availablePairsInThisTag); - } - } - if (allArePaused) - { - UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}"); - } - else - { - UiSharedService.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")) - { - UiSharedService.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag)); - ImGui.EndPopup(); - } - } - - private void DrawCategory(string tag, IEnumerable onlineUsers, IEnumerable allUsers, IEnumerable? visibleUsers = null) - { - IEnumerable 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.UID)) - .ToList(); - visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UID)) ?? 0; - } - - if (isSpecialTag && !usersInThisTag.Any()) return; - - DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count(), otherUidsTaggedWithTag?.Count); - if (!isSpecialTag) - { - if (onlineUsers.Any() && onlineUsers.First() is DrawUserPair) - { - UiSharedService.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Cast().Where(p => otherUidsTaggedWithTag!.Contains(p.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, IEnumerable availablePairsInThisCategory) - { - // These are all the OtherUIDs that are tagged with this tag - foreach (var pair in availablePairsInThisCategory) - { - UiSharedService.DrawWithID($"tag-{tag}-pair-${pair.UID}", () => pair.DrawPairedClient()); - } - ImGui.Separator(); - } - - private void DrawUserPairs(List tagsWithPairsInThem, List allUsers, IEnumerable visibleUsers, IEnumerable onlineUsers, IEnumerable offlineUsers) - { - if (_mareConfig.Current.ShowVisibleUsersSeparately) - { - UiSharedService.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers)); - } - foreach (var tag in tagsWithPairsInThem) - { - if (_mareConfig.Current.ShowOfflineUsersSeparately) - { - UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers)); - } - else - { - UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers)); - } - } - if (_mareConfig.Current.ShowOfflineUsersSeparately) - { - UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, - onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UID)).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.UID)).ToList(), allUsers)); - } - UiSharedService.DrawWithID($"group-UnpairedCustomTag", () => DrawCategory(TagHandler.CustomUnpairedTag, - offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers)); - } - - 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(new(pairToPause.UID), perm)); - } - } - - private void ResumeAllPairs(List availablePairs) - { - foreach (var pairToPause in availablePairs) - { - var perm = pairToPause.UserPair!.OwnPermissions; - perm.SetPaused(paused: false); - _ = _apiController.UserSetPairPermissions(new(new(pairToPause.UID), 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/Popup/BanUserPopupHandler.cs b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs new file mode 100644 index 0000000..97a633d --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs @@ -0,0 +1,46 @@ +using Dalamud.Interface; +using ImGuiNET; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public class BanUserPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private string _banReason = string.Empty; + private GroupFullInfoDto _group = null!; + private Pair _reportedPair = null!; + + public BanUserPopupHandler(ApiController apiController) + { + _apiController = apiController; + } + + public Vector2 PopupSize => new(500, 250); + + public void DrawContent() + { + UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell."); + ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + { + ImGui.CloseCurrentPopup(); + var reason = _banReason; + _ = _apiController.GroupBanUser(new GroupPairDto(_group.Group, _reportedPair.UserData), 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."); + } + + public void Open(OpenBanUserPopupMessage message) + { + _reportedPair = message.PairToBan; + _group = message.GroupFullInfoDto; + _banReason = string.Empty; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/CreateSyncshellPopupHandler.cs b/MareSynchronos/UI/Components/Popup/CreateSyncshellPopupHandler.cs new file mode 100644 index 0000000..4b9f0b9 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/CreateSyncshellPopupHandler.cs @@ -0,0 +1,100 @@ +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public class CreateSyncshellPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private bool _errorGroupCreate; + private GroupJoinDto? _lastCreatedGroup; + + public CreateSyncshellPopupHandler(ApiController apiController, UiSharedService uiSharedService) + { + _apiController = apiController; + _uiSharedService = uiSharedService; + } + + public Vector2 PopupSize => new(500, 300); + + public void DrawContent() + { + using (ImRaii.PushFont(_uiSharedService.UidFont)) + ImGui.TextUnformatted("Create new Syncshell"); + + if (_lastCreatedGroup == null) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create Syncshell")) + { + try + { + _lastCreatedGroup = _apiController.GroupCreate().Result; + } + catch + { + _lastCreatedGroup = null; + _errorGroupCreate = true; + } + } + ImGui.SameLine(); + } + + ImGui.Separator(); + + if (_lastCreatedGroup == null) + { + UiSharedService.TextWrapped("Creating a new Syncshell with create it defaulting to your current preferred permissions for Syncshells." + Environment.NewLine + + "- You can own up to " + _apiController.ServerInfo.MaxGroupsCreatedByUser + " Syncshells on this server." + Environment.NewLine + + "- You can join up to " + _apiController.ServerInfo.MaxGroupsJoinedByUser + " Syncshells on this server (including your own)" + Environment.NewLine + + "- Syncshells on this server can have a maximum of " + _apiController.ServerInfo.MaxGroupUserCount + " users"); + ImGui.Dummy(new(2f)); + ImGui.TextUnformatted("Your current Syncshell preferred permissions are:"); + ImGui.TextUnformatted("- Animations"); + UiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupAnimations); + ImGui.TextUnformatted("- Sounds"); + UiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupSounds); + ImGui.TextUnformatted("- VFX"); + UiSharedService.BooleanToColoredIcon(!_apiController.DefaultPermissions!.DisableGroupVFX); + UiSharedService.TextWrapped("(Those preferred permissions can be changed anytime after Syncshell creation, your defaults can be changed anytime in the Mare Settings)"); + } + else + { + _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."); + ImGui.Separator(); + UiSharedService.TextWrapped("These settings were set based on your preferred syncshell permissions:"); + ImGui.Dummy(new(2f)); + UiSharedService.TextWrapped("Suggest Animation sync:"); + UiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableAnimations()); + UiSharedService.TextWrapped("Suggest Sounds sync:"); + UiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableSounds()); + UiSharedService.TextWrapped("Suggest VFX sync:"); + UiSharedService.BooleanToColoredIcon(!_lastCreatedGroup.GroupUserPreferredPermissions.IsDisableVFX()); + } + + if (_errorGroupCreate) + { + UiSharedService.ColorTextWrapped("Something went wrong during creation of a new Syncshell", new Vector4(1, 0, 0, 1)); + } + } + + public void Open() + { + _lastCreatedGroup = null; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs new file mode 100644 index 0000000..c29d64e --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public interface IPopupHandler +{ + Vector2 PopupSize { get; } + + void DrawContent(); +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/JoinSyncshellPopupHandler.cs b/MareSynchronos/UI/Components/Popup/JoinSyncshellPopupHandler.cs new file mode 100644 index 0000000..8c73de4 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/JoinSyncshellPopupHandler.cs @@ -0,0 +1,163 @@ +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +internal class JoinSyncshellPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private string _desiredSyncshellToJoin = string.Empty; + private GroupJoinInfoDto? _groupJoinInfo = null; + private DefaultPermissionsDto _ownPermissions = null!; + private string _previousPassword = string.Empty; + private string _syncshellPassword = string.Empty; + + public JoinSyncshellPopupHandler(UiSharedService uiSharedService, ApiController apiController) + { + _uiSharedService = uiSharedService; + _apiController = apiController; + } + + public Vector2 PopupSize => new(700, 400); + + public void DrawContent() + { + using (ImRaii.PushFont(_uiSharedService.UidFont)) + ImGui.TextUnformatted((_groupJoinInfo == null || !_groupJoinInfo.Success) ? "Join Syncshell" : ("Finalize join Syncshell " + _groupJoinInfo.GroupAliasOrGID)); + ImGui.Separator(); + + if (_groupJoinInfo == null || !_groupJoinInfo.Success) + { + UiSharedService.TextWrapped("Here you can join existing Syncshells. " + + "Please keep in mind that you cannot join more than " + _apiController.ServerInfo.MaxGroupsJoinedByUser + " syncshells on this server." + Environment.NewLine + + "Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine + + "All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions."); + ImGui.Separator(); + ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs."); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Syncshell ID"); + ImGui.SameLine(200); + ImGui.InputTextWithHint("##syncshellId", "Full Syncshell ID", ref _desiredSyncshellToJoin, 20); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Syncshell Password"); + ImGui.SameLine(200); + ImGui.InputTextWithHint("##syncshellpw", "Password", ref _syncshellPassword, 20, ImGuiInputTextFlags.Password); + using (ImRaii.Disabled(string.IsNullOrEmpty(_desiredSyncshellToJoin) || string.IsNullOrEmpty(_syncshellPassword))) + { + if (UiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Join Syncshell")) + { + _groupJoinInfo = _apiController.GroupJoin(new GroupPasswordDto(new API.Data.GroupData(_desiredSyncshellToJoin), _syncshellPassword)).Result; + _previousPassword = _syncshellPassword; + _syncshellPassword = string.Empty; + } + } + if (_groupJoinInfo != null && !_groupJoinInfo.Success) + { + UiSharedService.ColorTextWrapped("Failed to join the Syncshell. This is due to one of following reasons:" + Environment.NewLine + + "- The Syncshell does not exist or the password is incorrect" + Environment.NewLine + + "- You are already in that Syncshell or are banned from that Syncshell" + Environment.NewLine + + "- The Syncshell is at capacity or has invites disabled" + Environment.NewLine, ImGuiColors.DalamudYellow); + } + } + else + { + ImGui.TextUnformatted("You are about to join the Syncshell " + _groupJoinInfo.GroupAliasOrGID + " by " + _groupJoinInfo.OwnerAliasOrUID); + ImGui.Dummy(new(2)); + ImGui.TextUnformatted("This Syncshell staff has set the following suggested Syncshell permissions:"); + ImGui.TextUnformatted("- Sounds "); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableSounds()); + ImGui.TextUnformatted("- Animations"); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations()); + ImGui.TextUnformatted("- VFX"); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableVFX()); + + if (_groupJoinInfo.GroupPermissions.IsPreferDisableSounds() != _ownPermissions.DisableGroupSounds + || _groupJoinInfo.GroupPermissions.IsPreferDisableVFX() != _ownPermissions.DisableGroupVFX + || _groupJoinInfo.GroupPermissions.IsPreferDisableAnimations() != _ownPermissions.DisableGroupAnimations) + { + ImGui.Dummy(new(2)); + UiSharedService.ColorText("Your current preferred default Syncshell permissions deviate from the suggested permissions:", ImGuiColors.DalamudYellow); + if (_groupJoinInfo.GroupPermissions.IsPreferDisableSounds() != _ownPermissions.DisableGroupSounds) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("- Sounds"); + UiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupSounds); + ImGui.SameLine(200); + ImGui.TextUnformatted("Suggested"); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableSounds()); + ImGui.SameLine(); + using var id = ImRaii.PushId("suggestedSounds"); + if (UiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested")) + { + _ownPermissions.DisableGroupSounds = _groupJoinInfo.GroupPermissions.IsPreferDisableSounds(); + } + } + if (_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations() != _ownPermissions.DisableGroupAnimations) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("- Animations"); + UiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupAnimations); + ImGui.SameLine(200); + ImGui.TextUnformatted("Suggested"); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableAnimations()); + ImGui.SameLine(); + using var id = ImRaii.PushId("suggestedAnims"); + if (UiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested")) + { + _ownPermissions.DisableGroupAnimations = _groupJoinInfo.GroupPermissions.IsPreferDisableAnimations(); + } + } + if (_groupJoinInfo.GroupPermissions.IsPreferDisableVFX() != _ownPermissions.DisableGroupVFX) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("- VFX"); + UiSharedService.BooleanToColoredIcon(!_ownPermissions.DisableGroupVFX); + ImGui.SameLine(200); + ImGui.TextUnformatted("Suggested"); + UiSharedService.BooleanToColoredIcon(!_groupJoinInfo.GroupPermissions.IsPreferDisableVFX()); + ImGui.SameLine(); + using var id = ImRaii.PushId("suggestedVfx"); + if (UiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowRight, "Apply suggested")) + { + _ownPermissions.DisableGroupVFX = _groupJoinInfo.GroupPermissions.IsPreferDisableVFX(); + } + } + UiSharedService.TextWrapped("Note: you do not need to apply the suggested Syncshell permissions, they are solely suggestions by the staff of the Syncshell."); + } + else + { + UiSharedService.TextWrapped("Your default syncshell permissions on joining are in line with the suggested Syncshell permissions through the owner."); + } + ImGui.Dummy(new(2)); + if (UiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, "Finalize and join " + _groupJoinInfo.GroupAliasOrGID)) + { + GroupUserPreferredPermissions joinPermissions = GroupUserPreferredPermissions.NoneSet; + joinPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds); + joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); + joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); + _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions)); + ImGui.CloseCurrentPopup(); + } + } + } + + public void Open() + { + _desiredSyncshellToJoin = string.Empty; + _syncshellPassword = string.Empty; + _previousPassword = string.Empty; + _groupJoinInfo = null; + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/PopupHandler.cs b/MareSynchronos/UI/Components/Popup/PopupHandler.cs new file mode 100644 index 0000000..b278014 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/PopupHandler.cs @@ -0,0 +1,95 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public class PopupHandler : WindowMediatorSubscriberBase +{ + protected bool _openPopup = false; + private readonly HashSet _handlers; + private IPopupHandler? _currentHandler = null; + + public PopupHandler(ILogger logger, MareMediator mediator, IEnumerable popupHandlers) + : base(logger, mediator, "MarePopupHandler") + { + Flags = ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoNav + | ImGuiWindowFlags.NoTitleBar; + IsOpen = true; + + _handlers = popupHandlers.ToHashSet(); + + Mediator.Subscribe(this, (msg) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((ReportPopupHandler)_currentHandler).Open(msg); + IsOpen = true; + }); + + Mediator.Subscribe(this, (msg) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((CreateSyncshellPopupHandler)_currentHandler).Open(); + IsOpen = true; + }); + + Mediator.Subscribe(this, (msg) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((BanUserPopupHandler)_currentHandler).Open(msg); + IsOpen = true; + }); + + Mediator.Subscribe(this, (_) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((JoinSyncshellPopupHandler)_currentHandler).Open(); + IsOpen = true; + }); + + Mediator.Subscribe(this, (msg) => + { + IsOpen = true; + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((SyncshellAdminPopupHandler)_currentHandler).Open(msg.GroupInfo); + IsOpen = true; + }); + } + + public override void Draw() + { + if (_currentHandler == null) return; + + if (_openPopup) + { + ImGui.OpenPopup(WindowName); + _openPopup = false; + } + + var viewportSize = ImGui.GetWindowViewport().Size; + ImGui.SetNextWindowSize(_currentHandler!.PopupSize); + ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); + using var popup = ImRaii.Popup(WindowName, ImGuiWindowFlags.Modal); + if (!popup) return; + _currentHandler.DrawContent(); + ImGui.Separator(); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close")) + { + ImGui.CloseCurrentPopup(); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs new file mode 100644 index 0000000..c72146f --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs @@ -0,0 +1,57 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +internal class ReportPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private Pair? _reportedPair; + private string _reportReason = string.Empty; + + public ReportPopupHandler(ApiController apiController, UiSharedService uiSharedService) + { + _apiController = apiController; + _uiSharedService = uiSharedService; + } + + public Vector2 PopupSize => new(500, 500); + + public void DrawContent() + { + using (ImRaii.PushFont(_uiSharedService.UidFont)) + UiSharedService.TextWrapped("Report " + _reportedPair!.UserData.AliasOrUID + " Mare Profile"); + + ImGui.InputTextMultiline("##reportReason", ref _reportReason, 500, new Vector2(500 - ImGui.GetStyle().ItemSpacing.X * 2, 200)); + UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" + + $"The report will be sent to the team of your currently connected Mare Synchronos Service.{Environment.NewLine}" + + $"The report will include your user and your contact info (Discord User).{Environment.NewLine}" + + $"Depending on the severity of the offense the users Mare profile or account can be permanently disabled or banned."); + UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("This is not for reporting misbehavior or Mare usage but solely for the actual profile. " + + "Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow); + + using (ImRaii.Disabled(string.IsNullOrEmpty(_reportReason))) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Send Report")) + { + ImGui.CloseCurrentPopup(); + var reason = _reportReason; + _ = _apiController.UserReportProfile(new(_reportedPair.UserData, reason)); + } + } + } + + public void Open(OpenReportPopupMessage msg) + { + _reportedPair = msg.PairToReport; + _reportReason = string.Empty; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs b/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs new file mode 100644 index 0000000..7fbdbb6 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs @@ -0,0 +1,234 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.WebAPI; +using System.Globalization; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +internal class SyncshellAdminPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly List _oneTimeInvites = []; + private readonly PairManager _pairManager; + private readonly UiSharedService _uiSharedService; + private List _bannedUsers = []; + private GroupFullInfoDto _groupFullInfo = null!; + private bool _isModerator = false; + private bool _isOwner = false; + private int _multiInvites = 30; + private string _newPassword = string.Empty; + private bool _pwChangeSuccess = true; + + public SyncshellAdminPopupHandler(ApiController apiController, UiSharedService uiSharedService, PairManager pairManager) + { + _apiController = apiController; + _uiSharedService = uiSharedService; + _pairManager = pairManager; + } + + public Vector2 PopupSize => new(700, 500); + + public void DrawContent() + { + if (!_isModerator && !_isOwner) return; + + _groupFullInfo = _pairManager.Groups[_groupFullInfo.Group]; + + using (ImRaii.PushFont(_uiSharedService.UidFont)) + ImGui.TextUnformatted(_groupFullInfo.GroupAliasOrGID + " Administrative Panel"); + + ImGui.Separator(); + var perm = _groupFullInfo.GroupPermissions; + + var inviteNode = ImRaii.TreeNode("Invites"); + if (inviteNode) + { + bool isInvitesDisabled = perm.IsDisableInvites(); + + if (UiSharedService.IconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock, + isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell")) + { + perm.SetDisableInvites(!isInvitesDisabled); + _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); + } + + ImGui.Dummy(new(2f)); + + UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password."); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) + { + ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(_groupFullInfo.Group), 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."); + ImGui.InputInt("##amountofinvites", ref _multiInvites); + ImGui.SameLine(); + using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100)) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites")) + { + _oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(_groupFullInfo.Group), _multiInvites).Result); + } + } + + if (_oneTimeInvites.Any()) + { + var invites = string.Join(Environment.NewLine, _oneTimeInvites); + ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly); + if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard")) + { + ImGui.SetClipboardText(invites); + } + } + } + inviteNode.Dispose(); + + var mgmtNode = ImRaii.TreeNode("User Management"); + if (mgmtNode) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) + { + _ = _apiController.GroupClear(new(_groupFullInfo.Group)); + } + UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell"); + + ImGui.Dummy(new(2f)); + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(_groupFullInfo.Group)).Result; + } + + if (ImGui.BeginTable("bannedusertable" + _groupFullInfo.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#" + bannedUser.UID)) + { + _ = _apiController.GroupUnbanUser(bannedUser); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + } + + ImGui.EndTable(); + } + } + mgmtNode.Dispose(); + + var permNode = ImRaii.TreeNode("Permissions"); + if (permNode) + { + bool isDisableAnimations = perm.IsPreferDisableAnimations(); + bool isDisableSounds = perm.IsPreferDisableSounds(); + bool isDisableVfx = perm.IsPreferDisableVFX(); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Suggest Sound Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableSounds); + ImGui.SameLine(230); + if (UiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, + isDisableSounds ? "Suggest to enable sound sync" : "Suggest to disable sound sync")) + { + perm.SetPreferDisableSounds(!perm.IsPreferDisableSounds()); + _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Suggest Animation Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableAnimations); + ImGui.SameLine(230); + if (UiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, + isDisableAnimations ? "Suggest to enable animation sync" : "Suggest to disable animation sync")) + { + perm.SetPreferDisableAnimations(!perm.IsPreferDisableAnimations()); + _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Suggest VFX Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableVfx); + ImGui.SameLine(230); + if (UiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, + isDisableVfx ? "Suggest to enable vfx sync" : "Suggest to disable vfx sync")) + { + perm.SetPreferDisableVFX(!perm.IsPreferDisableVFX()); + _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); + } + + UiSharedService.TextWrapped("Note: those suggested permissions will be shown to users on joining the Syncshell."); + } + permNode.Dispose(); + + if (_isOwner) + { + var ownerNode = ImRaii.TreeNode("Owner Settings"); + if (ownerNode) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("New Password"); + ImGui.SameLine(); + ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50); + ImGui.SameLine(); + using (ImRaii.Disabled(_newPassword.Length < 10)) + { + if (UiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) + { + _pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(_groupFullInfo.Group, _newPassword)).Result; + _newPassword = string.Empty; + } + } + UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible."); + + if (!_pwChangeSuccess) + { + UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow); + } + + if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + ImGui.CloseCurrentPopup(); + _ = _apiController.GroupDelete(new(_groupFullInfo.Group)); + } + UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); + } + ownerNode.Dispose(); + } + } + + public void Open(GroupFullInfoDto groupFullInfo) + { + _groupFullInfo = groupFullInfo; + _isOwner = string.Equals(_groupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); + _isModerator = _groupFullInfo.GroupUserInfo.IsModerator(); + _newPassword = string.Empty; + _bannedUsers.Clear(); + _oneTimeInvites.Clear(); + _multiInvites = 30; + _pwChangeSuccess = true; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectPairForGroupUi.cs b/MareSynchronos/UI/Components/SelectPairForTagUi.cs similarity index 92% rename from MareSynchronos/UI/Components/SelectPairForGroupUi.cs rename to MareSynchronos/UI/Components/SelectPairForTagUi.cs index 8d906e2..97752b9 100644 --- a/MareSynchronos/UI/Components/SelectPairForGroupUi.cs +++ b/MareSynchronos/UI/Components/SelectPairForTagUi.cs @@ -1,23 +1,24 @@ -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.UI.Handlers; +using System.Numerics; + namespace MareSynchronos.UI.Components; -public class SelectPairForGroupUi +public class SelectPairForTagUi { private readonly TagHandler _tagHandler; - private readonly UidDisplayHandler _uidDisplayHandler; + private readonly IdDisplayHandler _uidDisplayHandler; 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, UidDisplayHandler uidDisplayHandler) + public SelectPairForTagUi(TagHandler tagHandler, IdDisplayHandler uidDisplayHandler) { _tagHandler = tagHandler; _uidDisplayHandler = uidDisplayHandler; diff --git a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs b/MareSynchronos/UI/Components/SelectTagForPairUi.cs similarity index 94% rename from MareSynchronos/UI/Components/SelectGroupForPairUi.cs rename to MareSynchronos/UI/Components/SelectTagForPairUi.cs index 4be2e70..8157e7b 100644 --- a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs +++ b/MareSynchronos/UI/Components/SelectTagForPairUi.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using Dalamud.Interface; +using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -7,12 +6,14 @@ using ImGuiNET; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.UI.Handlers; +using System.Numerics; + namespace MareSynchronos.UI.Components; -public class SelectGroupForPairUi +public class SelectTagForPairUi { private readonly TagHandler _tagHandler; - private readonly UidDisplayHandler _uidDisplayHandler; + private readonly IdDisplayHandler _uidDisplayHandler; /// /// The group UI is always open for a specific pair. This defines which pair the UI is open for. @@ -30,7 +31,7 @@ public class SelectGroupForPairUi /// private string _tagNameToAdd = ""; - public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler) + public SelectTagForPairUi(TagHandler tagHandler, IdDisplayHandler uidDisplayHandler) { _show = false; _pair = null; diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs index 8003399..10cf97a 100644 --- a/MareSynchronos/UI/DataAnalysisUi.cs +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -15,20 +15,20 @@ namespace MareSynchronos.UI; public class DataAnalysisUi : WindowMediatorSubscriberBase { private readonly CharacterAnalyzer _characterAnalyzer; - private readonly IpcManager _ipcManager; - private bool _hasUpdate = false; - private Dictionary>? _cachedAnalysis; - private string _selectedHash = string.Empty; - private ObjectKind _selectedObjectTab; - private string _selectedFileTypeTab = string.Empty; - private bool _enableBc7ConversionMode = false; - private readonly Dictionary _texturesToConvert = new(StringComparer.Ordinal); - private Task? _conversionTask; - private CancellationTokenSource _conversionCancellationTokenSource = new(); private readonly Progress<(string, int)> _conversionProgress = new(); + private readonly IpcManager _ipcManager; + private readonly Dictionary _texturesToConvert = new(StringComparer.Ordinal); + private Dictionary>? _cachedAnalysis; + private CancellationTokenSource _conversionCancellationTokenSource = new(); private string _conversionCurrentFileName = string.Empty; private int _conversionCurrentFileProgress = 0; + private Task? _conversionTask; + private bool _enableBc7ConversionMode = false; + private bool _hasUpdate = false; private bool _modalOpen = false; + private string _selectedFileTypeTab = string.Empty; + private string _selectedHash = string.Empty; + private ObjectKind _selectedObjectTab; private bool _showModal = false; public DataAnalysisUi(ILogger logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager) : base(logger, mediator, "Mare Character Data Analysis") @@ -57,26 +57,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; } - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; - } - - private void ConversionProgress_ProgressChanged(object? sender, (string, int) e) - { - _conversionCurrentFileName = e.Item1; - _conversionCurrentFileProgress = e.Item2; - } - - public override void OnOpen() - { - _hasUpdate = true; - _selectedHash = string.Empty; - _enableBc7ConversionMode = false; - _texturesToConvert.Clear(); - } - public override void Draw() { if (_conversionTask != null && !_conversionTask.IsCompleted) @@ -84,7 +64,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _showModal = true; if (ImGui.BeginPopupModal("BC7 Conversion in Progress")) { - ImGui.Text("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count); + ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count); UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName); if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) { @@ -141,14 +121,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGuiColors.DalamudYellow); if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) { - _ = _characterAnalyzer.ComputeAnalysis(false); + _ = _characterAnalyzer.ComputeAnalysis(print: false); } } else { if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)")) { - _ = _characterAnalyzer.ComputeAnalysis(false, true); + _ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true); } } } @@ -266,7 +246,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); - if (_selectedFileTypeTab == "tex") + if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal)) { ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode); if (_enableBc7ConversionMode) @@ -298,13 +278,13 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.Separator(); - ImGui.Text("Selected file:"); + ImGui.TextUnformatted("Selected file:"); ImGui.SameLine(); UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); - if (_cachedAnalysis[_selectedObjectTab].ContainsKey(_selectedHash)) + if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) { - var filePaths = _cachedAnalysis[_selectedObjectTab][_selectedHash].FilePaths; + var filePaths = item.FilePaths; ImGui.TextUnformatted("Local file path:"); ImGui.SameLine(); UiSharedService.TextWrapped(filePaths[0]); @@ -317,7 +297,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); } - var gamepaths = _cachedAnalysis[_selectedObjectTab][_selectedHash].GamePaths; + var gamepaths = item.GamePaths; ImGui.TextUnformatted("Used by game path:"); ImGui.SameLine(); UiSharedService.TextWrapped(gamepaths[0]); @@ -332,17 +312,38 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase } } + public override void OnOpen() + { + _hasUpdate = true; + _selectedHash = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; + } + + private void ConversionProgress_ProgressChanged(object? sender, (string, int) e) + { + _conversionCurrentFileName = e.Item1; + _conversionCurrentFileProgress = e.Item2; + } + private void DrawTable(IGrouping fileGroup) { - using var table = ImRaii.Table("Analysis", fileGroup.Key == "tex" ? (_enableBc7ConversionMode ? 7 : 6) : 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, -new Vector2(0, 300)); + using var table = ImRaii.Table("Analysis", string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) ? + (_enableBc7ConversionMode ? 7 : 6) : 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(0, 300)); if (!table.Success) return; ImGui.TableSetupColumn("Hash"); ImGui.TableSetupColumn("Filepaths"); ImGui.TableSetupColumn("Gamepaths"); ImGui.TableSetupColumn("Original Size"); ImGui.TableSetupColumn("Compressed Size"); - if (fileGroup.Key == "tex") + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) { ImGui.TableSetupColumn("Format"); if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7"); @@ -375,9 +376,9 @@ new Vector2(0, 300)); _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (fileGroup.Key == "tex" && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (fileGroup.Key == "tex" && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); sortSpecs.SpecsDirty = false; @@ -385,7 +386,7 @@ new Vector2(0, 300)); foreach (var item in fileGroup) { - using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash)); + using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal)); using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); ImGui.TableNextColumn(); if (!item.IsComputed) @@ -412,7 +413,7 @@ new Vector2(0, 300)); ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize)); if (ImGui.IsItemClicked()) _selectedHash = item.Hash; - if (fileGroup.Key == "tex") + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) { ImGui.TableNextColumn(); ImGui.TextUnformatted(item.Format.Value); @@ -420,9 +421,9 @@ new Vector2(0, 300)); if (_enableBc7ConversionMode) { ImGui.TableNextColumn(); - if (item.Format.Value == "BC7") + if (string.Equals(item.Format.Value, "BC7", StringComparison.Ordinal)) { - ImGui.Text(""); + ImGui.TextUnformatted(""); continue; } var filePath = item.FilePaths[0]; @@ -442,4 +443,4 @@ new Vector2(0, 300)); } } } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/DrawEntityFactory.cs b/MareSynchronos/UI/DrawEntityFactory.cs new file mode 100644 index 0000000..92dffe0 --- /dev/null +++ b/MareSynchronos/UI/DrawEntityFactory.cs @@ -0,0 +1,61 @@ +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI.Components; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +public class DrawEntityFactory +{ + private readonly ILogger _logger; + private readonly ApiController _apiController; + private readonly MareMediator _mediator; + private readonly SelectPairForTagUi _selectPairForTagUi; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly SelectTagForPairUi _selectTagForPairUi; + private readonly TagHandler _tagHandler; + private readonly IdDisplayHandler _uidDisplayHandler; + + public DrawEntityFactory(ILogger logger, ApiController apiController, IdDisplayHandler uidDisplayHandler, + SelectTagForPairUi selectTagForPairUi, MareMediator mediator, + TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi, + ServerConfigurationManager serverConfigurationManager) + { + _logger = logger; + _apiController = apiController; + _uidDisplayHandler = uidDisplayHandler; + _selectTagForPairUi = selectTagForPairUi; + _mediator = mediator; + _tagHandler = tagHandler; + _selectPairForTagUi = selectPairForTagUi; + _serverConfigurationManager = serverConfigurationManager; + } + + public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto, Dictionary> pairs) + { + _logger.LogTrace("Creating new DrawGroupFolder for {gid}", groupFullInfoDto.GID); + return new DrawFolderGroup(groupFullInfoDto.Group.GID, groupFullInfoDto, _apiController, + pairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value)).ToList(), + _tagHandler, _uidDisplayHandler, _mediator); + } + + public DrawFolderTag CreateDrawTagFolder(string tag, Dictionary> pairs) + { + _logger.LogTrace("Creating new DrawTagFolder for {tag}", tag); + + return new(tag, pairs.Select(u => CreateDrawPair(tag, u.Key, u.Value)).ToList(), + _tagHandler, _apiController, _selectPairForTagUi); + } + + public DrawUserPair CreateDrawPair(string id, Pair user, List groups) + { + _logger.LogTrace("Creating new DrawPair for {id}", id + user.UserData.UID); + + return new DrawUserPair(id + user.UserData.UID, user, groups, _apiController, _uidDisplayHandler, + _mediator, _selectTagForPairUi, _serverConfigurationManager); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs index 1ee93a1..4cb46bf 100644 --- a/MareSynchronos/UI/DtrEntry.cs +++ b/MareSynchronos/UI/DtrEntry.cs @@ -14,11 +14,11 @@ public sealed class DtrEntry : IDisposable, IHostedService { private readonly ApiController _apiController; private readonly CancellationTokenSource _cancellationTokenSource = new(); - private readonly ILogger _logger; - private readonly IDtrBar _dtrBar; private readonly ConfigurationServiceBase _configService; - private readonly MareMediator _mareMediator; + private readonly IDtrBar _dtrBar; private readonly Lazy _entry; + private readonly ILogger _logger; + private readonly MareMediator _mareMediator; private readonly PairManager _pairManager; private Task? _runTask; private string? _text; @@ -67,15 +67,6 @@ public sealed class DtrEntry : IDisposable, IHostedService } } - private DtrBarEntry CreateEntry() - { - _logger.LogTrace("Creating new DtrBar entry"); - var entry = _dtrBar.Get("Mare Synchronos"); - entry.OnClick = () => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi))); - - return entry; - } - private void Clear() { if (!_entry.IsValueCreated) return; @@ -85,6 +76,15 @@ public sealed class DtrEntry : IDisposable, IHostedService _entry.Value.Shown = false; } + private DtrBarEntry CreateEntry() + { + _logger.LogTrace("Creating new DtrBar entry"); + var entry = _dtrBar.Get("Mare Synchronos"); + entry.OnClick = () => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi))); + + return entry; + } + private async Task RunAsync() { while (!_cancellationTokenSource.IsCancellationRequested) diff --git a/MareSynchronos/UI/EditProfileUi.cs b/MareSynchronos/UI/EditProfileUi.cs index 5aab875..ae83cb9 100644 --- a/MareSynchronos/UI/EditProfileUi.cs +++ b/MareSynchronos/UI/EditProfileUi.cs @@ -26,7 +26,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase private string _descriptionText = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; - private byte[] _profileImage = Array.Empty(); + private byte[] _profileImage = []; private bool _showFileDialogError = false; private bool _wasOpen; @@ -136,7 +136,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => { if (!success) return; - Task.Run(async () => + _ = Task.Run(async () => { var fileContent = File.ReadAllBytes(file); using MemoryStream ms = new(fileContent); @@ -155,7 +155,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase } _showFileDialogError = false; - await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), false, null, Convert.ToBase64String(fileContent), null)) + await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null)) .ConfigureAwait(false); }); }); @@ -164,7 +164,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.SameLine(); if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), false, null, "", null)); + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null)); } UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); if (_showFileDialogError) @@ -174,7 +174,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase var isNsfw = profile.IsNSFW; if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), false, isNsfw, null, null)); + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null)); } UiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); var widthTextBox = 400; @@ -213,13 +213,13 @@ public class EditProfileUi : WindowMediatorSubscriberBase if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), false, null, null, _descriptionText)); + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText)); } UiSharedService.AttachToolTip("Sets your profile description text"); ImGui.SameLine(); if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), false, null, null, "")); + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "")); } UiSharedService.AttachToolTip("Clears your profile description text"); } diff --git a/MareSynchronos/UI/GposeUi.cs b/MareSynchronos/UI/GposeUi.cs index fa0d280..ac7b5ab 100644 --- a/MareSynchronos/UI/GposeUi.cs +++ b/MareSynchronos/UI/GposeUi.cs @@ -51,7 +51,7 @@ public class GposeUi : WindowMediatorSubscriberBase _configService.Current.ExportFolder = Path.GetDirectoryName(path) ?? string.Empty; _configService.Save(); - Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); + _ = Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path)); }, 1, Directory.Exists(_configService.Current.ExportFolder) ? _configService.Current.ExportFolder : null); } UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor"); @@ -61,7 +61,7 @@ public class GposeUi : WindowMediatorSubscriberBase 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)); + _ = 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); diff --git a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs b/MareSynchronos/UI/Handlers/IdDisplayHandler.cs similarity index 51% rename from MareSynchronos/UI/Handlers/UidDisplayHandler.cs rename to MareSynchronos/UI/Handlers/IdDisplayHandler.cs index 75f1295..0220c09 100644 --- a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs +++ b/MareSynchronos/UI/Handlers/IdDisplayHandler.cs @@ -1,41 +1,96 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; -using MareSynchronos.PlayerData.Pairs; -using MareSynchronos.Services.ServerConfiguration; -using MareSynchronos.MareConfiguration; using ImGuiScene; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; namespace MareSynchronos.UI.Handlers; -public class UidDisplayHandler +public class IdDisplayHandler { private readonly MareConfigService _mareConfigService; private readonly MareMediator _mediator; - private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; - private readonly Dictionary _showUidForEntry = new(StringComparer.Ordinal); - private string _editNickEntry = string.Empty; - private string _editUserComment = string.Empty; + private readonly Dictionary _showIdForEntry = new(StringComparer.Ordinal); + private string _editComment = string.Empty; + private string _editEntry = string.Empty; + private bool _editIsUid = false; private string _lastMouseOverUid = string.Empty; private bool _popupShown = false; private DateTime? _popupTime; private TextureWrap? _textureWrap; - public UidDisplayHandler(MareMediator mediator, PairManager pairManager, - ServerConfigurationManager serverManager, MareConfigService mareConfigService) + public IdDisplayHandler(MareMediator mediator, ServerConfigurationManager serverManager, MareConfigService mareConfigService) { _mediator = mediator; - _pairManager = pairManager; _serverManager = serverManager; _mareConfigService = mareConfigService; } + public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, float originalY, Func editBoxWidth) + { + ImGui.SameLine(textPosX); + (bool textIsUid, string playerText) = GetGroupText(group); + if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal)) + { + ImGui.SetCursorPosY(originalY); + using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) + ImGui.TextUnformatted(playerText); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (_showIdForEntry.ContainsKey(group.GID)) + { + prevState = _showIdForEntry[group.GID]; + } + _showIdForEntry[group.GID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + if (_editIsUid) + { + _serverManager.SetNoteForUid(_editEntry, _editComment, save: true); + } + else + { + _serverManager.SetNoteForGid(_editEntry, _editComment, save: true); + } + + _editComment = _serverManager.GetNoteForGid(group.GID) ?? string.Empty; + _editEntry = group.GID; + _editIsUid = false; + } + } + else + { + ImGui.SetCursorPosY(originalY); + + ImGui.SetNextItemWidth(editBoxWidth.Invoke()); + if (ImGui.InputTextWithHint("", "Name/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _serverManager.SetNoteForGid(group.GID, _editComment, save: true); + _editEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editEntry = string.Empty; + } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + } + public void DrawPairText(string id, Pair pair, float textPosX, float originalY, Func editBoxWidth) { ImGui.SameLine(textPosX); (bool textIsUid, string playerText) = GetPlayerText(pair); - if (!string.Equals(_editNickEntry, pair.UserData.UID, StringComparison.Ordinal)) + if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal)) { ImGui.SetCursorPosY(originalY); if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); @@ -66,7 +121,7 @@ public class UidDisplayHandler { if (string.Equals(_lastMouseOverUid, id)) { - _mediator.Publish(new ProfilePopoutToggle(null)); + _mediator.Publish(new ProfilePopoutToggle(Pair: null)); _lastMouseOverUid = string.Empty; _popupShown = false; _textureWrap?.Dispose(); @@ -77,19 +132,27 @@ public class UidDisplayHandler if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { var prevState = textIsUid; - if (_showUidForEntry.ContainsKey(pair.UserData.UID)) + if (_showIdForEntry.ContainsKey(pair.UserData.UID)) { - prevState = _showUidForEntry[pair.UserData.UID]; + prevState = _showIdForEntry[pair.UserData.UID]; } - _showUidForEntry[pair.UserData.UID] = !prevState; + _showIdForEntry[pair.UserData.UID] = !prevState; } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - var nickEntryPair = _pairManager.DirectPairs.Find(p => string.Equals(p.UserData.UID, _editNickEntry, StringComparison.Ordinal)); - nickEntryPair?.SetNote(_editUserComment); - _editUserComment = pair.GetNote() ?? string.Empty; - _editNickEntry = pair.UserData.UID; + if (_editIsUid) + { + _serverManager.SetNoteForUid(_editEntry, _editComment, save: true); + } + else + { + _serverManager.SetNoteForGid(_editEntry, _editComment, save: true); + } + + _editComment = pair.GetNote() ?? string.Empty; + _editEntry = pair.UserData.UID; + _editIsUid = true; } if (ImGui.IsItemClicked(ImGuiMouseButton.Middle)) @@ -102,21 +165,45 @@ public class UidDisplayHandler ImGui.SetCursorPosY(originalY); ImGui.SetNextItemWidth(editBoxWidth.Invoke()); - if (ImGui.InputTextWithHint("", "Nick/Notes", ref _editUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputTextWithHint("", "Nick/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) { - _serverManager.SetNoteForUid(pair.UserData.UID, _editUserComment); + _serverManager.SetNoteForUid(pair.UserData.UID, _editComment); _serverManager.SaveNotes(); - _editNickEntry = string.Empty; + _editEntry = string.Empty; } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { - _editNickEntry = string.Empty; + _editEntry = string.Empty; } UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); } } + public (bool isGid, string text) GetGroupText(GroupFullInfoDto group) + { + var textIsGid = true; + bool showUidInsteadOfName = ShowGidInsteadOfName(group); + string? groupText = _serverManager.GetNoteForGid(group.GID); + if (!showUidInsteadOfName && groupText != null) + { + if (string.IsNullOrEmpty(groupText)) + { + groupText = group.GroupAliasOrGID; + } + else + { + textIsGid = false; + } + } + else + { + groupText = group.GroupAliasOrGID; + } + + return (textIsGid, groupText!); + } + public (bool isUid, string text) GetPlayerText(Pair pair) { var textIsUid = true; @@ -157,8 +244,8 @@ public class UidDisplayHandler internal void Clear() { - _editNickEntry = string.Empty; - _editUserComment = string.Empty; + _editEntry = string.Empty; + _editComment = string.Empty; } internal void OpenProfile(Pair entry) @@ -166,10 +253,17 @@ public class UidDisplayHandler _mediator.Publish(new ProfileOpenStandaloneMessage(entry)); } + private bool ShowGidInsteadOfName(GroupFullInfoDto group) + { + _showIdForEntry.TryGetValue(group.GID, out var showidInsteadOfName); + + return showidInsteadOfName; + } + private bool ShowUidInsteadOfName(Pair pair) { - _showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); + _showIdForEntry.TryGetValue(pair.UserData.UID, out var showidInsteadOfName); - return showUidInsteadOfName; + return showidInsteadOfName; } } \ No newline at end of file diff --git a/MareSynchronos/UI/Handlers/TagHandler.cs b/MareSynchronos/UI/Handlers/TagHandler.cs index c533f1c..f02f9fe 100644 --- a/MareSynchronos/UI/Handlers/TagHandler.cs +++ b/MareSynchronos/UI/Handlers/TagHandler.cs @@ -4,6 +4,7 @@ namespace MareSynchronos.UI.Handlers; public class TagHandler { + public const string CustomAllTag = "Mare_All"; public const string CustomOfflineTag = "Mare_Offline"; public const string CustomOnlineTag = "Mare_Online"; public const string CustomUnpairedTag = "Mare_Unpaired"; @@ -27,9 +28,12 @@ public class TagHandler public List GetAllTagsSorted() { - return _serverConfigurationManager.GetServerAvailablePairTags() - .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) - .ToList(); + return + [ + .. _serverConfigurationManager.GetServerAvailablePairTags() + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) +, + ]; } public HashSet GetOtherUidsForTag(string tag) @@ -59,7 +63,6 @@ public class TagHandler public void RemoveTag(string tag) { - // First remove the tag from teh available pair tags _serverConfigurationManager.RemoveTag(tag); } diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 49307c8..96d5247 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -214,7 +214,7 @@ public class IntroUi : WindowMediatorSubscriberBase var buttonWidth = _secretKey.Length != 64 ? 0 : ImGuiHelpers.GetButtonSize(buttonText).X + ImGui.GetStyle().ItemSpacing.X; var textSize = ImGui.CalcTextSize(text); ImGui.AlignTextToFramePadding(); - ImGui.Text(text); + ImGui.TextUnformatted(text); ImGui.SameLine(); ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); ImGui.InputText("", ref _secretKey, 64); @@ -235,7 +235,7 @@ public class IntroUi : WindowMediatorSubscriberBase }); _serverConfigurationManager.AddCurrentCharacterToServer(addLastSecretKey: true); _secretKey = string.Empty; - Task.Run(() => _uiShared.ApiController.CreateConnections(forceGetToken: true)); + _ = Task.Run(() => _uiShared.ApiController.CreateConnections()); } } } @@ -253,6 +253,6 @@ public class IntroUi : WindowMediatorSubscriberBase _uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value); } - _tosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 }; + _tosParagraphs = [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/PopoutProfileUi.cs b/MareSynchronos/UI/PopoutProfileUi.cs index 85c9ed8..a95ea87 100644 --- a/MareSynchronos/UI/PopoutProfileUi.cs +++ b/MareSynchronos/UI/PopoutProfileUi.cs @@ -1,47 +1,49 @@ using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; + +using Dalamud.Interface.Utility; using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using Microsoft.Extensions.Logging; using System.Numerics; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.MareConfiguration; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Internal; namespace MareSynchronos.UI; public class PopoutProfileUi : WindowMediatorSubscriberBase { private readonly MareProfileManager _mareProfileManager; + private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; private readonly UiSharedService _uiSharedService; private Vector2 _lastMainPos = Vector2.Zero; private Vector2 _lastMainSize = Vector2.Zero; - private byte[] _lastProfilePicture = Array.Empty(); - private byte[] _lastSupporterPicture = Array.Empty(); + private byte[] _lastProfilePicture = []; + private byte[] _lastSupporterPicture = []; private Pair? _pair; private IDalamudTextureWrap? _supporterTextureWrap; private IDalamudTextureWrap? _textureWrap; public PopoutProfileUi(ILogger logger, MareMediator mediator, UiSharedService uiBuilder, ServerConfigurationManager serverManager, MareConfigService mareConfigService, - MareProfileManager mareProfileManager) : base(logger, mediator, "###MareSynchronosPopoutProfileUI") + MareProfileManager mareProfileManager, PairManager pairManager) : base(logger, mediator, "###MareSynchronosPopoutProfileUI") { _uiSharedService = uiBuilder; _serverManager = serverManager; _mareProfileManager = mareProfileManager; - + _pairManager = pairManager; Flags = ImGuiWindowFlags.NoDecoration; Mediator.Subscribe(this, (msg) => { IsOpen = msg.Pair != null; _pair = msg.Pair; - _lastProfilePicture = Array.Empty(); - _lastSupporterPicture = Array.Empty(); + _lastProfilePicture = []; + _lastSupporterPicture = []; _textureWrap?.Dispose(); _textureWrap = null; _supporterTextureWrap?.Dispose(); @@ -54,7 +56,6 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase { var border = ImGui.GetStyle().WindowBorderSize; var padding = ImGui.GetStyle().WindowPadding; - var spacing = ImGui.GetStyle().ItemSpacing; Size = new(256 + (padding.X * 2) + border, msg.Size.Y / ImGuiHelpers.GlobalScale); _lastMainSize = msg.Size; } @@ -65,7 +66,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase } else { - Position = new(mainPos.X - Size.Value.X * ImGuiHelpers.GlobalScale, mainPos.Y); + Position = new(mainPos.X - Size!.Value.X * ImGuiHelpers.GlobalScale, mainPos.Y); } if (msg.Position != Vector2.Zero) @@ -129,7 +130,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted($"({_pair.PlayerName})"); } - if (_pair.UserPair != null) + if (_pair.UserPair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional) { ImGui.TextUnformatted("Directly paired"); if (_pair.UserPair.OwnPermissions.IsPaused()) @@ -143,13 +144,14 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow); } } - if (_pair.GroupPair.Any()) + if (_pair.UserPair.Groups.Any()) { ImGui.TextUnformatted("Paired through Syncshells:"); - foreach (var groupPair in _pair.GroupPair.Select(k => k.Key)) + foreach (var group in _pair.UserPair.Groups) { - var groupNote = _serverManager.GetNoteForGid(groupPair.GID); - var groupString = string.IsNullOrEmpty(groupNote) ? groupPair.GroupAliasOrGID : $"{groupNote} ({groupPair.GroupAliasOrGID})"; + var groupNote = _serverManager.GetNoteForGid(group); + var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID; + var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; ImGui.TextUnformatted("- " + groupString); } } @@ -162,7 +164,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase bool trimmed = textSize.Y > remaining; while (textSize.Y > remaining && descText.Contains(' ')) { - descText = descText.Substring(0, descText.LastIndexOf(' ')).TrimEnd(); + descText = descText[..descText.LastIndexOf(' ')].TrimEnd(); textSize = ImGui.CalcTextSize(descText + $"...{Environment.NewLine}[Open Full Profile for complete description]", 256f * ImGuiHelpers.GlobalScale); } UiSharedService.TextWrapped(trimmed ? descText + $"...{Environment.NewLine}[Open Full Profile for complete description]" : mareProfile.Description); diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 219bcea..b002ec9 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -1,36 +1,37 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; -using ImGuiNET; -using MareSynchronos.WebAPI; -using System.Numerics; using Dalamud.Utility; +using ImGuiNET; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Comparer; +using MareSynchronos.FileCache; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; -using Microsoft.Extensions.Logging; -using MareSynchronos.WebAPI.SignalR.Utils; -using MareSynchronos.PlayerData.Pairs; -using System.Text.Json; using MareSynchronos.PlayerData.Export; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; -using MareSynchronos.Services; +using MareSynchronos.WebAPI; using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files.Models; -using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using MareSynchronos.FileCache; +using System.Numerics; +using System.Text.Json; namespace MareSynchronos.UI; public class SettingsUi : WindowMediatorSubscriberBase { + private readonly ApiController _apiController; private readonly MareConfigService _configService; private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly FileCompactor _fileCompactor; private readonly FileUploadManager _fileTransferManager; private readonly FileTransferOrchestrator _fileTransferOrchestrator; - private readonly FileCompactor _fileCompactor; private readonly MareCharaFileManager _mareCharaFileManager; private readonly PairManager _pairManager; private readonly PerformanceCollectorService _performanceCollector; @@ -53,7 +54,7 @@ public class SettingsUi : WindowMediatorSubscriberBase MareMediator mediator, PerformanceCollectorService performanceCollector, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, - FileCompactor fileCompactor) : base(logger, mediator, "Mare Synchronos Settings") + FileCompactor fileCompactor, ApiController apiController) : base(logger, mediator, "Mare Synchronos Settings") { _configService = configService; _mareCharaFileManager = mareCharaFileManager; @@ -62,6 +63,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _performanceCollector = performanceCollector; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; + _apiController = apiController; _fileCompactor = fileCompactor; _uiShared = uiShared; @@ -117,14 +119,14 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TableNextColumn(); if (item is UploadFileTransfer transfer) { - ImGui.Text(transfer.LocalFile); + ImGui.TextUnformatted(transfer.LocalFile); } else { - ImGui.Text(item.Hash); + ImGui.TextUnformatted(item.Hash); } ImGui.TableNextColumn(); - ImGui.Text(item.ForbiddenBy); + ImGui.TextUnformatted(item.ForbiddenBy); } ImGui.EndTable(); } @@ -248,11 +250,11 @@ public class SettingsUi : WindowMediatorSubscriberBase var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); ImGui.PushStyleColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); + ImGui.TextUnformatted(transfer.Hash); ImGui.TableNextColumn(); - ImGui.Text(UiSharedService.ByteToString(transfer.Transferred)); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); - ImGui.Text(UiSharedService.ByteToString(transfer.Total)); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); ImGui.PopStyleColor(); ImGui.TableNextRow(); } @@ -276,14 +278,14 @@ public class SettingsUi : WindowMediatorSubscriberBase { var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, entry.Value.TotalBytes)); ImGui.TableNextColumn(); - ImGui.Text(userName); + ImGui.TextUnformatted(userName); ImGui.TableNextColumn(); - ImGui.Text(entry.Key); + ImGui.TextUnformatted(entry.Key); ImGui.PushStyleColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); - ImGui.Text(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); + ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); ImGui.TableNextColumn(); - ImGui.Text(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + UiSharedService.ByteToString(entry.Value.TotalBytes)); + ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + UiSharedService.ByteToString(entry.Value.TotalBytes)); ImGui.TableNextColumn(); ImGui.PopStyleColor(); ImGui.TableNextRow(); @@ -316,7 +318,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { foreach (var l in JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true }).Split('\n')) { - ImGui.Text($"{l}"); + ImGui.TextUnformatted($"{l}"); } ImGui.TreePop(); @@ -436,7 +438,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawFileScanState(); _uiShared.DrawTimeSpanBetweenScansSetting(); _uiShared.DrawCacheDirectorySetting(); - ImGui.Text($"Currently utilized local storage: {UiSharedService.ByteToString(_uiShared.FileCacheSize)}"); + ImGui.TextUnformatted($"Currently utilized local storage: {UiSharedService.ByteToString(_uiShared.FileCacheSize)}"); bool isLinux = Util.IsWine(); if (isLinux) ImGui.BeginDisabled(); bool useFileCompactor = _configService.Current.UseCompactor; @@ -452,14 +454,14 @@ public class SettingsUi : WindowMediatorSubscriberBase { if (UiSharedService.IconTextButton(FontAwesomeIcon.FileArchive, "Compact all files in storage")) { - _ = Task.Run(() => _fileCompactor.CompactStorage(true)); + _ = Task.Run(() => _fileCompactor.CompactStorage(compress: true)); } UiSharedService.AttachToolTip("This will run compression on all files in your current Mare Storage." + Environment.NewLine + "You do not need to run this manually if you keep the file compactor enabled."); ImGui.SameLine(); if (UiSharedService.IconTextButton(FontAwesomeIcon.File, "Decompact all files in storage")) { - _ = Task.Run(() => _fileCompactor.CompactStorage(false)); + _ = Task.Run(() => _fileCompactor.CompactStorage(compress: false)); } UiSharedService.AttachToolTip("This will run decompression on all files in your current Mare Storage."); } @@ -470,11 +472,11 @@ public class SettingsUi : WindowMediatorSubscriberBase if (isLinux) { ImGui.EndDisabled(); - ImGui.Text("The file compactor is only available on Windows."); + ImGui.TextUnformatted("The file compactor is only available on Windows."); } ImGui.Dummy(new Vector2(10, 10)); - ImGui.Text("To clear the local storage accept the following disclaimer"); + ImGui.TextUnformatted("To clear the local storage accept the following disclaimer"); ImGui.Indent(); ImGui.Checkbox("##readClearCache", ref _readClearCache); ImGui.SameLine(); @@ -557,6 +559,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var enableRightClickMenu = _configService.Current.EnableRightClickMenus; var enableDtrEntry = _configService.Current.EnableDtrEntry; var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible; + var groupUpSyncshells = _configService.Current.GroupUpSyncshells; if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) { @@ -576,6 +579,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } UiSharedService.DrawHelpText("This will show all currently visible users in a special 'Visible' group in the main UI."); @@ -583,13 +587,23 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } UiSharedService.DrawHelpText("This will show all currently offline users in a special 'Offline' group in the main UI."); + if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) + { + _configService.Current.GroupUpSyncshells = groupUpSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + UiSharedService.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) { _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } UiSharedService.DrawHelpText("This will show the character name instead of custom set note when a character is visible"); @@ -599,6 +613,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } UiSharedService.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); @@ -728,7 +743,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { 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.TextUnformatted("Are you sure you want to continue?"); ImGui.Separator(); ImGui.Spacing(); @@ -737,7 +752,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (ImGui.Button("Delete everything", new Vector2(buttonSize, 0))) { - Task.Run(_fileTransferManager.DeleteAllFiles); + _ = Task.Run(_fileTransferManager.DeleteAllFiles); _deleteFilesPopupModalShown = false; } @@ -765,7 +780,7 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.TextWrapped( "Your account and all associated files and data on the service will be deleted."); UiSharedService.TextWrapped("Your UID will be removed from all pairing lists."); - ImGui.Text("Are you sure you want to continue?"); + ImGui.TextUnformatted("Are you sure you want to continue?"); ImGui.Separator(); ImGui.Spacing(); @@ -774,7 +789,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (ImGui.Button("Delete account", new Vector2(buttonSize, 0))) { - Task.Run(ApiController.UserDelete); + _ = Task.Run(ApiController.UserDelete); _deleteAccountPopupModalShown = false; Mediator.Publish(new SwitchToIntroUiMessage()); } @@ -873,7 +888,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Separator(); - if (!selectedServer.Authentications.Any(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal) + if (!selectedServer.Authentications.Exists(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal) && c.WorldId == _uiShared.WorldId)) { if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Add current character")) @@ -914,7 +929,7 @@ public class SettingsUi : WindowMediatorSubscriberBase item.Value.Key = key; _serverConfigurationManager.Save(); } - if (!selectedServer.Authentications.Any(p => p.SecretKeyIdx == item.Key)) + if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) { if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) { @@ -982,6 +997,84 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.EndTabItem(); } + + if (ImGui.BeginTabItem("Permission Settings")) + { + UiSharedService.FontText("Default Permission Settings", _uiShared.UidFont); + if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) + { + UiSharedService.TextWrapped("Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); + UiSharedService.TextWrapped("Note: The default permissions settings here are sent and stored on the connected service."); + ImGui.Dummy(new(5f)); + var perms = _apiController.DefaultPermissions!; + bool individualIsSticky = perms.IndividualIsSticky; + bool disableIndividualSounds = perms.DisableIndividualSounds; + bool disableIndividualAnimations = perms.DisableIndividualAnimations; + bool disableIndividualVFX = perms.DisableIndividualVFX; + if (ImGui.Checkbox("Individually set permissions become preferred permissions", ref individualIsSticky)) + { + perms.IndividualIsSticky = individualIsSticky; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + + "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + + "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + Environment.NewLine + Environment.NewLine + + "This setting means:" + Environment.NewLine + + " - All new individual pairs get their permissions defaulted to preferred permissions." + Environment.NewLine + + " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + Environment.NewLine + Environment.NewLine + + "It is possible to remove or set the preferred permission state for any pair at any time." + Environment.NewLine + Environment.NewLine + + "If unsure, leave this setting off."); + ImGui.Dummy(new(3f)); + + if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds)) + { + perms.DisableIndividualSounds = disableIndividualSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable sound sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) + { + perms.DisableIndividualAnimations = disableIndividualAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable animation sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) + { + perms.DisableIndividualVFX = disableIndividualVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); + ImGui.Dummy(new(5f)); + bool disableGroundSounds = perms.DisableGroupSounds; + bool disableGroupAnimations = perms.DisableGroupAnimations; + bool disableGroupVFX = perms.DisableGroupVFX; + if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds)) + { + perms.DisableGroupSounds = disableGroundSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) + { + perms.DisableGroupAnimations = disableGroupAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) + { + perms.DisableGroupVFX = disableGroupVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + UiSharedService.DrawHelpText("This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); + } + else + { + UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + + "You need to connect to this service to change the default permissions since they are stored on the service.", ImGuiColors.DalamudYellow); + } + + ImGui.EndTabItem(); + } ImGui.EndTabBar(); } } @@ -990,7 +1083,7 @@ public class SettingsUi : WindowMediatorSubscriberBase { _uiShared.PrintServerState(); ImGui.AlignTextToFramePadding(); - ImGui.Text("Community and Support:"); + ImGui.TextUnformatted("Community and Support:"); ImGui.SameLine(); if (ImGui.Button("Mare Synchronos Discord")) { @@ -1043,4 +1136,4 @@ public class SettingsUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } -} +} \ No newline at end of file diff --git a/MareSynchronos/UI/StandaloneProfileUi.cs b/MareSynchronos/UI/StandaloneProfileUi.cs index dd396c7..6991e3f 100644 --- a/MareSynchronos/UI/StandaloneProfileUi.cs +++ b/MareSynchronos/UI/StandaloneProfileUi.cs @@ -1,37 +1,39 @@ using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; + +using Dalamud.Interface.Utility; using ImGuiNET; +using MareSynchronos.API.Data.Extensions; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using Microsoft.Extensions.Logging; using System.Numerics; -using MareSynchronos.API.Data.Extensions; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Internal; namespace MareSynchronos.UI; public class StandaloneProfileUi : WindowMediatorSubscriberBase { private readonly MareProfileManager _mareProfileManager; + private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; private readonly UiSharedService _uiSharedService; private bool _adjustedForScrollBars = false; - private byte[] _lastProfilePicture = Array.Empty(); - private byte[] _lastSupporterPicture = Array.Empty(); + private byte[] _lastProfilePicture = []; + private byte[] _lastSupporterPicture = []; private IDalamudTextureWrap? _supporterTextureWrap; private IDalamudTextureWrap? _textureWrap; public StandaloneProfileUi(ILogger logger, MareMediator mediator, UiSharedService uiBuilder, - ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, Pair pair) + ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair) : base(logger, mediator, "Mare Profile of " + pair.UserData.AliasOrUID + "##MareSynchronosStandaloneProfileUI" + pair.UserData.AliasOrUID) { _uiSharedService = uiBuilder; _serverManager = serverManager; _mareProfileManager = mareProfileManager; Pair = pair; - + _pairManager = pairManager; Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize; var spacing = ImGui.GetStyle().ItemSpacing; @@ -88,12 +90,12 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase var descriptionChildHeight = rectMax.Y - pos.Y - rectMin.Y - spacing.Y * 2; if (descriptionTextSize.Y > descriptionChildHeight && !_adjustedForScrollBars) { - Size = Size.Value with { X = Size.Value.X + ImGui.GetStyle().ScrollbarSize }; + Size = Size!.Value with { X = Size.Value.X + ImGui.GetStyle().ScrollbarSize }; _adjustedForScrollBars = true; } else if (descriptionTextSize.Y < descriptionChildHeight && _adjustedForScrollBars) { - Size = Size.Value with { X = Size.Value.X - ImGui.GetStyle().ScrollbarSize }; + Size = Size!.Value with { X = Size.Value.X - ImGui.GetStyle().ScrollbarSize }; _adjustedForScrollBars = false; } var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, descriptionChildHeight); @@ -137,13 +139,15 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow); } } - if (Pair.GroupPair.Any()) + + if (Pair.UserPair.Groups.Any()) { ImGui.TextUnformatted("Paired through Syncshells:"); - foreach (var groupPair in Pair.GroupPair.Select(k => k.Key)) + foreach (var group in Pair.UserPair.Groups) { - var groupNote = _serverManager.GetNoteForGid(groupPair.GID); - var groupString = string.IsNullOrEmpty(groupNote) ? groupPair.GroupAliasOrGID : $"{groupNote} ({groupPair.GroupAliasOrGID})"; + var groupNote = _serverManager.GetNoteForGid(group); + var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID; + var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; ImGui.TextUnformatted("- " + groupString); } } diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index 61594c4..5f79201 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -129,17 +129,51 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public uint WorldId => _dalamudUtil.GetWorldId(); + public const string TooltipSeparator = "--SEP--"; + public static void AttachToolTip(string text) { if (ImGui.IsItemHovered()) { - ImGui.SetTooltip(text); + ImGui.BeginTooltip(); + if (text.Contains(TooltipSeparator, StringComparison.Ordinal)) + { + var splitText = text.Split(TooltipSeparator, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < splitText.Length; i++) + { + ImGui.TextUnformatted(splitText[i]); + if (i != splitText.Length - 1) ImGui.Separator(); + } + } + else + { + ImGui.TextUnformatted(text); + } + ImGui.EndTooltip(); + } + } + + public static void BooleanToColoredIcon(bool value, bool inline = true) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, value); + using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value); + + if (inline) ImGui.SameLine(); + + if (value) + { + ImGui.TextUnformatted(FontAwesomeIcon.Check.ToIconString()); + } + else + { + ImGui.TextUnformatted(FontAwesomeIcon.Times.ToIconString()); } } public static string ByteToString(long bytes, bool addSuffix = true) { - string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; + string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) @@ -315,42 +349,55 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; } - public static bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null) + public static bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) { - var buttonClicked = false; + var wasClicked = false; var iconSize = GetIconSize(icon); var textSize = ImGui.CalcTextSize(text); var padding = ImGui.GetStyle().FramePadding; var spacing = ImGui.GetStyle().ItemSpacing; + var cursor = ImGui.GetCursorPos(); + var drawList = ImGui.GetWindowDrawList(); + var pos = ImGui.GetWindowPos(); Vector2 buttonSize; - var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; + var buttonSizeY = textSize.Y + padding.Y * 2; + var iconExtraSpacing = isInPopup ? padding.X * 2 : 0; - if (width == null) + var iconXoffset = iconSize.X <= iconSize.Y ? (iconSize.Y - iconSize.X) / 2f : 0; + var iconScaling = iconSize.X > iconSize.Y ? 1 / (iconSize.X / iconSize.Y) : 1; + + if (width == null || width <= 0) { - var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; - buttonSize = new Vector2(buttonSizeX, buttonSizeY); + var buttonSizeX = (iconScaling == 1 ? iconSize.Y : (iconSize.X * iconScaling)) + + textSize.X + padding.X * 2 + spacing.X + (iconXoffset * 2); + buttonSize = new Vector2(buttonSizeX + iconExtraSpacing, buttonSizeY); } else { buttonSize = new Vector2(width.Value, buttonSizeY); } - if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.PopupBg), isInPopup)) { - buttonClicked = true; + if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) + { + wasClicked = 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); + drawList.AddText(UiBuilder.IconFont, ImGui.GetFontSize() * iconScaling, + new(pos.X + cursor.X + iconXoffset + padding.X, + pos.Y + cursor.Y + (buttonSizeY - (iconSize.Y * iconScaling)) / 2f), + ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); - return buttonClicked; + drawList.AddText(UiBuilder.DefaultFont, ImGui.GetFontSize(), + new(pos.X + cursor.X + (padding.X) + spacing.X + (iconSize.X * iconScaling) + (iconXoffset * 2) + iconExtraSpacing, + pos.Y + cursor.Y + ((buttonSizeY - textSize.Y) / 2f)), + ImGui.GetColorU32(ImGuiCol.Text), text); + + return wasClicked; } public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) @@ -571,14 +618,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public void DrawFileScanState() { - ImGui.Text("File Scanner Status"); + ImGui.TextUnformatted("File Scanner Status"); ImGui.SameLine(); if (_cacheScanner.IsScanRunning) { - ImGui.Text("Scan is running"); - ImGui.Text("Current Progress:"); + ImGui.TextUnformatted("Scan is running"); + ImGui.TextUnformatted("Current Progress:"); ImGui.SameLine(); - ImGui.Text(_cacheScanner.TotalFiles == 1 + ImGui.TextUnformatted(_cacheScanner.TotalFiles == 1 ? "Collecting files" : $"Processing {_cacheScanner.CurrentFileProgress}/{_cacheScanner.TotalFilesStorage} from storage ({_cacheScanner.TotalFiles} scanned in)"); AttachToolTip("Note: it is possible to have more files in storage than scanned in, " + @@ -587,7 +634,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } else if (_configService.Current.FileScanPaused) { - ImGui.Text("File scanner is paused"); + ImGui.TextUnformatted("File scanner is paused"); ImGui.SameLine(); if (ImGui.Button("Force Rescan##forcedrescan")) { @@ -596,7 +643,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } 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.TextUnformatted("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")) { @@ -605,7 +652,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } else { - ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); + ImGui.TextUnformatted("Next scan in " + _cacheScanner.TimeUntilNextScan); } } @@ -619,10 +666,10 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase var honorificColor = _honorificExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; var check = FontAwesomeIcon.Check.ToIconString(); var cross = FontAwesomeIcon.SquareXmark.ToIconString(); - ImGui.Text("Mandatory Plugins:"); + ImGui.TextUnformatted("Mandatory Plugins:"); ImGui.SameLine(); - ImGui.Text("Penumbra"); + ImGui.TextUnformatted("Penumbra"); ImGui.SameLine(); FontText(_penumbraExists ? check : cross, UiBuilder.IconFont, penumbraColor); ImGui.SameLine(); @@ -630,16 +677,16 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Spacing(); ImGui.SameLine(); - ImGui.Text("Glamourer"); + ImGui.TextUnformatted("Glamourer"); ImGui.SameLine(); FontText(_glamourerExists ? check : cross, UiBuilder.IconFont, glamourerColor); ImGui.SameLine(); AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date.")); ImGui.Spacing(); - ImGui.Text("Optional Plugins:"); + ImGui.TextUnformatted("Optional Plugins:"); ImGui.SameLine(); - ImGui.Text("SimpleHeels"); + ImGui.TextUnformatted("SimpleHeels"); ImGui.SameLine(); FontText(_heelsExists ? check : cross, UiBuilder.IconFont, heelsColor); ImGui.SameLine(); @@ -647,7 +694,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Spacing(); ImGui.SameLine(); - ImGui.Text("Customize+"); + ImGui.TextUnformatted("Customize+"); ImGui.SameLine(); FontText(_customizePlusExists ? check : cross, UiBuilder.IconFont, customizeColor); ImGui.SameLine(); @@ -655,7 +702,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Spacing(); ImGui.SameLine(); - ImGui.Text("Palette+"); + ImGui.TextUnformatted("Palette+"); ImGui.SameLine(); FontText(_palettePlusExists ? check : cross, UiBuilder.IconFont, paletteColor); ImGui.SameLine(); @@ -663,7 +710,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Spacing(); ImGui.SameLine(); - ImGui.Text("Honorific"); + ImGui.TextUnformatted("Honorific"); ImGui.SameLine(); FontText(_honorificExists ? check : cross, UiBuilder.IconFont, honorificColor); ImGui.SameLine(); @@ -792,9 +839,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.SameLine(); ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); ImGui.SameLine(); - ImGui.Text("Users Online"); + ImGui.TextUnformatted("Users Online"); ImGui.SameLine(); - ImGui.Text(")"); + ImGui.TextUnformatted(")"); } } diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs index 7c9fca2..45f7482 100644 --- a/MareSynchronos/Utils/Crypto.cs +++ b/MareSynchronos/Utils/Crypto.cs @@ -1,6 +1,7 @@ -using System.Security.Cryptography; +using Dalamud.Game.ClientState.Objects.SubKinds; + +using System.Security.Cryptography; using System.Text; -using Dalamud.Game.ClientState.Objects.SubKinds; namespace MareSynchronos.Utils; diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs index 8584337..4b66828 100644 --- a/MareSynchronos/Utils/VariousExtensions.cs +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -36,7 +36,7 @@ public static class VariousExtensions var charaDataToUpdate = new Dictionary>(); foreach (ObjectKind objectKind in Enum.GetValues()) { - charaDataToUpdate[objectKind] = new(); + charaDataToUpdate[objectKind] = []; oldData.FileReplacements.TryGetValue(objectKind, out var existingFileReplacements); newData.FileReplacements.TryGetValue(objectKind, out var newFileReplacements); oldData.GlamourerData.TryGetValue(objectKind, out var existingGlamourerData); @@ -127,7 +127,7 @@ public static class VariousExtensions foreach (KeyValuePair> data in charaDataToUpdate.ToList()) { if (!data.Value.Any()) charaDataToUpdate.Remove(data.Key); - else charaDataToUpdate[data.Key] = data.Value.OrderByDescending(p => (int)p).ToHashSet(); + else charaDataToUpdate[data.Key] = [.. data.Value.OrderByDescending(p => (int)p)]; } return charaDataToUpdate; diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs index 5c723ea..87290be 100644 --- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -16,8 +16,8 @@ namespace MareSynchronos.WebAPI.Files; public partial class FileDownloadManager : DisposableMediatorSubscriberBase { private readonly Dictionary _downloadStatus; - private readonly FileCacheManager _fileDbManager; private readonly FileCompactor _fileCompactor; + private readonly FileCacheManager _fileDbManager; private readonly FileTransferOrchestrator _orchestrator; public FileDownloadManager(ILogger logger, MareMediator mediator, @@ -30,12 +30,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase _fileCompactor = fileCompactor; } - public List CurrentDownloads { get; private set; } = new(); + public List CurrentDownloads { get; private set; } = []; public List ForbiddenTransfers => _orchestrator.ForbiddenTransfers; public bool IsDownloading => !CurrentDownloads.Any(); + public static void MungeBuffer(Span buffer) + { + for (int i = 0; i < buffer.Length; ++i) + { + buffer[i] ^= 42; + } + } + public void CancelDownload() { CurrentDownloads.Clear(); @@ -66,14 +74,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase base.Dispose(disposing); } - public static void MungeBuffer(Span buffer) - { - for (int i = 0; i < buffer.Length; ++i) - { - buffer[i] ^= 42; - } - } - private static byte MungeByte(int byteOrEof) { if (byteOrEof == -1) @@ -86,8 +86,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private static (string fileHash, long fileLengthBytes) ReadBlockFileHeader(FileStream fileBlockStream) { - List hashName = new(); - List fileLength = new(); + List hashName = []; + List fileLength = []; var separator = (char)MungeByte(fileBlockStream.ReadByte()); if (separator != '#') throw new InvalidDataException("Data is invalid, first char is not #"); @@ -177,8 +177,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { Logger.LogDebug("Download start: {id}", gameObjectHandler.Name); - List downloadFileInfoFromService = new(); - downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false)); + List downloadFileInfoFromService = + [ + .. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).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))); @@ -219,9 +221,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase // let server predownload files var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri), fileGroup.Select(c => c.Hash), token).ConfigureAwait(false); - Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri, requestIdResponse.Content.ReadAsStringAsync().Result); + Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri, + await requestIdResponse.Content.ReadAsStringAsync(token).ConfigureAwait(false)); - Guid requestId = Guid.Parse(requestIdResponse.Content.ReadAsStringAsync().Result.Trim('"')); + Guid requestId = Guid.Parse((await requestIdResponse.Content.ReadAsStringAsync().ConfigureAwait(false)).Trim('"')); Logger.LogDebug("GUID {requestId} for {n} files on server {uri}", requestId, fileGroup.Count(), fileGroup.First().DownloadUri); @@ -235,15 +238,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { try { - if (!_downloadStatus.ContainsKey(fileGroup.Key)) return; - _downloadStatus[fileGroup.Key].TransferredBytes += bytesDownloaded; + if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return; + value.TransferredBytes += bytesDownloaded; } catch (Exception ex) { Logger.LogWarning(ex, "Could not set download progress"); } }); - await DownloadAndMungeFileHttpClient(fileGroup.Key, requestId, fileGroup.ToList(), blockFile, progress, token).ConfigureAwait(false); + await DownloadAndMungeFileHttpClient(fileGroup.Key, requestId, [.. fileGroup], blockFile, progress, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -275,7 +278,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { Logger.LogDebug("Found file {file} with length {le}, decompressing download", fileHash, fileLengthBytes); - var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".").Last(); + var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1]; byte[] compressedFileContent = new byte[fileLengthBytes]; _ = await fileBlockStream.ReadAsync(compressedFileContent, token).ConfigureAwait(false); @@ -300,7 +303,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase finally { _orchestrator.ReleaseDownloadSlot(); - fileBlockStream?.Dispose(); + if (fileBlockStream != null) + await fileBlockStream.DisposeAsync().ConfigureAwait(false); File.Delete(blockFile); } }).ConfigureAwait(false); @@ -314,7 +318,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { 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(); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; } private void PersistFileToStorage(string fileHash, string filePath) @@ -322,7 +326,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase var fi = new FileInfo(filePath); Func RandomDayInThePast() { - DateTime start = new(1995, 1, 1); + DateTime start = new(1995, 1, 1, 1, 1, 1, DateTimeKind.Local); Random gen = new(); int range = (DateTime.Today - start).Days; return () => start.AddDays(gen.Next(range)); @@ -334,9 +338,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { var entry = _fileDbManager.CreateCacheEntry(filePath); - if (!string.Equals(entry?.Hash, fileHash, StringComparison.OrdinalIgnoreCase)) + if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase)) { - Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry?.Hash, fileHash); + Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry.Hash, fileHash); File.Delete(filePath); _fileDbManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath); } diff --git a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs index bab2706..99aa36a 100644 --- a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs +++ b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs @@ -1,7 +1,7 @@ using MareSynchronos.MareConfiguration; using MareSynchronos.Services.Mediator; -using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.WebAPI.SignalR; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Net.Http.Headers; @@ -12,20 +12,23 @@ namespace MareSynchronos.WebAPI.Files; public class FileTransferOrchestrator : DisposableMediatorSubscriberBase { + private readonly ConcurrentDictionary _downloadReady = new(); private readonly HttpClient _httpClient; private readonly MareConfigService _mareConfig; private readonly object _semaphoreModificationLock = new(); - private readonly ServerConfigurationManager _serverManager; + private readonly TokenProvider _tokenProvider; private int _availableDownloadSlots; private SemaphoreSlim _downloadSemaphore; - private readonly ConcurrentDictionary _downloadReady = new(); - public FileTransferOrchestrator(ILogger logger, MareConfigService mareConfig, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) + public FileTransferOrchestrator(ILogger logger, MareConfigService mareConfig, + MareMediator mediator, TokenProvider tokenProvider) : base(logger, mediator) { _mareConfig = mareConfig; - _serverManager = serverManager; - _httpClient = new(); - _httpClient.Timeout = TimeSpan.FromSeconds(3000); + _tokenProvider = tokenProvider; + _httpClient = new() + { + Timeout = TimeSpan.FromSeconds(3000) + }; var ver = Assembly.GetExecutingAssembly().GetName().Version; _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); @@ -48,12 +51,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase } public Uri? FilesCdnUri { private set; get; } - public List ForbiddenTransfers { get; } = new(); + public List ForbiddenTransfers { get; } = []; public bool IsInitialized => FilesCdnUri != null; - public void ReleaseDownloadSlot() + public void ClearDownloadRequest(Guid guid) { - _downloadSemaphore.Release(); + _downloadReady.Remove(guid, out _); } public bool IsDownloadReady(Guid guid) @@ -66,12 +69,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase return false; } - public void ClearDownloadRequest(Guid guid) + public void ReleaseDownloadSlot() { - _downloadReady.Remove(guid, out _); + _downloadSemaphore.Release(); } - public async Task SendRequestAsync(HttpMethod method, Uri uri, + public async Task SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { using var requestMessage = new HttpRequestMessage(method, uri); @@ -109,10 +112,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false); } - private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, + private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverManager.GetToken()); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetOrUpdateToken(ct!.Value).ConfigureAwait(false)); if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent) { diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs index e764665..15e1935 100644 --- a/MareSynchronos/WebAPI/Files/FileUploadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -39,7 +39,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase }); } - public List CurrentUploads { get; } = new(); + public List CurrentUploads { get; } = []; public bool IsUploading => CurrentUploads.Count > 0; public bool CancelUpload() @@ -81,7 +81,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase foreach (var kvp in data.FileReplacements) { - data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase))); + data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase))); } return data; @@ -102,7 +102,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase 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(); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; } private HashSet GetUnverifiedFiles(CharacterData data) @@ -152,7 +152,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase if (!_mareConfigService.Current.UseAlternativeFileUpload && ex is not OperationCanceledException) { Logger.LogWarning(ex, "[{hash}] Error during file upload, trying alternative file upload", fileHash); - await UploadFileStream(compressedFile, fileHash, true, uploadToken).ConfigureAwait(false); + await UploadFileStream(compressedFile, fileHash, munged: true, uploadToken).ConfigureAwait(false); } else { @@ -196,7 +196,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase 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); + var filesToUpload = await FilesSend([.. unverifiedUploadHashes], visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false); foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) { @@ -215,7 +215,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase foreach (var file in filesToUpload.Where(c => c.IsForbidden)) { - if (_orchestrator.ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) + if (_orchestrator.ForbiddenTransfers.TrueForAll(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) { _orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file) { @@ -248,7 +248,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase 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)))) + foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal)))) { _verifiedUploadedHashes[file] = DateTime.UtcNow; } diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs index 55e6c42..0665b49 100644 --- a/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs @@ -4,8 +4,11 @@ namespace MareSynchronos.WebAPI.Files.Models; public class DownloadFileTransfer : FileTransfer { - private DownloadFileDto Dto => (DownloadFileDto)TransferDto; - public DownloadFileTransfer(DownloadFileDto dto) : base(dto) { } + public DownloadFileTransfer(DownloadFileDto dto) : base(dto) + { + } + + public override bool CanBeTransferred => Dto.FileExists && !Dto.IsForbidden && Dto.Size > 0; public Uri DownloadUri => new(Dto.Url); public override long Total { @@ -15,6 +18,5 @@ public class DownloadFileTransfer : FileTransfer } get => Dto.Size; } - - public override bool CanBeTransferred => Dto.FileExists && !Dto.IsForbidden && Dto.Size > 0; + private DownloadFileDto Dto => (DownloadFileDto)TransferDto; } \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs index 2de1432..13202c8 100644 --- a/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs +++ b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs @@ -7,4 +7,4 @@ public enum DownloadStatus WaitingForQueue, Downloading, Decompressing -} +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs index d018192..8a386ce 100644 --- a/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs +++ b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs @@ -3,8 +3,8 @@ public class FileDownloadStatus { public DownloadStatus DownloadStatus { get; set; } - public int TotalFiles { get; set; } - public int TransferredFiles { get; set; } public long TotalBytes { get; set; } + public int TotalFiles { get; set; } public long TransferredBytes { get; set; } -} + public int TransferredFiles { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs index a778022..f3c0f1e 100644 --- a/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs @@ -11,17 +11,17 @@ public abstract class FileTransfer TransferDto = transferDto; } + public virtual bool CanBeTransferred => !TransferDto.IsForbidden && (TransferDto is not DownloadFileDto dto || dto.FileExists); public string ForbiddenBy => TransferDto.ForbiddenBy; - public long Transferred { get; set; } = 0; - public abstract long Total { get; set; } public string Hash => TransferDto.Hash; + public bool IsForbidden => TransferDto.IsForbidden; public bool IsInTransfer => Transferred != Total && Transferred > 0; public bool IsTransferred => Transferred == Total; - public virtual bool CanBeTransferred => !TransferDto.IsForbidden && (TransferDto is not DownloadFileDto dto || dto.FileExists); - public bool IsForbidden => TransferDto.IsForbidden; + public abstract long Total { get; set; } + public long Transferred { get; set; } = 0; public override string ToString() { return Hash; } -} +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs index 47a3838..fab2efc 100644 --- a/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs +++ b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs @@ -4,7 +4,10 @@ namespace MareSynchronos.WebAPI.Files.Models; public class UploadFileTransfer : FileTransfer { - public UploadFileTransfer(UploadFileDto dto) : base(dto) { } - public override long Total { get; set; } + public UploadFileTransfer(UploadFileDto dto) : base(dto) + { + } + public string LocalFile { get; set; } = string.Empty; -} + public override long Total { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs index 6014c5a..f3d64a9 100644 --- a/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs +++ b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs @@ -1,3 +1,3 @@ namespace MareSynchronos.WebAPI.Files.Models; -public record UploadProgress(long Uploaded, long Size); +public record UploadProgress(long Uploaded, long Size); \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs index acf826d..1ae9602 100644 --- a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -1,4 +1,5 @@ using MareSynchronos.API.Data; +using MareSynchronos.API.Dto; using MareSynchronos.API.Dto.User; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; @@ -6,6 +7,7 @@ using System.Text; namespace MareSynchronos.WebAPI; +#pragma warning disable MA0040 public partial class ApiController { public async Task PushCharacterData(CharacterData data, List visibleCharacters) @@ -14,7 +16,7 @@ public partial class ApiController try { - await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false); + await PushCharacterDataInternal(data, [.. visibleCharacters]).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -44,14 +46,14 @@ public partial class ApiController return await _mareHub!.InvokeAsync>(nameof(UserGetOnlinePairs)).ConfigureAwait(false); } - public async Task> UserGetPairedClients() + public async Task> UserGetPairedClients() { - return await _mareHub!.InvokeAsync>(nameof(UserGetPairedClients)).ConfigureAwait(false); + return await _mareHub!.InvokeAsync>(nameof(UserGetPairedClients)).ConfigureAwait(false); } public async Task UserGetProfile(UserDto dto) { - if (!IsConnected) return new UserProfileDto(dto.User, false, null, null, null); + if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null); return await _mareHub!.InvokeAsync(nameof(UserGetProfile), dto).ConfigureAwait(false); } @@ -90,6 +92,12 @@ public partial class ApiController await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); } + public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto) + { + CheckConnection(); + await _mareHub!.InvokeAsync(nameof(UserUpdateDefaultPermissions), defaultPermissionsDto).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))); @@ -105,4 +113,5 @@ public partial class ApiController Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString()); await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false); } -} \ No newline at end of file +} +#pragma warning restore MA0040 \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index e3204b9..a17d13b 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -25,6 +25,13 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto) + { + Logger.LogDebug("Client_GroupChangeUserPairPermissions: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdateGroupPairPermissions(dto)); + return Task.CompletedTask; + } + public Task Client_GroupDelete(GroupDto groupDto) { Logger.LogTrace("Client_GroupDelete: {dto}", groupDto); @@ -32,17 +39,6 @@ public partial class ApiController return Task.CompletedTask; } - public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto) - { - Logger.LogTrace("Client_GroupPairChangePermissions: {perm}", permissionDto); - ExecuteSafely(() => - { - if (string.Equals(permissionDto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(permissionDto); - else _pairManager.SetGroupPairUserPermissions(permissionDto); - }); - return Task.CompletedTask; - } - public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo) { Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo); @@ -113,10 +109,17 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto) + { + Logger.LogDebug("Client_UpdateUserIndividualPairStatusDto: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdateIndividualPairStatus(dto)); + return Task.CompletedTask; + } + public Task Client_UserAddClientPair(UserPairDto dto) { Logger.LogDebug("Client_UserAddClientPair: {dto}", dto); - ExecuteSafely(() => _pairManager.AddUserPair(dto)); + ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true)); return Task.CompletedTask; } @@ -155,6 +158,13 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_UserUpdateDefaultPermissions(DefaultPermissionsDto dto) + { + Logger.LogDebug("Client_UserUpdateDefaultPermissions: {dto}", dto); + _connectionDto!.DefaultPreferredPermissions = dto; + return Task.CompletedTask; + } + public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) { Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto); @@ -188,18 +198,18 @@ public partial class ApiController _mareHub!.On(nameof(Client_GroupChangePermissions), act); } + public void OnGroupChangeUserPairPermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupChangeUserPairPermissions), 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; @@ -242,12 +252,24 @@ public partial class ApiController _mareHub!.On(nameof(Client_UpdateSystemInfo), act); } + public void OnUpdateUserIndividualPairStatusDto(Action action) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UpdateUserIndividualPairStatusDto), action); + } + public void OnUserAddClientPair(Action act) { if (_initialized) return; _mareHub!.On(nameof(Client_UserAddClientPair), act); } + public void OnUserDefaultPermissionUpdate(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateDefaultPermissions), act); + } + public void OnUserReceiveCharacterData(Action act) { if (_initialized) return; diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs index ec796b5..69ea973 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -42,10 +42,10 @@ public partial class ApiController await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); } - public async Task GroupCreate() + public async Task GroupCreate() { CheckConnection(); - return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); + return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); } public async Task> GroupCreateTempInvite(GroupDto group, int amount) @@ -66,10 +66,16 @@ public partial class ApiController return await _mareHub!.InvokeAsync>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false); } - public async Task GroupJoin(GroupPasswordDto passwordedGroup) + public async Task GroupJoin(GroupPasswordDto passwordedGroup) { CheckConnection(); - return await _mareHub!.InvokeAsync(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false); + return await _mareHub!.InvokeAsync(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false); + } + + public async Task GroupJoinFinalize(GroupJoinDto passwordedGroup) + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupJoinFinalize), passwordedGroup).ConfigureAwait(false); } public async Task GroupLeave(GroupDto group) @@ -96,12 +102,6 @@ public partial class ApiController 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(); diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs index 11492ee..2e08768 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -1,23 +1,21 @@ -using MareSynchronos.API.Routes; -using MareSynchronos.Utils; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; +using Dalamud.Utility; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto; using MareSynchronos.API.SignalR; -using Dalamud.Utility; -using System.Reflection; -using MareSynchronos.WebAPI.SignalR.Utils; -using MareSynchronos.WebAPI.SignalR; using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; -using MareSynchronos.Services; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.API.Data; -using System.Net.Http.Headers; +using MareSynchronos.WebAPI.SignalR; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using System.Reflection; namespace MareSynchronos.WebAPI; +#pragma warning disable MA0040 public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient { public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; @@ -27,21 +25,25 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM private readonly HubFactory _hubFactory; private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverManager; + private readonly TokenProvider _tokenProvider; private CancellationTokenSource _connectionCancellationTokenSource; private ConnectionDto? _connectionDto; private bool _doNotNotifyOnNextInfo = false; private CancellationTokenSource? _healthCheckTokenSource = new(); private bool _initialized; + private string? _lastUsedToken; private HubConnection? _mareHub; private ServerState _serverState; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, - PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) + PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator, + TokenProvider tokenProvider) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; _pairManager = pairManager; _serverManager = serverManager; + _tokenProvider = tokenProvider; _connectionCancellationTokenSource = new CancellationTokenSource(); Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); @@ -49,7 +51,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Mediator.Subscribe(this, (msg) => MareHubOnClosed(msg.Exception)); Mediator.Subscribe(this, (msg) => _ = Task.Run(MareHubOnReconnected)); Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); - Mediator.Subscribe(this, (msg) => CyclePause(msg.UserData)); + Mediator.Subscribe(this, (msg) => _ = CyclePause(msg.UserData)); ServerState = ServerState.Offline; @@ -63,6 +65,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0); + public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null; public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty; public bool IsConnected => ServerState == ServerState.Connected; @@ -94,7 +97,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM return await _mareHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); } - public async Task CreateConnections(bool forceGetToken = false) + public async Task CreateConnections() { Logger.LogDebug("CreateConnections called"); @@ -135,25 +138,14 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM { Logger.LogDebug("Building connection"); - if (_serverManager.GetToken() == null || forceGetToken) + try { - Logger.LogDebug("Requesting new JWT"); - using HttpClient httpClient = new(); - var ver = Assembly.GetExecutingAssembly().GetName().Version; - httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); - var postUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl - .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) - .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); - var auth = secretKey.GetHash256(); - var result = await httpClient.PostAsync(postUri, new FormUrlEncodedContent(new[] - { - new KeyValuePair("auth", auth), - new KeyValuePair("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)), - }), token).ConfigureAwait(false); - AuthFailureMessage = await result.Content.ReadAsStringAsync().ConfigureAwait(false); - result.EnsureSuccessStatusCode(); - _serverManager.SaveToken(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); - Logger.LogDebug("JWT Success"); + _lastUsedToken = await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false); + } + catch (MareAuthFailureException ex) + { + AuthFailureMessage = ex.Reason; + throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized); } while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested) @@ -164,13 +156,11 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM if (token.IsCancellationRequested) break; - _mareHub = _hubFactory.GetOrCreate(); + _mareHub = _hubFactory.GetOrCreate(token); + InitializeApiHooks(); await _mareHub.StartAsync(token).ConfigureAwait(false); - InitializeApiHooks(); - await LoadIninitialPairs().ConfigureAwait(false); - _connectionDto = await GetConnectionDto().ConfigureAwait(false); ServerState = ServerState.Connected; @@ -200,6 +190,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Dalamud.Interface.Internal.Notifications.NotificationType.Error)); } + await LoadIninitialPairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false); } catch (OperationCanceledException) @@ -239,7 +230,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM { var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); var perm = pair.UserPair!.OwnPermissions; - perm.SetPaused(true); + perm.SetPaused(paused: true); await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); // wait until it's changed while (pair.UserPair!.OwnPermissions != perm) @@ -247,7 +238,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM await Task.Delay(250, cts.Token).ConfigureAwait(false); Logger.LogTrace("Waiting for permissions change for {data}", userData); } - perm.SetPaused(false); + perm.SetPaused(paused: false); await UserSetPairPermissions(new API.Dto.User.UserPermissionsDto(userData, perm)).ConfigureAwait(false); }, cts.Token).ContinueWith((t) => cts.Dispose()); @@ -266,7 +257,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM base.Dispose(disposing); _healthCheckTokenSource?.Cancel(); - Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); _connectionCancellationTokenSource?.Cancel(); } @@ -275,19 +266,24 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM while (!ct.IsCancellationRequested && _mareHub != null) { await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + Logger.LogDebug("Checking Client Health State"); + + bool requireReconnect = await RefreshToken(ct).ConfigureAwait(false); + + if (requireReconnect) continue; + _ = await CheckClientHealth().ConfigureAwait(false); - Logger.LogDebug("Checked Client Health State"); } } private void DalamudUtilOnLogIn() { - Task.Run(() => CreateConnections(forceGetToken: true)); + _ = Task.Run(() => CreateConnections()); } private void DalamudUtilOnLogOut() { - Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); ServerState = ServerState.Offline; } @@ -296,28 +292,30 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM if (_mareHub == null) return; Logger.LogDebug("Initializing data"); - OnDownloadReady((guid) => Client_DownloadReady(guid)); - OnReceiveServerMessage((sev, msg) => Client_ReceiveServerMessage(sev, msg)); - OnUpdateSystemInfo((dto) => Client_UpdateSystemInfo(dto)); + OnDownloadReady((guid) => _ = Client_DownloadReady(guid)); + OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg)); + OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto)); - OnUserSendOffline((dto) => Client_UserSendOffline(dto)); - OnUserAddClientPair((dto) => Client_UserAddClientPair(dto)); - OnUserReceiveCharacterData((dto) => Client_UserReceiveCharacterData(dto)); - OnUserRemoveClientPair(dto => Client_UserRemoveClientPair(dto)); - OnUserSendOnline(dto => Client_UserSendOnline(dto)); - OnUserUpdateOtherPairPermissions(dto => Client_UserUpdateOtherPairPermissions(dto)); - OnUserUpdateSelfPairPermissions(dto => Client_UserUpdateSelfPairPermissions(dto)); - OnUserReceiveUploadStatus(dto => Client_UserReceiveUploadStatus(dto)); - OnUserUpdateProfile(dto => Client_UserUpdateProfile(dto)); + OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto)); + OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto)); + OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto)); + OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto)); + OnUserSendOnline(dto => _ = Client_UserSendOnline(dto)); + OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto)); + OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); + OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); + OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); + OnUserDefaultPermissionUpdate(dto => _ = Client_UserUpdateDefaultPermissions(dto)); + OnUpdateUserIndividualPairStatusDto(dto => _ = Client_UpdateUserIndividualPairStatusDto(dto)); - OnGroupChangePermissions((dto) => Client_GroupChangePermissions(dto)); - OnGroupDelete((dto) => Client_GroupDelete(dto)); - OnGroupPairChangePermissions((dto) => Client_GroupPairChangePermissions(dto)); - OnGroupPairChangeUserInfo((dto) => Client_GroupPairChangeUserInfo(dto)); - OnGroupPairJoined((dto) => Client_GroupPairJoined(dto)); - OnGroupPairLeft((dto) => Client_GroupPairLeft(dto)); - OnGroupSendFullInfo((dto) => Client_GroupSendFullInfo(dto)); - OnGroupSendInfo((dto) => Client_GroupSendInfo(dto)); + OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); + OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); + OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto)); + OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto)); + OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto)); + OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto)); + OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); + OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); _healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Dispose(); @@ -329,24 +327,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM private async Task LoadIninitialPairs() { - foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) - { - Logger.LogDebug("Individual Pair: {userPair}", userPair); - _pairManager.AddUserPair(userPair, addToLastAddedUser: false); - } foreach (var entry in await GroupsGetAll().ConfigureAwait(false)) { Logger.LogDebug("Group: {entry}", entry); _pairManager.AddGroup(entry); } - foreach (var group in _pairManager.GroupPairs.Keys) + + foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) { - var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false); - foreach (var user in users) - { - Logger.LogDebug("Group Pair: {user}", user); - _pairManager.AddGroupPair(user); - } + Logger.LogDebug("Individual Pair: {userPair}", userPair); + _pairManager.AddUserPair(userPair); } } @@ -405,6 +395,33 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Logger.LogWarning(arg, "Connection closed... Reconnecting"); } + private async Task RefreshToken(CancellationToken ct) + { + Logger.LogDebug("Checking token"); + + bool requireReconnect = false; + try + { + var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + if (!string.Equals(token, _lastUsedToken, StringComparison.Ordinal)) + { + Logger.LogDebug("Reconnecting due to updated token"); + + _doNotNotifyOnNextInfo = true; + await CreateConnections().ConfigureAwait(false); + requireReconnect = true; + } + } + catch (MareAuthFailureException ex) + { + AuthFailureMessage = ex.Reason; + await StopConnection(ServerState.Unauthorized).ConfigureAwait(false); + requireReconnect = true; + } + + return requireReconnect; + } + private async Task StopConnection(ServerState state) { ServerState = ServerState.Disconnecting; @@ -421,7 +438,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM _connectionDto = null; } - ServerState = state; } -} \ No newline at end of file +} +#pragma warning restore MA0040 \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs index bc7a9ac..e9c6562 100644 --- a/MareSynchronos/WebAPI/SignalR/HubFactory.cs +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -16,28 +16,58 @@ namespace MareSynchronos.WebAPI.SignalR; public class HubFactory : MediatorSubscriberBase { - private readonly ServerConfigurationManager _serverConfigurationManager; private readonly MareConfigService _configService; private readonly IPluginLog _pluginLog; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly TokenProvider _tokenProvider; private HubConnection? _instance; private bool _isDisposed = false; - public HubFactory(ILogger logger, MareMediator mediator, ServerConfigurationManager serverConfigurationManager, MareConfigService configService, - IPluginLog pluginLog) : base(logger, mediator) + public HubFactory(ILogger logger, MareMediator mediator, + ServerConfigurationManager serverConfigurationManager, MareConfigService configService, + TokenProvider tokenProvider, IPluginLog pluginLog) : base(logger, mediator) { _serverConfigurationManager = serverConfigurationManager; _configService = configService; + _tokenProvider = tokenProvider; _pluginLog = pluginLog; } - private HubConnection BuildHubConnection() + public async Task DisposeHubAsync() + { + if (_instance == null || _isDisposed) return; + + Logger.LogDebug("Disposing current HubConnection"); + + _isDisposed = true; + + _instance.Closed -= HubOnClosed; + _instance.Reconnecting -= HubOnReconnecting; + _instance.Reconnected -= HubOnReconnected; + + await _instance.StopAsync().ConfigureAwait(false); + await _instance.DisposeAsync().ConfigureAwait(false); + + _instance = null; + + Logger.LogDebug("Current HubConnection disposed"); + } + + public HubConnection GetOrCreate(CancellationToken ct) + { + if (!_isDisposed && _instance != null) return _instance; + + return BuildHubConnection(ct); + } + + private HubConnection BuildHubConnection(CancellationToken ct) { Logger.LogDebug("Building new HubConnection"); _instance = new HubConnectionBuilder() .WithUrl(_serverConfigurationManager.CurrentApiUrl + IMareHub.Path, options => { - options.Headers.Add("Authorization", "Bearer " + _serverConfigurationManager.GetToken()); + options.AccessTokenProvider = () => _tokenProvider.GetOrUpdateToken(ct); options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; }) .AddMessagePackProtocol(opt => @@ -76,6 +106,12 @@ public class HubFactory : MediatorSubscriberBase return _instance; } + private Task HubOnClosed(Exception? arg) + { + Mediator.Publish(new HubClosedMessage(arg)); + return Task.CompletedTask; + } + private Task HubOnReconnected(string? arg) { Mediator.Publish(new HubReconnectedMessage(arg)); @@ -87,37 +123,4 @@ public class HubFactory : MediatorSubscriberBase Mediator.Publish(new HubReconnectingMessage(arg)); return Task.CompletedTask; } - - private Task HubOnClosed(Exception? arg) - { - Mediator.Publish(new HubClosedMessage(arg)); - return Task.CompletedTask; - } - - public HubConnection GetOrCreate() - { - if (!_isDisposed && _instance != null) return _instance; - - return BuildHubConnection(); - } - - public async Task DisposeHubAsync() - { - if (_instance == null || _isDisposed) return; - - Logger.LogDebug("Disposing current HubConnection"); - - _isDisposed = true; - - _instance.Closed -= HubOnClosed; - _instance.Reconnecting -= HubOnReconnecting; - _instance.Reconnected -= HubOnReconnected; - - await _instance.StopAsync().ConfigureAwait(false); - await _instance.DisposeAsync().ConfigureAwait(false); - - _instance = null; - - Logger.LogDebug("Current HubConnection disposed"); - } -} +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs new file mode 100644 index 0000000..fa6c2f5 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs @@ -0,0 +1,3 @@ +namespace MareSynchronos.WebAPI.SignalR; + +public record JwtIdentifier(string ApiUrl, string SecretKey); \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs b/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs new file mode 100644 index 0000000..10620e8 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.WebAPI.SignalR; + +public class MareAuthFailureException : Exception +{ + public MareAuthFailureException(string reason) + { + Reason = reason; + } + + public string Reason { get; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs new file mode 100644 index 0000000..5940979 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -0,0 +1,118 @@ +using MareSynchronos.API.Routes; +using MareSynchronos.Services; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Reflection; + +namespace MareSynchronos.WebAPI.SignalR; + +public sealed class TokenProvider : IDisposable +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly ServerConfigurationManager _serverManager; + private readonly ConcurrentDictionary _tokenCache = new(); + + public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil) + { + _logger = logger; + _serverManager = serverManager; + _dalamudUtil = dalamudUtil; + _httpClient = new(); + var ver = Assembly.GetExecutingAssembly().GetName().Version; + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + } + + private JwtIdentifier CurrentIdentifier => new(_serverManager.CurrentApiUrl, _serverManager.GetSecretKey()!); + + public void Dispose() + { + _httpClient.Dispose(); + } + + public async Task GetNewToken(bool isRenewal, CancellationToken token) + { + Uri tokenUri; + string response = string.Empty; + HttpResponseMessage result; + + try + { + if (!isRenewal) + { + _logger.LogDebug("GetNewToken: Requesting"); + + tokenUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl + .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) + .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); + var secretKey = _serverManager.GetSecretKey()!; + var auth = secretKey.GetHash256(); + result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(new[] + { + new KeyValuePair("auth", auth), + new KeyValuePair("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)), + }), token).ConfigureAwait(false); + } + else + { + _logger.LogDebug("GetNewToken: Renewal"); + + tokenUri = MareAuth.RenewTokenFullPath(new Uri(_serverManager.CurrentApiUrl + .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) + .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); + HttpRequestMessage request = new(HttpMethod.Get, tokenUri.ToString()); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenCache[CurrentIdentifier]); + result = await _httpClient.SendAsync(request, token).ConfigureAwait(false); + } + + response = await result.Content.ReadAsStringAsync().ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + _tokenCache[CurrentIdentifier] = response; + } + catch (HttpRequestException ex) + { + _tokenCache.TryRemove(CurrentIdentifier, out _); + + _logger.LogError(ex, "GetNewToken: Failure to get token"); + + if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new MareAuthFailureException(response); + } + + throw; + } + + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(response); + _logger.LogTrace("GetNewToken: JWT {token}", response); + _logger.LogDebug("GetNewToken: Valid until {date}, ValidClaim until {date}", jwtToken.ValidTo, + new DateTime(long.Parse(jwtToken.Claims.Single(c => string.Equals(c.Type, "expiration_date", StringComparison.Ordinal)).Value), DateTimeKind.Utc)); + return response; + } + + public async Task GetOrUpdateToken(CancellationToken ct, bool forceRenew = false) + { + bool renewal = forceRenew; + if (!forceRenew && _tokenCache.TryGetValue(CurrentIdentifier, out var token)) + { + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + if (jwtToken.ValidTo == DateTime.MinValue || jwtToken.ValidTo.Subtract(TimeSpan.FromMinutes(5)) > DateTime.UtcNow) + { + _logger.LogTrace("GetOrUpdate: Returning token from cache"); + return token; + } + + renewal = true; + } + + _logger.LogTrace("GetOrUpdate: Getting new token"); + return await GetNewToken(renewal, ct).ConfigureAwait(false); + } +} \ No newline at end of file