diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7be38b9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,102 @@ +[*.cs] + +# MA0046: Use EventHandler to declare events +dotnet_diagnostic.MA0046.severity = silent +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +dotnet_diagnostic.MA0016.severity = suggestion +dotnet_diagnostic.MA0051.severity = suggestion + +# MA0009: Add regex evaluation timeout +dotnet_diagnostic.MA0009.severity = silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case diff --git a/MareAPI b/MareAPI index 9dc1e90..2d5d9d9 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 9dc1e901aa453e65bb432b2b288c1ff00021700a +Subproject commit 2d5d9d9d1cc87eebcf89cddc182e2c47fe31a8b4 diff --git a/MareSynchronos.sln b/MareSynchronos.sln index d92ff59..f38743f 100644 --- a/MareSynchronos.sln +++ b/MareSynchronos.sln @@ -9,6 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra\Penumbra.GameData\Penumbra.GameData.csproj", "{89DD407C-B2B7-4BB3-BF26-C550BA1841F8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index 017ef81..36e2ffd 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -7,195 +7,212 @@ using System.Linq; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos +namespace MareSynchronos; + +public static class ConfigurationExtensions { - public static class ConfigurationExtensions + public static bool HasValidSetup(this Configuration configuration) { - public static bool HasValidSetup(this Configuration configuration) - { - return configuration.AcceptedAgreement && configuration.InitialScanComplete - && !string.IsNullOrEmpty(configuration.CacheFolder) - && Directory.Exists(configuration.CacheFolder) - && configuration.ClientSecret.ContainsKey(configuration.ApiUri); - } - - public static Dictionary GetCurrentServerUidComments(this Configuration configuration) - { - return configuration.UidServerComments.ContainsKey(configuration.ApiUri) - ? configuration.UidServerComments[configuration.ApiUri] - : new Dictionary(); - } - - public static void SetCurrentServerUidComment(this Configuration configuration, string uid, string comment) - { - if (!configuration.UidServerComments.ContainsKey(configuration.ApiUri)) - { - configuration.UidServerComments[configuration.ApiUri] = new Dictionary(); - } - - configuration.UidServerComments[configuration.ApiUri][uid] = comment; - } + return configuration.AcceptedAgreement && configuration.InitialScanComplete + && !string.IsNullOrEmpty(configuration.CacheFolder) + && Directory.Exists(configuration.CacheFolder) + && configuration.ClientSecret.ContainsKey(configuration.ApiUri); } - [Serializable] - public class Configuration : IPluginConfiguration + public static Dictionary GetCurrentServerUidComments(this Configuration configuration) { - private string _apiUri = string.Empty; - private int _maxParallelScan = 10; - [NonSerialized] - private DalamudPluginInterface? _pluginInterface; + return configuration.UidServerComments.ContainsKey(configuration.ApiUri) + ? configuration.UidServerComments[configuration.ApiUri] + : new Dictionary(StringComparer.Ordinal); + } - public bool DarkSoulsAgreement { get; set; } = false; - public bool AcceptedAgreement { get; set; } = false; - public string ApiUri + public static Dictionary GetCurrentServerGidComments(this Configuration configuration) + { + return configuration.GidServerComments.ContainsKey(configuration.ApiUri) + ? configuration.GidServerComments[configuration.ApiUri] + : new Dictionary(StringComparer.Ordinal); + } + + public static void SetCurrentServerGidComment(this Configuration configuration, string gid, string comment) + { + if (!configuration.GidServerComments.ContainsKey(configuration.ApiUri)) { - get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; - set => _apiUri = value; + configuration.GidServerComments[configuration.ApiUri] = new Dictionary(StringComparer.Ordinal); } - public string CacheFolder { get; set; } = string.Empty; - public Dictionary ClientSecret { get; set; } = new(); - public Dictionary CustomServerList { get; set; } = new(); - public int MaxLocalCacheInGiB { get; set; } = 20; - public bool ReverseUserSort { get; set; } = true; + configuration.GidServerComments[configuration.ApiUri][gid] = comment; + } - 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 Dictionary> UidServerComments { get; set; } = new(); - - public Dictionary UidComments { get; set; } = new(); - public int Version { get; set; } = 5; - - public bool ShowTransferWindow { get; set; } = true; - - // the below exist just to make saving less cumbersome - public void Initialize(DalamudPluginInterface pluginInterface) + public static void SetCurrentServerUidComment(this Configuration configuration, string uid, string comment) + { + if (!configuration.UidServerComments.ContainsKey(configuration.ApiUri)) { - _pluginInterface = pluginInterface; + configuration.UidServerComments[configuration.ApiUri] = new Dictionary(StringComparer.Ordinal); + } - if (!Directory.Exists(CacheFolder)) + configuration.UidServerComments[configuration.ApiUri][uid] = comment; + } +} + +[Serializable] +public class Configuration : IPluginConfiguration +{ + private string _apiUri = string.Empty; + private int _maxParallelScan = 10; + [NonSerialized] + private DalamudPluginInterface? _pluginInterface; + + public bool DarkSoulsAgreement { get; set; } = false; + public bool AcceptedAgreement { get; set; } = false; + public string ApiUri + { + get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; + set => _apiUri = value; + } + + public string CacheFolder { get; set; } = string.Empty; + public Dictionary ClientSecret { get; set; } = new(StringComparer.Ordinal); + public Dictionary CustomServerList { get; set; } = new(StringComparer.Ordinal); + public int 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 Dictionary> UidServerComments { get; set; } = new(StringComparer.Ordinal); + public Dictionary> GidServerComments { get; set; } = new(StringComparer.Ordinal); + + public Dictionary UidComments { get; set; } = new(StringComparer.Ordinal); + public int Version { get; set; } = 5; + + public bool ShowTransferWindow { get; set; } = true; + + // the below exist just to make saving less cumbersome + public void Initialize(DalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + + if (!Directory.Exists(CacheFolder)) + { + InitialScanComplete = false; + } + + Save(); + } + + public void Save() + { + _pluginInterface!.SavePluginConfig(this); + } + + public void Migrate() + { + if (Version == 0) + { + Logger.Debug("Migrating Configuration from V0 to V1"); + Version = 1; + ApiUri = ApiUri.Replace("https", "wss", StringComparison.Ordinal); + foreach (var kvp in ClientSecret.ToList()) { - InitialScanComplete = false; + var newKey = kvp.Key.Replace("https", "wss", StringComparison.Ordinal); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) + { + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); + } } - + UidServerComments.Add(ApiUri, UidComments.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal)); + UidComments.Clear(); Save(); } - public void Save() + if (Version == 1) { - _pluginInterface!.SavePluginConfig(this); + Logger.Debug("Migrating Configuration from V1 to V2"); + ApiUri = ApiUri.Replace("5001", "5000", StringComparison.Ordinal); + foreach (var kvp in ClientSecret.ToList()) + { + var newKey = kvp.Key.Replace("5001", "5000", StringComparison.Ordinal); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) + { + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); + } + } + + foreach (var kvp in UidServerComments.ToList()) + { + var newKey = kvp.Key.Replace("5001", "5000", StringComparison.Ordinal); + UidServerComments.Remove(kvp.Key); + UidServerComments.Add(newKey, kvp.Value); + } + + Version = 2; + Save(); } - public void Migrate() + if (Version == 2) { - if (Version == 0) + Logger.Debug("Migrating Configuration from V2 to V3"); + ApiUri = "wss://v2202207178628194299.powersrv.de:6871"; + ClientSecret.Clear(); + UidServerComments.Clear(); + + Version = 3; + Save(); + } + + if (Version == 3) + { + Logger.Debug("Migrating Configuration from V3 to V4"); + + ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872", StringComparison.Ordinal); + foreach (var kvp in ClientSecret.ToList()) { - Logger.Debug("Migrating Configuration from V0 to V1"); - Version = 1; - ApiUri = ApiUri.Replace("https", "wss"); - foreach (var kvp in ClientSecret.ToList()) + var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872", StringComparison.Ordinal); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) { - var newKey = kvp.Key.Replace("https", "wss"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); } - UidServerComments.Add(ApiUri, UidComments.ToDictionary(k => k.Key, k => k.Value)); - UidComments.Clear(); - Save(); } - if (Version == 1) + foreach (var kvp in UidServerComments.ToList()) { - Logger.Debug("Migrating Configuration from V1 to V2"); - ApiUri = ApiUri.Replace("5001", "5000"); - foreach (var kvp in ClientSecret.ToList()) - { - var newKey = kvp.Key.Replace("5001", "5000"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } - } - - foreach (var kvp in UidServerComments.ToList()) - { - var newKey = kvp.Key.Replace("5001", "5000"); - UidServerComments.Remove(kvp.Key); - UidServerComments.Add(newKey, kvp.Value); - } - - Version = 2; - Save(); + var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872", StringComparison.Ordinal); + UidServerComments.Remove(kvp.Key); + UidServerComments.Add(newKey, kvp.Value); } - if (Version == 2) - { - Logger.Debug("Migrating Configuration from V2 to V3"); - ApiUri = "wss://v2202207178628194299.powersrv.de:6871"; - ClientSecret.Clear(); - UidServerComments.Clear(); + Version = 4; + Save(); + } - Version = 3; - Save(); - } + if (Version == 4) + { + Logger.Debug("Migrating Configuration from V4 to V5"); - if (Version == 3) - { - Logger.Debug("Migrating Configuration from V3 to V4"); + ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6872", "wss://maresynchronos.com", StringComparison.Ordinal); + ClientSecret.Remove("wss://v2202207178628194299.powersrv.de:6872"); + UidServerComments.Remove("wss://v2202207178628194299.powersrv.de:6872"); - ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - foreach (var kvp in ClientSecret.ToList()) - { - var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } - } - - foreach (var kvp in UidServerComments.ToList()) - { - var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - UidServerComments.Remove(kvp.Key); - UidServerComments.Add(newKey, kvp.Value); - } - - Version = 4; - Save(); - } - - if (Version == 4) - { - Logger.Debug("Migrating Configuration from V4 to V5"); - - ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6872", "wss://maresynchronos.com"); - ClientSecret.Remove("wss://v2202207178628194299.powersrv.de:6872"); - UidServerComments.Remove("wss://v2202207178628194299.powersrv.de:6872"); - - Version = 5; - Save(); - } + Version = 5; + Save(); } } } diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index f7e767b..f2cadd0 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -158,7 +158,7 @@ public class CharacterDataFactory if (cache.FileReplacements.ContainsKey(objectKind)) { - if (cache.FileReplacements[objectKind].Any(c => c.ResolvedPath.Contains(mtrlPath))) + if (cache.FileReplacements[objectKind].Any(c => c.ResolvedPath.Contains(mtrlPath, StringComparison.Ordinal))) { return; } @@ -196,7 +196,7 @@ public class CharacterDataFactory if (cache.FileReplacements.ContainsKey(objectKind)) { - if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath))) + if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath, StringComparer.Ordinal))) { return; } @@ -214,7 +214,7 @@ public class CharacterDataFactory if (cache.FileReplacements.ContainsKey(objectKind)) { - if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(texPath))) + if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(texPath, StringComparer.Ordinal))) { return; } @@ -225,7 +225,7 @@ public class CharacterDataFactory cache.AddFileReplacement(objectKind, texFileReplacement); - if (texPath.Contains("/--")) return; + if (texPath.Contains("/--", StringComparison.Ordinal)) return; var texDx11Replacement = CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve); @@ -322,13 +322,13 @@ public class CharacterDataFactory previousData.FileReplacements.Add(objectKind, new()); } - if (!previousData.FileReplacements[objectKind].Any(k => k.GamePaths.Any(p => item.GamePaths.Select(p => p.ToLowerInvariant()).Contains(p.ToLowerInvariant())))) + if (!previousData.FileReplacements[objectKind].Any(k => k.GamePaths.Any(p => item.GamePaths.Contains(p, StringComparer.OrdinalIgnoreCase)))) { var penumResolve = _ipcManager.PenumbraResolvePath(item.GamePaths.First()).ToLowerInvariant(); var gamePath = item.GamePaths.First().ToLowerInvariant(); - if (penumResolve == gamePath) + if (string.Equals(penumResolve, gamePath, StringComparison.Ordinal)) { - Logger.Debug("PenumResolve was same as GamePath, not adding " + item); + Logger.Verbose("PenumResolve was same as GamePath, not adding " + item); transientResourceManager.RemoveTransientResource(charaPointer, item); } else diff --git a/MareSynchronos/FileCache/FileCache.cs b/MareSynchronos/FileCache/FileCache.cs index 38aceb9..125173d 100644 --- a/MareSynchronos/FileCache/FileCache.cs +++ b/MareSynchronos/FileCache/FileCache.cs @@ -6,14 +6,14 @@ using System.Globalization; namespace MareSynchronos.FileCache; -public class FileCache +public class FileCacheEntity { public string ResolvedFilepath { get; private set; } = string.Empty; public string Hash { get; set; } public string PrefixedFilePath { get; init; } public string LastModifiedDateTicks { get; set; } - public FileCache(string hash, string path, string lastModifiedDateTicks) + public FileCacheEntity(string hash, string path, string lastModifiedDateTicks) { Hash = hash; PrefixedFilePath = path; @@ -22,7 +22,7 @@ public class FileCache public void SetResolvedFilePath(string filePath) { - ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\"); + ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", System.StringComparison.Ordinal); } public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks.ToString(CultureInfo.InvariantCulture)}"; diff --git a/MareSynchronos/FileCache/FileDbManager.cs b/MareSynchronos/FileCache/FileDbManager.cs index 7d97516..e2dee5c 100644 --- a/MareSynchronos/FileCache/FileDbManager.cs +++ b/MareSynchronos/FileCache/FileDbManager.cs @@ -26,9 +26,9 @@ public class FileCacheManager : IDisposable private readonly Configuration _configuration; private readonly string CsvPath; private string CsvBakPath => CsvPath + ".bak"; - private readonly ConcurrentDictionary FileCaches = new(); + private readonly ConcurrentDictionary FileCaches = new(StringComparer.Ordinal); public const string CsvSplit = "|"; - private object _fileWriteLock = new object(); + private object _fileWriteLock = new(); public FileCacheManager(IpcManager ipcManager, Configuration configuration, string configDirectoryName) { @@ -52,7 +52,7 @@ public class FileCacheManager : IDisposable var hash = splittedEntry[0]; var path = splittedEntry[1]; var time = splittedEntry[2]; - FileCaches[path] = new FileCache(hash, path, time); + FileCaches[path] = new FileCacheEntity(hash, path, time); } catch (Exception) { @@ -64,7 +64,7 @@ public class FileCacheManager : IDisposable public void WriteOutFullCsv() { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); foreach (var entry in FileCaches.OrderBy(f => f.Value.PrefixedFilePath)) { sb.AppendLine(entry.Value.CsvEntry); @@ -87,19 +87,19 @@ public class FileCacheManager : IDisposable } } - public List GetAllFileCaches() => FileCaches.Values.ToList(); + public List GetAllFileCaches() => FileCaches.Values.ToList(); - public FileCache? GetFileCacheByHash(string hash) + public FileCacheEntity? GetFileCacheByHash(string hash) { - if (FileCaches.Any(f => f.Value.Hash == hash)) + if (FileCaches.Any(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal))) { - return GetValidatedFileCache(FileCaches.FirstOrDefault(f => f.Value.Hash == hash).Value); + return GetValidatedFileCache(FileCaches.FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)).Value); } return null; } - public (FileState, FileCache) ValidateFileCacheEntity(FileCache fileCache) + public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache) { fileCache = ReplacePathPrefixes(fileCache); FileInfo fi = new(fileCache.ResolvedFilepath); @@ -107,7 +107,7 @@ public class FileCacheManager : IDisposable { return (FileState.RequireDeletion, fileCache); } - if (fi.LastWriteTimeUtc.Ticks.ToString() != fileCache.LastModifiedDateTicks) + if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) { return (FileState.RequireUpdate, fileCache); } @@ -115,10 +115,10 @@ public class FileCacheManager : IDisposable return (FileState.Valid, fileCache); } - public FileCache? GetFileCacheByPath(string path) + public FileCacheEntity? GetFileCacheByPath(string path) { - var cleanedPath = path.Replace("/", "\\").ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), ""); - var entry = FileCaches.FirstOrDefault(f => f.Value.ResolvedFilepath.EndsWith(cleanedPath)).Value; + var cleanedPath = path.Replace("/", "\\", StringComparison.Ordinal).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), "", StringComparison.Ordinal); + var entry = FileCaches.FirstOrDefault(f => f.Value.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.Ordinal)).Value; if (entry == null) { @@ -131,35 +131,35 @@ public class FileCacheManager : IDisposable return validatedCacheEntry; } - public FileCache? CreateCacheEntry(string path) + public FileCacheEntity? CreateCacheEntry(string path) { - Logger.Debug("Creating cache entry for " + path); + Logger.Verbose("Creating cache entry for " + path); FileInfo fi = new(path); if (!fi.Exists) return null; var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_configuration.CacheFolder.ToLowerInvariant())) return null; - string prefixedPath = fullName.Replace(_configuration.CacheFolder.ToLowerInvariant(), CachePrefix + "\\").Replace("\\\\", "\\"); - return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper()); + if (!fullName.Contains(_configuration.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_configuration.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper(CultureInfo.InvariantCulture)); } - public FileCache? CreateFileEntry(string path) + public FileCacheEntity? CreateFileEntry(string path) { - Logger.Debug("Creating file entry for " + path); + Logger.Verbose("Creating file entry for " + path); FileInfo fi = new(path); if (!fi.Exists) return null; var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant())) return null; - string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), PenumbraPrefix + "\\").Replace("\\\\", "\\"); + if (!fullName.Contains(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); return CreateFileCacheEntity(fi, prefixedPath); } - private FileCache? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) + private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { if (hash == null) { hash = Crypto.GetFileHash(fileInfo.FullName); } - var entity = new FileCache(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)); + var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)); entity = ReplacePathPrefixes(entity); FileCaches[prefixedPath] = entity; lock (_fileWriteLock) @@ -171,14 +171,14 @@ public class FileCacheManager : IDisposable return result; } - private FileCache? GetValidatedFileCache(FileCache fileCache) + private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) { var resulingFileCache = ReplacePathPrefixes(fileCache); resulingFileCache = Validate(resulingFileCache); return resulingFileCache; } - private FileCache? Validate(FileCache fileCache) + private FileCacheEntity? Validate(FileCacheEntity fileCache) { var file = new FileInfo(fileCache.ResolvedFilepath); if (!file.Exists) @@ -187,7 +187,7 @@ public class FileCacheManager : IDisposable return null; } - if (file.LastWriteTimeUtc.Ticks.ToString() != fileCache.LastModifiedDateTicks) + if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) { UpdateHash(fileCache); } @@ -195,12 +195,12 @@ public class FileCacheManager : IDisposable return fileCache; } - public void RemoveHash(FileCache entity) + public void RemoveHash(FileCacheEntity entity) { FileCaches.Remove(entity.Hash, out _); } - public void UpdateHash(FileCache fileCache) + public void UpdateHash(FileCacheEntity fileCache) { Logger.Debug("Updating hash for " + fileCache.ResolvedFilepath); fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); @@ -209,15 +209,15 @@ public class FileCacheManager : IDisposable FileCaches[fileCache.PrefixedFilePath] = fileCache; } - private FileCache ReplacePathPrefixes(FileCache fileCache) + private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) { - if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix)) + if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase)) { - fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(PenumbraPrefix, _ipcManager.PenumbraModDirectory())); + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(PenumbraPrefix, _ipcManager.PenumbraModDirectory(), StringComparison.Ordinal)); } - else if (fileCache.PrefixedFilePath.StartsWith(CachePrefix)) + else if (fileCache.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase)) { - fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(CachePrefix, _configuration.CacheFolder)); + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(CachePrefix, _configuration.CacheFolder, StringComparison.Ordinal)); } return fileCache; diff --git a/MareSynchronos/FileCache/PeriodicFileScanner.cs b/MareSynchronos/FileCache/PeriodicFileScanner.cs index 72c5ae6..6181c2c 100644 --- a/MareSynchronos/FileCache/PeriodicFileScanner.cs +++ b/MareSynchronos/FileCache/PeriodicFileScanner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -19,7 +20,7 @@ public class PeriodicFileScanner : IDisposable private readonly DalamudUtil _dalamudUtil; private CancellationTokenSource? _scanCancellationTokenSource; private Task? _fileScannerTask = null; - public ConcurrentDictionary haltScanLocks = new(); + public ConcurrentDictionary haltScanLocks = new(StringComparer.Ordinal); public PeriodicFileScanner(IpcManager ipcManager, Configuration pluginConfiguration, FileCacheManager fileDbManager, ApiController apiController, DalamudUtil dalamudUtil) { Logger.Verbose("Creating " + nameof(PeriodicFileScanner)); @@ -127,7 +128,7 @@ public class PeriodicFileScanner : IDisposable { while (haltScanLocks.Any(f => f.Value > 0)) { - await Task.Delay(TimeSpan.FromSeconds(1)); + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); } isForced |= RecalculateFileCacheSize(); @@ -143,7 +144,7 @@ public class PeriodicFileScanner : IDisposable _timeUntilNextScan = TimeSpan.FromSeconds(timeBetweenScans); while (_timeUntilNextScan.TotalSeconds >= 0) { - await Task.Delay(TimeSpan.FromSeconds(1), token); + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); _timeUntilNextScan -= TimeSpan.FromSeconds(1); } } @@ -203,13 +204,16 @@ public class PeriodicFileScanner : IDisposable Logger.Debug("Getting files from " + penumbraDir + " and " + _pluginConfiguration.CacheFolder); string[] ext = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp" }; - var scannedFiles = Directory.EnumerateFiles(penumbraDir, "*.*", SearchOption.AllDirectories) + var scannedFiles = new ConcurrentDictionary(Directory.EnumerateFiles(penumbraDir, "*.*", SearchOption.AllDirectories) .Select(s => s.ToLowerInvariant()) - .Where(f => ext.Any(e => f.EndsWith(e)) && !f.Contains(@"\bg\") && !f.Contains(@"\bgcommon\") && !f.Contains(@"\ui\")) + .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)) .Concat(Directory.EnumerateFiles(_pluginConfiguration.CacheFolder, "*.*", SearchOption.TopDirectoryOnly) .Where(f => new FileInfo(f).Name.Length == 40) .Select(s => s.ToLowerInvariant()).ToList()) - .ToDictionary(c => c, c => false); + .Select(c => new KeyValuePair(c, false)), StringComparer.OrdinalIgnoreCase); TotalFiles = scannedFiles.Count; @@ -217,8 +221,8 @@ public class PeriodicFileScanner : IDisposable var cpuCount = (int)(Environment.ProcessorCount / 2.0f); Task[] dbTasks = Enumerable.Range(0, cpuCount).Select(c => Task.CompletedTask).ToArray(); - ConcurrentBag entitiesToRemove = new(); - ConcurrentBag entitiesToUpdate = new(); + ConcurrentBag entitiesToRemove = new(); + ConcurrentBag entitiesToUpdate = new(); try { foreach (var cache in _fileDbManager.GetAllFileCaches()) @@ -280,7 +284,7 @@ public class PeriodicFileScanner : IDisposable _fileDbManager.WriteOutFullCsv(); } - Logger.Debug("Scanner validated existing db files"); + Logger.Verbose("Scanner validated existing db files"); if (ct.IsCancellationRequested) return; @@ -311,7 +315,7 @@ public class PeriodicFileScanner : IDisposable Task.WaitAll(dbTasks); - Logger.Debug("Scanner added new files to db"); + Logger.Verbose("Scanner added new files to db"); Logger.Debug("Scan complete"); TotalFiles = 0; diff --git a/MareSynchronos/Interop/Weapon.cs b/MareSynchronos/Interop/Weapon.cs index 5670471..b02cbf2 100644 --- a/MareSynchronos/Interop/Weapon.cs +++ b/MareSynchronos/Interop/Weapon.cs @@ -4,35 +4,34 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Interop.Structs; -namespace MareSynchronos.Interop +namespace MareSynchronos.Interop; + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct Weapon { - [StructLayout(LayoutKind.Explicit)] - public unsafe struct Weapon - { - [FieldOffset(0x18)] public IntPtr Parent; - [FieldOffset(0x20)] public IntPtr NextSibling; - [FieldOffset(0x28)] public IntPtr PreviousSibling; - [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct WeaponDrawObject - { - [FieldOffset(0x00)] public RenderModel* RenderModel; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct HumanExt - { - [FieldOffset(0x0)] public Human Human; - [FieldOffset(0x9E8)] public ResourceHandle* Decal; - [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct CharaExt - { - [FieldOffset(0x0)] public Character Character; - [FieldOffset(0x650)] public Character* Mount; - } + [FieldOffset(0x18)] public IntPtr Parent; + [FieldOffset(0x20)] public IntPtr NextSibling; + [FieldOffset(0x28)] public IntPtr PreviousSibling; + [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct WeaponDrawObject +{ + [FieldOffset(0x00)] public RenderModel* RenderModel; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct HumanExt +{ + [FieldOffset(0x0)] public Human Human; + [FieldOffset(0x9E8)] public ResourceHandle* Decal; + [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct CharaExt +{ + [FieldOffset(0x0)] public Character Character; + [FieldOffset(0x650)] public Character* Mount; } diff --git a/MareSynchronos/Localization/Strings.cs b/MareSynchronos/Localization/Strings.cs index e55f8e8..4f1da9c 100644 --- a/MareSynchronos/Localization/Strings.cs +++ b/MareSynchronos/Localization/Strings.cs @@ -1,68 +1,67 @@ using CheapLoc; -namespace MareSynchronos.Localization +namespace MareSynchronos.Localization; + +public static class Strings { - public static class Strings + public class ToSStrings { - public class ToSStrings - { - public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); - 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 LanguageLabel = Loc.Localize("LanguageLabel", "Language"); + 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 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. " + - "The plugin will exclusively upload the necessary mod files and not the whole mod."); + 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. " + + "The plugin will exclusively upload the necessary mod files and not the whole mod."); - public readonly string Paragraph2 = Loc.Localize("Paragraph2", - "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " + - "Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " + - "Files present on the service that already represent your active mod files will not be uploaded again."); + public readonly string Paragraph2 = Loc.Localize("Paragraph2", + "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " + + "Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " + + "Files present on the service that already represent your active mod files will not be uploaded again."); - public readonly string Paragraph3 = Loc.Localize("Paragraph3", - "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " + - "Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " + - "Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."); + public readonly string Paragraph3 = Loc.Localize("Paragraph3", + "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " + + "Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " + + "Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."); - public readonly string Paragraph4 = Loc.Localize("Paragraph4", - "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."); + public readonly string Paragraph4 = Loc.Localize("Paragraph4", + "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."); - public readonly string Paragraph5 = Loc.Localize("Paragraph5", - "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " + - "After a period of not being used, the mod files will be automatically deleted. " + - "You will also be able to wipe all the files you have personally uploaded on request. " + - "The service holds no information about which mod files belong to which mod."); + public readonly string Paragraph5 = Loc.Localize("Paragraph5", + "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " + + "After a period of not being used, the mod files will be automatically deleted. " + + "You will also be able to wipe all the files you have personally uploaded on request. " + + "The service holds no information about which mod files belong to which mod."); - public readonly string Paragraph6 = Loc.Localize("Paragraph6", - "This service is provided as-is. In case of abuse, contact darkarchon#4313 on Discord or join the Mare Synchronos Discord. " + - "To accept those conditions hold CTRL while clicking 'I agree'"); + public readonly string Paragraph6 = Loc.Localize("Paragraph6", + "This service is provided as-is. In case of abuse, contact darkarchon#4313 on Discord or join the Mare Synchronos Discord. " + + "To accept those conditions hold CTRL while clicking 'I agree'"); - public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); + public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); - public readonly string RemainingLabel = Loc.Localize("RemainingLabel", "remaining"); + public readonly string RemainingLabel = Loc.Localize("RemainingLabel", "remaining"); - public readonly string FailedLabel = Loc.Localize("FailedLabel", - "Congratulations. You have failed to read the agreements."); + public readonly string FailedLabel = Loc.Localize("FailedLabel", + "Congratulations. You have failed to read the agreements."); - public readonly string TimeoutLabel = Loc.Localize("TimeoutLabel", - "I'm going to give you 1 minute to read the agreements carefully again. If you fail once more you will have to solve an annoying puzzle."); + public readonly string TimeoutLabel = Loc.Localize("TimeoutLabel", + "I'm going to give you 1 minute to read the agreements carefully again. If you fail once more you will have to solve an annoying puzzle."); - public readonly string FailedAgainLabel = Loc.Localize("FailedAgainLabel", - "Congratulations. You have failed to read the agreements. Again."); + public readonly string FailedAgainLabel = Loc.Localize("FailedAgainLabel", + "Congratulations. You have failed to read the agreements. Again."); - public readonly string PuzzleLabel = Loc.Localize("PuzzleLabel", - "I did warn you. Here's your annoying puzzle:"); + public readonly string PuzzleLabel = Loc.Localize("PuzzleLabel", + "I did warn you. Here's your annoying puzzle:"); - public readonly string PuzzleDescLabel = Loc.Localize("PuzzleDescLabel", - "Enter the following 3 words from the agreement exactly as described without punctuation to make the \"I agree\" button visible again."); + public readonly string PuzzleDescLabel = Loc.Localize("PuzzleDescLabel", + "Enter the following 3 words from the agreement exactly as described without punctuation to make the \"I agree\" button visible again."); - public readonly string ParagraphLabel = Loc.Localize("ParagraphLabel", "Paragraph"); + public readonly string ParagraphLabel = Loc.Localize("ParagraphLabel", "Paragraph"); - public readonly string SentenceLabel = Loc.Localize("SentenceLabel", "Sentence"); + public readonly string SentenceLabel = Loc.Localize("SentenceLabel", "Sentence"); - public readonly string WordLabel = Loc.Localize("WordLabel", "Word"); - } - - public static ToSStrings ToS { get; set; } = new(); + public readonly string WordLabel = Loc.Localize("WordLabel", "Word"); } + + public static ToSStrings ToS { get; set; } = new(); } \ No newline at end of file diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index b85d936..70318a4 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -71,7 +71,7 @@ public class CachedPlayer if (characterData.GetHashCode() == _cachedData.GetHashCode()) return; bool updateModdedPaths = false; - List charaDataToUpdate = new List(); + List charaDataToUpdate = new(); foreach (var objectKind in Enum.GetValues()) { _cachedData.FileReplacements.TryGetValue(objectKind, out var existingFileReplacements); @@ -108,7 +108,7 @@ public class CachedPlayer if (hasNewAndOldGlamourerData) { - bool glamourerDataDifferent = _cachedData.GlamourerData[objectKind] != characterData.GlamourerData[objectKind]; + bool glamourerDataDifferent = !string.Equals(_cachedData.GlamourerData[objectKind], characterData.GlamourerData[objectKind], StringComparison.Ordinal); if (glamourerDataDifferent) { Logger.Debug("Updating " + objectKind); @@ -159,7 +159,7 @@ public class CachedPlayer Logger.Debug("Downloading missing files for player " + PlayerName + ", kind: " + objectKind); if (toDownloadReplacements.Any()) { - await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken); + await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken).ConfigureAwait(false); _apiController.CancelDownload(downloadId); } if (downloadToken.IsCancellationRequested) @@ -168,7 +168,7 @@ public class CachedPlayer return; } - if ((TryCalculateModdedDictionary(out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => f.Hash == c.Hash))) + if ((TryCalculateModdedDictionary(out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) { break; } @@ -195,7 +195,7 @@ public class CachedPlayer private List TryCalculateModdedDictionary(out Dictionary moddedDictionary) { List missingFiles = new(); - moddedDictionary = new Dictionary(); + moddedDictionary = new Dictionary(StringComparer.Ordinal); try { foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList()) @@ -454,7 +454,7 @@ public class CachedPlayer private void IpcManagerOnPenumbraRedrawEvent(IntPtr address, int idx) { var player = _dalamudUtil.GetCharacterFromObjectTableByIndex(idx); - if (player == null || player.Name.ToString() != PlayerName) return; + if (player == null || !string.Equals(player.Name.ToString(), PlayerName, StringComparison.OrdinalIgnoreCase)) return; if (!_penumbraRedrawEventTask?.IsCompleted ?? false) return; _penumbraRedrawEventTask = Task.Run(() => diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 47186af..cdd351c 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -8,394 +8,393 @@ using MareSynchronos.WebAPI; using Action = System.Action; using System.Collections.Concurrent; -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); +public delegate void HeelsOffsetChange(float change); +public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); +public class IpcManager : IDisposable { - public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); - public delegate void HeelsOffsetChange(float change); - public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); - public class IpcManager : IDisposable + private readonly ICallGateSubscriber _glamourerApiVersion; + private readonly ICallGateSubscriber? _glamourerApplyAll; + private readonly ICallGateSubscriber? _glamourerGetAllCustomization; + private readonly ICallGateSubscriber _glamourerRevertCustomization; + private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; + private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; + private readonly ICallGateSubscriber<(int, int)> _penumbraApiVersion; + private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; + private readonly ICallGateSubscriber _penumbraGetMetaManipulations; + private readonly ICallGateSubscriber _penumbraInit; + private readonly ICallGateSubscriber _penumbraDispose; + private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; + private readonly ICallGateSubscriber? _penumbraRedraw; + private readonly ICallGateSubscriber? _penumbraRedrawObject; + private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; + private readonly ICallGateSubscriber? _penumbraResolveModDir; + private readonly ICallGateSubscriber? _penumbraResolvePlayer; + private readonly ICallGateSubscriber? _reverseResolvePlayer; + private readonly ICallGateSubscriber, string, int, int> + _penumbraSetTemporaryMod; + private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; + + private readonly ICallGateSubscriber _heelsGetApiVersion; + private readonly ICallGateSubscriber _heelsGetOffset; + private readonly ICallGateSubscriber _heelsOffsetUpdate; + private readonly ICallGateSubscriber _heelsRegisterPlayer; + private readonly ICallGateSubscriber _heelsUnregisterPlayer; + + private readonly DalamudUtil _dalamudUtil; + private readonly ConcurrentQueue actionQueue = new(); + + public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) { - private readonly ICallGateSubscriber _glamourerApiVersion; - private readonly ICallGateSubscriber? _glamourerApplyAll; - private readonly ICallGateSubscriber? _glamourerGetAllCustomization; - private readonly ICallGateSubscriber _glamourerRevertCustomization; - private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; - private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; - private readonly ICallGateSubscriber<(int, int)> _penumbraApiVersion; - private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; - private readonly ICallGateSubscriber _penumbraGetMetaManipulations; - private readonly ICallGateSubscriber _penumbraInit; - private readonly ICallGateSubscriber _penumbraDispose; - private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; - private readonly ICallGateSubscriber? _penumbraRedraw; - private readonly ICallGateSubscriber? _penumbraRedrawObject; - private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; - private readonly ICallGateSubscriber? _penumbraResolveModDir; - private readonly ICallGateSubscriber? _penumbraResolvePlayer; - private readonly ICallGateSubscriber? _reverseResolvePlayer; - private readonly ICallGateSubscriber, string, int, int> - _penumbraSetTemporaryMod; - private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; + Logger.Verbose("Creating " + nameof(IpcManager)); - private readonly ICallGateSubscriber _heelsGetApiVersion; - private readonly ICallGateSubscriber _heelsGetOffset; - private readonly ICallGateSubscriber _heelsOffsetUpdate; - private readonly ICallGateSubscriber _heelsRegisterPlayer; - private readonly ICallGateSubscriber _heelsUnregisterPlayer; + _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); + _penumbraDispose = pi.GetIpcSubscriber("Penumbra.Disposed"); + _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); + _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); + _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); + _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); + _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); + _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); + _penumbraGetMetaManipulations = + pi.GetIpcSubscriber("Penumbra.GetPlayerMetaManipulations"); + _penumbraSetTemporaryMod = pi.GetIpcSubscriber, string, int, + int>("Penumbra.AddTemporaryMod"); + _penumbraCreateTemporaryCollection = + pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); + _penumbraRemoveTemporaryCollection = + pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); + _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); - private readonly DalamudUtil _dalamudUtil; - private readonly ConcurrentQueue actionQueue = new(); + _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); + _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); + _penumbraInit.Subscribe(PenumbraInit); + _penumbraDispose.Subscribe(PenumbraDispose); - public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) - { - Logger.Verbose("Creating " + nameof(IpcManager)); + _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); + _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); + _glamourerApplyAll = pi.GetIpcSubscriber("Glamourer.ApplyAllToCharacter"); + _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); + _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); + _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); - _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); - _penumbraDispose = pi.GetIpcSubscriber("Penumbra.Disposed"); - _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); - _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); - _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); - _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); - _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); - _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); - _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); - _penumbraGetMetaManipulations = - pi.GetIpcSubscriber("Penumbra.GetPlayerMetaManipulations"); - _penumbraSetTemporaryMod = pi.GetIpcSubscriber, string, int, - int>("Penumbra.AddTemporaryMod"); - _penumbraCreateTemporaryCollection = - pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); - _penumbraRemoveTemporaryCollection = - pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); - _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); + _heelsGetApiVersion = pi.GetIpcSubscriber("HeelsPlugin.ApiVersion"); + _heelsGetOffset = pi.GetIpcSubscriber("HeelsPlugin.GetOffset"); + _heelsRegisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.RegisterPlayer"); + _heelsUnregisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.UnregisterPlayer"); + _heelsOffsetUpdate = pi.GetIpcSubscriber("HeelsPlugin.OffsetChanged"); - _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); - _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); - _penumbraInit.Subscribe(PenumbraInit); - _penumbraDispose.Subscribe(PenumbraDispose); + _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); - _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); - _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); - _glamourerApplyAll = pi.GetIpcSubscriber("Glamourer.ApplyAllToCharacter"); - _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); - _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); - _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); - - _heelsGetApiVersion = pi.GetIpcSubscriber("HeelsPlugin.ApiVersion"); - _heelsGetOffset = pi.GetIpcSubscriber("HeelsPlugin.GetOffset"); - _heelsRegisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.RegisterPlayer"); - _heelsUnregisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.UnregisterPlayer"); - _heelsOffsetUpdate = pi.GetIpcSubscriber("HeelsPlugin.OffsetChanged"); - - _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); - - if (Initialized) - { - PenumbraInitialized?.Invoke(); - } - - _dalamudUtil = dalamudUtil; - _dalamudUtil.FrameworkUpdate += HandleActionQueue; - _dalamudUtil.ZoneSwitchEnd += ClearActionQueue; - } - - private void ClearActionQueue() - { - actionQueue.Clear(); - } - - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) - { - if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) - { - PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); - //Logger.Debug($"Resolved {ptr:X}: {arg1} => {arg2}"); - } - } - - private void HandleActionQueue() - { - if (actionQueue.TryDequeue(out var action)) - { - if (action == null) return; - Logger.Debug("Execution action in queue: " + action.Method); - action(); - } - } - - public event VoidDelegate? PenumbraInitialized; - public event VoidDelegate? PenumbraDisposed; - public event PenumbraRedrawEvent? PenumbraRedrawEvent; - public event HeelsOffsetChange? HeelsOffsetChangeEvent; - public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; - - public bool Initialized => CheckPenumbraApi(); - public bool CheckGlamourerApi() - { - try - { - return _glamourerApiVersion.InvokeFunc() >= 0; - } - catch - { - return false; - } - } - - public bool CheckPenumbraApi() - { - try - { - return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; - } - catch - { - return false; - } - } - - public bool CheckHeelsApi() - { - try - { - return _heelsGetApiVersion.InvokeFunc() == "1.0.1"; - } - catch - { - return false; - } - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(IpcManager)); - - int totalSleepTime = 0; - while (actionQueue.Count > 0 && totalSleepTime < 2000) - { - Logger.Verbose("Waiting for actionqueue to clear..."); - HandleActionQueue(); - System.Threading.Thread.Sleep(16); - totalSleepTime += 16; - } - - if (totalSleepTime >= 2000) - { - Logger.Verbose("Action queue clear or not, disposing"); - } - - _dalamudUtil.FrameworkUpdate -= HandleActionQueue; - _dalamudUtil.ZoneSwitchEnd -= ClearActionQueue; - actionQueue.Clear(); - - _penumbraDispose.Unsubscribe(PenumbraDispose); - _penumbraInit.Unsubscribe(PenumbraInit); - _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); - _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); - _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); - } - - public float GetHeelsOffset() - { - if (!CheckHeelsApi()) return 0.0f; - return _heelsGetOffset.InvokeFunc(); - } - - public void HeelsSetOffsetForPlayer(float offset, IntPtr character) - { - if (!CheckHeelsApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) - { - Logger.Verbose("Applying Heels data to " + character.ToString("X")); - _heelsRegisterPlayer.InvokeAction(gameObj, offset); - } - }); - } - - public void HeelsRestoreOffsetForPlayer(IntPtr character) - { - if (!CheckHeelsApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) - { - Logger.Verbose("Restoring Heels data to " + character.ToString("X")); - _heelsUnregisterPlayer.InvokeAction(gameObj); - } - }); - } - - public void GlamourerApplyAll(string? customization, IntPtr obj) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(obj); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); - _glamourerApplyAll!.InvokeAction(customization, c); - } - }); - } - - public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); - _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); - } - }); - } - - public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); - _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); - } - }); - } - - public string GlamourerGetCharacterCustomization(IntPtr character) - { - if (!CheckGlamourerApi()) return string.Empty; - try - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); - byte[] bytes = Convert.FromBase64String(glamourerString); - // ignore transparency - bytes[88] = 128; - bytes[89] = 63; - return Convert.ToBase64String(bytes); - } - return string.Empty; - } - catch - { - return string.Empty; - } - } - - public void GlamourerRevertCharacterCustomization(GameObject character) - { - if (!CheckGlamourerApi()) return; - actionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); - } - - public string PenumbraGetMetaManipulations() - { - if (!CheckPenumbraApi()) return string.Empty; - return _penumbraGetMetaManipulations.InvokeFunc(); - } - - public string? PenumbraModDirectory() - { - if (!CheckPenumbraApi()) return null; - return _penumbraResolveModDir!.InvokeFunc().ToLowerInvariant(); - } - - public void PenumbraRedraw(IntPtr obj) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(obj); - if (gameObj != null) - { - Logger.Verbose("Redrawing " + gameObj); - _penumbraRedrawObject!.InvokeAction(gameObj, 0); - } - }); - } - - public void PenumbraRedraw(string actorName) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => _penumbraRedraw!.InvokeAction(actorName, 0)); - } - - public void PenumbraRemoveTemporaryCollection(string characterName) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => - { - Logger.Verbose("Removing temp collection for " + characterName); - _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); - }); - } - - public string PenumbraResolvePath(string path) - { - if (!CheckPenumbraApi()) return path; - var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); - return resolvedPath ?? path; - } - - public string[] PenumbraReverseResolvePlayer(string path) - { - if (!CheckPenumbraApi()) return new[] { path }; - var resolvedPaths = _reverseResolvePlayer!.InvokeFunc(path); - if (resolvedPaths.Length == 0) - { - resolvedPaths = new[] { path }; - } - return resolvedPaths; - } - - public void PenumbraSetTemporaryMods(string characterName, Dictionary modPaths, string manipulationData) - { - if (!CheckPenumbraApi()) return; - - actionQueue.Enqueue(() => - { - var ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); - Logger.Verbose("Assigning temp mods for " + ret.Item2); - foreach (var mod in modPaths) - { - Logger.Verbose(mod.Key + " => " + mod.Value); - } - _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", ret.Item2, modPaths, manipulationData, 0); - }); - } - - private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) - { - PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); - } - - private void PenumbraInit() + if (Initialized) { PenumbraInitialized?.Invoke(); - _penumbraRedraw!.InvokeAction("self", 0); } - private void HeelsOffsetChange(float offset) - { - HeelsOffsetChangeEvent?.Invoke(offset); - } + _dalamudUtil = dalamudUtil; + _dalamudUtil.FrameworkUpdate += HandleActionQueue; + _dalamudUtil.ZoneSwitchEnd += ClearActionQueue; + } - private void PenumbraDispose() + private void ClearActionQueue() + { + actionQueue.Clear(); + } + + private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) + { + if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) { - PenumbraDisposed?.Invoke(); - actionQueue.Clear(); + PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); + //Logger.Debug($"Resolved {ptr:X}: {arg1} => {arg2}"); } } + + private void HandleActionQueue() + { + if (actionQueue.TryDequeue(out var action)) + { + if (action == null) return; + Logger.Debug("Execution action in queue: " + action.Method); + action(); + } + } + + public event VoidDelegate? PenumbraInitialized; + public event VoidDelegate? PenumbraDisposed; + public event PenumbraRedrawEvent? PenumbraRedrawEvent; + public event HeelsOffsetChange? HeelsOffsetChangeEvent; + public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; + + public bool Initialized => CheckPenumbraApi(); + public bool CheckGlamourerApi() + { + try + { + return _glamourerApiVersion.InvokeFunc() >= 0; + } + catch + { + return false; + } + } + + public bool CheckPenumbraApi() + { + try + { + return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; + } + catch + { + return false; + } + } + + public bool CheckHeelsApi() + { + try + { + return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal); + } + catch + { + return false; + } + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(IpcManager)); + + int totalSleepTime = 0; + while (actionQueue.Count > 0 && totalSleepTime < 2000) + { + Logger.Verbose("Waiting for actionqueue to clear..."); + HandleActionQueue(); + System.Threading.Thread.Sleep(16); + totalSleepTime += 16; + } + + if (totalSleepTime >= 2000) + { + Logger.Verbose("Action queue clear or not, disposing"); + } + + _dalamudUtil.FrameworkUpdate -= HandleActionQueue; + _dalamudUtil.ZoneSwitchEnd -= ClearActionQueue; + actionQueue.Clear(); + + _penumbraDispose.Unsubscribe(PenumbraDispose); + _penumbraInit.Unsubscribe(PenumbraInit); + _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); + _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); + _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); + } + + public float GetHeelsOffset() + { + if (!CheckHeelsApi()) return 0.0f; + return _heelsGetOffset.InvokeFunc(); + } + + public void HeelsSetOffsetForPlayer(float offset, IntPtr character) + { + if (!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Applying Heels data to " + character.ToString("X")); + _heelsRegisterPlayer.InvokeAction(gameObj, offset); + } + }); + } + + public void HeelsRestoreOffsetForPlayer(IntPtr character) + { + if (!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Restoring Heels data to " + character.ToString("X")); + _heelsUnregisterPlayer.InvokeAction(gameObj); + } + }); + } + + public void GlamourerApplyAll(string? customization, IntPtr obj) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); + _glamourerApplyAll!.InvokeAction(customization, c); + } + }); + } + + public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); + _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); + } + }); + } + + public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); + _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); + } + }); + } + + public string GlamourerGetCharacterCustomization(IntPtr character) + { + if (!CheckGlamourerApi()) return string.Empty; + try + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); + byte[] bytes = Convert.FromBase64String(glamourerString); + // ignore transparency + bytes[88] = 128; + bytes[89] = 63; + return Convert.ToBase64String(bytes); + } + return string.Empty; + } + catch + { + return string.Empty; + } + } + + public void GlamourerRevertCharacterCustomization(GameObject character) + { + if (!CheckGlamourerApi()) return; + actionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); + } + + public string PenumbraGetMetaManipulations() + { + if (!CheckPenumbraApi()) return string.Empty; + return _penumbraGetMetaManipulations.InvokeFunc(); + } + + public string? PenumbraModDirectory() + { + if (!CheckPenumbraApi()) return null; + return _penumbraResolveModDir!.InvokeFunc().ToLowerInvariant(); + } + + public void PenumbraRedraw(IntPtr obj) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj != null) + { + Logger.Verbose("Redrawing " + gameObj); + _penumbraRedrawObject!.InvokeAction(gameObj, 0); + } + }); + } + + public void PenumbraRedraw(string actorName) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => _penumbraRedraw!.InvokeAction(actorName, 0)); + } + + public void PenumbraRemoveTemporaryCollection(string characterName) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => + { + Logger.Verbose("Removing temp collection for " + characterName); + _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); + }); + } + + public string PenumbraResolvePath(string path) + { + if (!CheckPenumbraApi()) return path; + var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); + return resolvedPath ?? path; + } + + public string[] PenumbraReverseResolvePlayer(string path) + { + if (!CheckPenumbraApi()) return new[] { path }; + var resolvedPaths = _reverseResolvePlayer!.InvokeFunc(path); + if (resolvedPaths.Length == 0) + { + resolvedPaths = new[] { path }; + } + return resolvedPaths; + } + + public void PenumbraSetTemporaryMods(string characterName, Dictionary modPaths, string manipulationData) + { + if (!CheckPenumbraApi()) return; + + actionQueue.Enqueue(() => + { + var ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); + Logger.Verbose("Assigning temp mods for " + ret.Item2); + foreach (var mod in modPaths) + { + Logger.Verbose(mod.Key + " => " + mod.Value); + } + _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", ret.Item2, modPaths, manipulationData, 0); + }); + } + + private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) + { + PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); + } + + private void PenumbraInit() + { + PenumbraInitialized?.Invoke(); + _penumbraRedraw!.InvokeAction("self", 0); + } + + private void HeelsOffsetChange(float offset) + { + HeelsOffsetChangeEvent?.Invoke(offset); + } + + private void PenumbraDispose() + { + PenumbraDisposed?.Invoke(); + actionQueue.Clear(); + } } diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index 3b99e51..20a683c 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -19,8 +19,8 @@ public class OnlinePlayerManager : IDisposable private readonly IpcManager _ipcManager; private readonly PlayerManager _playerManager; private readonly FileCacheManager _fileDbManager; - private readonly ConcurrentDictionary _onlineCachedPlayers = new(); - private readonly ConcurrentDictionary _temporaryStoredCharacterCache = new(); + private readonly ConcurrentDictionary _onlineCachedPlayers = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _temporaryStoredCharacterCache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _playerTokenDisposal = new(); private List OnlineVisiblePlayerHashes => _onlineCachedPlayers.Select(p => p.Value).Where(p => p.PlayerCharacter != IntPtr.Zero) @@ -37,8 +37,6 @@ public class OnlinePlayerManager : IDisposable _fileDbManager = fileDbManager; _apiController.PairedClientOnline += ApiControllerOnPairedClientOnline; _apiController.PairedClientOffline += ApiControllerOnPairedClientOffline; - _apiController.PairedWithOther += ApiControllerOnPairedWithOther; - _apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther; _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiControllerOnDisconnected; _apiController.CharacterReceived += ApiControllerOnCharacterReceived; @@ -137,8 +135,6 @@ public class OnlinePlayerManager : IDisposable _apiController.PairedClientOnline -= ApiControllerOnPairedClientOnline; _apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline; - _apiController.PairedWithOther -= ApiControllerOnPairedWithOther; - _apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther; _apiController.Disconnected -= ApiControllerOnDisconnected; _apiController.Connected -= ApiControllerOnConnected; @@ -169,20 +165,6 @@ public class OnlinePlayerManager : IDisposable return; } - private void ApiControllerOnPairedWithOther(string charHash) - { - if (string.IsNullOrEmpty(charHash)) return; - Logger.Debug("Pairing with " + charHash); - AddPlayer(charHash); - } - - private void ApiControllerOnUnpairedFromOther(string? characterHash) - { - if (string.IsNullOrEmpty(characterHash)) return; - Logger.Debug("Unpairing from " + characterHash); - RemovePlayer(characterHash); - } - private void AddPlayer(string characterNameHash) { if (_onlineCachedPlayers.TryGetValue(characterNameHash, out var cachedPlayer)) @@ -244,7 +226,7 @@ public class OnlinePlayerManager : IDisposable Task.Run(async () => { await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData, - visiblePlayers); + visiblePlayers).ConfigureAwait(false); }); } } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index 95eed2f..accfd0c 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -14,226 +14,229 @@ using MareSynchronos.FileCache; using Newtonsoft.Json; #endif -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void PlayerHasChanged(CharacterCacheDto characterCache); + +public class PlayerManager : IDisposable { - public delegate void PlayerHasChanged(CharacterCacheDto characterCache); + private readonly ApiController _apiController; + private readonly CharacterDataFactory _characterDataFactory; + private readonly DalamudUtil _dalamudUtil; + private readonly TransientResourceManager _transientResourceManager; + private readonly PeriodicFileScanner _periodicFileScanner; + private readonly IpcManager _ipcManager; + public event PlayerHasChanged? PlayerHasChanged; + public CharacterCacheDto? LastCreatedCharacterData { get; private set; } + public CharacterData PermanentDataCache { get; private set; } = new(); + private readonly Dictionary> objectKindsToUpdate = new(); - public class PlayerManager : IDisposable + private CancellationTokenSource? _playerChangedCts = new(); + private CancellationTokenSource _transientUpdateCts = new(); + + private List playerRelatedObjects = new(); + + public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, + CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, TransientResourceManager transientResourceManager, + PeriodicFileScanner periodicFileScanner) { - private readonly ApiController _apiController; - private readonly CharacterDataFactory _characterDataFactory; - private readonly DalamudUtil _dalamudUtil; - private readonly TransientResourceManager _transientResourceManager; - private readonly PeriodicFileScanner _periodicFileScanner; - private readonly IpcManager _ipcManager; - public event PlayerHasChanged? PlayerHasChanged; - public CharacterCacheDto? LastCreatedCharacterData { get; private set; } - public CharacterData PermanentDataCache { get; private set; } = new(); - private readonly Dictionary> objectKindsToUpdate = new(); + Logger.Verbose("Creating " + nameof(PlayerManager)); - private CancellationTokenSource? _playerChangedCts = new(); - private CancellationTokenSource _transientUpdateCts = new(); + _apiController = apiController; + _ipcManager = ipcManager; + _characterDataFactory = characterDataFactory; + _dalamudUtil = dalamudUtil; + _transientResourceManager = transientResourceManager; + _periodicFileScanner = periodicFileScanner; + _apiController.Connected += ApiControllerOnConnected; + _apiController.Disconnected += ApiController_Disconnected; + _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; + _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; + _ipcManager.HeelsOffsetChangeEvent += HeelsOffsetChanged; + _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - private List playerRelatedObjects = new List(); - public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, - CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, TransientResourceManager transientResourceManager, - PeriodicFileScanner periodicFileScanner) + Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); + if (_apiController.IsConnected) { - Logger.Verbose("Creating " + nameof(PlayerManager)); - - _apiController = apiController; - _ipcManager = ipcManager; - _characterDataFactory = characterDataFactory; - _dalamudUtil = dalamudUtil; - _transientResourceManager = transientResourceManager; - _periodicFileScanner = periodicFileScanner; - _apiController.Connected += ApiControllerOnConnected; - _apiController.Disconnected += ApiController_Disconnected; - _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; - _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; - _ipcManager.HeelsOffsetChangeEvent += HeelsOffsetChanged; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - - - Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); - if (_apiController.IsConnected) - { - ApiControllerOnConnected(); - } - - playerRelatedObjects = new List() - { - new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), - new PlayerRelatedObject(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), - new PlayerRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), - new PlayerRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), - }; + ApiControllerOnConnected(); } - private void DalamudUtilOnFrameworkUpdate() + playerRelatedObjects = new List() { - _transientResourceManager.PlayerRelatedPointers = playerRelatedObjects.Select(f => f.CurrentAddress).ToArray(); - } + new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), + new PlayerRelatedObject(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), + new PlayerRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), + new PlayerRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), + }; + } - public void HandleTransientResourceLoad(IntPtr gameObj) + private void DalamudUtilOnFrameworkUpdate() + { + _transientResourceManager.PlayerRelatedPointers = playerRelatedObjects.Select(f => f.CurrentAddress).ToArray(); + } + + public void HandleTransientResourceLoad(IntPtr gameObj) + { + foreach (var obj in playerRelatedObjects) { - foreach (var obj in playerRelatedObjects) + if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) { - if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) + _transientUpdateCts.Cancel(); + _transientUpdateCts = new CancellationTokenSource(); + var token = _transientUpdateCts.Token; + Task.Run(async () => { - _transientUpdateCts.Cancel(); - _transientUpdateCts = new CancellationTokenSource(); - var token = _transientUpdateCts.Token; - Task.Run(async () => - { - Logger.Debug("Delaying transient resource load update"); - await Task.Delay(750, token); - if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; - Logger.Debug("Firing transient resource load update"); - obj.HasTransientsUpdate = true; - }, token); + Logger.Debug("Delaying transient resource load update"); + await Task.Delay(750, token).ConfigureAwait(false); + if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; + Logger.Debug("Firing transient resource load update"); + obj.HasTransientsUpdate = true; + }, token); - return; - } - } - } - - private void HeelsOffsetChanged(float change) - { - var player = playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); - if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) - { - Logger.Debug("Heels offset changed to " + change); - playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true; - } - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(PlayerManager)); - - _apiController.Connected -= ApiControllerOnConnected; - _apiController.Disconnected -= ApiController_Disconnected; - - _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - - _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; - - _playerChangedCts?.Cancel(); - _ipcManager.HeelsOffsetChangeEvent -= HeelsOffsetChanged; - } - - private unsafe void DalamudUtilOnDelayedFrameworkUpdate() - { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; - - playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); - if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing)) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void ApiControllerOnConnected() - { - Logger.Debug("ApiController Connected"); - - _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - } - - private void ApiController_Disconnected() - { - Logger.Debug(nameof(ApiController_Disconnected)); - - _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - } - - private async Task CreateFullCharacterCacheDto(CancellationToken token) - { - foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) - { - Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); - PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); - if (!token.IsCancellationRequested) - { - unprocessedObject.HasUnprocessedUpdate = false; - unprocessedObject.IsProcessing = false; - unprocessedObject.HasTransientsUpdate = false; - } - token.ThrowIfCancellationRequested(); - } - - while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) - { - Logger.Verbose("Waiting until cache is ready"); - await Task.Delay(50, token); - } - - if (token.IsCancellationRequested) return null; - - Logger.Verbose("Cache creation complete"); - - var cache = PermanentDataCache.ToCharacterCacheDto(); - //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); - return cache; - } - - private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) - { - Logger.Verbose("RedrawEvent for addr " + address); - - foreach (var item in playerRelatedObjects) - { - if (address == item.Address) - { - Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); - item.HasUnprocessedUpdate = true; - } - } - - if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate)))) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void OnPlayerOrAttachedObjectsChanged() - { - var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); - foreach (var unprocessedObject in unprocessedObjects) - { - unprocessedObject.IsProcessing = true; - } - Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); - bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate); - unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false); - _playerChangedCts?.Cancel(); - _playerChangedCts = new CancellationTokenSource(); - var token = _playerChangedCts.Token; - - // fix for redraw from anamnesis - while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) - { - Logger.Debug("Waiting Until Player is Present"); - Thread.Sleep(100); - } - - if (token.IsCancellationRequested) - { - Logger.Debug("Cancelled"); return; } + } + } - if (!_ipcManager.Initialized) + private void HeelsOffsetChanged(float change) + { + var player = playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); + if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) + { + Logger.Debug("Heels offset changed to " + change); + playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true; + } + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(PlayerManager)); + + _apiController.Connected -= ApiControllerOnConnected; + _apiController.Disconnected -= ApiController_Disconnected; + + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; + _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + + _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; + + _playerChangedCts?.Cancel(); + _ipcManager.HeelsOffsetChangeEvent -= HeelsOffsetChanged; + } + + private unsafe void DalamudUtilOnDelayedFrameworkUpdate() + { + if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; + + playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); + if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing)) + { + OnPlayerOrAttachedObjectsChanged(); + } + } + + private void ApiControllerOnConnected() + { + Logger.Debug("ApiController Connected"); + + _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; + } + + private void ApiController_Disconnected() + { + Logger.Debug(nameof(ApiController_Disconnected)); + + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; + } + + private async Task CreateFullCharacterCacheDto(CancellationToken token) + { + foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) + { + Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); + if (!token.IsCancellationRequested) { - Logger.Warn("Penumbra not active, doing nothing."); - return; + unprocessedObject.HasUnprocessedUpdate = false; + unprocessedObject.IsProcessing = false; + unprocessedObject.HasTransientsUpdate = false; } + token.ThrowIfCancellationRequested(); + } - Task.Run(async () => + while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) + { + Logger.Verbose("Waiting until cache is ready"); + await Task.Delay(50, token).ConfigureAwait(false); + } + + if (token.IsCancellationRequested) return null; + + Logger.Verbose("Cache creation complete"); + + var cache = PermanentDataCache.ToCharacterCacheDto(); + //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); + return cache; + } + + private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) + { + Logger.Verbose("RedrawEvent for addr " + address); + + foreach (var item in playerRelatedObjects) + { + if (address == item.Address) + { + Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + item.HasUnprocessedUpdate = true; + } + } + + if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate)))) + { + OnPlayerOrAttachedObjectsChanged(); + } + } + + private void OnPlayerOrAttachedObjectsChanged() + { + var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); + foreach (var unprocessedObject in unprocessedObjects) + { + unprocessedObject.IsProcessing = true; + } + Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); + bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate); + unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false); + _playerChangedCts?.Cancel(); + _playerChangedCts = new CancellationTokenSource(); + var token = _playerChangedCts.Token; + + // fix for redraw from anamnesis + while ((!_dalamudUtil.IsPlayerPresent || string.Equals(_dalamudUtil.PlayerName, "--", StringComparison.Ordinal)) && !token.IsCancellationRequested) + { + Logger.Debug("Waiting Until Player is Present"); + Thread.Sleep(100); + } + + if (token.IsCancellationRequested) + { + Logger.Debug("Cancelled"); + return; + } + + if (!_ipcManager.Initialized) + { + Logger.Warn("Penumbra not active, doing nothing."); + return; + } + + Task.Run(async () => + { + CharacterCacheDto? cacheDto = null; + try { _periodicFileScanner.HaltScan("Character creation"); foreach (var item in unprocessedObjects) @@ -241,31 +244,35 @@ namespace MareSynchronos.Managers _dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, 10000, token); } - CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); + cacheDto = (await CreateFullCharacterCacheDto(token).ConfigureAwait(false)); + } + catch { } + finally + { _periodicFileScanner.ResumeScan("Character creation"); - if (cacheDto == null || token.IsCancellationRequested) return; + } + if (cacheDto == null || token.IsCancellationRequested) return; #if DEBUG - //var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented); - //Logger.Verbose(json); + //var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented); + //Logger.Verbose(json); #endif - if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) - { - Logger.Debug("Not sending data, already sent"); - return; - } - else - { - LastCreatedCharacterData = cacheDto; - } + if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) + { + Logger.Debug("Not sending data, already sent"); + return; + } + else + { + LastCreatedCharacterData = cacheDto; + } - if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate) - { - Logger.Verbose("Invoking PlayerHasChanged"); - PlayerHasChanged?.Invoke(cacheDto); - } - }, token); - } + if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate) + { + Logger.Verbose("Invoking PlayerHasChanged"); + PlayerHasChanged?.Invoke(cacheDto); + } + }, token); } } diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs index 560355f..d82c61a 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -8,198 +8,197 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void TransientResourceLoadedEvent(IntPtr drawObject); + +public class TransientResourceManager : IDisposable { - public delegate void TransientResourceLoadedEvent(IntPtr drawObject); + private readonly IpcManager manager; + private readonly DalamudUtil dalamudUtil; - public class TransientResourceManager : IDisposable + public event TransientResourceLoadedEvent? TransientResourceLoaded; + public IntPtr[] PlayerRelatedPointers = Array.Empty(); + private readonly string[] FileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp" }; + + private ConcurrentDictionary> TransientResources { get; } = new(); + private ConcurrentDictionary> SemiTransientResources { get; } = new(); + public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) { - private readonly IpcManager manager; - private readonly DalamudUtil dalamudUtil; + manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; + this.manager = manager; + this.dalamudUtil = dalamudUtil; + dalamudUtil.FrameworkUpdate += DalamudUtil_FrameworkUpdate; + dalamudUtil.ClassJobChanged += DalamudUtil_ClassJobChanged; + } - public event TransientResourceLoadedEvent? TransientResourceLoaded; - public IntPtr[] PlayerRelatedPointers = Array.Empty(); - private readonly string[] FileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp" }; - - private ConcurrentDictionary> TransientResources { get; } = new(); - private ConcurrentDictionary> SemiTransientResources { get; } = new(); - public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) + private void DalamudUtil_ClassJobChanged() + { + if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) { - manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; - this.manager = manager; - this.dalamudUtil = dalamudUtil; - dalamudUtil.FrameworkUpdate += DalamudUtil_FrameworkUpdate; - dalamudUtil.ClassJobChanged += DalamudUtil_ClassJobChanged; + SemiTransientResources[ObjectKind.Pet].Clear(); } + } - private void DalamudUtil_ClassJobChanged() + private void DalamudUtil_FrameworkUpdate() + { + foreach (var item in TransientResources.ToList()) { - if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) + if (!dalamudUtil.IsGameObjectPresent(item.Key)) { - SemiTransientResources[ObjectKind.Pet].Clear(); - } - } - - private void DalamudUtil_FrameworkUpdate() - { - foreach (var item in TransientResources.ToList()) - { - if (!dalamudUtil.IsGameObjectPresent(item.Key)) - { - Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); - TransientResources.TryRemove(item.Key, out _); - } - } - } - - public void CleanSemiTransientResources(ObjectKind objectKind) - { - if (SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind].Clear(); - } - } - - public List GetTransientResources(IntPtr gameObject) - { - if (TransientResources.TryGetValue(gameObject, out var result)) - { - return result.ToList(); - } - - return new List(); - } - - public List GetSemiTransientResources(ObjectKind objectKind) - { - if (SemiTransientResources.TryGetValue(objectKind, out var result)) - { - return result.ToList(); - } - - return new List(); - } - - private void Manager_PenumbraResourceLoadEvent(IntPtr gameObject, string gamePath, string filePath) - { - if (!FileTypesToHandle.Any(type => gamePath.ToLowerInvariant().EndsWith(type))) - { - return; - } - if (!PlayerRelatedPointers.Contains(gameObject)) - { - return; - } - - if (!TransientResources.ContainsKey(gameObject)) - { - TransientResources[gameObject] = new(); - } - - if (filePath.StartsWith("|")) - { - filePath = filePath.Split("|")[2]; - } - - filePath = filePath.ToLowerInvariant().Replace("\\", "/"); - - var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/"); - - if (TransientResources[gameObject].Contains(replacedGamePath) || - SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath - && f.ResolvedPath.ToLowerInvariant() == filePath))) - { - Logger.Debug("Not adding " + replacedGamePath + ":" + filePath); - Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath - && f.ResolvedPath.ToLowerInvariant() == filePath)).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); - } - else - { - TransientResources[gameObject].Add(replacedGamePath); - Logger.Debug($"Adding {replacedGamePath} for {gameObject} ({filePath})"); - TransientResourceLoaded?.Invoke(gameObject); - } - } - - public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) - { - if (TransientResources.ContainsKey(gameObject)) - { - TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => g.ToLowerInvariant() == f.ToLowerInvariant())); - } - } - - public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(); - } - - if (!TransientResources.TryGetValue(gameObject, out var resources)) - { - return; - } - - var transientResources = resources.ToList(); - Logger.Debug("Persisting " + transientResources.Count + " transient resources"); - foreach (var gamePath in transientResources) - { - var existingResource = SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); - if (existingResource) - { - Logger.Debug("Semi Transient resource replaced: " + gamePath); - SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); - } - - try - { - var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), true); - if (!fileReplacement.HasFileReplacement) - fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), false); - if (fileReplacement.HasFileReplacement) - { - Logger.Debug("Persisting " + gamePath.ToLowerInvariant()); - if (SemiTransientResources[objectKind].Add(fileReplacement)) - { - Logger.Debug("Added " + fileReplacement); - } - else - { - Logger.Debug("Not added " + fileReplacement); - } - } - } - catch (Exception ex) - { - Logger.Warn("Issue during transient file persistence"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace.ToString()); - } - } - - TransientResources[gameObject].Clear(); - } - - public void Dispose() - { - dalamudUtil.FrameworkUpdate -= DalamudUtil_FrameworkUpdate; - manager.PenumbraResourceLoadEvent -= Manager_PenumbraResourceLoadEvent; - dalamudUtil.ClassJobChanged -= DalamudUtil_ClassJobChanged; - TransientResources.Clear(); - } - - internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(); - } - - if (!SemiTransientResources[objectKind].Any(f => f.ResolvedPath.ToLowerInvariant() == item.ResolvedPath.ToLowerInvariant())) - { - SemiTransientResources[objectKind].Add(item); + Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); + TransientResources.TryRemove(item.Key, out _); } } } + + public void CleanSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind].Clear(); + } + } + + public List GetTransientResources(IntPtr gameObject) + { + if (TransientResources.TryGetValue(gameObject, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + public List GetSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.TryGetValue(objectKind, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + private void Manager_PenumbraResourceLoadEvent(IntPtr gameObject, string gamePath, string filePath) + { + if (!FileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) + { + return; + } + if (!PlayerRelatedPointers.Contains(gameObject)) + { + return; + } + + if (!TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject] = new(StringComparer.OrdinalIgnoreCase); + } + + if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase)) + { + filePath = filePath.Split("|")[2]; + } + + filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); + + var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); + + if (TransientResources[gameObject].Contains(replacedGamePath) || + SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f.GamePaths.First(), replacedGamePath , StringComparison.OrdinalIgnoreCase) + && string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase)))) + { + Logger.Verbose("Not adding " + replacedGamePath + ":" + filePath); + Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase) + && string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); + } + else + { + TransientResources[gameObject].Add(replacedGamePath); + Logger.Debug($"Adding {replacedGamePath} for {gameObject} ({filePath})"); + TransientResourceLoaded?.Invoke(gameObject); + } + } + + public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) + { + if (TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => string.Equals(g, f, StringComparison.OrdinalIgnoreCase))); + } + } + + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!TransientResources.TryGetValue(gameObject, out var resources)) + { + return; + } + + var transientResources = resources.ToList(); + Logger.Debug("Persisting " + transientResources.Count + " transient resources"); + foreach (var gamePath in transientResources) + { + var existingResource = SemiTransientResources[objectKind].Any(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase)); + if (existingResource) + { + Logger.Debug("Semi Transient resource replaced: " + gamePath); + SemiTransientResources[objectKind].RemoveWhere(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase)); + } + + try + { + var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), true); + if (!fileReplacement.HasFileReplacement) + fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), false); + if (fileReplacement.HasFileReplacement) + { + Logger.Debug("Persisting " + gamePath.ToLowerInvariant()); + if (SemiTransientResources[objectKind].Add(fileReplacement)) + { + Logger.Debug("Added " + fileReplacement); + } + else + { + Logger.Debug("Not added " + fileReplacement); + } + } + } + catch (Exception ex) + { + Logger.Warn("Issue during transient file persistence"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace.ToString()); + } + } + + TransientResources[gameObject].Clear(); + } + + public void Dispose() + { + dalamudUtil.FrameworkUpdate -= DalamudUtil_FrameworkUpdate; + manager.PenumbraResourceLoadEvent -= Manager_PenumbraResourceLoadEvent; + dalamudUtil.ClassJobChanged -= DalamudUtil_ClassJobChanged; + TransientResources.Clear(); + } + + internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!SemiTransientResources[objectKind].Any(f => string.Equals(f.ResolvedPath, item.ResolvedPath, StringComparison.OrdinalIgnoreCase))) + { + SemiTransientResources[objectKind].Add(item); + } + } } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 79ec69e..0cb2bd2 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.4.24 + 0.5.0 https://github.com/Penumbra-Sync/client @@ -28,6 +28,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -86,4 +90,8 @@ + + + + diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index c0c349f..0724ef8 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -6,83 +6,82 @@ using MareSynchronos.API; using MareSynchronos.Utils; using Lumina.Excel.GeneratedSheets; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +[JsonObject(MemberSerialization.OptIn)] +public class CharacterData { - [JsonObject(MemberSerialization.OptIn)] - public class CharacterData + [JsonProperty] + public Dictionary> FileReplacements { get; set; } = new(); + + [JsonProperty] + public Dictionary GlamourerString { get; set; } = new(); + + public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); + + [JsonProperty] + public string ManipulationString { get; set; } = string.Empty; + + [JsonProperty] + public float HeelsOffset { get; set; } = 0f; + + public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) { - [JsonProperty] - public Dictionary> FileReplacements { get; set; } = new(); + if (!fileReplacement.HasFileReplacement) return; - [JsonProperty] - public Dictionary GlamourerString { get; set; } = new(); + if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); - public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); - - [JsonProperty] - public string ManipulationString { get; set; } = string.Empty; - - [JsonProperty] - public float HeelsOffset { get; set; } = 0f; - - public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) + var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => string.Equals(f.ResolvedPath, fileReplacement.ResolvedPath, System.StringComparison.OrdinalIgnoreCase)); + if (existingReplacement != null) { - if (!fileReplacement.HasFileReplacement) return; - - if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); - - var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); - if (existingReplacement != null) - { - existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e))); - } - else - { - FileReplacements[objectKind].Add(fileReplacement); - } + existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e, System.StringComparer.OrdinalIgnoreCase))); } - - public CharacterCacheDto ToCharacterCacheDto() + else { - var fileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap).GroupBy(f => f.Hash).Select(g => - { - return new FileReplacementDto() - { - GamePaths = g.SelectMany(f => f.GamePaths).Distinct().ToArray(), - Hash = g.First().Hash, - }; - }).ToList()); - - Logger.Debug("Adding fileSwaps"); - foreach (var item in FileReplacements) - { - Logger.Debug("Checking fileSwaps for " + item.Key); - var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); - Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps"); - foreach (var swap in fileSwapsToAdd) - { - Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath); - } - fileReplacements[item.Key].AddRange(fileSwapsToAdd); - } - - return new CharacterCacheDto() - { - FileReplacements = fileReplacements, - GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), - ManipulationData = ManipulationString, - HeelsOffset = HeelsOffset - }; - } - - public override string ToString() - { - StringBuilder stringBuilder = new(); - foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) - { - stringBuilder.AppendLine(fileReplacement.ToString()); - } - return stringBuilder.ToString(); + FileReplacements[objectKind].Add(fileReplacement); } } + + public CharacterCacheDto ToCharacterCacheDto() + { + var fileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap).GroupBy(f => f.Hash, System.StringComparer.OrdinalIgnoreCase).Select(g => + { + return new FileReplacementDto() + { + GamePaths = g.SelectMany(f => f.GamePaths).Distinct(System.StringComparer.OrdinalIgnoreCase).ToArray(), + Hash = g.First().Hash, + }; + }).ToList()); + + Logger.Debug("Adding fileSwaps"); + foreach (var item in FileReplacements) + { + Logger.Debug("Checking fileSwaps for " + item.Key); + var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); + Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps"); + foreach (var swap in fileSwapsToAdd) + { + Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath); + } + fileReplacements[item.Key].AddRange(fileSwapsToAdd); + } + + return new CharacterCacheDto() + { + FileReplacements = fileReplacements, + GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), + ManipulationData = ManipulationString, + HeelsOffset = HeelsOffset + }; + } + + public override string ToString() + { + StringBuilder stringBuilder = new(); + foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) + { + stringBuilder.AppendLine(fileReplacement.ToString()); + } + return stringBuilder.ToString(); + } } diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index d4b2406..238bdc1 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -6,55 +6,54 @@ using MareSynchronos.API; using System.Text.RegularExpressions; using MareSynchronos.FileCache; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +public class FileReplacement { - public class FileReplacement + private readonly FileCacheManager fileDbManager; + + public FileReplacement(FileCacheManager fileDbManager) { - private readonly FileCacheManager fileDbManager; + this.fileDbManager = fileDbManager; + } - public FileReplacement(FileCacheManager fileDbManager) + public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); + + public List GamePaths { get; set; } = new(); + + public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, System.StringComparison.Ordinal)); + + public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths.First(), ResolvedPath, System.StringComparison.Ordinal); + + public string Hash { get; set; } = string.Empty; + + public string ResolvedPath { get; set; } = string.Empty; + + public void SetResolvedPath(string path) + { + ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); + if (!HasFileReplacement || IsFileSwap) return; + + _ = Task.Run(() => { - this.fileDbManager = fileDbManager; - } + var cache = fileDbManager.GetFileCacheByPath(ResolvedPath); + Hash = cache.Hash; + }); + } - public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); - - public List GamePaths { get; set; } = new(); - - public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => p != ResolvedPath); - - public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && GamePaths.First() != ResolvedPath; - - public string Hash { get; set; } = string.Empty; - - public string ResolvedPath { get; set; } = string.Empty; - - public void SetResolvedPath(string path) + public FileReplacementDto ToFileReplacementDto() + { + return new FileReplacementDto { - ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); - if (!HasFileReplacement || IsFileSwap) return; - - _ = Task.Run(() => - { - var cache = fileDbManager.GetFileCacheByPath(ResolvedPath); - Hash = cache.Hash; - }); - } - - public FileReplacementDto ToFileReplacementDto() - { - return new FileReplacementDto - { - GamePaths = GamePaths.ToArray(), - Hash = Hash, - FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty - }; - } - public override string ToString() - { - StringBuilder builder = new(); - builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); - return builder.ToString(); - } + GamePaths = GamePaths.ToArray(), + Hash = Hash, + FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty + }; + } + public override string ToString() + { + StringBuilder builder = new(); + builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); + return builder.ToString(); } } diff --git a/MareSynchronos/Models/PlayerRelatedObject.cs b/MareSynchronos/Models/PlayerRelatedObject.cs index f91b221..50ad1b8 100644 --- a/MareSynchronos/Models/PlayerRelatedObject.cs +++ b/MareSynchronos/Models/PlayerRelatedObject.cs @@ -5,132 +5,131 @@ using System.Runtime.InteropServices; using MareSynchronos.Utils; using Penumbra.GameData.ByteString; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +public class PlayerRelatedObject { - public class PlayerRelatedObject + private readonly Func getAddress; + + public unsafe Character* Character => (Character*)Address; + + private string _name; + + public ObjectKind ObjectKind { get; } + public IntPtr Address { get; set; } + public IntPtr DrawObjectAddress { get; set; } + + public IntPtr CurrentAddress { - private readonly Func getAddress; - - public unsafe Character* Character => (Character*)Address; - - private string _name; - - public ObjectKind ObjectKind { get; } - public IntPtr Address { get; set; } - public IntPtr DrawObjectAddress { get; set; } - - public IntPtr CurrentAddress + get { - get + try { - try - { - return getAddress.Invoke(); - } - catch - { return IntPtr.Zero; } + return getAddress.Invoke(); } - } - - public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) - { - ObjectKind = objectKind; - Address = address; - DrawObjectAddress = drawObjectAddress; - this.getAddress = getAddress; - _name = string.Empty; - } - - public byte[] EquipSlotData { get; set; } = new byte[40]; - public byte[] CustomizeData { get; set; } = new byte[26]; - public byte? HatState { get; set; } - public byte? VisorWeaponState { get; set; } - - public bool HasTransientsUpdate { get; set; } = false; - public bool HasUnprocessedUpdate { get; set; } = false; - public bool DoNotSendUpdate { get; set; } = false; - public bool IsProcessing { get; set; } = false; - - public unsafe void CheckAndUpdateObject() - { - var curPtr = CurrentAddress; - if (curPtr != IntPtr.Zero) - { - var chara = (Character*)curPtr; - bool addr = Address == IntPtr.Zero || Address != curPtr; - bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); - bool drawObj = (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress; - var name = new Utf8String(chara->GameObject.Name).ToString(); - bool nameChange = (name != _name); - if (addr || equip || drawObj || nameChange) - { - _name = name; - Logger.Verbose($"{ObjectKind} changed: {_name}, now: {curPtr:X}, {(IntPtr)chara->GameObject.DrawObject:X}"); - - Address = curPtr; - DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; - HasUnprocessedUpdate = true; - } - } - else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) - { - Address = IntPtr.Zero; - DrawObjectAddress = IntPtr.Zero; - Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress); - } - } - - private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) - { - bool hasChanges = false; - DoNotSendUpdate = false; - for (int i = 0; i < EquipSlotData.Length; i++) - { - var data = Marshal.ReadByte((IntPtr)equipSlotData, i); - if (EquipSlotData[i] != data) - { - EquipSlotData[i] = data; - hasChanges = true; - } - } - - for (int i = 0; i < CustomizeData.Length; i++) - { - var data = Marshal.ReadByte((IntPtr)customizeData, i); - if (CustomizeData[i] != data) - { - CustomizeData[i] = data; - hasChanges = true; - } - } - - var newHatState = Marshal.ReadByte((IntPtr)customizeData + 30, 0); - var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0); - if (newHatState != HatState) - { - if (HatState != null && !hasChanges && !HasUnprocessedUpdate) - { - Logger.Debug("Not Sending Update, only Hat changed"); - DoNotSendUpdate = true; - } - HatState = newHatState; - hasChanges = true; - } - - newWeaponOrVisorState &= 0b1101; // ignore drawing weapon - - if (newWeaponOrVisorState != VisorWeaponState) - { - if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate) - { - Logger.Debug("Not Sending Update, only Visor/Weapon changed"); - DoNotSendUpdate = true; - } - VisorWeaponState = newWeaponOrVisorState; - hasChanges = true; - } - - return hasChanges; + catch + { return IntPtr.Zero; } } } + + public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + { + ObjectKind = objectKind; + Address = address; + DrawObjectAddress = drawObjectAddress; + this.getAddress = getAddress; + _name = string.Empty; + } + + public byte[] EquipSlotData { get; set; } = new byte[40]; + public byte[] CustomizeData { get; set; } = new byte[26]; + public byte? HatState { get; set; } + public byte? VisorWeaponState { get; set; } + + public bool HasTransientsUpdate { get; set; } = false; + public bool HasUnprocessedUpdate { get; set; } = false; + public bool DoNotSendUpdate { get; set; } = false; + public bool IsProcessing { get; set; } = false; + + public unsafe void CheckAndUpdateObject() + { + var curPtr = CurrentAddress; + if (curPtr != IntPtr.Zero) + { + var chara = (Character*)curPtr; + bool addr = Address == IntPtr.Zero || Address != curPtr; + bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); + bool drawObj = (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress; + var name = new Utf8String(chara->GameObject.Name).ToString(); + bool nameChange = (!string.Equals(name, _name, StringComparison.Ordinal)); + if (addr || equip || drawObj || nameChange) + { + _name = name; + Logger.Verbose($"{ObjectKind} changed: {_name}, now: {curPtr:X}, {(IntPtr)chara->GameObject.DrawObject:X}"); + + Address = curPtr; + DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; + HasUnprocessedUpdate = true; + } + } + else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) + { + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress); + } + } + + private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) + { + bool hasChanges = false; + DoNotSendUpdate = false; + for (int i = 0; i < EquipSlotData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)equipSlotData, i); + if (EquipSlotData[i] != data) + { + EquipSlotData[i] = data; + hasChanges = true; + } + } + + for (int i = 0; i < CustomizeData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)customizeData, i); + if (CustomizeData[i] != data) + { + CustomizeData[i] = data; + hasChanges = true; + } + } + + var newHatState = Marshal.ReadByte((IntPtr)customizeData + 30, 0); + var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0); + if (newHatState != HatState) + { + if (HatState != null && !hasChanges && !HasUnprocessedUpdate) + { + Logger.Debug("Not Sending Update, only Hat changed"); + DoNotSendUpdate = true; + } + HatState = newHatState; + hasChanges = true; + } + + newWeaponOrVisorState &= 0b1101; // ignore drawing weapon + + if (newWeaponOrVisorState != VisorWeaponState) + { + if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate) + { + Logger.Debug("Not Sending Update, only Visor/Weapon changed"); + DoNotSendUpdate = true; + } + VisorWeaponState = newWeaponOrVisorState; + hasChanges = true; + } + + return hasChanges; + } } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 7a97189..b1770bd 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -14,193 +14,194 @@ using MareSynchronos.UI; using MareSynchronos.Utils; using Dalamud.Game.ClientState.Conditions; using MareSynchronos.FileCache; +using Dalamud.Logging; -namespace MareSynchronos +namespace MareSynchronos; + +public sealed class Plugin : IDalamudPlugin { - public sealed class Plugin : IDalamudPlugin + private const string CommandName = "/mare"; + private readonly ApiController _apiController; + private readonly CommandManager _commandManager; + private readonly Configuration _configuration; + private readonly PeriodicFileScanner _periodicFileScanner; + private readonly IntroUi _introUi; + private readonly IpcManager _ipcManager; + private readonly DalamudPluginInterface _pluginInterface; + private readonly SettingsUi _settingsUi; + private readonly WindowSystem _windowSystem; + private PlayerManager? _playerManager; + private TransientResourceManager? _transientResourceManager; + private readonly DalamudUtil _dalamudUtil; + private OnlinePlayerManager? _characterCacheManager; + private readonly DownloadUi _downloadUi; + private readonly FileDialogManager _fileDialogManager; + private readonly FileCacheManager _fileDbManager; + private readonly CompactUi _compactUi; + private readonly UiShared _uiSharedComponent; + private readonly Dalamud.Localization _localization; + + + public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, + Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition) { - private const string CommandName = "/mare"; - private readonly ApiController _apiController; - private readonly CommandManager _commandManager; - private readonly Configuration _configuration; - private readonly PeriodicFileScanner _periodicFileScanner; - private readonly IntroUi _introUi; - private readonly IpcManager _ipcManager; - private readonly DalamudPluginInterface _pluginInterface; - private readonly SettingsUi _settingsUi; - private readonly WindowSystem _windowSystem; - private PlayerManager? _playerManager; - private TransientResourceManager? _transientResourceManager; - private readonly DalamudUtil _dalamudUtil; - private OnlinePlayerManager? _characterCacheManager; - private readonly DownloadUi _downloadUi; - private readonly FileDialogManager _fileDialogManager; - private readonly FileCacheManager _fileDbManager; - private readonly CompactUi _compactUi; - private readonly UiShared _uiSharedComponent; - private readonly Dalamud.Localization _localization; + Logger.Debug("Launching " + Name); + _pluginInterface = pluginInterface; + _commandManager = commandManager; + _configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + _configuration.Initialize(_pluginInterface); + _configuration.Migrate(); + _localization = new Dalamud.Localization("MareSynchronos.Localization.", "", true); + _localization.SetupWithLangCode("en"); - public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition) + _windowSystem = new WindowSystem("MareSynchronos"); + + // those can be initialized outside of game login + _dalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition); + + _ipcManager = new IpcManager(_pluginInterface, _dalamudUtil); + _fileDialogManager = new FileDialogManager(); + _fileDbManager = new FileCacheManager(_ipcManager, _configuration, _pluginInterface.ConfigDirectory.FullName); + _apiController = new ApiController(_configuration, _dalamudUtil, _fileDbManager); + _periodicFileScanner = new PeriodicFileScanner(_ipcManager, _configuration, _fileDbManager, _apiController, _dalamudUtil); + + _uiSharedComponent = + new UiShared(_ipcManager, _apiController, _periodicFileScanner, _fileDialogManager, _configuration, _dalamudUtil, _pluginInterface, _localization); + _settingsUi = new SettingsUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); + _compactUi = new CompactUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); + + _introUi = new IntroUi(_windowSystem, _uiSharedComponent, _configuration, _periodicFileScanner); + _settingsUi.SwitchToIntroUi += () => { - Logger.Debug("Launching " + Name); - _pluginInterface = pluginInterface; - _commandManager = commandManager; - _configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - _configuration.Initialize(_pluginInterface); - _configuration.Migrate(); - - _localization = new Dalamud.Localization("MareSynchronos.Localization.", "", true); - _localization.SetupWithLangCode("en"); - - _windowSystem = new WindowSystem("MareSynchronos"); - - // those can be initialized outside of game login - _dalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition); - - _ipcManager = new IpcManager(_pluginInterface, _dalamudUtil); - _fileDialogManager = new FileDialogManager(); - _fileDbManager = new FileCacheManager(_ipcManager, _configuration, _pluginInterface.ConfigDirectory.FullName); - _apiController = new ApiController(_configuration, _dalamudUtil, _fileDbManager); - _periodicFileScanner = new PeriodicFileScanner(_ipcManager, _configuration, _fileDbManager, _apiController, _dalamudUtil); - - _uiSharedComponent = - new UiShared(_ipcManager, _apiController, _periodicFileScanner, _fileDialogManager, _configuration, _dalamudUtil, _pluginInterface, _localization); - _settingsUi = new SettingsUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); - _compactUi = new CompactUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); - - _introUi = new IntroUi(_windowSystem, _uiSharedComponent, _configuration, _periodicFileScanner); - _settingsUi.SwitchToIntroUi += () => - { - _introUi.IsOpen = true; - _settingsUi.IsOpen = false; - _compactUi.IsOpen = false; - }; - _introUi.SwitchToMainUi += () => - { - _introUi.IsOpen = false; - _compactUi.IsOpen = true; - _periodicFileScanner.StartScan(); - ReLaunchCharacterManager(); - }; - _compactUi.OpenSettingsUi += () => - { - _settingsUi.Toggle(); - }; - _downloadUi = new DownloadUi(_windowSystem, _configuration, _apiController, _uiSharedComponent); - - - _dalamudUtil.LogIn += DalamudUtilOnLogIn; - _dalamudUtil.LogOut += DalamudUtilOnLogOut; - - if (_dalamudUtil.IsLoggedIn) - { - DalamudUtilOnLogIn(); - } - } - - public string Name => "Mare Synchronos"; - public void Dispose() + _introUi.IsOpen = true; + _settingsUi.IsOpen = false; + _compactUi.IsOpen = false; + }; + _introUi.SwitchToMainUi += () => { - Logger.Verbose("Disposing " + Name); - _apiController?.Dispose(); - - _commandManager.RemoveHandler(CommandName); - _dalamudUtil.LogIn -= DalamudUtilOnLogIn; - _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - - _uiSharedComponent.Dispose(); - _settingsUi?.Dispose(); - _introUi?.Dispose(); - _downloadUi?.Dispose(); - _compactUi?.Dispose(); - - _periodicFileScanner?.Dispose(); - _fileDbManager?.Dispose(); - _playerManager?.Dispose(); - _characterCacheManager?.Dispose(); - _ipcManager?.Dispose(); - _transientResourceManager?.Dispose(); - _dalamudUtil.Dispose(); - Logger.Debug("Shut down"); - } - - - private void DalamudUtilOnLogIn() - { - Logger.Debug("Client login"); - - _pluginInterface.UiBuilder.Draw += Draw; - _pluginInterface.UiBuilder.OpenConfigUi += OpenUi; - _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) - { - HelpMessage = "Opens the Mare Synchronos UI" - }); - - if (!_configuration.HasValidSetup()) - { - _introUi.IsOpen = true; - _configuration.FullPause = false; - _configuration.Save(); - return; - } - + _introUi.IsOpen = false; + _compactUi.IsOpen = true; _periodicFileScanner.StartScan(); ReLaunchCharacterManager(); + }; + _compactUi.OpenSettingsUi += () => + { + _settingsUi.Toggle(); + }; + _downloadUi = new DownloadUi(_windowSystem, _configuration, _apiController, _uiSharedComponent); + + + _dalamudUtil.LogIn += DalamudUtilOnLogIn; + _dalamudUtil.LogOut += DalamudUtilOnLogOut; + + if (_dalamudUtil.IsLoggedIn) + { + DalamudUtilOnLogIn(); + } + } + + public string Name => "Mare Synchronos"; + public void Dispose() + { + Logger.Verbose("Disposing " + Name); + _apiController?.Dispose(); + + _commandManager.RemoveHandler(CommandName); + _dalamudUtil.LogIn -= DalamudUtilOnLogIn; + _dalamudUtil.LogOut -= DalamudUtilOnLogOut; + + _uiSharedComponent.Dispose(); + _settingsUi?.Dispose(); + _introUi?.Dispose(); + _downloadUi?.Dispose(); + _compactUi?.Dispose(); + + _periodicFileScanner?.Dispose(); + _fileDbManager?.Dispose(); + _playerManager?.Dispose(); + _characterCacheManager?.Dispose(); + _ipcManager?.Dispose(); + _transientResourceManager?.Dispose(); + _dalamudUtil.Dispose(); + Logger.Debug("Shut down"); + } + + + private void DalamudUtilOnLogIn() + { + Logger.Debug("Client login"); + + _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenUi; + _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Mare Synchronos UI" + }); + + if (!_configuration.HasValidSetup()) + { + _introUi.IsOpen = true; + _configuration.FullPause = false; + _configuration.Save(); + return; } - private void DalamudUtilOnLogOut() + _periodicFileScanner.StartScan(); + ReLaunchCharacterManager(); + } + + private void DalamudUtilOnLogOut() + { + Logger.Debug("Client logout"); + _characterCacheManager?.Dispose(); + _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenUi; + _commandManager.RemoveHandler(CommandName); + } + + public void ReLaunchCharacterManager() + { + _characterCacheManager?.Dispose(); + _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); + + Task.Run(WaitForPlayerAndLaunchCharacterManager); + } + + private async Task WaitForPlayerAndLaunchCharacterManager() + { + while (!_dalamudUtil.IsPlayerPresent) { - Logger.Debug("Client logout"); - _characterCacheManager?.Dispose(); - _playerManager?.Dispose(); - _transientResourceManager?.Dispose(); - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenUi; - _commandManager.RemoveHandler(CommandName); + await Task.Delay(100).ConfigureAwait(false); } - public void ReLaunchCharacterManager() + try { - _characterCacheManager?.Dispose(); - _playerManager?.Dispose(); - _transientResourceManager?.Dispose(); - - Task.Run(WaitForPlayerAndLaunchCharacterManager); + _transientResourceManager = new TransientResourceManager(_ipcManager, _dalamudUtil); + var characterCacheFactory = + new CharacterDataFactory(_dalamudUtil, _ipcManager, _transientResourceManager, _fileDbManager); + _playerManager = new PlayerManager(_apiController, _ipcManager, + characterCacheFactory, _dalamudUtil, _transientResourceManager, _periodicFileScanner); + _characterCacheManager = new OnlinePlayerManager(_apiController, + _dalamudUtil, _ipcManager, _playerManager, _fileDbManager); } - - private async Task WaitForPlayerAndLaunchCharacterManager() + catch (Exception ex) { - while (!_dalamudUtil.IsPlayerPresent) - { - await Task.Delay(100); - } - - try - { - _transientResourceManager = new TransientResourceManager(_ipcManager, _dalamudUtil); - var characterCacheFactory = - new CharacterDataFactory(_dalamudUtil, _ipcManager, _transientResourceManager, _fileDbManager); - _playerManager = new PlayerManager(_apiController, _ipcManager, - characterCacheFactory, _dalamudUtil, _transientResourceManager, _periodicFileScanner); - _characterCacheManager = new OnlinePlayerManager(_apiController, - _dalamudUtil, _ipcManager, _playerManager, _fileDbManager); - } - catch (Exception ex) - { - Logger.Debug(ex.Message); - } + Logger.Debug(ex.Message); } + } - private void Draw() - { - _windowSystem.Draw(); - _fileDialogManager.Draw(); - } + private void Draw() + { + _windowSystem.Draw(); + _fileDialogManager.Draw(); + } - private void OnCommand(string command, string args) - { + private void OnCommand(string command, string args) + { var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); if (splitArgs == null || splitArgs.Length == 0) @@ -226,14 +227,13 @@ namespace MareSynchronos _ = _apiController.CreateConnections(); } } - } + } - private void OpenUi() - { - if (_configuration.HasValidSetup()) - _compactUi.Toggle(); - else - _introUi.Toggle(); - } + private void OpenUi() + { + if (_configuration.HasValidSetup()) + _compactUi.Toggle(); + else + _introUi.Toggle(); } } diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 6818fd2..d0162c1 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; @@ -14,550 +15,615 @@ using MareSynchronos.API; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public class CompactUi : Window, IDisposable { - public class CompactUi : Window, IDisposable + private readonly ApiController _apiController; + private readonly Configuration _configuration; + public readonly Dictionary ShowUidForEntry = new(StringComparer.Ordinal); + private readonly UiShared _uiShared; + private readonly WindowSystem _windowSystem; + private string _characterOrCommentFilter = string.Empty; + + public string EditUserComment = string.Empty; + public string EditNickEntry = string.Empty; + + private string _pairToAdd = string.Empty; + + private readonly Stopwatch _timeout = new(); + private bool _buttonState; + + public float TransferPartHeight = 0; + public float _windowContentWidth = 0; + + + private bool showSyncShells = false; + private GroupPanel groupPanel; + + public CompactUi(WindowSystem windowSystem, + UiShared uiShared, Configuration configuration, ApiController apiController) : base("###MareSynchronosMainUI") { - private readonly ApiController _apiController; - private readonly Configuration _configuration; - private readonly Dictionary _showUidForEntry = new(); - private readonly UiShared _uiShared; - private readonly WindowSystem _windowSystem; - private string _characterOrCommentFilter = string.Empty; - - private string _editCharComment = string.Empty; - private string _editNickEntry = string.Empty; - private string _pairToAdd = string.Empty; - private readonly Stopwatch _timeout = new(); - private bool _buttonState; - - private float _transferPartHeight = 0; - - private float _windowContentWidth = 0; - - public CompactUi(WindowSystem windowSystem, - UiShared uiShared, Configuration configuration, ApiController apiController) : base("###MareSynchronosMainUI") - { #if DEBUG - string dateTime = "DEV VERSION"; - try - { - dateTime = VariousExtensions.GetLinkerTime(Assembly.GetCallingAssembly()).ToString("yyyyMMddHHmmss"); - } - catch (Exception ex) - { - Logger.Warn("Could not get assembly name"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace); - } - this.WindowName = "Mare Synchronos " + dateTime + "###MareSynchronosMainUI"; - Toggle(); + string dateTime = "DEV VERSION"; + try + { + dateTime = VariousExtensions.GetLinkerTime(Assembly.GetCallingAssembly()).ToString("yyyyMMddHHmmss"); + } + catch (Exception ex) + { + Logger.Warn("Could not get assembly name"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + } + this.WindowName = "Mare Synchronos " + dateTime + "###MareSynchronosMainUI"; + Toggle(); #else - this.WindowName = "Mare Synchronos " + Assembly.GetExecutingAssembly().GetName().Version; + this.WindowName = "Mare Synchronos " + Assembly.GetExecutingAssembly().GetName().Version + "###MareSynchronosMainUI"; #endif - Logger.Verbose("Creating " + nameof(CompactUi)); + Logger.Verbose("Creating " + nameof(CompactUi)); - _windowSystem = windowSystem; - _uiShared = uiShared; - _configuration = configuration; - _apiController = apiController; + _windowSystem = windowSystem; + _uiShared = uiShared; + _configuration = configuration; + _apiController = apiController; - SizeConstraints = new WindowSizeConstraints() + groupPanel = new(this, uiShared, configuration, apiController); + + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(350, 400), + MaximumSize = new Vector2(350, 2000), + }; + + windowSystem.AddWindow(this); + } + + public event SwitchUi? OpenSettingsUi; + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(CompactUi)); + _windowSystem.RemoveWindow(this); + } + + public override void Draw() + { + _windowContentWidth = UiShared.GetWindowContentRegionWidth(); + UiShared.DrawWithID("header", DrawUIDHeader); + ImGui.Separator(); + UiShared.DrawWithID("serverstatus", DrawServerStatus); + + if (_apiController.ServerState is ServerState.Connected) + { + var hasShownSyncShells = showSyncShells; + + ImGui.PushFont(UiBuilder.IconFont); + if (!hasShownSyncShells) { - MinimumSize = new Vector2(300, 400), - MaximumSize = new Vector2(300, 2000), - }; + ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); + } + if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30))) + { + showSyncShells = false; + } + if (!hasShownSyncShells) + { + ImGui.PopStyleColor(); + } + ImGui.PopFont(); + UiShared.AttachToolTip("Individual pairs"); - windowSystem.AddWindow(this); - } + ImGui.SameLine(); - public event SwitchUi? OpenSettingsUi; - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(CompactUi)); - _windowSystem.RemoveWindow(this); - } + ImGui.PushFont(UiBuilder.IconFont); + if (hasShownSyncShells) + { + ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); + } + if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30))) + { + showSyncShells = true; + } + if (hasShownSyncShells) + { + ImGui.PopStyleColor(); + } + ImGui.PopFont(); + + UiShared.AttachToolTip("Syncshells"); - public override void Draw() - { - _windowContentWidth = UiShared.GetWindowContentRegionWidth(); - UiShared.DrawWithID("header", DrawUIDHeader); ImGui.Separator(); - UiShared.DrawWithID("serverstatus", DrawServerStatus); - - if (_apiController.ServerState is ServerState.Connected) + if (!hasShownSyncShells) { - ImGui.Separator(); UiShared.DrawWithID("pairlist", DrawPairList); - ImGui.Separator(); - UiShared.DrawWithID("transfers", DrawTransfers); - _transferPartHeight = ImGui.GetCursorPosY() - _transferPartHeight; + } + else + { + UiShared.DrawWithID("syncshells", groupPanel.DrawSyncshells); + + } + ImGui.Separator(); + UiShared.DrawWithID("transfers", DrawTransfers); + TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; + } + } + + public override void OnClose() + { + EditNickEntry = string.Empty; + EditUserComment = string.Empty; + base.OnClose(); + } + private void DrawAddPair() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + if (_apiController.PairedClients.All(w => !string.Equals(w.OtherUID, _pairToAdd, StringComparison.Ordinal))) + { + _ = _apiController.SendPairedClientAddition(_pairToAdd); + _pairToAdd = string.Empty; } } + UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); - public override void OnClose() + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawFilter() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp); + var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play); + if (!_configuration.ReverseUserSort) { - _editNickEntry = string.Empty; - _editCharComment = string.Empty; - base.OnClose(); - } - private void DrawAddPair() - { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); - ImGui.InputTextWithHint("##otheruid", "Other players UID", ref _pairToAdd, 10); - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) { - if (_apiController.PairedClients.All(w => w.OtherUID != _pairToAdd)) - { - _ = _apiController.SendPairedClientAddition(_pairToAdd); - _pairToAdd = string.Empty; - } + _configuration.ReverseUserSort = true; + _configuration.Save(); } - UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); + UiShared.AttachToolTip("Sort by name descending"); + } + else + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowUp)) + { + _configuration.ReverseUserSort = false; + _configuration.Save(); + } + UiShared.AttachToolTip("Sort by name ascending"); + } + ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(2); + var users = GetFilteredUsers().ToList(); + 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; + ImGui.SameLine(); + + var pausedUsers = users.Where(u => u.IsPaused).ToList(); + var resumedUsers = users.Where(u => !u.IsPaused).ToList(); + + 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; } - private void DrawFilter() + var button = _buttonState ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + if (!_timeout.IsRunning || _timeout.ElapsedMilliseconds > 15000) { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp); - var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play); - if (!_configuration.ReverseUserSort) - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) - { - _configuration.ReverseUserSort = true; - _configuration.Save(); - } - UiShared.AttachToolTip("Sort by newest additions first"); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowUp)) - { - _configuration.ReverseUserSort = false; - _configuration.Save(); - } - UiShared.AttachToolTip("Sort by oldest additions first"); - } - ImGui.SameLine(); - - var users = GetFilteredUsers().ToList(); - 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); + _timeout.Reset(); - if (userCount == 0) return; - ImGui.SameLine(); - - var pausedUsers = users.Where(u => u.IsPaused).ToList(); - var resumedUsers = users.Where(u => !u.IsPaused).ToList(); - - 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)) - { - if (UiShared.CtrlPressed()) - { - Logger.Debug(users.Count.ToString()); - foreach (var entry in users) - { - _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); - } - - _timeout.Start(); - _buttonState = !_buttonState; - } - } - UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); - } - else - { - var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000; - ImGuiComponents.DisabledButton(button); - UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds"); - } - } - - private void DrawPairedClient(ClientPairDto entry) - { - var pauseIcon = entry.IsPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - - var buttonSize = UiShared.GetIconButtonSize(pauseIcon); - var trashButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Trash); - var entryUID = string.IsNullOrEmpty(entry.VanityUID) ? entry.OtherUID : entry.VanityUID; - var textSize = ImGui.CalcTextSize(entryUID); - var originalY = ImGui.GetCursorPosY(); - var buttonSizes = buttonSize.Y + trashButtonSize.Y; - - var textPos = originalY + buttonSize.Y / 2 - textSize.Y / 2; - ImGui.SetCursorPosY(textPos); - if (!entry.IsSynced) - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed); - ImGui.PopFont(); - - UiShared.AttachToolTip(entryUID + " has not added you back"); - } - else if (entry.IsPaused || entry.IsPausedFromOthers) - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); - ImGui.PopFont(); - - UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); - } - else - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); - ImGui.PopFont(); - - UiShared.AttachToolTip("You are paired with " + entryUID); - } - - var textIsUid = true; - _showUidForEntry.TryGetValue(entry.OtherUID, out var showUidInsteadOfName); - if (!showUidInsteadOfName && _configuration.GetCurrentServerUidComments().TryGetValue(entry.OtherUID, out var playerText)) - { - if (string.IsNullOrEmpty(playerText)) - { - playerText = entryUID; - } - else - { - textIsUid = false; - } - } - else - { - playerText = entryUID; - } - - ImGui.SameLine(); - if (_editNickEntry != entry.OtherUID) - { - ImGui.SetCursorPosY(textPos); - if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(playerText); - if (textIsUid) ImGui.PopFont(); - UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + - "Right click to change nick for " + entryUID); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - var prevState = textIsUid; - if (_showUidForEntry.ContainsKey(entry.OtherUID)) - { - prevState = _showUidForEntry[entry.OtherUID]; - } - - _showUidForEntry[entry.OtherUID] = !prevState; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _configuration.SetCurrentServerUidComment(_editNickEntry, _editCharComment); - _configuration.Save(); - _editCharComment = _configuration.GetCurrentServerUidComments().ContainsKey(entry.OtherUID) - ? _configuration.GetCurrentServerUidComments()[entry.OtherUID] - : string.Empty; - _editNickEntry = entry.OtherUID; - } - } - else - { - ImGui.SetCursorPosY(originalY); - - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); - if (ImGui.InputTextWithHint("", "Nick/Notes", ref _editCharComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - _configuration.SetCurrentServerUidComment(entry.OtherUID, _editCharComment); - _configuration.Save(); - _editNickEntry = string.Empty; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _editNickEntry = string.Empty; - } - UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); - } - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - ImGui.SetCursorPosY(originalY); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + if (ImGuiComponents.IconButton(button)) { if (UiShared.CtrlPressed()) { - _ = _apiController.SendPairedClientRemoval(entry.OtherUID); - _apiController.PairedClients.Remove(entry); + Logger.Debug(users.Count.ToString()); + foreach (var entry in users) + { + _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); + } + + _timeout.Start(); + _buttonState = !_buttonState; } } - UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); - - if (entry.IsSynced) - { - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X - ImGui.GetStyle().ItemSpacing.X - trashButtonSize.X); - ImGui.SetCursorPosY(originalY); - if (ImGuiComponents.IconButton(pauseIcon)) - { - _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); - } - UiShared.AttachToolTip(!entry.IsPaused - ? "Pause pairing with " + entryUID - : "Resume pairing with " + entryUID); - } + UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); } - - private void DrawPairList() + else { - UiShared.DrawWithID("addpair", DrawAddPair); - UiShared.DrawWithID("pairs", DrawPairs); - _transferPartHeight = ImGui.GetCursorPosY(); - UiShared.DrawWithID("filter", DrawFilter); - } - - private void DrawPairs() - { - var ySize = _transferPartHeight == 0 - ? 1 - : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _transferPartHeight - ImGui.GetCursorPosY(); - var users = GetFilteredUsers(); - - if (_configuration.ReverseUserSort) users = users.Reverse(); - - ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), false); - foreach (var entry in users.ToList()) - { - UiShared.DrawWithID(entry.OtherUID, () => DrawPairedClient(entry)); - } - ImGui.EndChild(); - } - - private IEnumerable GetFilteredUsers() - { - return _apiController.PairedClients.Where(p => - { - if (_characterOrCommentFilter.IsNullOrEmpty()) return true; - _configuration.GetCurrentServerUidComments().TryGetValue(p.OtherUID, out var comment); - var uid = p.VanityUID.IsNullOrEmpty() ? p.OtherUID : p.VanityUID; - return uid.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) || - (comment?.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) ?? false); - }); - } - - private void DrawServerStatus() - { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link); - var userCount = _apiController.OnlineUsers.ToString(); - var userSize = ImGui.CalcTextSize(userCount); - var textSize = ImGui.CalcTextSize("Users Online"); - - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2); - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("Users Online"); - } - else - { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); - } - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - var color = UiShared.GetBoolColor(!_configuration.FullPause); - var connectedIcon = !_configuration.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; - - ImGui.PushStyleColor(ImGuiCol.Text, color); - if (ImGuiComponents.IconButton(connectedIcon)) - { - _configuration.FullPause = !_configuration.FullPause; - _configuration.Save(); - _ = _apiController.CreateConnections(); - } - ImGui.PopStyleColor(); - UiShared.AttachToolTip(!_configuration.FullPause ? "Disconnect from " + _apiController.ServerDictionary[_configuration.ApiUri] : "Connect to " + _apiController.ServerDictionary[_configuration.ApiUri]); - } - - private void DrawTransfers() - { - var currentUploads = _apiController.CurrentUploads.ToList(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); - ImGui.PopFont(); - ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - - if (currentUploads.Any()) - { - var totalUploads = currentUploads.Count; - - var doneUploads = currentUploads.Count(c => c.IsTransferred); - var totalUploaded = currentUploads.Sum(c => c.Transferred); - var totalToUpload = currentUploads.Sum(c => c.Total); - - ImGui.Text($"{doneUploads}/{totalUploads}"); - var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})"; - var textSize = ImGui.CalcTextSize(uploadText); - ImGui.SameLine(_windowContentWidth - textSize.X); - ImGui.Text(uploadText); - } - else - { - ImGui.Text("No uploads in progress"); - } - - var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Download.ToIconString()); - ImGui.PopFont(); - ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - - if (currentDownloads.Any()) - { - var totalDownloads = currentDownloads.Count(); - var doneDownloads = currentDownloads.Count(c => c.IsTransferred); - var totalDownloaded = currentDownloads.Sum(c => c.Transferred); - var totalToDownload = currentDownloads.Sum(c => c.Total); - - ImGui.Text($"{doneDownloads}/{totalDownloads}"); - var downloadText = - $"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})"; - var textSize = ImGui.CalcTextSize(downloadText); - ImGui.SameLine(_windowContentWidth - textSize.X); - ImGui.Text(downloadText); - } - else - { - ImGui.Text("No downloads in progress"); - } - - ImGui.SameLine(); - } - - private void DrawUIDHeader() - { - var uidText = GetUidText(); - var buttonSizeX = 0f; - - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - var uidTextSize = ImGui.CalcTextSize(uidText); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - - var originalPos = ImGui.GetCursorPos(); - ImGui.SetWindowFontScale(1.5f); - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog); - buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) - { - OpenSettingsUi?.Invoke(); - } - UiShared.AttachToolTip("Open the Mare Synchronos Settings"); - - ImGui.SameLine(); //Important to draw the uidText consistently - ImGui.SetCursorPos(originalPos); - - if (_apiController.ServerState is ServerState.Connected) - { - buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; - ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) - { - ImGui.SetClipboardText(_apiController.UID); - } - UiShared.AttachToolTip("Copy your UID to clipboard"); - ImGui.SameLine(); - } - ImGui.SetWindowFontScale(1f); - - ImGui.SetCursorPosY(originalPos.Y + buttonSize.Y / 2 - uidTextSize.Y / 2 - ImGui.GetStyle().ItemSpacing.Y / 2); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 + buttonSizeX - uidTextSize.X / 2); - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextColored(GetUidColor(), uidText); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - - if (_apiController.ServerState is not ServerState.Connected) - { - UiShared.ColorTextWrapped(GetServerError(), GetUidColor()); - } - } - - - private string GetServerError() - { - return _apiController.ServerState switch - { - ServerState.Disconnected => "You are currently disconnected from the Mare Synchronos server.", - ServerState.Unauthorized => "Your account is not present on the server anymore or you are banned.", - ServerState.Offline => "Your selected Mare Synchronos server is currently offline.", - ServerState.VersionMisMatch => - "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", - ServerState.RateLimited => "You are rate limited for (re)connecting too often. Wait and try again later.", - ServerState.Connected => string.Empty, - _ => string.Empty - }; - } - - private Vector4 GetUidColor() - { - return _apiController.ServerState switch - { - ServerState.Connected => ImGuiColors.ParsedGreen, - ServerState.Disconnected => ImGuiColors.DalamudYellow, - ServerState.Unauthorized => ImGuiColors.DalamudRed, - ServerState.VersionMisMatch => ImGuiColors.DalamudRed, - ServerState.Offline => ImGuiColors.DalamudRed, - ServerState.RateLimited => ImGuiColors.DalamudYellow, - _ => ImGuiColors.DalamudRed - }; - } - - private string GetUidText() - { - return _apiController.ServerState switch - { - ServerState.Disconnected => "Disconnected", - ServerState.Unauthorized => "Unauthorized", - ServerState.VersionMisMatch => "Version mismatch", - ServerState.Offline => "Unavailable", - ServerState.RateLimited => "Rate Limited", - ServerState.Connected => _apiController.UID, - _ => string.Empty - }; + var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000; + ImGuiComponents.DisabledButton(button); + UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds"); } } + + private void DrawPairedClient(ClientPairDto entry) + { + var pauseIcon = entry.IsPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + var buttonSize = UiShared.GetIconButtonSize(pauseIcon); + var trashButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Trash); + var entryUID = string.IsNullOrEmpty(entry.VanityUID) ? entry.OtherUID : entry.VanityUID; + var textSize = ImGui.CalcTextSize(entryUID); + var originalY = ImGui.GetCursorPosY(); + var buttonSizes = buttonSize.Y + trashButtonSize.Y; + + var textPos = originalY + buttonSize.Y / 2 - textSize.Y / 2; + ImGui.SetCursorPosY(textPos); + if (!entry.IsSynced) + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed); + ImGui.PopFont(); + + UiShared.AttachToolTip(entryUID + " has not added you back"); + } + else if (entry.IsPaused || entry.IsPausedFromOthers) + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); + ImGui.PopFont(); + + UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); + } + else + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); + ImGui.PopFont(); + + UiShared.AttachToolTip("You are paired with " + entryUID); + } + + var textIsUid = true; + ShowUidForEntry.TryGetValue(entry.OtherUID, out var showUidInsteadOfName); + if (!showUidInsteadOfName && _configuration.GetCurrentServerUidComments().TryGetValue(entry.OtherUID, out var playerText)) + { + if (string.IsNullOrEmpty(playerText)) + { + playerText = entryUID; + } + else + { + textIsUid = false; + } + } + else + { + playerText = entryUID; + } + + ImGui.SameLine(); + if (!string.Equals(EditNickEntry, entry.OtherUID, StringComparison.Ordinal)) + { + ImGui.SetCursorPosY(textPos); + if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(playerText); + if (textIsUid) ImGui.PopFont(); + UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + + "Right click to change nick for " + entryUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (ShowUidForEntry.ContainsKey(entry.OtherUID)) + { + prevState = ShowUidForEntry[entry.OtherUID]; + } + + ShowUidForEntry[entry.OtherUID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configuration.SetCurrentServerUidComment(EditNickEntry, EditUserComment); + _configuration.Save(); + EditUserComment = _configuration.GetCurrentServerUidComments().ContainsKey(entry.OtherUID) + ? _configuration.GetCurrentServerUidComments()[entry.OtherUID] + : string.Empty; + EditNickEntry = entry.OtherUID; + } + } + else + { + ImGui.SetCursorPosY(originalY); + + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + if (ImGui.InputTextWithHint("", "Nick/Notes", ref EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _configuration.SetCurrentServerUidComment(entry.OtherUID, EditUserComment); + _configuration.Save(); + EditNickEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + EditNickEntry = string.Empty; + } + UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SetCursorPosY(originalY); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + { + if (UiShared.CtrlPressed()) + { + _ = _apiController.SendPairedClientRemoval(entry.OtherUID); + } + } + UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); + + if (entry.IsSynced) + { + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X - ImGui.GetStyle().ItemSpacing.X - trashButtonSize.X); + ImGui.SetCursorPosY(originalY); + if (ImGuiComponents.IconButton(pauseIcon)) + { + _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); + } + UiShared.AttachToolTip(!entry.IsPaused + ? "Pause pairing with " + entryUID + : "Resume pairing with " + entryUID); + } + } + + private void DrawPairList() + { + UiShared.DrawWithID("addpair", DrawAddPair); + UiShared.DrawWithID("pairs", DrawPairs); + TransferPartHeight = ImGui.GetCursorPosY(); + UiShared.DrawWithID("filter", DrawFilter); + } + + private void DrawPairs() + { + var ySize = TransferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY(); + var users = GetFilteredUsers(); + + users = users.OrderBy(u => _configuration.GetCurrentServerUidComments().ContainsKey(u.OtherUID) ? _configuration.GetCurrentServerUidComments()[u.OtherUID] : !string.IsNullOrEmpty(u.VanityUID) ? u.VanityUID : u.OtherUID); + if (_configuration.ReverseUserSort) users = users.Reverse(); + + ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), false); + foreach (var entry in users.ToList()) + { + UiShared.DrawWithID(entry.OtherUID, () => DrawPairedClient(entry)); + } + ImGui.EndChild(); + } + + + + private IEnumerable GetFilteredUsers() + { + return _apiController.PairedClients.Where(p => + { + if (_characterOrCommentFilter.IsNullOrEmpty()) return true; + _configuration.GetCurrentServerUidComments().TryGetValue(p.OtherUID, out var comment); + var uid = p.VanityUID.IsNullOrEmpty() ? p.OtherUID : p.VanityUID; + return uid.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) || + (comment?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false); + }); + } + + private void DrawServerStatus() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link); + var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture); + var userSize = ImGui.CalcTextSize(userCount); + var textSize = ImGui.CalcTextSize("Users Online"); +#if DEBUG + string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}"; +#else + string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}"; +#endif + var shardTextSize = ImGui.CalcTextSize(shardConnection); + + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2); + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Users Online"); + } + else + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); + } + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + var color = UiShared.GetBoolColor(!_configuration.FullPause); + var connectedIcon = !_configuration.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; + + ImGui.PushStyleColor(ImGuiCol.Text, color); + if (ImGuiComponents.IconButton(connectedIcon)) + { + _configuration.FullPause = !_configuration.FullPause; + _configuration.Save(); + _ = _apiController.CreateConnections(); + } + ImGui.PopStyleColor(); + UiShared.AttachToolTip(!_configuration.FullPause ? "Disconnect from " + _apiController.ServerDictionary[_configuration.ApiUri] : "Connect to " + _apiController.ServerDictionary[_configuration.ApiUri]); + + if (!string.IsNullOrEmpty(_apiController.ServerInfo.ShardName)) + { + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2); + ImGui.TextUnformatted(shardConnection); + } + } + + private void DrawTransfers() + { + var currentUploads = _apiController.CurrentUploads.ToList(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + if (currentUploads.Any()) + { + var totalUploads = currentUploads.Count; + + var doneUploads = currentUploads.Count(c => c.IsTransferred); + var totalUploaded = currentUploads.Sum(c => c.Transferred); + var totalToUpload = currentUploads.Sum(c => c.Total); + + ImGui.Text($"{doneUploads}/{totalUploads}"); + var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})"; + var textSize = ImGui.CalcTextSize(uploadText); + ImGui.SameLine(_windowContentWidth - textSize.X); + ImGui.Text(uploadText); + } + else + { + ImGui.Text("No uploads in progress"); + } + + var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Download.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + if (currentDownloads.Any()) + { + var totalDownloads = currentDownloads.Count(); + var doneDownloads = currentDownloads.Count(c => c.IsTransferred); + var totalDownloaded = currentDownloads.Sum(c => c.Transferred); + var totalToDownload = currentDownloads.Sum(c => c.Total); + + ImGui.Text($"{doneDownloads}/{totalDownloads}"); + var downloadText = + $"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})"; + var textSize = ImGui.CalcTextSize(downloadText); + ImGui.SameLine(_windowContentWidth - textSize.X); + ImGui.Text(downloadText); + } + else + { + ImGui.Text("No downloads in progress"); + } + + ImGui.SameLine(); + } + + private void DrawUIDHeader() + { + var uidText = GetUidText(); + var buttonSizeX = 0f; + + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + var uidTextSize = ImGui.CalcTextSize(uidText); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + var originalPos = ImGui.GetCursorPos(); + ImGui.SetWindowFontScale(1.5f); + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog); + buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) + { + OpenSettingsUi?.Invoke(); + } + UiShared.AttachToolTip("Open the Mare Synchronos Settings"); + + ImGui.SameLine(); //Important to draw the uidText consistently + ImGui.SetCursorPos(originalPos); + + if (_apiController.ServerState is ServerState.Connected) + { + buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_apiController.UID); + } + UiShared.AttachToolTip("Copy your UID to clipboard"); + ImGui.SameLine(); + } + ImGui.SetWindowFontScale(1f); + + ImGui.SetCursorPosY(originalPos.Y + buttonSize.Y / 2 - uidTextSize.Y / 2 - ImGui.GetStyle().ItemSpacing.Y / 2); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 + buttonSizeX - uidTextSize.X / 2); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextColored(GetUidColor(), uidText); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + if (_apiController.ServerState is not ServerState.Connected) + { + UiShared.ColorTextWrapped(GetServerError(), GetUidColor()); + } + } + + + private string GetServerError() + { + return _apiController.ServerState switch + { + ServerState.Disconnected => "You are currently disconnected from the Mare Synchronos server.", + ServerState.Unauthorized => "Your account is not present on the server anymore or you are banned.", + ServerState.Offline => "Your selected Mare Synchronos server is currently offline.", + ServerState.VersionMisMatch => + "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", + ServerState.RateLimited => "You are rate limited for (re)connecting too often. Wait and try again later.", + ServerState.Connected => string.Empty, + _ => string.Empty + }; + } + + private Vector4 GetUidColor() + { + return _apiController.ServerState switch + { + ServerState.Connected => ImGuiColors.ParsedGreen, + ServerState.Disconnected => ImGuiColors.DalamudYellow, + ServerState.Unauthorized => ImGuiColors.DalamudRed, + ServerState.VersionMisMatch => ImGuiColors.DalamudRed, + ServerState.Offline => ImGuiColors.DalamudRed, + ServerState.RateLimited => ImGuiColors.DalamudYellow, + _ => ImGuiColors.DalamudRed + }; + } + + private string GetUidText() + { + return _apiController.ServerState switch + { + ServerState.Disconnected => "Disconnected", + ServerState.Unauthorized => "Unauthorized", + ServerState.VersionMisMatch => "Version mismatch", + ServerState.Offline => "Unavailable", + ServerState.RateLimited => "Rate Limited", + ServerState.Connected => _apiController.UID, + _ => string.Empty + }; + } } diff --git a/MareSynchronos/UI/GroupPanel.cs b/MareSynchronos/UI/GroupPanel.cs new file mode 100644 index 0000000..37565b8 --- /dev/null +++ b/MareSynchronos/UI/GroupPanel.cs @@ -0,0 +1,544 @@ +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface; +using Dalamud.Utility; +using ImGuiNET; +using MareSynchronos.API; +using MareSynchronos.WebAPI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace MareSynchronos.UI +{ + internal class GroupPanel + { + private readonly CompactUi _mainUi; + private UiShared _uiShared; + private Configuration _configuration; + private ApiController _apiController; + + private readonly Dictionary _showGidForEntry = new(StringComparer.Ordinal); + private string _editGroupEntry = string.Empty; + private string _editGroupComment = string.Empty; + private string _syncShellPassword = string.Empty; + private string _syncShellToJoin = string.Empty; + + private bool _showModalEnterPassword; + private bool _showModalCreateGroup; + private bool _showModalChangePassword; + private string _newSyncShellPassword = string.Empty; + private bool _isPasswordValid; + private bool _errorGroupJoin; + private bool _errorGroupCreate = false; + private GroupCreatedDto? _lastCreatedGroup = null; + private readonly Dictionary ExpandedGroupState = new(StringComparer.Ordinal); + + public GroupPanel(CompactUi mainUi, UiShared uiShared, Configuration configuration, ApiController apiController) + { + _mainUi = mainUi; + _uiShared = uiShared; + _configuration = configuration; + _apiController = apiController; + } + + public void DrawSyncshells() + { + UiShared.DrawWithID("addsyncshell", DrawAddSyncshell); + UiShared.DrawWithID("syncshelllist", DrawSyncshellList); + _mainUi.TransferPartHeight = ImGui.GetCursorPosY(); + } + + private void DrawAddSyncshell() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias", ref _syncShellToJoin, 20); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + + bool userCanJoinMoreGroups = _apiController.Groups.Count < _apiController.ServerInfo.MaxGroupsJoinedByUser; + bool userCanCreateMoreGroups = _apiController.Groups.Count(u => string.Equals(u.OwnedBy, _apiController.UID, StringComparison.Ordinal)) < _apiController.ServerInfo.MaxGroupsCreatedByUser; + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + if (_apiController.Groups.All(w => !string.Equals(w.GID, _syncShellToJoin, StringComparison.Ordinal)) && !string.IsNullOrEmpty(_syncShellToJoin)) + { + if (userCanJoinMoreGroups) + { + _errorGroupJoin = false; + _showModalEnterPassword = true; + ImGui.OpenPopup("Enter Syncshell Password"); + } + } + else + { + if (userCanCreateMoreGroups) + { + _lastCreatedGroup = null; + _errorGroupCreate = false; + _showModalCreateGroup = true; + ImGui.OpenPopup("Create Syncshell"); + } + } + } + UiShared.AttachToolTip(_syncShellToJoin.IsNullOrEmpty() + ? (userCanCreateMoreGroups ? "Create Syncshell" : $"You cannot create more than {_apiController.ServerInfo.MaxGroupsCreatedByUser} Syncshells") + : (userCanJoinMoreGroups ? "Join Syncshell" + _syncShellToJoin : $"You cannot join more than {_apiController.ServerInfo.MaxGroupsJoinedByUser} Syncshells")); + + if (ImGui.BeginPopupModal("Enter Syncshell Password", ref _showModalEnterPassword, ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped("Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell."); + ImGui.Separator(); + UiShared.TextWrapped("Enter the password for Syncshell " + _syncShellToJoin + ":"); + ImGui.InputTextWithHint("##password", _syncShellToJoin + " Password", ref _syncShellPassword, 255, ImGuiInputTextFlags.Password); + if (_errorGroupJoin) + { + UiShared.ColorTextWrapped($"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({_apiController.ServerInfo.MaxGroupsJoinedByUser}), " + + $"it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({_apiController.ServerInfo.MaxGroupUserCount} users) or the Syncshell has closed invites.", + new Vector4(1, 0, 0, 1)); + } + if (ImGui.Button("Join " + _syncShellToJoin)) + { + var shell = _syncShellToJoin; + var pw = _syncShellPassword; + _errorGroupJoin = !_apiController.SendGroupJoin(shell, pw).Result; + if (!_errorGroupJoin) + { + _syncShellToJoin = string.Empty; + _showModalEnterPassword = false; + } + _syncShellPassword = string.Empty; + } + ImGui.EndPopup(); + } + + if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup)) + { + ImGui.SetWindowSize(new(400, 200)); + UiShared.TextWrapped("Press the button below to create a new Syncshell."); + ImGui.SetNextItemWidth(200); + if (ImGui.Button("Create Syncshell")) + { + try + { + _lastCreatedGroup = _apiController.CreateGroup().Result; + } + catch + { + _lastCreatedGroup = null; + _errorGroupCreate = true; + } + } + + if (_lastCreatedGroup != null) + { + ImGui.Separator(); + _errorGroupCreate = false; + ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.GID); + ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_lastCreatedGroup.Password); + } + UiShared.TextWrapped("You can change the Syncshell password later at any time."); + } + + if (_errorGroupCreate) + { + UiShared.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.", + new Vector4(1, 0, 0, 1)); + } + + ImGui.EndPopup(); + } + + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawSyncshellList() + { + var ySize = _mainUi.TransferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY(); + ImGui.BeginChild("list", new Vector2(_mainUi._windowContentWidth, ySize), false); + foreach (var entry in _apiController.Groups.OrderBy(g => string.IsNullOrEmpty(g.Alias) ? g.GID : g.Alias).ToList()) + { + UiShared.DrawWithID(entry.GID, () => DrawSyncshell(entry)); + } + ImGui.EndChild(); + } + + private void DrawSyncshell(GroupDto group) + { + var name = group.Alias ?? group.GID; + var pairsInGroup = _apiController.GroupPairedClients.Where(p => string.Equals(p.GroupGID, group.GID, StringComparison.Ordinal)).ToList(); + if (!ExpandedGroupState.TryGetValue(group.GID, out bool isExpanded)) + { + isExpanded = false; + ExpandedGroupState.Add(group.GID, isExpanded); + } + var icon = isExpanded ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + var collapseButton = UiShared.GetIconButtonSize(icon); + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0)); + if (ImGuiComponents.IconButton(icon)) + { + ExpandedGroupState[group.GID] = !ExpandedGroupState[group.GID]; + } + ImGui.PopStyleColor(2); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + collapseButton.X); + var pauseIcon = (group.IsPaused ?? false) ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + if (ImGuiComponents.IconButton(pauseIcon)) + { + _ = _apiController.SendPauseGroup(group.GID, !group.IsPaused ?? false); + } + UiShared.AttachToolTip(((group.IsPaused ?? false) ? "Resume" : "Pause") + " pairing with all users in this Syncshell"); + ImGui.SameLine(); + + var groupName = string.IsNullOrEmpty(group.Alias) ? group.GID : group.Alias; + var textIsGid = true; + + if (string.Equals(group.OwnedBy, _apiController.UID, StringComparison.Ordinal)) + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Crown.ToIconString()); + ImGui.PopFont(); + UiShared.AttachToolTip("You are the owner of Syncshell " + groupName); + ImGui.SameLine(); + } + + _showGidForEntry.TryGetValue(group.GID, out var showGidInsteadOfName); + if (!showGidInsteadOfName && _configuration.GetCurrentServerGidComments().TryGetValue(group.GID, out var groupComment)) + { + if (!string.IsNullOrEmpty(groupComment)) + { + groupName = groupComment; + textIsGid = false; + } + } + + if (!string.Equals(_editGroupEntry, group.GID, StringComparison.Ordinal)) + { + if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(groupName); + if (textIsGid) ImGui.PopFont(); + UiShared.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine + + "Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine + + "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + group.OwnedBy); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsGid; + if (_showGidForEntry.ContainsKey(group.GID)) + { + prevState = _showGidForEntry[group.GID]; + } + + _showGidForEntry[group.GID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configuration.SetCurrentServerGidComment(_editGroupEntry, _editGroupComment); + _configuration.Save(); + _editGroupComment = _configuration.GetCurrentServerGidComments().ContainsKey(group.GID) + ? _configuration.GetCurrentServerGidComments()[group.GID] + : string.Empty; + _editGroupEntry = group.GID; + } + } + else + { + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX()); + if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _configuration.SetCurrentServerGidComment(group.GID, _editGroupComment); + _configuration.Save(); + _editGroupEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editGroupEntry = string.Empty; + } + UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + UiShared.DrawWithID(group.GID + "settings", () => DrawSyncShellButtons(group, name)); + + ImGui.Indent(collapseButton.X); + if (ExpandedGroupState[group.GID]) + { + pairsInGroup = pairsInGroup.OrderBy(p => string.Equals(p.UserUID, group.OwnedBy, StringComparison.Ordinal) ? 0 : 1).ThenBy(p => p.IsPinned ?? false ? 0 : 1).ThenBy(p => p.UserAlias ?? p.UserUID).ToList(); + ImGui.Indent(ImGui.GetStyle().ItemSpacing.X / 2); + ImGui.Separator(); + foreach (var pair in pairsInGroup) + { + UiShared.DrawWithID(group.GID + pair.UserUID, () => DrawSyncshellPairedClient(pair, string.Equals(group.OwnedBy, _apiController.UID, StringComparison.Ordinal), group?.IsPaused ?? false)); + } + + ImGui.Separator(); + ImGui.Unindent(ImGui.GetStyle().ItemSpacing.X / 2); + } + ImGui.Unindent(collapseButton.X); + } + + private void DrawSyncShellButtons(GroupDto entry, string name) + { + bool invitesEnabled = entry.InvitesEnabled ?? true; + var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; + var iconSize = UiShared.GetIconSize(lockedIcon); + var barbuttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars); + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - barbuttonSize.X - iconSize.X - ImGui.GetStyle().ItemSpacing.X); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(lockedIcon.ToIconString()); + ImGui.PopFont(); + UiShared.AttachToolTip(invitesEnabled ? "Syncshell is open for new joiners" : "Syncshell is closed for new joiners"); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("ShellPopup"); + } + + if (ImGui.BeginPopup("ShellPopup")) + { + if (UiShared.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell")) + { + if (UiShared.CtrlPressed()) + { + _ = _apiController.SendLeaveGroup(entry.GID); + } + } + UiShared.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(entry.OwnedBy, _apiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine + + "WARNING: This action is irreverisble" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")); + + if (UiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy ID")) + { + ImGui.SetClipboardText(string.IsNullOrEmpty(entry.Alias) ? entry.GID : entry.Alias); + } + UiShared.AttachToolTip("Copy Syncshell ID to Clipboard"); + + if (string.Equals(entry.OwnedBy, _apiController.UID, StringComparison.Ordinal)) + { + ImGui.Separator(); + + if (UiShared.IconTextButton(lockedIcon, invitesEnabled ? "Lock Syncshell" : "Unlock Syncshell")) + { + _ = _apiController.SendGroupChangeInviteState(entry.GID, !entry.InvitesEnabled ?? true); + } + UiShared.AttachToolTip("Change Syncshell joining permissions" + Environment.NewLine + "Syncshell is currently " + (invitesEnabled ? "open" : "closed") + " for people to join"); + + if (UiShared.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) + { + ImGui.OpenPopup("Change Syncshell Password"); + _isPasswordValid = true; + _showModalChangePassword = true; + } + UiShared.AttachToolTip("Change Syncshell Password"); + + if (ImGui.BeginPopupModal("Change Syncshell Password", ref _showModalChangePassword, ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped("Enter the new Syncshell password for Syncshell " + name + " here."); + UiShared.TextWrapped("This action is irreversible"); + ImGui.InputTextWithHint("##changepw", "New password for " + name, ref _newSyncShellPassword, 255); + if (ImGui.Button("Change password")) + { + var pw = _newSyncShellPassword; + _isPasswordValid = _apiController.ChangeGroupPassword(entry.GID, pw).Result; + _newSyncShellPassword = string.Empty; + if (_isPasswordValid) _showModalChangePassword = false; + } + + if (!_isPasswordValid) + { + UiShared.ColorTextWrapped("The selected password is too short. It must be at least 10 characters.", new Vector4(1, 0, 0, 1)); + } + + ImGui.EndPopup(); + } + + if (UiShared.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) + { + if (UiShared.CtrlPressed()) + { + _ = _apiController.SendClearGroup(entry.GID); + } + } + UiShared.AttachToolTip("Hold CTRL and click to clear this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible." + Environment.NewLine + + "Clearing the Syncshell will remove all not pinned users from it."); + + if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell")) + { + if (UiShared.CtrlPressed() && UiShared.ShiftPressed()) + { + _ = _apiController.SendDeleteGroup(entry.GID); + } + } + UiShared.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); + } + + ImGui.EndPopup(); + } + } + + private void DrawSyncshellPairedClient(GroupPairDto entry, bool isOwner, bool isPausedByYou) + { + var plusButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); + var barButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars); + var entryUID = string.IsNullOrEmpty(entry.UserAlias) ? entry.UserUID : entry.UserAlias; + var textSize = ImGui.CalcTextSize(entryUID); + var originalY = ImGui.GetCursorPosY(); + var buttonSizes = plusButtonSize.Y; + + var textPos = originalY + plusButtonSize.Y / 2 - textSize.Y / 2; + ImGui.SetCursorPosY(textPos); + if (isPausedByYou || (entry.IsPaused ?? false)) + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); + ImGui.PopFont(); + + UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); + } + else + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); + ImGui.PopFont(); + + UiShared.AttachToolTip("You are paired with " + entryUID); + } + + if (entry.IsPinned ?? false) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPos); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); + ImGui.PopFont(); + UiShared.AttachToolTip("User is pinned in this Syncshell"); + } + + var textIsUid = true; + _mainUi.ShowUidForEntry.TryGetValue(entry.UserUID, out var showUidInsteadOfName); + if (!showUidInsteadOfName && _configuration.GetCurrentServerUidComments().TryGetValue(entry.UserUID, out var playerText)) + { + if (string.IsNullOrEmpty(playerText)) + { + playerText = entryUID; + } + else + { + textIsUid = false; + } + } + else + { + playerText = entryUID; + } + + ImGui.SameLine(); + if (!string.Equals(_mainUi.EditNickEntry, entry.UserUID, StringComparison.Ordinal)) + { + ImGui.SetCursorPosY(textPos); + if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(playerText); + if (textIsUid) ImGui.PopFont(); + UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + + "Right click to change nick for " + entryUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (_mainUi.ShowUidForEntry.ContainsKey(entry.UserUID)) + { + prevState = _mainUi.ShowUidForEntry[entry.UserUID]; + } + + _mainUi.ShowUidForEntry[entry.UserUID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configuration.SetCurrentServerUidComment(_mainUi.EditNickEntry, _mainUi.EditUserComment); + _configuration.Save(); + _mainUi.EditUserComment = _configuration.GetCurrentServerUidComments().ContainsKey(entry.UserUID) + ? _configuration.GetCurrentServerUidComments()[entry.UserUID] + : string.Empty; + _mainUi.EditNickEntry = entry.UserUID; + } + } + else + { + ImGui.SetCursorPosY(originalY); + + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + if (ImGui.InputTextWithHint("", "Nick/Notes", ref _mainUi.EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _configuration.SetCurrentServerUidComment(entry.UserUID, _mainUi.EditUserComment); + _configuration.Save(); + _mainUi.EditNickEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _mainUi.EditNickEntry = string.Empty; + } + UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + bool plusButtonShown = !_apiController.PairedClients.Any(p => string.Equals(p.OtherUID, entry.UserUID, StringComparison.Ordinal)); + + if (plusButtonShown) + { + ImGui.SetCursorPosY(originalY); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - plusButtonSize.X - (isOwner ? barButtonSize.X + ImGui.GetStyle().ItemSpacing.X : 0)); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + _ = _apiController.SendPairedClientAddition(entry.UserUID); + } + UiShared.AttachToolTip("Pair with " + entryUID + " individually"); + } + + if (isOwner) + { + ImGui.SetCursorPosY(originalY); + var subtractedWidth = plusButtonShown ? (plusButtonSize.X) : 0; + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - barButtonSize.X); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("Popup"); + } + } + + if (ImGui.BeginPopup("Popup")) + { + if (UiShared.IconTextButton(FontAwesomeIcon.Thumbtack, "Pin user")) + { + _ = _apiController.SendChangeUserPinned(entry.GroupGID, entry.UserUID, !entry.IsPinned ?? false); + } + UiShared.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); + if (UiShared.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership")) + { + if (UiShared.CtrlPressed() && UiShared.ShiftPressed()) + { + _ = _apiController.ChangeOwnerOfGroup(entry.GroupGID, entry.UserUID); + } + } + UiShared.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (entry.UserAlias ?? entry.UserUID) + Environment.NewLine + "WARNING: This action is irreversible."); + if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Remove user")) + { + if (UiShared.CtrlPressed()) + { + _ = _apiController.SendRemoveUserFromGroup(entry.GroupGID, entry.UserUID); + } + } + UiShared.AttachToolTip("Hold CTRL and click to remove user " + (entry.UserAlias ?? entry.UserUID) + " from Syncshell"); + ImGui.EndPopup(); + } + } + } +} diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index e15d85b..3135309 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -12,287 +12,286 @@ using MareSynchronos.Localization; using Dalamud.Utility; using MareSynchronos.FileCache; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +internal class IntroUi : Window, IDisposable { - internal class IntroUi : Window, IDisposable + private readonly UiShared _uiShared; + private readonly Configuration _pluginConfiguration; + private readonly PeriodicFileScanner _fileCacheManager; + private readonly WindowSystem _windowSystem; + private bool _readFirstPage; + + public event SwitchUi? SwitchToMainUi; + + private string[] TosParagraphs; + + private Tuple _darkSoulsCaptcha1 = new(string.Empty, string.Empty); + private Tuple _darkSoulsCaptcha2 = new(string.Empty, string.Empty); + private Tuple _darkSoulsCaptcha3 = new(string.Empty, string.Empty); + private string _enteredDarkSoulsCaptcha1 = string.Empty; + private string _enteredDarkSoulsCaptcha2 = string.Empty; + private string _enteredDarkSoulsCaptcha3 = string.Empty; + + private bool _failedOnce = false; + private Task _timeoutTask; + private string _timeoutTime; + + private Dictionary _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; + private int _currentLanguage; + + private bool DarkSoulsCaptchaValid => string.Equals(_darkSoulsCaptcha1.Item2, _enteredDarkSoulsCaptcha1.Trim() +, StringComparison.Ordinal) && string.Equals(_darkSoulsCaptcha2.Item2, _enteredDarkSoulsCaptcha2.Trim() +, StringComparison.Ordinal) && string.Equals(_darkSoulsCaptcha3.Item2, _enteredDarkSoulsCaptcha3.Trim(), StringComparison.Ordinal); + + + public void Dispose() { - private readonly UiShared _uiShared; - private readonly Configuration _pluginConfiguration; - private readonly PeriodicFileScanner _fileCacheManager; - private readonly WindowSystem _windowSystem; - private bool _readFirstPage; + Logger.Verbose("Disposing " + nameof(IntroUi)); - public event SwitchUi? SwitchToMainUi; + _windowSystem.RemoveWindow(this); + } - private string[] TosParagraphs; + public IntroUi(WindowSystem windowSystem, UiShared uiShared, Configuration pluginConfiguration, + PeriodicFileScanner fileCacheManager) : base("Mare Synchronos Setup") + { + Logger.Verbose("Creating " + nameof(IntroUi)); - private Tuple _darkSoulsCaptcha1 = new(string.Empty, string.Empty); - private Tuple _darkSoulsCaptcha2 = new(string.Empty, string.Empty); - private Tuple _darkSoulsCaptcha3 = new(string.Empty, string.Empty); - private string _enteredDarkSoulsCaptcha1 = string.Empty; - private string _enteredDarkSoulsCaptcha2 = string.Empty; - private string _enteredDarkSoulsCaptcha3 = string.Empty; + _uiShared = uiShared; + _pluginConfiguration = pluginConfiguration; + _fileCacheManager = fileCacheManager; + _windowSystem = windowSystem; - private bool _failedOnce = false; - private Task _timeoutTask; - private string _timeoutTime; - - private Dictionary _languages = new() { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; - private int _currentLanguage; - - private bool DarkSoulsCaptchaValid => _darkSoulsCaptcha1.Item2 == _enteredDarkSoulsCaptcha1.Trim() - && _darkSoulsCaptcha2.Item2 == _enteredDarkSoulsCaptcha2.Trim() - && _darkSoulsCaptcha3.Item2 == _enteredDarkSoulsCaptcha3.Trim(); - - - public void Dispose() + SizeConstraints = new WindowSizeConstraints() { - Logger.Verbose("Disposing " + nameof(IntroUi)); + MinimumSize = new Vector2(600, 400), + MaximumSize = new Vector2(600, 2000) + }; - _windowSystem.RemoveWindow(this); - } + GetToSLocalization(); - public IntroUi(WindowSystem windowSystem, UiShared uiShared, Configuration pluginConfiguration, - PeriodicFileScanner fileCacheManager) : base("Mare Synchronos Setup") + _windowSystem.AddWindow(this); + } + + public override void Draw() + { + if (!_pluginConfiguration.AcceptedAgreement && !_readFirstPage) { - Logger.Verbose("Creating " + nameof(IntroUi)); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("Welcome to Mare Synchronos"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + + "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); + UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); - _uiShared = uiShared; - _pluginConfiguration = pluginConfiguration; - _fileCacheManager = fileCacheManager; - _windowSystem = windowSystem; - - SizeConstraints = new WindowSizeConstraints() + UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + + "might look broken because of this or others players mods might not apply on your end altogether. " + + "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); + if (!_uiShared.DrawOtherPluginState()) return; + ImGui.Separator(); + if (ImGui.Button("Next##toAgreement")) { - MinimumSize = new Vector2(600, 400), - MaximumSize = new Vector2(600, 2000) - }; - - GetToSLocalization(); - - _windowSystem.AddWindow(this); - } - - public override void Draw() - { - if (!_pluginConfiguration.AcceptedAgreement && !_readFirstPage) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("Welcome to Mare Synchronos"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + - "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); - UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); - - UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + - "might look broken because of this or others players mods might not apply on your end altogether. " + - "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); - if (!_uiShared.DrawOtherPluginState()) return; - ImGui.Separator(); - if (ImGui.Button("Next##toAgreement")) - { - _readFirstPage = true; - } + _readFirstPage = true; } - else if (!_pluginConfiguration.AcceptedAgreement && _readFirstPage) + } + else if (!_pluginConfiguration.AcceptedAgreement && _readFirstPage) + { + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + var textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); + ImGui.TextUnformatted(Strings.ToS.AgreementLabel); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + ImGui.SameLine(); + var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); + ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2); + + ImGui.TextUnformatted(Strings.ToS.LanguageLabel); + ImGui.SameLine(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2); + ImGui.SetNextItemWidth(80); + if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count)) { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - var textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); - ImGui.TextUnformatted(Strings.ToS.AgreementLabel); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); + GetToSLocalization(_currentLanguage); + } - ImGui.SameLine(); - var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); - ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2); + ImGui.Separator(); + ImGui.SetWindowFontScale(1.5f); + string readThis = Strings.ToS.ReadLabel; + textSize = ImGui.CalcTextSize(readThis); + ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); + UiShared.ColorText(readThis, ImGuiColors.DalamudRed); + ImGui.SetWindowFontScale(1.0f); + ImGui.Separator(); - ImGui.TextUnformatted(Strings.ToS.LanguageLabel); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2); - ImGui.SetNextItemWidth(80); - if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count)) + + UiShared.TextWrapped(TosParagraphs[0]); + UiShared.TextWrapped(TosParagraphs[1]); + UiShared.TextWrapped(TosParagraphs[2]); + UiShared.TextWrapped(TosParagraphs[3]); + UiShared.TextWrapped(TosParagraphs[4]); + UiShared.TextWrapped(TosParagraphs[5]); + + ImGui.Separator(); + if ((!_pluginConfiguration.DarkSoulsAgreement || DarkSoulsCaptchaValid) && (_timeoutTask?.IsCompleted ?? true)) + { + if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup")) { - GetToSLocalization(_currentLanguage); - } + _enteredDarkSoulsCaptcha1 = string.Empty; + _enteredDarkSoulsCaptcha2 = string.Empty; + _enteredDarkSoulsCaptcha3 = string.Empty; - ImGui.Separator(); - ImGui.SetWindowFontScale(1.5f); - string readThis = Strings.ToS.ReadLabel; - textSize = ImGui.CalcTextSize(readThis); - ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); - UiShared.ColorText(readThis, ImGuiColors.DalamudRed); - ImGui.SetWindowFontScale(1.0f); - ImGui.Separator(); - - - UiShared.TextWrapped(TosParagraphs[0]); - UiShared.TextWrapped(TosParagraphs[1]); - UiShared.TextWrapped(TosParagraphs[2]); - UiShared.TextWrapped(TosParagraphs[3]); - UiShared.TextWrapped(TosParagraphs[4]); - UiShared.TextWrapped(TosParagraphs[5]); - - ImGui.Separator(); - if ((!_pluginConfiguration.DarkSoulsAgreement || DarkSoulsCaptchaValid) && (_timeoutTask?.IsCompleted ?? true)) - { - if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup")) + if (UiShared.CtrlPressed()) { - _enteredDarkSoulsCaptcha1 = string.Empty; - _enteredDarkSoulsCaptcha2 = string.Empty; - _enteredDarkSoulsCaptcha3 = string.Empty; - - if (UiShared.CtrlPressed()) - { - _pluginConfiguration.AcceptedAgreement = true; - _pluginConfiguration.Save(); - } - else - { - if (!_failedOnce) - { - _failedOnce = true; - _timeoutTask = Task.Run(async () => - { - for (int i = 60; i > 0; i--) - { - _timeoutTime = $"{i}s " + Strings.ToS.RemainingLabel; - Logger.Debug(_timeoutTime); - await Task.Delay(TimeSpan.FromSeconds(1)); - } - }); - } - else - { - _pluginConfiguration.DarkSoulsAgreement = true; - _pluginConfiguration.Save(); - GenerateDarkSoulsAgreementCaptcha(); - } - } - } - } - else - { - if (_failedOnce && (!_timeoutTask?.IsCompleted ?? true)) - { - UiShared.ColorTextWrapped(Strings.ToS.FailedLabel, ImGuiColors.DalamudYellow); - UiShared.TextWrapped(Strings.ToS.TimeoutLabel); - UiShared.TextWrapped(_timeoutTime); + _pluginConfiguration.AcceptedAgreement = true; + _pluginConfiguration.Save(); } else { - UiShared.ColorTextWrapped(Strings.ToS.FailedAgainLabel, ImGuiColors.DalamudYellow); - UiShared.TextWrapped(Strings.ToS.PuzzleLabel); - UiShared.TextWrapped(Strings.ToS.PuzzleDescLabel); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha1.Item1, ref _enteredDarkSoulsCaptcha1, 255); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha2.Item1, ref _enteredDarkSoulsCaptcha2, 255); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha3.Item1, ref _enteredDarkSoulsCaptcha3, 255); + if (!_failedOnce) + { + _failedOnce = true; + _timeoutTask = Task.Run(async () => + { + for (int i = 60; i > 0; i--) + { + _timeoutTime = $"{i}s " + Strings.ToS.RemainingLabel; + Logger.Debug(_timeoutTime); + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + } + }); + } + else + { + _pluginConfiguration.DarkSoulsAgreement = true; + _pluginConfiguration.Save(); + GenerateDarkSoulsAgreementCaptcha(); + } } } } - else if (_pluginConfiguration.AcceptedAgreement - && (string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) - || _pluginConfiguration.InitialScanComplete == false - || !Directory.Exists(_pluginConfiguration.CacheFolder))) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("File Cache Setup"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - - if (!_uiShared.HasValidPenumbraModPath) - { - UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); - } - else - { - UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + - "Additionally, a local cache folder must be set where Mare Synchronos will download its local file cache to. " + - "Once the Cache Folder is set and the scan complete, this page will automatically forward to registration at a service."); - UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); - UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.db of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + - "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); - _uiShared.DrawCacheDirectorySetting(); - } - - if (!_fileCacheManager.IsScanRunning && !string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_pluginConfiguration.CacheFolder)) - { - if (ImGui.Button("Start Scan##startScan")) - { - _fileCacheManager.InvokeScan(true); - } - } - else - { - _uiShared.DrawFileScanState(); - } - } - else if (!_uiShared.ApiController.ServerAlive) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("Service Registration"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); - UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); - UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #registration."); - - if (ImGui.Button("Join the Mare Synchronos Discord")) - { - Util.OpenLink("https://discord.gg/mpNdkrTRjW"); - } - - UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); - - ImGui.Separator(); - - UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); - - _uiShared.DrawServiceSelection(() => { }); - } else { - SwitchToMainUi?.Invoke(); - IsOpen = false; + if (_failedOnce && (!_timeoutTask?.IsCompleted ?? true)) + { + UiShared.ColorTextWrapped(Strings.ToS.FailedLabel, ImGuiColors.DalamudYellow); + UiShared.TextWrapped(Strings.ToS.TimeoutLabel); + UiShared.TextWrapped(_timeoutTime); + } + else + { + UiShared.ColorTextWrapped(Strings.ToS.FailedAgainLabel, ImGuiColors.DalamudYellow); + UiShared.TextWrapped(Strings.ToS.PuzzleLabel); + UiShared.TextWrapped(Strings.ToS.PuzzleDescLabel); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha1.Item1, ref _enteredDarkSoulsCaptcha1, 255); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha2.Item1, ref _enteredDarkSoulsCaptcha2, 255); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha3.Item1, ref _enteredDarkSoulsCaptcha3, 255); + } } } - - private string _secretKey = string.Empty; - - private void GetToSLocalization(int changeLanguageTo = -1) + else if (_pluginConfiguration.AcceptedAgreement + && (string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) + || _pluginConfiguration.InitialScanComplete == false + || !Directory.Exists(_pluginConfiguration.CacheFolder))) { - if (changeLanguageTo != -1) + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("File Cache Setup"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + + if (!_uiShared.HasValidPenumbraModPath) { - _uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value); + UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); } - - TosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 }; - - if (_pluginConfiguration.DarkSoulsAgreement) + else { - GenerateDarkSoulsAgreementCaptcha(); + UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + + "Additionally, a local cache folder must be set where Mare Synchronos will download its local file cache to. " + + "Once the Cache Folder is set and the scan complete, this page will automatically forward to registration at a service."); + UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); + UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.db of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + + "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); + _uiShared.DrawCacheDirectorySetting(); + } + + if (!_fileCacheManager.IsScanRunning && !string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_pluginConfiguration.CacheFolder)) + { + if (ImGui.Button("Start Scan##startScan")) + { + _fileCacheManager.InvokeScan(true); + } + } + else + { + _uiShared.DrawFileScanState(); } } - - private void GenerateDarkSoulsAgreementCaptcha() + else if (!_uiShared.ApiController.ServerAlive) { - _darkSoulsCaptcha1 = GetCaptchaTuple(); - _darkSoulsCaptcha2 = GetCaptchaTuple(); - _darkSoulsCaptcha3 = GetCaptchaTuple(); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("Service Registration"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); + UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); + UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #registration."); + + if (ImGui.Button("Join the Mare Synchronos Discord")) + { + Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + } + + UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); + + ImGui.Separator(); + + UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); + + _uiShared.DrawServiceSelection(() => { }); } - - private Tuple GetCaptchaTuple() + else { - Random random = new Random(); - var paragraphIdx = random.Next(TosParagraphs.Length); - var splitParagraph = TosParagraphs[paragraphIdx].Split(".", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray(); - var sentenceIdx = random.Next(splitParagraph.Length); - var splitSentence = splitParagraph[sentenceIdx].Split(" ").Select(c => c.Trim()).Select(c => c.Replace(".", "").Replace(",", "").Replace("'", "")).ToArray(); - var wordIdx = random.Next(splitSentence.Length); - return new($"{Strings.ToS.ParagraphLabel} {paragraphIdx + 1}, {Strings.ToS.SentenceLabel} {sentenceIdx + 1}, {Strings.ToS.WordLabel} {wordIdx + 1}", splitSentence[wordIdx]); + SwitchToMainUi?.Invoke(); + IsOpen = false; } } + + private string _secretKey = string.Empty; + + private void GetToSLocalization(int changeLanguageTo = -1) + { + if (changeLanguageTo != -1) + { + _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 }; + + if (_pluginConfiguration.DarkSoulsAgreement) + { + GenerateDarkSoulsAgreementCaptcha(); + } + } + + private void GenerateDarkSoulsAgreementCaptcha() + { + _darkSoulsCaptcha1 = GetCaptchaTuple(); + _darkSoulsCaptcha2 = GetCaptchaTuple(); + _darkSoulsCaptcha3 = GetCaptchaTuple(); + } + + private Tuple GetCaptchaTuple() + { + Random random = new(); + var paragraphIdx = random.Next(TosParagraphs.Length); + var splitParagraph = TosParagraphs[paragraphIdx].Split(".", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray(); + var sentenceIdx = random.Next(splitParagraph.Length); + var splitSentence = splitParagraph[sentenceIdx].Split(" ").Select(c => c.Trim()).Select(c => c.Replace(".", "", StringComparison.Ordinal).Replace(",", "", StringComparison.Ordinal).Replace("'", "", StringComparison.Ordinal)).ToArray(); + var wordIdx = random.Next(splitSentence.Length); + return new($"{Strings.ToS.ParagraphLabel} {paragraphIdx + 1}, {Strings.ToS.SentenceLabel} {sentenceIdx + 1}, {Strings.ToS.WordLabel} {wordIdx + 1}", splitSentence[wordIdx]); + } } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 295f9cc..0d72429 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -14,600 +14,599 @@ using MareSynchronos.WebAPI.Utils; using System.Diagnostics; using Dalamud.Utility; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public delegate void SwitchUi(); +public class SettingsUi : Window, IDisposable { - public delegate void SwitchUi(); - public class SettingsUi : Window, IDisposable + private readonly Configuration _configuration; + private readonly WindowSystem _windowSystem; + private readonly ApiController _apiController; + private readonly UiShared _uiShared; + public event SwitchUi? SwitchToIntroUi; + + public SettingsUi(WindowSystem windowSystem, + UiShared uiShared, Configuration configuration, ApiController apiController) : base("Mare Synchronos Settings") { - private readonly Configuration _configuration; - private readonly WindowSystem _windowSystem; - private readonly ApiController _apiController; - private readonly UiShared _uiShared; - public event SwitchUi? SwitchToIntroUi; + Logger.Verbose("Creating " + nameof(SettingsUi)); - public SettingsUi(WindowSystem windowSystem, - UiShared uiShared, Configuration configuration, ApiController apiController) : base("Mare Synchronos Settings") + SizeConstraints = new WindowSizeConstraints() { - Logger.Verbose("Creating " + nameof(SettingsUi)); + MinimumSize = new Vector2(800, 400), + MaximumSize = new Vector2(800, 2000), + }; - SizeConstraints = new WindowSizeConstraints() + _configuration = configuration; + _windowSystem = windowSystem; + _apiController = apiController; + _uiShared = uiShared; + windowSystem.AddWindow(this); + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(SettingsUi)); + + _windowSystem.RemoveWindow(this); + } + + public override void Draw() + { + var pluginState = _uiShared.DrawOtherPluginState(); + + DrawSettingsContent(); + } + + private void DrawSettingsContent() + { + _uiShared.PrintServerState(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Community and Support:"); + ImGui.SameLine(); + if (ImGui.Button("Mare Synchronos Discord")) + { + Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + } + ImGui.Separator(); + if (ImGui.BeginTabBar("mainTabBar")) + { + if (ImGui.BeginTabItem("Cache Settings")) { - MinimumSize = new Vector2(800, 400), - MaximumSize = new Vector2(800, 2000), - }; - - _configuration = configuration; - _windowSystem = windowSystem; - _apiController = apiController; - _uiShared = uiShared; - windowSystem.AddWindow(this); - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(SettingsUi)); - - _windowSystem.RemoveWindow(this); - } - - public override void Draw() - { - var pluginState = _uiShared.DrawOtherPluginState(); - - DrawSettingsContent(); - } - - private void DrawSettingsContent() - { - _uiShared.PrintServerState(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("Community and Support:"); - ImGui.SameLine(); - if (ImGui.Button("Mare Synchronos Discord")) - { - Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + DrawFileCacheSettings(); + ImGui.EndTabItem(); } - ImGui.Separator(); - if (ImGui.BeginTabBar("mainTabBar")) + + if (_apiController.ServerState is ServerState.Connected) { - if (ImGui.BeginTabItem("Cache Settings")) + if (ImGui.BeginTabItem("Transfers")) { - DrawFileCacheSettings(); + DrawCurrentTransfers(); ImGui.EndTabItem(); } - - if (_apiController.ServerState is ServerState.Connected) - { - if (ImGui.BeginTabItem("Transfers")) - { - DrawCurrentTransfers(); - ImGui.EndTabItem(); - } - } - - if (ImGui.BeginTabItem("Blocked Transfers")) - { - DrawBlockedTransfers(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("User Administration")) - { - DrawUserAdministration(_apiController.IsConnected); - ImGui.EndTabItem(); - } - - if (_apiController.IsConnected && _apiController.IsModerator) - { - if (ImGui.BeginTabItem("Administration")) - { - DrawAdministration(); - ImGui.EndTabItem(); - } - } - - ImGui.EndTabBar(); } - } - private string _forbiddenFileHashEntry = string.Empty; - private string _forbiddenFileHashForbiddenBy = string.Empty; - private string _bannedUserHashEntry = string.Empty; - private string _bannedUserReasonEntry = string.Empty; - - private void DrawAdministration() - { - if (ImGui.TreeNode("Forbidden Files Changes")) + if (ImGui.BeginTabItem("Blocked Transfers")) { - if (ImGui.BeginTable("ForbiddenFilesTable", 3, ImGuiTableFlags.RowBg)) + DrawBlockedTransfers(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("User Administration")) + { + DrawUserAdministration(_apiController.IsConnected); + ImGui.EndTabItem(); + } + + if (_apiController.IsConnected && _apiController.IsModerator) + { + if (ImGui.BeginTabItem("Administration")) { - ImGui.TableSetupColumn("File Hash", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Forbidden By", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + DrawAdministration(); + ImGui.EndTabItem(); + } + } - ImGui.TableHeadersRow(); + ImGui.EndTabBar(); + } + } - foreach (var forbiddenFile in _apiController.AdminForbiddenFiles) + private string _forbiddenFileHashEntry = string.Empty; + private string _forbiddenFileHashForbiddenBy = string.Empty; + private string _bannedUserHashEntry = string.Empty; + private string _bannedUserReasonEntry = string.Empty; + + private void DrawAdministration() + { + if (ImGui.TreeNode("Forbidden Files Changes")) + { + if (ImGui.BeginTable("ForbiddenFilesTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("File Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Forbidden By", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var forbiddenFile in _apiController.AdminForbiddenFiles) + { + ImGui.TableNextColumn(); + + ImGui.Text(forbiddenFile.Hash); + ImGui.TableNextColumn(); + string by = forbiddenFile.ForbiddenBy; + if (ImGui.InputText("##forbiddenBy" + forbiddenFile.Hash, ref by, 255)) { - ImGui.TableNextColumn(); - - ImGui.Text(forbiddenFile.Hash); - ImGui.TableNextColumn(); - string by = forbiddenFile.ForbiddenBy; - if (ImGui.InputText("##forbiddenBy" + forbiddenFile.Hash, ref by, 255)) - { - forbiddenFile.ForbiddenBy = by; - } - - ImGui.TableNextColumn(); - if (_apiController.IsAdmin) - { - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button( - FontAwesomeIcon.Upload.ToIconString() + "##updateFile" + forbiddenFile.Hash)) - { - _ = _apiController.AddOrUpdateForbiddenFileEntry(forbiddenFile); - } - - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteFile" + - forbiddenFile.Hash)) - { - _ = _apiController.DeleteForbiddenFileEntry(forbiddenFile); - } - - ImGui.PopFont(); - } - + forbiddenFile.ForbiddenBy = by; } + ImGui.TableNextColumn(); if (_apiController.IsAdmin) { - ImGui.TableNextColumn(); - ImGui.InputText("##addFileHash", ref _forbiddenFileHashEntry, 255); - ImGui.TableNextColumn(); - ImGui.InputText("##addForbiddenBy", ref _forbiddenFileHashForbiddenBy, 255); - ImGui.TableNextColumn(); ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + if (ImGui.Button( + FontAwesomeIcon.Upload.ToIconString() + "##updateFile" + forbiddenFile.Hash)) { - _ = _apiController.AddOrUpdateForbiddenFileEntry(new ForbiddenFileDto() - { - ForbiddenBy = _forbiddenFileHashForbiddenBy, - Hash = _forbiddenFileHashEntry - }); + _ = _apiController.AddOrUpdateForbiddenFileEntry(forbiddenFile); } - ImGui.PopFont(); - ImGui.NextColumn(); - } - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Banned Users")) - { - if (ImGui.BeginTable("BannedUsersTable", 3, ImGuiTableFlags.RowBg)) - { - ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); - - ImGui.TableHeadersRow(); - - foreach (var bannedUser in _apiController.AdminBannedUsers) - { - ImGui.TableNextColumn(); - ImGui.Text(bannedUser.CharacterHash); - - ImGui.TableNextColumn(); - string reason = bannedUser.Reason; - ImGuiInputTextFlags moderatorFlags = _apiController.IsModerator - ? ImGuiInputTextFlags.ReadOnly - : ImGuiInputTextFlags.None; - if (ImGui.InputText("##bannedReason" + bannedUser.CharacterHash, ref reason, 255, - moderatorFlags)) + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteFile" + + forbiddenFile.Hash)) { - bannedUser.Reason = reason; - } - - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - if (_apiController.IsAdmin) - { - if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString() + "##updateUser" + - bannedUser.CharacterHash)) - { - _ = _apiController.AddOrUpdateBannedUserEntry(bannedUser); - } - - ImGui.SameLine(); - } - - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteUser" + - bannedUser.CharacterHash)) - { - _ = _apiController.DeleteBannedUserEntry(bannedUser); + _ = _apiController.DeleteForbiddenFileEntry(forbiddenFile); } ImGui.PopFont(); } - ImGui.TableNextColumn(); - ImGui.InputText("##addUserHash", ref _bannedUserHashEntry, 255); + } + if (_apiController.IsAdmin) + { ImGui.TableNextColumn(); - if (_apiController.IsAdmin) - { - ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255); - } - else - { - _bannedUserReasonEntry = "Banned by " + _uiShared.PlayerName; - ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255, - ImGuiInputTextFlags.ReadOnly); - } - + ImGui.InputText("##addFileHash", ref _forbiddenFileHashEntry, 255); + ImGui.TableNextColumn(); + ImGui.InputText("##addForbiddenBy", ref _forbiddenFileHashForbiddenBy, 255); ImGui.TableNextColumn(); ImGui.PushFont(UiBuilder.IconFont); if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) { - _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto() + _ = _apiController.AddOrUpdateForbiddenFileEntry(new ForbiddenFileDto() { - CharacterHash = _forbiddenFileHashForbiddenBy, - Reason = _forbiddenFileHashEntry + ForbiddenBy = _forbiddenFileHashForbiddenBy, + Hash = _forbiddenFileHashEntry }); } ImGui.PopFont(); - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Online Users")) - { - if (ImGui.Button("Refresh Online Users")) - { - _ = _apiController.RefreshOnlineUsers(); - } - - if (ImGui.BeginTable("OnlineUsersTable", 3, ImGuiTableFlags.RowBg)) - { - ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 300); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); - - ImGui.TableHeadersRow(); - - foreach (var onlineUser in _apiController.AdminOnlineUsers) - { - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - string icon = onlineUser.IsModerator - ? FontAwesomeIcon.ChessKing.ToIconString() - : onlineUser.IsAdmin - ? FontAwesomeIcon.Crown.ToIconString() - : FontAwesomeIcon.User.ToIconString(); - ImGui.Text(icon); - ImGui.PopFont(); - ImGui.SameLine(); - - ImGui.Text(onlineUser.UID); - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyUID" + - onlineUser.CharacterNameHash)) - { - ImGui.SetClipboardText(onlineUser.UID); - } - - ImGui.PopFont(); - - ImGui.TableNextColumn(); - string charNameHash = onlineUser.CharacterNameHash; - ImGui.InputText("##onlineUserHash" + onlineUser.CharacterNameHash, ref charNameHash, 255, - ImGuiInputTextFlags.ReadOnly); - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyHash" + - onlineUser.CharacterNameHash)) - { - ImGui.SetClipboardText(onlineUser.UID); - } - - ImGui.PopFont(); - - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.SkullCrossbones.ToIconString() + "##onlineUserBan" + - onlineUser.CharacterNameHash)) - { - _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto - { - CharacterHash = onlineUser.CharacterNameHash, - Reason = "Banned by " + _uiShared.PlayerName - }); - } - ImGui.SameLine(); - if (onlineUser.UID != _apiController.UID && _apiController.IsAdmin) - { - if (!onlineUser.IsModerator) - { - if (ImGui.Button(FontAwesomeIcon.ChessKing.ToIconString() + - "##onlineUserModerator" + - onlineUser.CharacterNameHash)) - { - _apiController.PromoteToModerator(onlineUser.UID); - } - } - else - { - if (ImGui.Button(FontAwesomeIcon.User.ToIconString() + - "##onlineUserNonModerator" + - onlineUser.CharacterNameHash)) - { - _apiController.DemoteFromModerator(onlineUser.UID); - } - } - } - - ImGui.PopFont(); - } - ImGui.EndTable(); - } - ImGui.TreePop(); - } - } - - private bool _deleteFilesPopupModalShown = false; - private bool _deleteAccountPopupModalShown = false; - - private void DrawUserAdministration(bool serverAlive) - { - if (serverAlive) - { - if (ImGui.Button("Delete all my files")) - { - _deleteFilesPopupModalShown = true; - ImGui.OpenPopup("Delete all your files?"); - } - - UiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); - - if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, - ImGuiWindowFlags.AlwaysAutoResize)) - { - UiShared.TextWrapped( - "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); - ImGui.Text("Are you sure you want to continue?"); - ImGui.Separator(); - if (ImGui.Button("Delete everything", new Vector2(150, 0))) - { - Task.Run(() => _apiController.DeleteAllMyFiles()); - ImGui.CloseCurrentPopup(); - _deleteFilesPopupModalShown = false; - } - - ImGui.SameLine(); - - if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) - { - ImGui.CloseCurrentPopup(); - _deleteFilesPopupModalShown = false; - } - - ImGui.EndPopup(); - } - - if (ImGui.Button("Delete account")) - { - _deleteAccountPopupModalShown = true; - ImGui.OpenPopup("Delete your account?"); - } - - UiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); - - if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, - ImGuiWindowFlags.AlwaysAutoResize)) - { - UiShared.TextWrapped( - "Your account and all associated files and data on the service will be deleted."); - UiShared.TextWrapped("Your UID will be removed from all pairing lists."); - ImGui.Text("Are you sure you want to continue?"); - ImGui.Separator(); - if (ImGui.Button("Delete account", new Vector2(150, 0))) - { - Task.Run(() => _apiController.DeleteAccount()); - ImGui.CloseCurrentPopup(); - _deleteAccountPopupModalShown = false; - SwitchToIntroUi?.Invoke(); - } - - ImGui.SameLine(); - - if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) - { - ImGui.CloseCurrentPopup(); - _deleteAccountPopupModalShown = false; - } - - ImGui.EndPopup(); - } - } - - if (!_configuration.FullPause) - { - UiShared.ColorTextWrapped("Note: to change servers you need to disconnect from your current Mare Synchronos server.", ImGuiColors.DalamudYellow); - } - - var marePaused = _configuration.FullPause; - - if (_configuration.HasValidSetup()) - { - if (ImGui.Checkbox("Disconnect Mare Synchronos", ref marePaused)) - { - _configuration.FullPause = marePaused; - _configuration.Save(); - Task.Run(_apiController.CreateConnections); - } - - UiShared.DrawHelpText("Completely pauses the sync and clears your current data (not uploaded files) on the service."); - } - else - { - UiShared.ColorText("You cannot reconnect without a valid account on the service.", ImGuiColors.DalamudYellow); - } - - if (marePaused) - { - _uiShared.DrawServiceSelection(() => { }); - } - } - - private void DrawBlockedTransfers() - { - UiShared.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + - "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + - "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", - ImGuiColors.DalamudGrey); - - if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) - { - ImGui.TableSetupColumn( - $"Hash/Filename"); - ImGui.TableSetupColumn($"Forbidden by"); - - ImGui.TableHeadersRow(); - - foreach (var item in _apiController.ForbiddenTransfers) - { - ImGui.TableNextColumn(); - if (item is UploadFileTransfer transfer) - { - ImGui.Text(transfer.LocalFile); - } - else - { - ImGui.Text(item.Hash); - } - ImGui.TableNextColumn(); - ImGui.Text(item.ForbiddenBy); - } - ImGui.EndTable(); - } - } - - private void DrawCurrentTransfers() - { - bool showTransferWindow = _configuration.ShowTransferWindow; - if (ImGui.Checkbox("Show separate Transfer window while transfers are active", ref showTransferWindow)) - { - _configuration.ShowTransferWindow = showTransferWindow; - _configuration.Save(); - } - - if (_configuration.ShowTransferWindow) - { - ImGui.Indent(); - bool editTransferWindowPosition = _uiShared.EditTrackerPosition; - if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) - { - _uiShared.EditTrackerPosition = editTransferWindowPosition; - } - ImGui.Unindent(); - } - - if (ImGui.BeginTable("TransfersTable", 2)) - { - ImGui.TableSetupColumn( - $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Total))})"); - ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Total))})"); - - ImGui.TableHeadersRow(); - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("UploadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Uploaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in _apiController.CurrentUploads.ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); - } - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("DownloadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Downloaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in _apiController.CurrentDownloads.SelectMany(k => k.Value).ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); + ImGui.NextColumn(); } ImGui.EndTable(); } + + ImGui.TreePop(); } - private void DrawFileCacheSettings() + if (ImGui.TreeNode("Banned Users")) { - _uiShared.DrawFileScanState(); - _uiShared.DrawTimeSpanBetweenScansSetting(); - _uiShared.DrawCacheDirectorySetting(); - ImGui.Text($"Local cache size: {UiShared.ByteToString(_uiShared.FileCacheSize)}"); - ImGui.SameLine(); - if (ImGui.Button("Clear local cache")) + if (ImGui.BeginTable("BannedUsersTable", 3, ImGuiTableFlags.RowBg)) { - Task.Run(() => + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var bannedUser in _apiController.AdminBannedUsers) { - foreach (var file in Directory.GetFiles(_configuration.CacheFolder)) + ImGui.TableNextColumn(); + ImGui.Text(bannedUser.CharacterHash); + + ImGui.TableNextColumn(); + string reason = bannedUser.Reason; + ImGuiInputTextFlags moderatorFlags = _apiController.IsModerator + ? ImGuiInputTextFlags.ReadOnly + : ImGuiInputTextFlags.None; + if (ImGui.InputText("##bannedReason" + bannedUser.CharacterHash, ref reason, 255, + moderatorFlags)) { - File.Delete(file); + bannedUser.Reason = reason; } - _uiShared.RecalculateFileCacheSize(); - }); + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (_apiController.IsAdmin) + { + if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString() + "##updateUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(bannedUser); + } + + ImGui.SameLine(); + } + + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.DeleteBannedUserEntry(bannedUser); + } + + ImGui.PopFont(); + } + + ImGui.TableNextColumn(); + ImGui.InputText("##addUserHash", ref _bannedUserHashEntry, 255); + + ImGui.TableNextColumn(); + if (_apiController.IsAdmin) + { + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255); + } + else + { + _bannedUserReasonEntry = "Banned by " + _uiShared.PlayerName; + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255, + ImGuiInputTextFlags.ReadOnly); + } + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto() + { + CharacterHash = _forbiddenFileHashForbiddenBy, + Reason = _forbiddenFileHashEntry + }); + } + + ImGui.PopFont(); + + ImGui.EndTable(); } + + ImGui.TreePop(); } - public override void OnClose() + if (ImGui.TreeNode("Online Users")) { - _uiShared.EditTrackerPosition = false; - base.OnClose(); + if (ImGui.Button("Refresh Online Users")) + { + _ = _apiController.RefreshOnlineUsers(); + } + + if (ImGui.BeginTable("OnlineUsersTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 100); + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 300); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var onlineUser in _apiController.AdminOnlineUsers) + { + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + string icon = onlineUser.IsModerator + ? FontAwesomeIcon.ChessKing.ToIconString() + : onlineUser.IsAdmin + ? FontAwesomeIcon.Crown.ToIconString() + : FontAwesomeIcon.User.ToIconString(); + ImGui.Text(icon); + ImGui.PopFont(); + ImGui.SameLine(); + + ImGui.Text(onlineUser.UID); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyUID" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + string charNameHash = onlineUser.CharacterNameHash; + ImGui.InputText("##onlineUserHash" + onlineUser.CharacterNameHash, ref charNameHash, 255, + ImGuiInputTextFlags.ReadOnly); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyHash" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.SkullCrossbones.ToIconString() + "##onlineUserBan" + + onlineUser.CharacterNameHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto + { + CharacterHash = onlineUser.CharacterNameHash, + Reason = "Banned by " + _uiShared.PlayerName + }); + } + ImGui.SameLine(); + if (!string.Equals(onlineUser.UID, _apiController.UID, StringComparison.Ordinal) && _apiController.IsAdmin) + { + if (!onlineUser.IsModerator) + { + if (ImGui.Button(FontAwesomeIcon.ChessKing.ToIconString() + + "##onlineUserModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.PromoteToModerator(onlineUser.UID); + } + } + else + { + if (ImGui.Button(FontAwesomeIcon.User.ToIconString() + + "##onlineUserNonModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.DemoteFromModerator(onlineUser.UID); + } + } + } + + ImGui.PopFont(); + } + ImGui.EndTable(); + } + ImGui.TreePop(); } } + + private bool _deleteFilesPopupModalShown = false; + private bool _deleteAccountPopupModalShown = false; + + private void DrawUserAdministration(bool serverAlive) + { + if (serverAlive) + { + if (ImGui.Button("Delete all my files")) + { + _deleteFilesPopupModalShown = true; + ImGui.OpenPopup("Delete all your files?"); + } + + UiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); + + if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, + ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped( + "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); + ImGui.Text("Are you sure you want to continue?"); + ImGui.Separator(); + if (ImGui.Button("Delete everything", new Vector2(150, 0))) + { + Task.Run(() => _apiController.DeleteAllMyFiles()); + ImGui.CloseCurrentPopup(); + _deleteFilesPopupModalShown = false; + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) + { + ImGui.CloseCurrentPopup(); + _deleteFilesPopupModalShown = false; + } + + ImGui.EndPopup(); + } + + if (ImGui.Button("Delete account")) + { + _deleteAccountPopupModalShown = true; + ImGui.OpenPopup("Delete your account?"); + } + + UiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); + + if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, + ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped( + "Your account and all associated files and data on the service will be deleted."); + UiShared.TextWrapped("Your UID will be removed from all pairing lists."); + ImGui.Text("Are you sure you want to continue?"); + ImGui.Separator(); + if (ImGui.Button("Delete account", new Vector2(150, 0))) + { + Task.Run(() => _apiController.DeleteAccount()); + ImGui.CloseCurrentPopup(); + _deleteAccountPopupModalShown = false; + SwitchToIntroUi?.Invoke(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) + { + ImGui.CloseCurrentPopup(); + _deleteAccountPopupModalShown = false; + } + + ImGui.EndPopup(); + } + } + + if (!_configuration.FullPause) + { + UiShared.ColorTextWrapped("Note: to change servers you need to disconnect from your current Mare Synchronos server.", ImGuiColors.DalamudYellow); + } + + var marePaused = _configuration.FullPause; + + if (_configuration.HasValidSetup()) + { + if (ImGui.Checkbox("Disconnect Mare Synchronos", ref marePaused)) + { + _configuration.FullPause = marePaused; + _configuration.Save(); + Task.Run(_apiController.CreateConnections); + } + + UiShared.DrawHelpText("Completely pauses the sync and clears your current data (not uploaded files) on the service."); + } + else + { + UiShared.ColorText("You cannot reconnect without a valid account on the service.", ImGuiColors.DalamudYellow); + } + + if (marePaused) + { + _uiShared.DrawServiceSelection(() => { }); + } + } + + private void DrawBlockedTransfers() + { + UiShared.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + + "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + + "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", + ImGuiColors.DalamudGrey); + + if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) + { + ImGui.TableSetupColumn( + $"Hash/Filename"); + ImGui.TableSetupColumn($"Forbidden by"); + + ImGui.TableHeadersRow(); + + foreach (var item in _apiController.ForbiddenTransfers) + { + ImGui.TableNextColumn(); + if (item is UploadFileTransfer transfer) + { + ImGui.Text(transfer.LocalFile); + } + else + { + ImGui.Text(item.Hash); + } + ImGui.TableNextColumn(); + ImGui.Text(item.ForbiddenBy); + } + ImGui.EndTable(); + } + } + + private void DrawCurrentTransfers() + { + bool showTransferWindow = _configuration.ShowTransferWindow; + if (ImGui.Checkbox("Show separate Transfer window while transfers are active", ref showTransferWindow)) + { + _configuration.ShowTransferWindow = showTransferWindow; + _configuration.Save(); + } + + if (_configuration.ShowTransferWindow) + { + ImGui.Indent(); + bool editTransferWindowPosition = _uiShared.EditTrackerPosition; + if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) + { + _uiShared.EditTrackerPosition = editTransferWindowPosition; + } + ImGui.Unindent(); + } + + if (ImGui.BeginTable("TransfersTable", 2)) + { + ImGui.TableSetupColumn( + $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Total))})"); + ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Total))})"); + + ImGui.TableHeadersRow(); + + ImGui.TableNextColumn(); + if (ImGui.BeginTable("UploadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _apiController.CurrentUploads.ToArray()) + { + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Total)); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + + ImGui.EndTable(); + } + + ImGui.TableNextColumn(); + if (ImGui.BeginTable("DownloadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Downloaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _apiController.CurrentDownloads.SelectMany(k => k.Value).ToArray()) + { + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Total)); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + + ImGui.EndTable(); + } + + ImGui.EndTable(); + } + } + + private void DrawFileCacheSettings() + { + _uiShared.DrawFileScanState(); + _uiShared.DrawTimeSpanBetweenScansSetting(); + _uiShared.DrawCacheDirectorySetting(); + ImGui.Text($"Local cache size: {UiShared.ByteToString(_uiShared.FileCacheSize)}"); + ImGui.SameLine(); + if (ImGui.Button("Clear local cache")) + { + Task.Run(() => + { + foreach (var file in Directory.GetFiles(_configuration.CacheFolder)) + { + File.Delete(file); + } + + _uiShared.RecalculateFileCacheSize(); + }); + } + } + + public override void OnClose() + { + _uiShared.EditTrackerPosition = false; + base.OnClose(); + } } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index 3164223..3867c1f 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -18,539 +18,579 @@ using MareSynchronos.Managers; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public class UiShared : IDisposable { - public class UiShared : IDisposable + [DllImport("user32")] + public static extern short GetKeyState(int nVirtKey); + + private readonly IpcManager _ipcManager; + private readonly ApiController _apiController; + private readonly PeriodicFileScanner _cacheScanner; + private readonly FileDialogManager _fileDialogManager; + private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; + private readonly DalamudPluginInterface _pluginInterface; + private readonly Dalamud.Localization _localization; + public long FileCacheSize => _cacheScanner.FileCacheSize; + public string PlayerName => _dalamudUtil.PlayerName; + public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory()); + public bool EditTrackerPosition { get; set; } + public ImFontPtr UidFont { get; private set; } + public bool UidFontBuilt { get; private set; } + public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; + + public ApiController ApiController => _apiController; + + public UiShared(IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, + Configuration pluginConfiguration, DalamudUtil dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization) { - [DllImport("user32")] - public static extern short GetKeyState(int nVirtKey); + _ipcManager = ipcManager; + _apiController = apiController; + _cacheScanner = cacheScanner; + _fileDialogManager = fileDialogManager; + _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; + _pluginInterface = pluginInterface; + _localization = localization; + _isDirectoryWritable = IsDirectoryWritable(_pluginConfiguration.CacheFolder); - private readonly IpcManager _ipcManager; - private readonly ApiController _apiController; - private readonly PeriodicFileScanner _cacheScanner; - private readonly FileDialogManager _fileDialogManager; - private readonly Configuration _pluginConfiguration; - private readonly DalamudUtil _dalamudUtil; - private readonly DalamudPluginInterface _pluginInterface; - private readonly Dalamud.Localization _localization; - public long FileCacheSize => _cacheScanner.FileCacheSize; - public string PlayerName => _dalamudUtil.PlayerName; - public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory()); - public bool EditTrackerPosition { get; set; } - public ImFontPtr UidFont { get; private set; } - public bool UidFontBuilt { get; private set; } + _pluginInterface.UiBuilder.BuildFonts += BuildFont; + _pluginInterface.UiBuilder.RebuildFonts(); + } - public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + public static float GetWindowContentRegionWidth() + { + return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + } - public ApiController ApiController => _apiController; + public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) + { + ImGui.PushFont(UiBuilder.IconFont); + var buttonSize = ImGuiHelpers.GetButtonSize(icon.ToIconString()); + ImGui.PopFont(); + return buttonSize; + } - public UiShared(IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, - Configuration pluginConfiguration, DalamudUtil dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization) - { - _ipcManager = ipcManager; - _apiController = apiController; - _cacheScanner = cacheScanner; - _fileDialogManager = fileDialogManager; - _pluginConfiguration = pluginConfiguration; - _dalamudUtil = dalamudUtil; - _pluginInterface = pluginInterface; - _localization = localization; - _isDirectoryWritable = IsDirectoryWritable(_pluginConfiguration.CacheFolder); + private void BuildFont() + { + var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + UidFontBuilt = false; - _pluginInterface.UiBuilder.BuildFonts += BuildFont; - _pluginInterface.UiBuilder.RebuildFonts(); - } - - public static float GetWindowContentRegionWidth() - { - return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - } - - public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) - { - ImGui.PushFont(UiBuilder.IconFont); - var buttonSize = ImGuiHelpers.GetButtonSize(icon.ToIconString()); - ImGui.PopFont(); - return buttonSize; - } - - private void BuildFont() - { - var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - UidFontBuilt = false; - - if (File.Exists(fontFile)) - { - try - { - UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); - UidFontBuilt = true; - } - catch (Exception ex) - { - Logger.Debug($"Font failed to load. {fontFile}"); - Logger.Debug(ex.ToString()); - } - } - else - { - Logger.Debug($"Font doesn't exist. {fontFile}"); - } - } - - public static void DrawWithID(string id, Action drawSubSection) - { - ImGui.PushID(id); - drawSubSection.Invoke(); - ImGui.PopID(); - } - - public static void AttachToolTip(string text) - { - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(text); - } - } - - public bool DrawOtherPluginState() - { - var penumbraExists = _ipcManager.CheckPenumbraApi(); - var glamourerExists = _ipcManager.CheckGlamourerApi(); - var heelsExists = _ipcManager.CheckHeelsApi(); - - var penumbraColor = penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var glamourerColor = glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var heelsColor = heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - ImGui.Text("Penumbra:"); - ImGui.SameLine(); - ImGui.TextColored(penumbraColor, penumbraExists ? "Available" : "Unavailable"); - ImGui.SameLine(); - ImGui.Text("Glamourer:"); - ImGui.SameLine(); - ImGui.TextColored(glamourerColor, glamourerExists ? "Available" : "Unavailable"); - ImGui.Text("Optional Addons"); - ImGui.SameLine(); - ImGui.Text("Heels:"); - ImGui.SameLine(); - ImGui.TextColored(heelsColor, heelsExists ? "Available" : "Unavailable"); - - if (!penumbraExists || !glamourerExists) - { - ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); - return false; - } - - return true; - } - - public void DrawFileScanState() - { - ImGui.Text("File Scanner Status"); - ImGui.SameLine(); - if (_cacheScanner.IsScanRunning) - { - ImGui.Text("Scan is running"); - ImGui.Text("Current Progress:"); - ImGui.SameLine(); - ImGui.Text(_cacheScanner.TotalFiles == 1 - ? "Collecting files" - : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); - } - else if (_pluginConfiguration.FileScanPaused) - { - ImGui.Text("File scanner is paused"); - ImGui.SameLine(); - if (ImGui.Button("Force Rescan##forcedrescan")) - { - _cacheScanner.InvokeScan(true); - } - } - else if (_cacheScanner.haltScanLocks.Any(f => f.Value > 0)) - { - ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.haltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); - ImGui.SameLine(); - if (ImGui.Button("Reset halt requests##clearlocks")) - { - _cacheScanner.ResetLocks(); - } - } - else - { - ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); - } - } - - public void PrintServerState() - { - var serverName = _apiController.ServerDictionary.ContainsKey(_pluginConfiguration.ApiUri) - ? _apiController.ServerDictionary[_pluginConfiguration.ApiUri] - : _pluginConfiguration.ApiUri; - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.TextUnformatted("Service " + serverName + ":"); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); - ImGui.SameLine(); - ImGui.TextUnformatted("("); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString()); - ImGui.SameLine(); - ImGui.Text("Users Online"); - ImGui.SameLine(); - ImGui.Text(")"); - } - } - - public static void ColorText(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); - } - - public static void ColorTextWrapped(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - TextWrapped(text); - ImGui.PopStyleColor(); - } - - public static void TextWrapped(string text) - { - ImGui.PushTextWrapPos(0); - ImGui.TextUnformatted(text); - ImGui.PopTextWrapPos(); - } - - public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : - input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; - - public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - - public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : - data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; - - public void LoadLocalization(string languageCode) - { - _localization.SetupWithLangCode(languageCode); - Strings.ToS = new Strings.ToSStrings(); - } - - public static uint Color(byte r, byte g, byte b, byte a) - { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } - - public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) - { - drawList.AddText(textPos with { Y = textPos.Y - thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X - thickness }, - outlineColor, text); - drawList.AddText(textPos with { Y = textPos.Y + thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X + thickness }, - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), - outlineColor, text); - - drawList.AddText(textPos, fontColor, text); - drawList.AddText(textPos, fontColor, text); - } - - public static string ByteToString(long bytes) - { - string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; - int i; - double dblSByte = bytes; - for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) - { - dblSByte = bytes / 1024.0; - } - - return $"{dblSByte:0.00} {suffix[i]}"; - } - - private int _serverSelectionIndex = 0; - private string _customServerName = ""; - private string _customServerUri = ""; - private bool _enterSecretKey = false; - private bool _cacheDirectoryHasOtherFilesThanCache = false; - private bool _cacheDirectoryIsValidPath = true; - - public void DrawServiceSelection(Action? callBackOnExit = null) - { - string[] comboEntries = _apiController.ServerDictionary.Values.ToArray(); - _serverSelectionIndex = Array.IndexOf(_apiController.ServerDictionary.Keys.ToArray(), _pluginConfiguration.ApiUri); - if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) - { - for (int i = 0; i < comboEntries.Length; i++) - { - bool isSelected = _serverSelectionIndex == i; - if (ImGui.Selectable(comboEntries[i], isSelected)) - { - _pluginConfiguration.ApiUri = _apiController.ServerDictionary.Single(k => k.Value == comboEntries[i]).Key; - _pluginConfiguration.Save(); - _ = _apiController.CreateConnections(); - } - - if (isSelected) - { - ImGui.SetItemDefaultFocus(); - } - } - - ImGui.EndCombo(); - } - - if (_serverSelectionIndex != 0) - { - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteService")) - { - _pluginConfiguration.CustomServerList.Remove(_pluginConfiguration.ApiUri); - _pluginConfiguration.ApiUri = _apiController.ServerDictionary.First().Key; - _pluginConfiguration.Save(); - } - ImGui.PopFont(); - } - - if (ImGui.TreeNode("Add Custom Service")) - { - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service Name", ref _customServerName, 255); - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service Address", ref _customServerUri, 255); - if (ImGui.Button("Add Custom Service")) - { - if (!string.IsNullOrEmpty(_customServerUri) - && !string.IsNullOrEmpty(_customServerName) - && !_pluginConfiguration.CustomServerList.ContainsValue(_customServerName) - && !_pluginConfiguration.CustomServerList.ContainsKey(_customServerUri)) - { - _pluginConfiguration.CustomServerList[_customServerUri] = _customServerName; - _customServerUri = string.Empty; - _customServerName = string.Empty; - _pluginConfiguration.Save(); - } - } - ImGui.TreePop(); - } - - PrintServerState(); - - if (!_apiController.ServerAlive && (_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) && !_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri].IsNullOrEmpty())) - { - ColorTextWrapped("You already have an account on this server.", ImGuiColors.DalamudYellow); - ImGui.SameLine(); - if (ImGui.Button("Connect##connectToService")) - { - _pluginConfiguration.FullPause = false; - _pluginConfiguration.Save(); - Task.Run(_apiController.CreateConnections); - } - } - - string checkboxText = _pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) - ? "I want to switch accounts" - : "I have an account"; - ImGui.Checkbox(checkboxText, ref _enterSecretKey); - - if (_enterSecretKey) - { - ColorTextWrapped("This will overwrite your currently used secret key for the selected service. Make sure to have a backup for the current secret key if you want to switch back to this account.", ImGuiColors.DalamudYellow); - if (!_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri)) - { - ColorTextWrapped("IF YOU HAVE NEVER MADE AN ACCOUNT BEFORE DO NOT ENTER ANYTHING HERE", ImGuiColors.DalamudYellow); - } - - var text = "Enter Secret Key"; - var buttonText = "Save"; - 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.SameLine(); - ImGui.SetNextItemWidth(GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); - ImGui.InputText("", ref _secretKey, 64); - if (_secretKey.Length > 0 && _secretKey.Length != 64) - { - ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); - } - else if (_secretKey.Length == 64) - { - ImGui.SameLine(); - if (ImGui.Button(buttonText)) - { - _pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri] = _secretKey; - _pluginConfiguration.Save(); - _secretKey = string.Empty; - Task.Run(_apiController.CreateConnections); - _enterSecretKey = false; - callBackOnExit?.Invoke(); - } - } - } - } - - private string _secretKey = ""; - - public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) - { - var cursorPos = ImGui.GetCursorPos(); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); - UiShared.ColorTextWrapped(text, outlineColor); - - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - UiShared.ColorTextWrapped(text, textcolor); - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - UiShared.ColorTextWrapped(text, textcolor); - } - - public static void DrawHelpText(string helpText) - { - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SetWindowFontScale(0.8f); - ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); - ImGui.SetWindowFontScale(1.0f); - ImGui.PopFont(); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); - ImGui.TextUnformatted(helpText); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } - } - - public void DrawCacheDirectorySetting() - { - ColorTextWrapped("Note: The cache folder should be somewhere close to root (i.e. C:\\MareCache) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); - var cacheDirectory = _pluginConfiguration.CacheFolder; - ImGui.InputText("Cache Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); - - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - string folderIcon = FontAwesomeIcon.Folder.ToIconString(); - if (ImGui.Button(folderIcon + "##chooseCacheFolder")) - { - _fileDialogManager.OpenFolderDialog("Pick Mare Synchronos Cache Folder", (success, path) => - { - if (!success) return; - - _isPenumbraDirectory = path.ToLowerInvariant() == _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(); - _isDirectoryWritable = IsDirectoryWritable(path); - _cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); - _cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript); - - if (!string.IsNullOrEmpty(path) - && Directory.Exists(path) - && _isDirectoryWritable - && !_isPenumbraDirectory - && !_cacheDirectoryHasOtherFilesThanCache - && _cacheDirectoryIsValidPath) - { - _pluginConfiguration.CacheFolder = path; - _pluginConfiguration.Save(); - _cacheScanner.StartScan(); - } - }); - } - ImGui.PopFont(); - - if (_isPenumbraDirectory) - { - ColorTextWrapped("Do not point the cache path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); - } - else if (!_isDirectoryWritable) - { - ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); - } - else if (_cacheDirectoryHasOtherFilesThanCache) - { - ColorTextWrapped("Your selected directory has files inside that are not Mare related. Use an empty directory or a previous Mare cache directory only.", ImGuiColors.DalamudRed); - } - else if (!_cacheDirectoryIsValidPath) - { - ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + - "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); - } - - int maxCacheSize = _pluginConfiguration.MaxLocalCacheInGiB; - if (ImGui.SliderInt("Maximum Cache Size in GB", ref maxCacheSize, 1, 50, "%d GB")) - { - _pluginConfiguration.MaxLocalCacheInGiB = maxCacheSize; - _pluginConfiguration.Save(); - } - DrawHelpText("The cache is automatically governed by Mare. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); - } - - private bool _isDirectoryWritable = false; - private bool _isPenumbraDirectory = false; - - public bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + if (File.Exists(fontFile)) { try { - using (FileStream fs = File.Create( - Path.Combine( - dirPath, - Path.GetRandomFileName() - ), - 1, - FileOptions.DeleteOnClose) - ) - { } - return true; + UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); + UidFontBuilt = true; } - catch + catch (Exception ex) { - if (throwIfFails) - throw; - else - return false; + Logger.Warn($"Font failed to load. {fontFile}"); + Logger.Warn(ex.ToString()); } } - - public void RecalculateFileCacheSize() + else { - _cacheScanner.InvokeScan(true); - } - - public void DrawTimeSpanBetweenScansSetting() - { - var timeSpan = _pluginConfiguration.TimeSpanBetweenScansInSeconds; - if (ImGui.SliderInt("Seconds between scans##timespan", ref timeSpan, 20, 60)) - { - _pluginConfiguration.TimeSpanBetweenScansInSeconds = timeSpan; - _pluginConfiguration.Save(); - } - DrawHelpText("This is the time in seconds between file scans. Increase it to reduce system load. A too high setting can cause issues when manually fumbling about in the cache or Penumbra mods folders."); - var isPaused = _pluginConfiguration.FileScanPaused; - if (ImGui.Checkbox("Pause periodic file scan##filescanpause", ref isPaused)) - { - _pluginConfiguration.FileScanPaused = isPaused; - _pluginConfiguration.Save(); - } - DrawHelpText("This allows you to stop the periodic scans of your Penumbra and Mare cache directories. Use this to move the Mare cache and Penumbra mod folders around. If you enable this permanently, run a Force rescan after adding mods to Penumbra."); - } - - public void Dispose() - { - _pluginInterface.UiBuilder.BuildFonts -= BuildFont; + Logger.Debug($"Font doesn't exist. {fontFile}"); } } + + public static void DrawWithID(string id, Action drawSubSection) + { + ImGui.PushID(id); + drawSubSection.Invoke(); + ImGui.PopID(); + } + + public static void AttachToolTip(string text) + { + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(text); + } + } + + public bool DrawOtherPluginState() + { + var penumbraExists = _ipcManager.CheckPenumbraApi(); + var glamourerExists = _ipcManager.CheckGlamourerApi(); + var heelsExists = _ipcManager.CheckHeelsApi(); + + var penumbraColor = penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var glamourerColor = glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var heelsColor = heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + ImGui.Text("Penumbra:"); + ImGui.SameLine(); + ImGui.TextColored(penumbraColor, penumbraExists ? "Available" : "Unavailable"); + ImGui.SameLine(); + ImGui.Text("Glamourer:"); + ImGui.SameLine(); + ImGui.TextColored(glamourerColor, glamourerExists ? "Available" : "Unavailable"); + ImGui.Text("Optional Addons"); + ImGui.SameLine(); + ImGui.Text("Heels:"); + ImGui.SameLine(); + ImGui.TextColored(heelsColor, heelsExists ? "Available" : "Unavailable"); + + if (!penumbraExists || !glamourerExists) + { + ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); + return false; + } + + return true; + } + + public void DrawFileScanState() + { + ImGui.Text("File Scanner Status"); + ImGui.SameLine(); + if (_cacheScanner.IsScanRunning) + { + ImGui.Text("Scan is running"); + ImGui.Text("Current Progress:"); + ImGui.SameLine(); + ImGui.Text(_cacheScanner.TotalFiles == 1 + ? "Collecting files" + : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); + } + else if (_pluginConfiguration.FileScanPaused) + { + ImGui.Text("File scanner is paused"); + ImGui.SameLine(); + if (ImGui.Button("Force Rescan##forcedrescan")) + { + _cacheScanner.InvokeScan(true); + } + } + else if (_cacheScanner.haltScanLocks.Any(f => f.Value > 0)) + { + ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.haltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); + ImGui.SameLine(); + if (ImGui.Button("Reset halt requests##clearlocks")) + { + _cacheScanner.ResetLocks(); + } + } + else + { + ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); + } + } + + public void PrintServerState() + { + var serverName = _apiController.ServerDictionary.ContainsKey(_pluginConfiguration.ApiUri) + ? _apiController.ServerDictionary[_pluginConfiguration.ApiUri] + : _pluginConfiguration.ApiUri; + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.TextUnformatted("Service " + serverName + ":"); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString()); + ImGui.SameLine(); + ImGui.Text("Users Online"); + ImGui.SameLine(); + ImGui.Text(")"); + } + } + + public static void ColorText(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(text); + ImGui.PopStyleColor(); + } + + public static void ColorTextWrapped(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + TextWrapped(text); + ImGui.PopStyleColor(); + } + + public static void TextWrapped(string text) + { + ImGui.PushTextWrapPos(0); + ImGui.TextUnformatted(text); + ImGui.PopTextWrapPos(); + } + + public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : + input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; + + public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + + public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : + data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; + + public void LoadLocalization(string languageCode) + { + _localization.SetupWithLangCode(languageCode); + Strings.ToS = new Strings.ToSStrings(); + } + + public static uint Color(byte r, byte g, byte b, byte a) + { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } + + public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) + { + drawList.AddText(textPos with { Y = textPos.Y - thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X - thickness }, + outlineColor, text); + drawList.AddText(textPos with { Y = textPos.Y + thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X + thickness }, + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), + outlineColor, text); + + drawList.AddText(textPos, fontColor, text); + drawList.AddText(textPos, fontColor, text); + } + + public static string ByteToString(long bytes) + { + string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; + int i; + double dblSByte = bytes; + for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return $"{dblSByte:0.00} {suffix[i]}"; + } + + private int _serverSelectionIndex = 0; + private string _customServerName = ""; + private string _customServerUri = ""; + private bool _enterSecretKey = false; + private bool _cacheDirectoryHasOtherFilesThanCache = false; + private bool _cacheDirectoryIsValidPath = true; + + public void DrawServiceSelection(Action? callBackOnExit = null) + { + string[] comboEntries = _apiController.ServerDictionary.Values.ToArray(); + _serverSelectionIndex = Array.IndexOf(_apiController.ServerDictionary.Keys.ToArray(), _pluginConfiguration.ApiUri); + if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) + { + for (int i = 0; i < comboEntries.Length; i++) + { + bool isSelected = _serverSelectionIndex == i; + if (ImGui.Selectable(comboEntries[i], isSelected)) + { + _pluginConfiguration.ApiUri = _apiController.ServerDictionary.Single(k => string.Equals(k.Value, comboEntries[i], StringComparison.Ordinal)).Key; + _pluginConfiguration.Save(); + _ = _apiController.CreateConnections(); + } + + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + if (_serverSelectionIndex != 0) + { + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteService")) + { + _pluginConfiguration.CustomServerList.Remove(_pluginConfiguration.ApiUri); + _pluginConfiguration.ApiUri = _apiController.ServerDictionary.First().Key; + _pluginConfiguration.Save(); + } + ImGui.PopFont(); + } + + if (ImGui.TreeNode("Add Custom Service")) + { + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service Name", ref _customServerName, 255); + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service Address", ref _customServerUri, 255); + if (ImGui.Button("Add Custom Service")) + { + if (!string.IsNullOrEmpty(_customServerUri) + && !string.IsNullOrEmpty(_customServerName) + && !_pluginConfiguration.CustomServerList.ContainsValue(_customServerName) + && !_pluginConfiguration.CustomServerList.ContainsKey(_customServerUri)) + { + _pluginConfiguration.CustomServerList[_customServerUri] = _customServerName; + _customServerUri = string.Empty; + _customServerName = string.Empty; + _pluginConfiguration.Save(); + } + } + ImGui.TreePop(); + } + + PrintServerState(); + + if (!_apiController.ServerAlive && (_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) && !_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri].IsNullOrEmpty())) + { + ColorTextWrapped("You already have an account on this server.", ImGuiColors.DalamudYellow); + ImGui.SameLine(); + if (ImGui.Button("Connect##connectToService")) + { + _pluginConfiguration.FullPause = false; + _pluginConfiguration.Save(); + Task.Run(_apiController.CreateConnections); + } + } + + string checkboxText = _pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) + ? "I want to switch accounts" + : "I have an account"; + ImGui.Checkbox(checkboxText, ref _enterSecretKey); + + if (_enterSecretKey) + { + ColorTextWrapped("This will overwrite your currently used secret key for the selected service. Make sure to have a backup for the current secret key if you want to switch back to this account.", ImGuiColors.DalamudYellow); + if (!_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri)) + { + ColorTextWrapped("IF YOU HAVE NEVER MADE AN ACCOUNT BEFORE DO NOT ENTER ANYTHING HERE", ImGuiColors.DalamudYellow); + } + + var text = "Enter Secret Key"; + var buttonText = "Save"; + 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.SameLine(); + ImGui.SetNextItemWidth(GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); + ImGui.InputText("", ref _secretKey, 64); + if (_secretKey.Length > 0 && _secretKey.Length != 64) + { + ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); + } + else if (_secretKey.Length == 64) + { + ImGui.SameLine(); + if (ImGui.Button(buttonText)) + { + _pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri] = _secretKey; + _pluginConfiguration.Save(); + _secretKey = string.Empty; + Task.Run(_apiController.CreateConnections); + _enterSecretKey = false; + callBackOnExit?.Invoke(); + } + } + } + } + + private string _secretKey = ""; + + public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) + { + var cursorPos = ImGui.GetCursorPos(); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); + UiShared.ColorTextWrapped(text, outlineColor); + + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + UiShared.ColorTextWrapped(text, textcolor); + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + UiShared.ColorTextWrapped(text, textcolor); + } + + public static void DrawHelpText(string helpText) + { + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.SetWindowFontScale(0.8f); + ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); + ImGui.SetWindowFontScale(1.0f); + ImGui.PopFont(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(helpText); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + public void DrawCacheDirectorySetting() + { + ColorTextWrapped("Note: The cache folder should be somewhere close to root (i.e. C:\\MareCache) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); + var cacheDirectory = _pluginConfiguration.CacheFolder; + ImGui.InputText("Cache Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + string folderIcon = FontAwesomeIcon.Folder.ToIconString(); + if (ImGui.Button(folderIcon + "##chooseCacheFolder")) + { + _fileDialogManager.OpenFolderDialog("Pick Mare Synchronos Cache Folder", (success, path) => + { + if (!success) return; + + _isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(), StringComparison.Ordinal); + _isDirectoryWritable = IsDirectoryWritable(path); + _cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); + _cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript); + + if (!string.IsNullOrEmpty(path) + && Directory.Exists(path) + && _isDirectoryWritable + && !_isPenumbraDirectory + && !_cacheDirectoryHasOtherFilesThanCache + && _cacheDirectoryIsValidPath) + { + _pluginConfiguration.CacheFolder = path; + _pluginConfiguration.Save(); + _cacheScanner.StartScan(); + } + }); + } + ImGui.PopFont(); + + if (_isPenumbraDirectory) + { + ColorTextWrapped("Do not point the cache path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); + } + else if (!_isDirectoryWritable) + { + ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); + } + else if (_cacheDirectoryHasOtherFilesThanCache) + { + ColorTextWrapped("Your selected directory has files inside that are not Mare related. Use an empty directory or a previous Mare cache directory only.", ImGuiColors.DalamudRed); + } + else if (!_cacheDirectoryIsValidPath) + { + ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + + "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); + } + + int maxCacheSize = _pluginConfiguration.MaxLocalCacheInGiB; + if (ImGui.SliderInt("Maximum Cache Size in GB", ref maxCacheSize, 1, 50, "%d GB")) + { + _pluginConfiguration.MaxLocalCacheInGiB = maxCacheSize; + _pluginConfiguration.Save(); + } + DrawHelpText("The cache is automatically governed by Mare. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); + } + + private bool _isDirectoryWritable = false; + private bool _isPenumbraDirectory = false; + + public bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + { + try + { + using (FileStream fs = File.Create( + Path.Combine( + dirPath, + Path.GetRandomFileName() + ), + 1, + FileOptions.DeleteOnClose) + ) + { } + return true; + } + catch + { + if (throwIfFails) + throw; + else + return false; + } + } + + public void RecalculateFileCacheSize() + { + _cacheScanner.InvokeScan(true); + } + + public void DrawTimeSpanBetweenScansSetting() + { + var timeSpan = _pluginConfiguration.TimeSpanBetweenScansInSeconds; + if (ImGui.SliderInt("Seconds between scans##timespan", ref timeSpan, 20, 60)) + { + _pluginConfiguration.TimeSpanBetweenScansInSeconds = timeSpan; + _pluginConfiguration.Save(); + } + DrawHelpText("This is the time in seconds between file scans. Increase it to reduce system load. A too high setting can cause issues when manually fumbling about in the cache or Penumbra mods folders."); + var isPaused = _pluginConfiguration.FileScanPaused; + if (ImGui.Checkbox("Pause periodic file scan##filescanpause", ref isPaused)) + { + _pluginConfiguration.FileScanPaused = isPaused; + _pluginConfiguration.Save(); + } + DrawHelpText("This allows you to stop the periodic scans of your Penumbra and Mare cache directories. Use this to move the Mare cache and Penumbra mod folders around. If you enable this permanently, run a Force rescan after adding mods to Penumbra."); + } + + public static Vector2 GetIconSize(FontAwesomeIcon icon) + { + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + ImGui.PopFont(); + return iconSize; + } + + public static bool IconTextButton(FontAwesomeIcon icon, string text) + { + var buttonClicked = false; + + var iconSize = GetIconSize(icon); + var textSize = ImGui.CalcTextSize(text); + var padding = ImGui.GetStyle().FramePadding; + var spacing = ImGui.GetStyle().ItemSpacing; + + var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; + var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; + var buttonSize = new Vector2(buttonSizeX, buttonSizeY); + + if (ImGui.BeginChild(icon.ToIconString() + text, buttonSize)) + { + if (ImGui.Button("", buttonSize)) + { + buttonClicked = true; + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(padding.X); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(icon.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(); + ImGui.Text(text); + ImGui.EndChild(); + } + + return buttonClicked; + } + + public void Dispose() + { + _pluginInterface.UiBuilder.BuildFonts -= BuildFont; + } } diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs index 36dba7c..7a10b54 100644 --- a/MareSynchronos/Utils/Crypto.cs +++ b/MareSynchronos/Utils/Crypto.cs @@ -4,32 +4,31 @@ using System.Security.Cryptography; using System.Text; using Dalamud.Game.ClientState.Objects.SubKinds; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +public class Crypto { - public class Crypto + public static string GetFileHash(string filePath) { - public static string GetFileHash(string filePath) - { - using SHA1CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", ""); - } + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "", StringComparison.Ordinal); + } - public static string GetHash(string stringToHash) - { - using SHA1CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); - } + public static string GetHash(string stringToHash) + { + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "", StringComparison.Ordinal); + } - public static string GetHash256(string stringToHash) - { - using SHA256CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); - } + public static string GetHash256(string stringToHash) + { + using SHA256CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "", StringComparison.Ordinal); + } - public static string GetHash256(PlayerCharacter character) - { - using SHA256CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", ""); - } + public static string GetHash256(PlayerCharacter character) + { + using SHA256CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", "", StringComparison.Ordinal); } } diff --git a/MareSynchronos/Utils/DalamudLoggingProvider.cs b/MareSynchronos/Utils/DalamudLoggingProvider.cs new file mode 100644 index 0000000..f4813f8 --- /dev/null +++ b/MareSynchronos/Utils/DalamudLoggingProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Utils; + +[ProviderAlias("Dalamud")] +public class DalamudLoggingProvider : ILoggerProvider +{ + private readonly ConcurrentDictionary _loggers = + new(StringComparer.OrdinalIgnoreCase); + + public DalamudLoggingProvider() + { + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, name => new Logger(categoryName)); + } + + public void Dispose() + { + _loggers.Clear(); + } +} diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index d2eba06..fe23079 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -11,232 +11,231 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using FFXIVClientStructs.FFXIV.Client.Game.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.Character actor); + +public delegate void LogIn(); +public delegate void LogOut(); +public delegate void ClassJobChanged(); + +public delegate void FrameworkUpdate(); +public delegate void VoidDelegate(); + +public class DalamudUtil : IDisposable { - public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.Character actor); + private readonly ClientState _clientState; + private readonly ObjectTable _objectTable; + private readonly Framework _framework; + private readonly Condition _condition; - public delegate void LogIn(); - public delegate void LogOut(); - public delegate void ClassJobChanged(); + public event LogIn? LogIn; + public event LogOut? LogOut; + public event FrameworkUpdate? FrameworkUpdate; + public event ClassJobChanged? ClassJobChanged; + private uint? classJobId = 0; + public event FrameworkUpdate? DelayedFrameworkUpdate; + public event VoidDelegate? ZoneSwitchStart; + public event VoidDelegate? ZoneSwitchEnd; + private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; + private bool _sentBetweenAreas = false; - public delegate void FrameworkUpdate(); - public delegate void VoidDelegate(); - - public class DalamudUtil : IDisposable + public unsafe bool IsGameObjectPresent(IntPtr key) { - private readonly ClientState _clientState; - private readonly ObjectTable _objectTable; - private readonly Framework _framework; - private readonly Condition _condition; - - public event LogIn? LogIn; - public event LogOut? LogOut; - public event FrameworkUpdate? FrameworkUpdate; - public event ClassJobChanged? ClassJobChanged; - private uint? classJobId = 0; - public event FrameworkUpdate? DelayedFrameworkUpdate; - public event VoidDelegate? ZoneSwitchStart; - public event VoidDelegate? ZoneSwitchEnd; - private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; - private bool _sentBetweenAreas = false; - - public unsafe bool IsGameObjectPresent(IntPtr key) + foreach (var obj in _objectTable) { - foreach (var obj in _objectTable) + if (obj.Address == key) { - if (obj.Address == key) - { - return true; - } - } - - return false; - } - - public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework, Condition condition) - { - _clientState = clientState; - _objectTable = objectTable; - _framework = framework; - _condition = condition; - _clientState.Login += ClientStateOnLogin; - _clientState.Logout += ClientStateOnLogout; - _framework.Update += FrameworkOnUpdate; - if (IsLoggedIn) - { - classJobId = _clientState.LocalPlayer!.ClassJob.Id; - ClientStateOnLogin(null, EventArgs.Empty); + return true; } } - private void FrameworkOnUpdate(Framework framework) - { - if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) - { - if (!_sentBetweenAreas) - { - Logger.Debug("Zone switch/Gpose start"); - _sentBetweenAreas = true; - ZoneSwitchStart?.Invoke(); - } + return false; + } - return; - } - else if (_sentBetweenAreas) + public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework, Condition condition) + { + _clientState = clientState; + _objectTable = objectTable; + _framework = framework; + _condition = condition; + _clientState.Login += ClientStateOnLogin; + _clientState.Logout += ClientStateOnLogout; + _framework.Update += FrameworkOnUpdate; + if (IsLoggedIn) + { + classJobId = _clientState.LocalPlayer!.ClassJob.Id; + ClientStateOnLogin(null, EventArgs.Empty); + } + } + + private void FrameworkOnUpdate(Framework framework) + { + if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) + { + if (!_sentBetweenAreas) { - Logger.Debug("Zone switch/Gpose end"); - _sentBetweenAreas = false; - ZoneSwitchEnd?.Invoke(); + Logger.Debug("Zone switch/Gpose start"); + _sentBetweenAreas = true; + ZoneSwitchStart?.Invoke(); } - foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + return; + } + else if (_sentBetweenAreas) + { + Logger.Debug("Zone switch/Gpose end"); + _sentBetweenAreas = false; + ZoneSwitchEnd?.Invoke(); + } + + foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + { + try { - try - { - frameworkInvocation?.Invoke(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } + frameworkInvocation?.Invoke(); } - - if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; - if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) + catch (Exception ex) { - var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; - - if (classJobId != newclassJobId) - { - classJobId = newclassJobId; - ClassJobChanged?.Invoke(); - } + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); } + } - foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; + if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) + { + var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; + + if (classJobId != newclassJobId) { - try - { - frameworkInvocation?.Invoke(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } + classJobId = newclassJobId; + ClassJobChanged?.Invoke(); } - _delayedFrameworkUpdateCheck = DateTime.Now; } - private void ClientStateOnLogout(object? sender, EventArgs e) + foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) { - LogOut?.Invoke(); - } - - private void ClientStateOnLogin(object? sender, EventArgs e) - { - LogIn?.Invoke(); - } - - public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) - { - return _objectTable.CreateObjectReference(reference); - } - - public bool IsLoggedIn => _clientState.IsLoggedIn; - - public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); - - public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) - { - return obj != null && obj.IsValid(); - } - - public unsafe IntPtr GetMinion() - { - return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; - } - - public unsafe IntPtr GetPet(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - if (playerPointer == null) playerPointer = PlayerPointer; - return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); - } - - public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - if (playerPointer == null) playerPointer = PlayerPointer; - return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); - } - - public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; - - public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; - - public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; - - public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); - - public bool IsInGpose => _objectTable[201] != null; - - public List GetPlayerCharacters() - { - return _objectTable.Where(obj => - obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && - obj.Name.ToString() != PlayerName).Select(p => (PlayerCharacter)p).ToList(); - } - - public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) - { - var objTableObj = _objectTable[index]; - if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; - return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; - } - - public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) - { - foreach (var item in _objectTable) + try { - if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - if (item.Name.ToString() == characterName) return (PlayerCharacter)item; + frameworkInvocation?.Invoke(); } - - return null; - } - - public async Task RunOnFrameworkThread(Func func) - { - return await _framework.RunOnFrameworkThread(func); - } - - public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, int timeOut = 5000, CancellationToken? ct = null) - { - if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; - - var obj = (GameObject*)characterAddress; - const int tick = 250; - int curWaitTime = 0; - // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something + catch (Exception ex) { - Logger.Verbose($"Waiting for {name} to finish drawing"); - curWaitTime += tick; - Thread.Sleep(tick); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); } + } + _delayedFrameworkUpdateCheck = DateTime.Now; + } - if (ct?.IsCancellationRequested ?? false) return; - // wait quarter a second just in case + private void ClientStateOnLogout(object? sender, EventArgs e) + { + LogOut?.Invoke(); + } + + private void ClientStateOnLogin(object? sender, EventArgs e) + { + LogIn?.Invoke(); + } + + public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) + { + return _objectTable.CreateObjectReference(reference); + } + + public bool IsLoggedIn => _clientState.IsLoggedIn; + + public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + + public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) + { + return obj != null && obj.IsValid(); + } + + public unsafe IntPtr GetMinion() + { + return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; + } + + public unsafe IntPtr GetPet(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + + public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + + public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; + + public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; + + public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; + + public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); + + public bool IsInGpose => _objectTable[201] != null; + + public List GetPlayerCharacters() + { + return _objectTable.Where(obj => + obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && + !string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Select(p => (PlayerCharacter)p).ToList(); + } + + public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) + { + var objTableObj = _objectTable[index]; + if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; + return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; + } + + public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) + { + foreach (var item in _objectTable) + { + if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item; + } + + return null; + } + + public async Task RunOnFrameworkThread(Func func) + { + return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); + } + + public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, int timeOut = 5000, CancellationToken? ct = null) + { + if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; + + var obj = (GameObject*)characterAddress; + const int tick = 250; + int curWaitTime = 0; + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something + { + Logger.Verbose($"Waiting for {name} to finish drawing"); + curWaitTime += tick; Thread.Sleep(tick); } - public void Dispose() - { - _clientState.Login -= ClientStateOnLogin; - _clientState.Logout -= ClientStateOnLogout; - _framework.Update -= FrameworkOnUpdate; - } + if (ct?.IsCancellationRequested ?? false) return; + // wait quarter a second just in case + Thread.Sleep(tick); + } + + public void Dispose() + { + _clientState.Login -= ClientStateOnLogin; + _clientState.Logout -= ClientStateOnLogout; + _framework.Update -= FrameworkOnUpdate; } } diff --git a/MareSynchronos/Utils/Logger.cs b/MareSynchronos/Utils/Logger.cs index dcf0b61..f816a5f 100644 --- a/MareSynchronos/Utils/Logger.cs +++ b/MareSynchronos/Utils/Logger.cs @@ -1,112 +1,89 @@ using System; -using System.Collections.Concurrent; using System.Diagnostics; using Dalamud.Logging; using Dalamud.Utility; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +internal class Logger : ILogger { - [ProviderAlias("Dalamud")] - public class DalamudLoggingProvider : ILoggerProvider + private readonly string name; + + public static void Info(string info) { - private readonly ConcurrentDictionary _loggers = - new(StringComparer.OrdinalIgnoreCase); + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + PluginLog.Information($"[{caller}] {info}"); + } - public DalamudLoggingProvider() + public static void Debug(string debug, string stringToHighlight = "") + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + if (debug.Contains(stringToHighlight, StringComparison.Ordinal) && !stringToHighlight.IsNullOrEmpty()) { + PluginLog.Warning($"[{caller}] {debug}"); } - - public ILogger CreateLogger(string categoryName) + else { - return _loggers.GetOrAdd(categoryName, name => new Logger(categoryName)); - } - - public void Dispose() - { - _loggers.Clear(); + PluginLog.Debug($"[{caller}] {debug}"); } } - internal class Logger : ILogger + public static void Warn(string warn) { - private readonly string name; - - public static void Info(string info) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - PluginLog.Information($"[{caller}] {info}"); - } - - public static void Debug(string debug, string stringToHighlight = "") - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - if (debug.Contains(stringToHighlight) && !stringToHighlight.IsNullOrEmpty()) - { - PluginLog.Warning($"[{caller}] {debug}"); - } - else - { - PluginLog.Debug($"[{caller}] {debug}"); - } - } - - public static void Warn(string warn) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - PluginLog.Warning($"[{caller}] {warn}"); - } - - public static void Verbose(string verbose) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; -#if DEBUG - PluginLog.Debug($"[{caller}] {verbose}"); -#else - PluginLog.Verbose($"[{caller}] {verbose}"); -#endif - } - - public Logger(string name) - { - this.name = name; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) return; - - switch (logLevel) - { - case LogLevel.Debug: - PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Error: - case LogLevel.Critical: - PluginLog.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Information: - PluginLog.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Warning: - PluginLog.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Trace: - default: -#if DEBUG - PluginLog.Verbose($"[{name}] [{eventId}] {formatter(state, exception)}"); -#else - PluginLog.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); -#endif - break; - } - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public IDisposable BeginScope(TState state) => default!; + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + PluginLog.Warning($"[{caller}] {warn}"); } + + public static void Verbose(string verbose) + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; +#if DEBUG + PluginLog.Debug($"[{caller}] {verbose}"); +#else + PluginLog.Verbose($"[{caller}] {verbose}"); +#endif + } + + public Logger(string name) + { + this.name = name; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + + switch (logLevel) + { + case LogLevel.Debug: + PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Error: + case LogLevel.Critical: + PluginLog.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Information: + PluginLog.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Warning: + PluginLog.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Trace: + default: +#if DEBUG + PluginLog.Verbose($"[{name}] [{eventId}] {formatter(state, exception)}"); +#else + PluginLog.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); +#endif + break; + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) => default!; } diff --git a/MareSynchronos/Utils/Various.cs b/MareSynchronos/Utils/Various.cs deleted file mode 100644 index fa96e17..0000000 --- a/MareSynchronos/Utils/Various.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace MareSynchronos.Utils -{ - public static class VariousExtensions - { - public static DateTime GetLinkerTime(Assembly assembly) - { - const string BuildVersionMetadataPrefix = "+build"; - - var attribute = assembly.GetCustomAttribute(); - if (attribute?.InformationalVersion != null) - { - var value = attribute.InformationalVersion; - var index = value.IndexOf(BuildVersionMetadataPrefix); - if (index > 0) - { - value = value[(index + BuildVersionMetadataPrefix.Length)..]; - return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture); - } - } - - return default; - } - } -} diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs new file mode 100644 index 0000000..32fa32b --- /dev/null +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.Utils; + +public static class VariousExtensions +{ + public static DateTime GetLinkerTime(Assembly assembly) + { + const string BuildVersionMetadataPrefix = "+build"; + + var attribute = assembly.GetCustomAttribute(); + if (attribute?.InformationalVersion != null) + { + var value = attribute.InformationalVersion; + var index = value.IndexOf(BuildVersionMetadataPrefix, StringComparison.Ordinal); + if (index > 0) + { + value = value[(index + BuildVersionMetadataPrefix.Length)..]; + return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture); + } + } + + return default; + } +} diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index 48e2712..89727b4 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -13,306 +13,305 @@ using MareSynchronos.Utils; using MareSynchronos.WebAPI.Utils; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + private readonly HashSet _verifiedUploadedHashes; + + private int _downloadId = 0; + public void CancelUpload() { - private readonly HashSet _verifiedUploadedHashes; - - private int _downloadId = 0; - public void CancelUpload() + if (_uploadCancellationTokenSource != null) { - if (_uploadCancellationTokenSource != null) - { - Logger.Debug("Cancelling upload"); - _uploadCancellationTokenSource?.Cancel(); - _mareHub!.SendAsync(Api.SendFileAbortUpload); - CurrentUploads.Clear(); - } - } - - public async Task DeleteAllMyFiles() - { - await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); - } - - private async Task DownloadFile(int downloadId, string hash, Uri downloadUri, CancellationToken ct) - { - using WebClient wc = new(); - wc.Headers.Add("Authorization", SecretKey); - DownloadProgressChangedEventHandler progChanged = (s, e) => - { - try - { - CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = e.BytesReceived; - } - catch (Exception ex) - { - Logger.Warn("Could not set download progress for " + hash); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } - }; - wc.DownloadProgressChanged += progChanged; - - string fileName = Path.GetTempFileName(); - - ct.Register(wc.CancelAsync); - - try - { - await wc.DownloadFileTaskAsync(downloadUri, fileName); - } - catch { } - - CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = CurrentDownloads[downloadId].Single(f => f.Hash == hash).Total; - - wc.DownloadProgressChanged -= progChanged; - return fileName; - } - - public int GetDownloadId() => _downloadId++; - - public async Task DownloadFiles(int currentDownloadId, List fileReplacementDto, CancellationToken ct) - { - DownloadStarted?.Invoke(); - try - { - await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct); - } - catch - { - CancelDownload(currentDownloadId); - } - finally - { - DownloadFinished?.Invoke(); - } - } - - private async Task DownloadFilesInternal(int currentDownloadId, List fileReplacementDto, CancellationToken ct) - { - Logger.Debug("Downloading files (Download ID " + currentDownloadId + ")"); - - List downloadFileInfoFromService = new List(); - downloadFileInfoFromService.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGetFilesSizes, fileReplacementDto.Select(f => f.Hash).ToList(), ct)); - - Logger.Debug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); - - CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) - .Where(d => d.CanBeTransferred).ToList(); - - foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) - { - if (ForbiddenTransfers.All(f => f.Hash != dto.Hash)) - { - ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); - } - } - - await Parallel.ForEachAsync(CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred), new ParallelOptions() - { - MaxDegreeOfParallelism = 5, - CancellationToken = ct - }, - async (file, token) => - { - var hash = file.Hash; - var tempFile = await DownloadFile(currentDownloadId, file.Hash, file.DownloadUri, token); - if (token.IsCancellationRequested) - { - File.Delete(tempFile); - Logger.Debug("Detected cancellation, removing " + currentDownloadId); - DownloadFinished?.Invoke(); - CancelDownload(currentDownloadId); - return; - } - - var tempFileData = await File.ReadAllBytesAsync(tempFile, token); - var extractedFile = LZ4Codec.Unwrap(tempFileData); - File.Delete(tempFile); - var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); - await File.WriteAllBytesAsync(filePath, extractedFile, token); - var fi = new FileInfo(filePath); - Func RandomDayFunc() - { - DateTime start = new DateTime(1995, 1, 1); - Random gen = new Random(); - int range = (DateTime.Today - start).Days; - return () => start.AddDays(gen.Next(range)); - } - - fi.CreationTime = RandomDayFunc().Invoke(); - fi.LastAccessTime = RandomDayFunc().Invoke(); - fi.LastWriteTime = RandomDayFunc().Invoke(); - try - { - _ = _fileDbManager.CreateCacheEntry(filePath); - } - catch (Exception ex) - { - Logger.Warn("Issue adding file to the DB"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace); - } - }); - - Logger.Debug("Download complete, removing " + currentDownloadId); - CancelDownload(currentDownloadId); - } - - public async Task PushCharacterData(CharacterCacheDto character, List visibleCharacterIds) - { - if (!IsConnected || SecretKey == "-") return; - Logger.Debug("Sending Character data to service " + ApiUri); - - CancelUpload(); - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; - Logger.Verbose("New Token Created"); - - List unverifiedUploadHashes = new(); - foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct()).Distinct().ToList()) - { - if (!_verifiedUploadedHashes.Contains(item)) - { - unverifiedUploadHashes.Add(item); - } - } - - if (unverifiedUploadHashes.Any()) - { - Logger.Debug("Verifying " + unverifiedUploadHashes.Count + " files"); - var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, unverifiedUploadHashes, uploadToken); - - foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) - { - try - { - CurrentUploads.Add(new UploadFileTransfer(file) - { - Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length - }); - } - catch (Exception ex) - { - Logger.Warn("Tried to request file " + file.Hash + " but file was not present"); - Logger.Warn(ex.StackTrace!); - } - } - - foreach (var file in filesToUpload.Where(c => c.IsForbidden)) - { - if (ForbiddenTransfers.All(f => f.Hash != file.Hash)) - { - ForbiddenTransfers.Add(new UploadFileTransfer(file) - { - LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty - }); - } - } - - var totalSize = CurrentUploads.Sum(c => c.Total); - Logger.Debug("Compressing and uploading files"); - foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) - { - Logger.Debug("Compressing and uploading " + file); - var data = await GetCompressedFileData(file.Hash, uploadToken); - CurrentUploads.Single(e => e.Hash == data.Item1).Total = data.Item2.Length; - await UploadFile(data.Item2, file.Hash, uploadToken); - if (!uploadToken.IsCancellationRequested) continue; - Logger.Warn("Cancel in filesToUpload loop detected"); - CurrentUploads.Clear(); - break; - } - - if (CurrentUploads.Any()) - { - var compressedSize = CurrentUploads.Sum(c => c.Total); - Logger.Debug($"Compressed {totalSize} to {compressedSize} ({(compressedSize / (double)totalSize):P2})"); - } - - Logger.Debug("Upload tasks complete, waiting for server to confirm"); - var anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); - Logger.Debug("Uploads open: " + anyUploadsOpen); - while (anyUploadsOpen && !uploadToken.IsCancellationRequested) - { - anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); - await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); - Logger.Debug("Waiting for uploads to finish"); - } - - foreach (var item in unverifiedUploadHashes) - { - _verifiedUploadedHashes.Add(item); - } - - CurrentUploads.Clear(); - } - else - { - Logger.Debug("All files already verified"); - } - - if (!uploadToken.IsCancellationRequested) - { - Logger.Info("Pushing character data for " + character.GetHashCode() + " to " + string.Join(", ", visibleCharacterIds)); - StringBuilder sb = new StringBuilder(); - foreach (var item in character.FileReplacements) - { - sb.AppendLine($"FileReplacements for {item.Key}: {item.Value.Count}"); - } - foreach (var item in character.GlamourerData) - { - sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); - } - Logger.Debug("Chara data contained: " + Environment.NewLine + sb.ToString()); - await _mareHub!.InvokeAsync(Api.InvokeUserPushCharacterDataToVisibleClients, character, visibleCharacterIds, uploadToken); - } - else - { - Logger.Warn("=== Upload operation was cancelled ==="); - } - - Logger.Verbose("Upload complete for " + character.GetHashCode()); - _uploadCancellationTokenSource = null; - } - - private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken), 0, - (int)new FileInfo(fileCache).Length)); - } - - private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) - { - if (uploadToken.IsCancellationRequested) return; - - async IAsyncEnumerable AsyncFileData([EnumeratorCancellation] CancellationToken token) - { - var chunkSize = 1024 * 512; // 512kb - using var ms = new MemoryStream(compressedFile); - var buffer = new byte[chunkSize]; - int bytesRead; - while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token)) > 0 && !token.IsCancellationRequested) - { - CurrentUploads.Single(f => f.Hash == fileHash).Transferred += bytesRead; - token.ThrowIfCancellationRequested(); - yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); - } - } - - await _mareHub!.SendAsync(Api.SendFileUploadFileStreamAsync, fileHash, AsyncFileData(uploadToken), uploadToken); - } - - public void CancelDownload(int downloadId) - { - while (CurrentDownloads.ContainsKey(downloadId)) - { - CurrentDownloads.TryRemove(downloadId, out _); - } + Logger.Debug("Cancelling upload"); + _uploadCancellationTokenSource?.Cancel(); + _mareHub!.SendAsync(Api.SendFileAbortUpload); + CurrentUploads.Clear(); } } + public async Task DeleteAllMyFiles() + { + await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles).ConfigureAwait(false); + } + + private async Task DownloadFile(int downloadId, string hash, Uri downloadUri, CancellationToken ct) + { + using WebClient wc = new(); + wc.Headers.Add("Authorization", SecretKey); + DownloadProgressChangedEventHandler progChanged = (s, e) => + { + try + { + CurrentDownloads[downloadId].Single(f => string.Equals(f.Hash, hash, StringComparison.Ordinal)).Transferred = e.BytesReceived; + } + catch (Exception ex) + { + Logger.Warn("Could not set download progress for " + hash); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + } + }; + wc.DownloadProgressChanged += progChanged; + + string fileName = Path.GetTempFileName(); + + ct.Register(wc.CancelAsync); + + try + { + await wc.DownloadFileTaskAsync(downloadUri, fileName).ConfigureAwait(false); + } + catch { } + + CurrentDownloads[downloadId].Single(f => string.Equals(f.Hash, hash, StringComparison.Ordinal)).Transferred = CurrentDownloads[downloadId].Single(f => string.Equals(f.Hash, hash, StringComparison.Ordinal)).Total; + + wc.DownloadProgressChanged -= progChanged; + return fileName; + } + + public int GetDownloadId() => _downloadId++; + + public async Task DownloadFiles(int currentDownloadId, List fileReplacementDto, CancellationToken ct) + { + DownloadStarted?.Invoke(); + try + { + await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct).ConfigureAwait(false); + } + catch + { + CancelDownload(currentDownloadId); + } + finally + { + DownloadFinished?.Invoke(); + } + } + + private async Task DownloadFilesInternal(int currentDownloadId, List fileReplacementDto, CancellationToken ct) + { + Logger.Debug("Downloading files (Download ID " + currentDownloadId + ")"); + + List downloadFileInfoFromService = new(); + downloadFileInfoFromService.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGetFilesSizes, fileReplacementDto.Select(f => f.Hash).ToList(), ct).ConfigureAwait(false)); + + Logger.Debug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); + + CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) + .Where(d => d.CanBeTransferred).ToList(); + + foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) + { + if (ForbiddenTransfers.All(f => !string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) + { + ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); + } + } + + await Parallel.ForEachAsync(CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred), new ParallelOptions() + { + MaxDegreeOfParallelism = 5, + CancellationToken = ct + }, + async (file, token) => + { + var hash = file.Hash; + var tempFile = await DownloadFile(currentDownloadId, file.Hash, file.DownloadUri, token).ConfigureAwait(false); + if (token.IsCancellationRequested) + { + File.Delete(tempFile); + Logger.Debug("Detected cancellation, removing " + currentDownloadId); + DownloadFinished?.Invoke(); + CancelDownload(currentDownloadId); + return; + } + + var tempFileData = await File.ReadAllBytesAsync(tempFile, token).ConfigureAwait(false); + var extractedFile = LZ4Codec.Unwrap(tempFileData); + File.Delete(tempFile); + var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); + await File.WriteAllBytesAsync(filePath, extractedFile, token).ConfigureAwait(false); + var fi = new FileInfo(filePath); + Func RandomDayFunc() + { + DateTime start = new(1995, 1, 1); + Random gen = new(); + int range = (DateTime.Today - start).Days; + return () => start.AddDays(gen.Next(range)); + } + + fi.CreationTime = RandomDayFunc().Invoke(); + fi.LastAccessTime = RandomDayFunc().Invoke(); + fi.LastWriteTime = RandomDayFunc().Invoke(); + try + { + _ = _fileDbManager.CreateCacheEntry(filePath); + } + catch (Exception ex) + { + Logger.Warn("Issue adding file to the DB"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + } + }).ConfigureAwait(false); + + Logger.Debug("Download complete, removing " + currentDownloadId); + CancelDownload(currentDownloadId); + } + + public async Task PushCharacterData(CharacterCacheDto character, List visibleCharacterIds) + { + if (!IsConnected || string.Equals(SecretKey, "-", StringComparison.Ordinal)) return; + Logger.Debug("Sending Character data to service " + ApiUri); + + CancelUpload(); + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.Verbose("New Token Created"); + + List unverifiedUploadHashes = new(); + foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList()) + { + if (!_verifiedUploadedHashes.Contains(item)) + { + unverifiedUploadHashes.Add(item); + } + } + + if (unverifiedUploadHashes.Any()) + { + Logger.Debug("Verifying " + unverifiedUploadHashes.Count + " files"); + var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, unverifiedUploadHashes, uploadToken).ConfigureAwait(false); + + foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) + { + try + { + CurrentUploads.Add(new UploadFileTransfer(file) + { + Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length + }); + } + catch (Exception ex) + { + Logger.Warn("Tried to request file " + file.Hash + " but file was not present"); + Logger.Warn(ex.StackTrace!); + } + } + + foreach (var file in filesToUpload.Where(c => c.IsForbidden)) + { + if (ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) + { + ForbiddenTransfers.Add(new UploadFileTransfer(file) + { + LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty + }); + } + } + + var totalSize = CurrentUploads.Sum(c => c.Total); + Logger.Debug("Compressing and uploading files"); + foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) + { + Logger.Debug("Compressing and uploading " + file); + var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false); + CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length; + await UploadFile(data.Item2, file.Hash, uploadToken).ConfigureAwait(false); + if (!uploadToken.IsCancellationRequested) continue; + Logger.Warn("Cancel in filesToUpload loop detected"); + CurrentUploads.Clear(); + break; + } + + if (CurrentUploads.Any()) + { + var compressedSize = CurrentUploads.Sum(c => c.Total); + Logger.Debug($"Compressed {totalSize} to {compressedSize} ({(compressedSize / (double)totalSize):P2})"); + } + + Logger.Debug("Upload tasks complete, waiting for server to confirm"); + var anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken).ConfigureAwait(false); + Logger.Debug("Uploads open: " + anyUploadsOpen); + while (anyUploadsOpen && !uploadToken.IsCancellationRequested) + { + anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken).ConfigureAwait(false); + await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken).ConfigureAwait(false); + Logger.Debug("Waiting for uploads to finish"); + } + + foreach (var item in unverifiedUploadHashes) + { + _verifiedUploadedHashes.Add(item); + } + + CurrentUploads.Clear(); + } + else + { + Logger.Debug("All files already verified"); + } + + if (!uploadToken.IsCancellationRequested) + { + Logger.Info("Pushing character data for " + character.GetHashCode() + " to " + string.Join(", ", visibleCharacterIds)); + StringBuilder sb = new(); + foreach (var item in character.FileReplacements) + { + sb.AppendLine($"FileReplacements for {item.Key}: {item.Value.Count}"); + } + foreach (var item in character.GlamourerData) + { + sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); + } + Logger.Debug("Chara data contained: " + Environment.NewLine + sb.ToString()); + await _mareHub!.InvokeAsync(Api.InvokeUserPushCharacterDataToVisibleClients, character, visibleCharacterIds, uploadToken).ConfigureAwait(false); + } + else + { + Logger.Warn("=== Upload operation was cancelled ==="); + } + + Logger.Verbose("Upload complete for " + character.GetHashCode()); + _uploadCancellationTokenSource = null; + } + + private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, + (int)new FileInfo(fileCache).Length)); + } + + private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) + { + if (uploadToken.IsCancellationRequested) return; + + async IAsyncEnumerable AsyncFileData([EnumeratorCancellation] CancellationToken token) + { + var chunkSize = 1024 * 512; // 512kb + using var ms = new MemoryStream(compressedFile); + var buffer = new byte[chunkSize]; + int bytesRead; + while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token).ConfigureAwait(false)) > 0 && !token.IsCancellationRequested) + { + CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred += bytesRead; + token.ThrowIfCancellationRequested(); + yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); + } + } + + await _mareHub!.SendAsync(Api.SendFileUploadFileStreamAsync, fileHash, AsyncFileData(uploadToken), uploadToken).ConfigureAwait(false); + } + + public void CancelDownload(int downloadId) + { + while (CurrentDownloads.ContainsKey(downloadId)) + { + CurrentDownloads.TryRemove(downloadId, out _); + } + } } + diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs index 967045f..1b69cce 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs @@ -1,44 +1,43 @@ using System.Collections.Generic; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronos.Utils; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + public async Task DeleteAccount() { - public async Task DeleteAccount() - { - _pluginConfiguration.ClientSecret.Remove(ApiUri); - _pluginConfiguration.Save(); - await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); - await _mareHub!.SendAsync(Api.SendUserDeleteAccount); - await CreateConnections(); - } - - public async Task> GetOnlineCharacters() - { - return await _mareHub!.InvokeAsync>(Api.InvokeUserGetOnlineCharacters); - } - - public async Task SendPairedClientAddition(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientAddition, uid); - } - - public async Task SendPairedClientPauseChange(string uid, bool paused) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientPauseChange, uid, paused); - } - - public async Task SendPairedClientRemoval(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientRemoval, uid); - } + _pluginConfiguration.ClientSecret.Remove(ApiUri); + _pluginConfiguration.Save(); + await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles).ConfigureAwait(false); + await _mareHub!.SendAsync(Api.SendUserDeleteAccount).ConfigureAwait(false); + await CreateConnections().ConfigureAwait(false); } + public async Task> GetOnlineCharacters() + { + return await _mareHub!.InvokeAsync>(Api.InvokeUserGetOnlineCharacters).ConfigureAwait(false); + } + + public async Task SendPairedClientAddition(string uid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendUserPairedClientAddition, uid).ConfigureAwait(false); + } + + public async Task SendPairedClientPauseChange(string uid, bool paused) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendUserPairedClientPauseChange, uid, paused).ConfigureAwait(false); + } + + public async Task SendPairedClientRemoval(string uid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendUserPairedClientRemoval, uid).ConfigureAwait(false); + } } + + diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs deleted file mode 100644 index 6088d99..0000000 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MareSynchronos.API; -using MareSynchronos.FileCache; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI.Utils; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.WebAPI -{ - public delegate void SimpleStringDelegate(string str); - public enum ServerState - { - Offline, - Disconnected, - Connected, - Unauthorized, - VersionMisMatch, - RateLimited - } - - public partial class ApiController : IDisposable - { - public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; - public const string MainServiceUri = "wss://maresynchronos.com"; - - public readonly int[] SupportedServerVersions = { Api.Version }; - - private readonly Configuration _pluginConfiguration; - private readonly DalamudUtil _dalamudUtil; - private readonly FileCacheManager _fileDbManager; - private CancellationTokenSource _connectionCancellationTokenSource; - - private HubConnection? _mareHub; - - private CancellationTokenSource? _uploadCancellationTokenSource = new(); - - private ConnectionDto? _connectionDto; - public SystemInfoDto SystemInfoDto { get; private set; } = new(); - public bool IsModerator => (_connectionDto?.IsAdmin ?? false) || (_connectionDto?.IsModerator ?? false); - - public bool IsAdmin => _connectionDto?.IsAdmin ?? false; - - public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil, FileCacheManager fileDbManager) - { - Logger.Verbose("Creating " + nameof(ApiController)); - - _pluginConfiguration = pluginConfiguration; - _dalamudUtil = dalamudUtil; - _fileDbManager = fileDbManager; - _connectionCancellationTokenSource = new CancellationTokenSource(); - _dalamudUtil.LogIn += DalamudUtilOnLogIn; - _dalamudUtil.LogOut += DalamudUtilOnLogOut; - ServerState = ServerState.Offline; - _verifiedUploadedHashes = new(); - - if (_dalamudUtil.IsLoggedIn) - { - DalamudUtilOnLogIn(); - } - } - - private void DalamudUtilOnLogOut() - { - Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); - ServerState = ServerState.Offline; - } - - private void DalamudUtilOnLogIn() - { - Task.Run(CreateConnections); - } - - - public event EventHandler? CharacterReceived; - - public event VoidDelegate? Connected; - - public event VoidDelegate? Disconnected; - - public event SimpleStringDelegate? PairedClientOffline; - - public event SimpleStringDelegate? PairedClientOnline; - - public event SimpleStringDelegate? PairedWithOther; - - public event SimpleStringDelegate? UnpairedFromOther; - public event VoidDelegate? DownloadStarted; - public event VoidDelegate? DownloadFinished; - - public ConcurrentDictionary> CurrentDownloads { get; } = new(); - - public List CurrentUploads { get; } = new(); - - public List ForbiddenTransfers { get; } = new(); - - public List AdminBannedUsers { get; private set; } = new(); - - public List AdminForbiddenFiles { get; private set; } = new(); - - public bool IsConnected => ServerState == ServerState.Connected; - - public bool IsDownloading => CurrentDownloads.Count > 0; - - public bool IsUploading => CurrentUploads.Count > 0; - - public List PairedClients { get; set; } = new(); - - public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) - ? _pluginConfiguration.ClientSecret[ApiUri] : string.Empty; - - public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; - - public Dictionary ServerDictionary => new Dictionary() - { { MainServiceUri, MainServer } } - .Concat(_pluginConfiguration.CustomServerList) - .ToDictionary(k => k.Key, k => k.Value); - - public string UID => _connectionDto?.UID ?? string.Empty; - private string ApiUri => _pluginConfiguration.ApiUri; - public int OnlineUsers => SystemInfoDto.OnlineUsers; - - private ServerState _serverState; - public ServerState ServerState - { - get => _serverState; - private set - { - Logger.Debug($"New ServerState: {value}, prev ServerState: {_serverState}"); - _serverState = value; - } - } - - public async Task CreateConnections() - { - Logger.Debug("CreateConnections called"); - - if (_pluginConfiguration.FullPause) - { - Logger.Info("Not recreating Connection, paused"); - ServerState = ServerState.Disconnected; - _connectionDto = null; - await StopConnection(_connectionCancellationTokenSource.Token); - return; - } - - await StopConnection(_connectionCancellationTokenSource.Token); - - Logger.Info("Recreating Connection"); - - _connectionCancellationTokenSource.Cancel(); - _connectionCancellationTokenSource = new CancellationTokenSource(); - var token = _connectionCancellationTokenSource.Token; - _verifiedUploadedHashes.Clear(); - while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) - { - if (string.IsNullOrEmpty(SecretKey)) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - continue; - } - - await StopConnection(token); - - try - { - Logger.Debug("Building connection"); - - while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) - { - Logger.Debug("Player not loaded in yet, waiting"); - await Task.Delay(TimeSpan.FromSeconds(1), token); - } - - if (token.IsCancellationRequested) break; - - _mareHub = BuildHubConnection(Api.Path); - - await _mareHub.StartAsync(token); - - _mareHub.On(Api.OnUpdateSystemInfo, (dto) => SystemInfoDto = dto); - - _connectionDto = - await _mareHub.InvokeAsync(Api.InvokeHeartbeat, _dalamudUtil.PlayerNameHashed, token); - - ServerState = ServerState.Connected; - - if (_connectionDto.ServerVersion != Api.Version) - { - ServerState = ServerState.VersionMisMatch; - await StopConnection(token); - return; - } - - if (ServerState is ServerState.Connected) // user is authorized && server is legit - { - await InitializeData(token); - - _mareHub.Closed += MareHubOnClosed; - _mareHub.Reconnecting += MareHubOnReconnecting; - _mareHub.Reconnected += MareHubOnReconnected; - } - } - catch (HubException ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - - ServerState = ServerState.RateLimited; - await StopConnection(token); - return; - } - catch (HttpRequestException ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - - if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - ServerState = ServerState.Unauthorized; - await StopConnection(token); - return; - } - else - { - ServerState = ServerState.Offline; - Logger.Info("Failed to establish connection, retrying"); - await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); - } - } - catch (Exception ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - Logger.Info("Failed to establish connection, retrying"); - await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); - } - } - } - - private Task MareHubOnReconnected(string? arg) - { - _ = Task.Run(CreateConnections); - return Task.CompletedTask; - } - - private async Task InitializeData(CancellationToken token) - { - if (_mareHub == null) return; - - Logger.Debug("Initializing data"); - _mareHub.On(Api.OnUserUpdateClientPairs, - UpdateLocalClientPairsCallback); - _mareHub.On(Api.OnUserReceiveCharacterData, - ReceiveCharacterDataCallback); - _mareHub.On(Api.OnUserRemoveOnlinePairedPlayer, - (s) => PairedClientOffline?.Invoke(s)); - _mareHub.On(Api.OnUserAddOnlinePairedPlayer, - (s) => PairedClientOnline?.Invoke(s)); - _mareHub.On(Api.OnAdminForcedReconnect, UserForcedReconnectCallback); - - PairedClients = - await _mareHub!.InvokeAsync>(Api.InvokeUserGetPairedClients, token); - - if (IsModerator) - { - AdminForbiddenFiles = - await _mareHub.InvokeAsync>(Api.InvokeAdminGetForbiddenFiles, - token); - AdminBannedUsers = - await _mareHub.InvokeAsync>(Api.InvokeAdminGetBannedUsers, - token); - _mareHub.On(Api.OnAdminUpdateOrAddBannedUser, - UpdateOrAddBannedUserCallback); - _mareHub.On(Api.OnAdminDeleteBannedUser, DeleteBannedUserCallback); - _mareHub.On(Api.OnAdminUpdateOrAddForbiddenFile, - UpdateOrAddForbiddenFileCallback); - _mareHub.On(Api.OnAdminDeleteForbiddenFile, - DeleteForbiddenFileCallback); - } - - Connected?.Invoke(); - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(ApiController)); - - _dalamudUtil.LogIn -= DalamudUtilOnLogIn; - _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - - Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); - _connectionCancellationTokenSource?.Cancel(); - } - - private HubConnection BuildHubConnection(string hubName) - { - return new HubConnectionBuilder() - .WithUrl(ApiUri + hubName, options => - { - options.Headers.Add("Authorization", SecretKey); - options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; - }) - .WithAutomaticReconnect(new ForeverRetryPolicy()) - .ConfigureLogging(a => { - a.ClearProviders().AddProvider(new DalamudLoggingProvider()); - a.SetMinimumLevel(LogLevel.Warning); - }) - .Build(); - } - - private Task MareHubOnClosed(Exception? arg) - { - CurrentUploads.Clear(); - CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); - Disconnected?.Invoke(); - ServerState = ServerState.Offline; - Logger.Info("Connection closed"); - return Task.CompletedTask; - } - - private Task MareHubOnReconnecting(Exception? arg) - { - ServerState = ServerState.Disconnected; - Logger.Warn("Connection closed... Reconnecting"); - Logger.Warn(arg?.Message ?? string.Empty); - Logger.Warn(arg?.StackTrace ?? string.Empty); - Disconnected?.Invoke(); - ServerState = ServerState.Offline; - return Task.CompletedTask; - } - - private async Task StopConnection(CancellationToken token) - { - if (_mareHub is not null) - { - _uploadCancellationTokenSource?.Cancel(); - Logger.Info("Stopping existing connection"); - _mareHub.Closed -= MareHubOnClosed; - _mareHub.Reconnecting -= MareHubOnReconnecting; - _mareHub.Reconnected -= MareHubOnReconnected; - await _mareHub.StopAsync(token); - await _mareHub.DisposeAsync(); - CurrentUploads.Clear(); - CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); - Disconnected?.Invoke(); - _mareHub = null; - } - - if (ServerState != ServerState.Disconnected) - { - while (ServerState != ServerState.Offline) - { - await Task.Delay(16); - } - } - } - } -} diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs index 868c4e9..c19df84 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs +++ b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs @@ -1,47 +1,47 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using MareSynchronos.API; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + public async Task AddOrUpdateForbiddenFileEntry(ForbiddenFileDto forbiddenFile) { - public async Task AddOrUpdateForbiddenFileEntry(ForbiddenFileDto forbiddenFile) - { - await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddForbiddenFile, forbiddenFile); - } + await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddForbiddenFile, forbiddenFile).ConfigureAwait(false); + } - public async Task DeleteForbiddenFileEntry(ForbiddenFileDto forbiddenFile) - { - await _mareHub!.SendAsync(Api.SendAdminDeleteForbiddenFile, forbiddenFile); - } + public async Task DeleteForbiddenFileEntry(ForbiddenFileDto forbiddenFile) + { + await _mareHub!.SendAsync(Api.SendAdminDeleteForbiddenFile, forbiddenFile).ConfigureAwait(false); + } - public async Task AddOrUpdateBannedUserEntry(BannedUserDto bannedUser) - { - await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddBannedUser, bannedUser); - } + public async Task AddOrUpdateBannedUserEntry(BannedUserDto bannedUser) + { + await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddBannedUser, bannedUser).ConfigureAwait(false); + } - public async Task DeleteBannedUserEntry(BannedUserDto bannedUser) - { - await _mareHub!.SendAsync(Api.SendAdminDeleteBannedUser, bannedUser); - } + public async Task DeleteBannedUserEntry(BannedUserDto bannedUser) + { + await _mareHub!.SendAsync(Api.SendAdminDeleteBannedUser, bannedUser).ConfigureAwait(false); + } - public async Task RefreshOnlineUsers() - { - AdminOnlineUsers = await _mareHub!.InvokeAsync>(Api.InvokeAdminGetOnlineUsers); - } + public async Task RefreshOnlineUsers() + { + AdminOnlineUsers = await _mareHub!.InvokeAsync>(Api.InvokeAdminGetOnlineUsers).ConfigureAwait(false); + } - public List AdminOnlineUsers { get; set; } = new List(); + public List AdminOnlineUsers { get; set; } = new List(); - public void PromoteToModerator(string onlineUserUID) - { - _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, true); - } + public void PromoteToModerator(string onlineUserUID) + { + _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, true); + } - public void DemoteFromModerator(string onlineUserUID) - { - _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, false); - } + public void DemoteFromModerator(string onlineUserUID) + { + _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, false); } } diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs index beebc73..04a8da9 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs +++ b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs @@ -1,90 +1,121 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using MareSynchronos.API; using MareSynchronos.Utils; using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + private void UserForcedReconnectCallback() { - private void UserForcedReconnectCallback() + _ = CreateConnections(); + } + + private void UpdateLocalClientPairsCallback(ClientPairDto dto) + { + var entry = PairedClients.SingleOrDefault(e => string.Equals(e.OtherUID, dto.OtherUID, System.StringComparison.Ordinal)); + if (dto.IsRemoved) { - _ = CreateConnections(); + PairedClients.RemoveAll(p => string.Equals(p.OtherUID, dto.OtherUID, System.StringComparison.Ordinal)); + return; + } + if (entry == null) + { + PairedClients.Add(dto); + return; } - private void UpdateLocalClientPairsCallback(ClientPairDto dto, string characterIdentifier) + entry.IsPaused = dto.IsPaused; + entry.IsPausedFromOthers = dto.IsPausedFromOthers; + entry.IsSynced = dto.IsSynced; + } + + private Task ReceiveCharacterDataCallback(CharacterCacheDto character, string characterHash) + { + Logger.Verbose("Received DTO for " + characterHash); + CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); + return Task.CompletedTask; + } + + private void UpdateOrAddBannedUserCallback(BannedUserDto obj) + { + var user = AdminBannedUsers.SingleOrDefault(b => string.Equals(b.CharacterHash, obj.CharacterHash, System.StringComparison.Ordinal)); + if (user == null) { - var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); - if (dto.IsRemoved) - { - PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); - UnpairedFromOther?.Invoke(characterIdentifier); - return; - } - if (entry == null) - { - PairedClients.Add(dto); - return; - } - - if ((entry.IsPausedFromOthers != dto.IsPausedFromOthers || entry.IsSynced != dto.IsSynced || entry.IsPaused != dto.IsPaused) - && !dto.IsPaused && dto.IsSynced && !dto.IsPausedFromOthers) - { - PairedWithOther?.Invoke(characterIdentifier); - } - - entry.IsPaused = dto.IsPaused; - entry.IsPausedFromOthers = dto.IsPausedFromOthers; - entry.IsSynced = dto.IsSynced; - - if (dto.IsPaused || dto.IsPausedFromOthers || !dto.IsSynced) - { - UnpairedFromOther?.Invoke(characterIdentifier); - } + AdminBannedUsers.Add(obj); } - - private Task ReceiveCharacterDataCallback(CharacterCacheDto character, string characterHash) + else { - Logger.Verbose("Received DTO for " + characterHash); - CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); - return Task.CompletedTask; - } - - private void UpdateOrAddBannedUserCallback(BannedUserDto obj) - { - var user = AdminBannedUsers.SingleOrDefault(b => b.CharacterHash == obj.CharacterHash); - if (user == null) - { - AdminBannedUsers.Add(obj); - } - else - { - user.Reason = obj.Reason; - } - } - - private void DeleteBannedUserCallback(BannedUserDto obj) - { - AdminBannedUsers.RemoveAll(a => a.CharacterHash == obj.CharacterHash); - } - - private void UpdateOrAddForbiddenFileCallback(ForbiddenFileDto obj) - { - var user = AdminForbiddenFiles.SingleOrDefault(b => b.Hash == obj.Hash); - if (user == null) - { - AdminForbiddenFiles.Add(obj); - } - else - { - user.ForbiddenBy = obj.ForbiddenBy; - } - } - - private void DeleteForbiddenFileCallback(ForbiddenFileDto obj) - { - AdminForbiddenFiles.RemoveAll(f => f.Hash == obj.Hash); + user.Reason = obj.Reason; } } + + private void DeleteBannedUserCallback(BannedUserDto obj) + { + AdminBannedUsers.RemoveAll(a => string.Equals(a.CharacterHash, obj.CharacterHash, System.StringComparison.Ordinal)); + } + + private void UpdateOrAddForbiddenFileCallback(ForbiddenFileDto obj) + { + var user = AdminForbiddenFiles.SingleOrDefault(b => string.Equals(b.Hash, obj.Hash, System.StringComparison.Ordinal)); + if (user == null) + { + AdminForbiddenFiles.Add(obj); + } + else + { + user.ForbiddenBy = obj.ForbiddenBy; + } + } + + private void DeleteForbiddenFileCallback(ForbiddenFileDto obj) + { + AdminForbiddenFiles.RemoveAll(f => string.Equals(f.Hash, obj.Hash, System.StringComparison.Ordinal)); + } + + private void GroupPairChangedCallback(GroupPairDto dto) + { + if (dto.IsRemoved.GetValueOrDefault(false)) + { + GroupPairedClients.RemoveAll(g => string.Equals(g.GroupGID, dto.GroupGID, System.StringComparison.Ordinal) && string.Equals(g.UserUID, dto.UserUID, System.StringComparison.Ordinal)); + return; + } + + var existingUser = GroupPairedClients.FirstOrDefault(f => string.Equals(f.GroupGID, dto.GroupGID, System.StringComparison.Ordinal) && string.Equals(f.UserUID, dto.UserUID, System.StringComparison.Ordinal)); + if (existingUser == null) + { + GroupPairedClients.Add(dto); + return; + } + + existingUser.IsPaused = dto.IsPaused ?? existingUser.IsPaused; + existingUser.UserAlias = dto.UserAlias ?? existingUser.UserAlias; + existingUser.IsPinned = dto.IsPinned ?? existingUser.IsPinned; + } + + private async Task GroupChangedCallback(GroupDto dto) + { + if (dto.IsDeleted.GetValueOrDefault(false)) + { + Groups.RemoveAll(g => string.Equals(g.GID, dto.GID, System.StringComparison.Ordinal)); + GroupPairedClients.RemoveAll(g => string.Equals(g.GroupGID, dto.GID, System.StringComparison.Ordinal)); + return; + } + + var existingGroup = Groups.FirstOrDefault(g => string.Equals(g.GID, dto.GID, System.StringComparison.Ordinal)); + if (existingGroup == null) + { + Groups.Add(dto); + GroupPairedClients.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGroupGetUsersInGroup, dto.GID).ConfigureAwait(false)); + return; + } + + existingGroup.OwnedBy = dto.OwnedBy ?? existingGroup.OwnedBy; + existingGroup.InvitesEnabled = dto.InvitesEnabled ?? existingGroup.InvitesEnabled; + existingGroup.IsPaused = dto.IsPaused ?? existingGroup.IsPaused; + } } diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs new file mode 100644 index 0000000..0ba2e71 --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs @@ -0,0 +1,87 @@ +using MareSynchronos.API; +using MareSynchronos.Utils; +using Microsoft.AspNetCore.SignalR.Client; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MareSynchronos.WebAPI; +public partial class ApiController +{ + public async Task CreateGroup() + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return new GroupCreatedDto(); + return await _mareHub!.InvokeAsync(Api.InvokeGroupCreate).ConfigureAwait(false); + } + + public async Task ChangeGroupPassword(string gid, string newpassword) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return false; + return await _mareHub!.InvokeAsync(Api.InvokeGroupChangePassword, gid, newpassword).ConfigureAwait(false); + } + + public async Task> GetGroups() + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return new List(); + return await _mareHub!.InvokeAsync>(Api.InvokeGroupGetGroups).ConfigureAwait(false); + } + + public async Task> GetUsersInGroup(string gid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return new List(); + return await _mareHub!.InvokeAsync>(Api.InvokeGroupGetUsersInGroup, gid).ConfigureAwait(false); + } + + public async Task SendGroupJoin(string gid, string password) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return false; + return await _mareHub!.InvokeAsync(Api.InvokeGroupJoin, gid, password).ConfigureAwait(false); + } + + public async Task SendGroupChangeInviteState(string gid, bool opened) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupChangeInviteState, gid, opened).ConfigureAwait(false); + } + + public async Task SendDeleteGroup(string gid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupDelete, gid).ConfigureAwait(false); + } + + public async Task SendChangeUserPinned(string gid, string uid, bool isPinned) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupChangePinned, gid, uid, isPinned).ConfigureAwait(false); + } + + public async Task SendClearGroup(string gid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupClear, gid).ConfigureAwait(false); + } + + public async Task SendLeaveGroup(string gid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupLeave, gid).ConfigureAwait(false); + } + + public async Task SendPauseGroup(string gid, bool isPaused) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupPause, gid, isPaused).ConfigureAwait(false); + } + + public async Task SendRemoveUserFromGroup(string gid, string uid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupRemoveUser, gid, uid).ConfigureAwait(false); + } + + public async Task ChangeOwnerOfGroup(string gid, string uid) + { + if (!IsConnected || string.Equals(SecretKey, "-", System.StringComparison.Ordinal)) return; + await _mareHub!.SendAsync(Api.SendGroupChangeOwner, gid, uid).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs new file mode 100644 index 0000000..a84923f --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using MareSynchronos.API; +using MareSynchronos.FileCache; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.WebAPI; + +public delegate void SimpleStringDelegate(string str); + +public partial class ApiController : IDisposable +{ + public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; + public const string MainServiceUri = "wss://maresynchronos.com"; + + public readonly int[] SupportedServerVersions = { Api.Version }; + + private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; + private readonly FileCacheManager _fileDbManager; + private CancellationTokenSource _connectionCancellationTokenSource; + + private HubConnection? _mareHub; + + private CancellationTokenSource? _uploadCancellationTokenSource = new(); + private CancellationTokenSource? _healthCheckTokenSource = new(); + + private ConnectionDto? _connectionDto; + public ServerInfoDto ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfoDto(); + + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + public bool IsModerator => (_connectionDto?.IsAdmin ?? false) || (_connectionDto?.IsModerator ?? false); + + public bool IsAdmin => _connectionDto?.IsAdmin ?? false; + + public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil, FileCacheManager fileDbManager) + { + Logger.Verbose("Creating " + nameof(ApiController)); + + _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; + _fileDbManager = fileDbManager; + _connectionCancellationTokenSource = new CancellationTokenSource(); + _dalamudUtil.LogIn += DalamudUtilOnLogIn; + _dalamudUtil.LogOut += DalamudUtilOnLogOut; + ServerState = ServerState.Offline; + _verifiedUploadedHashes = new(StringComparer.Ordinal); + + if (_dalamudUtil.IsLoggedIn) + { + DalamudUtilOnLogIn(); + } + } + + private void DalamudUtilOnLogOut() + { + Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token).ConfigureAwait(false)); + ServerState = ServerState.Offline; + } + + private void DalamudUtilOnLogIn() + { + Task.Run(CreateConnections); + } + + + public event EventHandler? CharacterReceived; + + public event VoidDelegate? Connected; + + public event VoidDelegate? Disconnected; + + public event SimpleStringDelegate? PairedClientOffline; + + public event SimpleStringDelegate? PairedClientOnline; + public event VoidDelegate? DownloadStarted; + public event VoidDelegate? DownloadFinished; + + public ConcurrentDictionary> CurrentDownloads { get; } = new(); + + public List CurrentUploads { get; } = new(); + + public List ForbiddenTransfers { get; } = new(); + + public List AdminBannedUsers { get; private set; } = new(); + + public List AdminForbiddenFiles { get; private set; } = new(); + + public bool IsConnected => ServerState == ServerState.Connected; + + public bool IsDownloading => CurrentDownloads.Count > 0; + + public bool IsUploading => CurrentUploads.Count > 0; + + public List PairedClients { get; set; } = new(); + public List GroupPairedClients { get; set; } = new(); + public List Groups { get; set; } = new(); + + public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) + ? _pluginConfiguration.ClientSecret[ApiUri] : string.Empty; + + public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; + + public Dictionary ServerDictionary => new Dictionary(StringComparer.Ordinal) + { { MainServiceUri, MainServer } } + .Concat(_pluginConfiguration.CustomServerList) + .ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal); + + public string UID => _connectionDto?.UID ?? string.Empty; + public string DisplayName => _connectionDto?.UID ?? string.Empty; + private string ApiUri => _pluginConfiguration.ApiUri; + public int OnlineUsers => SystemInfoDto.OnlineUsers; + + private ServerState _serverState; + public ServerState ServerState + { + get => _serverState; + private set + { + Logger.Debug($"New ServerState: {value}, prev ServerState: {_serverState}"); + _serverState = value; + } + } + + public async Task CreateConnections() + { + Logger.Debug("CreateConnections called"); + + if (_pluginConfiguration.FullPause) + { + Logger.Info("Not recreating Connection, paused"); + ServerState = ServerState.Disconnected; + _connectionDto = null; + await StopConnection(_connectionCancellationTokenSource.Token).ConfigureAwait(false); + return; + } + + await StopConnection(_connectionCancellationTokenSource.Token).ConfigureAwait(false); + + Logger.Info("Recreating Connection"); + + _connectionCancellationTokenSource.Cancel(); + _connectionCancellationTokenSource = new CancellationTokenSource(); + var token = _connectionCancellationTokenSource.Token; + _verifiedUploadedHashes.Clear(); + while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) + { + if (string.IsNullOrEmpty(SecretKey)) + { + await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false); + continue; + } + + await StopConnection(token).ConfigureAwait(false); + + try + { + Logger.Debug("Building connection"); + + while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) + { + Logger.Debug("Player not loaded in yet, waiting"); + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + } + + if (token.IsCancellationRequested) break; + + _mareHub = BuildHubConnection(Api.Path); + + await _mareHub.StartAsync(token).ConfigureAwait(false); + + _mareHub.On(Api.OnUpdateSystemInfo, (dto) => SystemInfoDto = dto); + + _connectionDto = + await _mareHub.InvokeAsync(Api.InvokeHeartbeat, _dalamudUtil.PlayerNameHashed, token).ConfigureAwait(false); + + ServerState = ServerState.Connected; + + if (_connectionDto.ServerVersion != Api.Version) + { + ServerState = ServerState.VersionMisMatch; + await StopConnection(token).ConfigureAwait(false); + return; + } + + if (ServerState is ServerState.Connected) // user is authorized && server is legit + { + await InitializeData(token).ConfigureAwait(false); + + _mareHub.Closed += MareHubOnClosed; + _mareHub.Reconnecting += MareHubOnReconnecting; + _mareHub.Reconnected += MareHubOnReconnected; + } + } + catch (HubException ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + + ServerState = ServerState.RateLimited; + await StopConnection(token).ConfigureAwait(false); + return; + } + catch (HttpRequestException ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + + if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + ServerState = ServerState.Unauthorized; + await StopConnection(token).ConfigureAwait(false); + return; + } + else + { + ServerState = ServerState.Offline; + Logger.Info("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + Logger.Info("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); + } + } + } + + private Task MareHubOnReconnected(string? arg) + { + _ = Task.Run(CreateConnections); + return Task.CompletedTask; + } + + private async Task ClientHealthCheck(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + if (ct.IsCancellationRequested) break; + var needsRestart = await _mareHub!.InvokeAsync(Api.InvokeCheckClientHealth, ct).ConfigureAwait(false); + Logger.Debug("Checked Client Health State, healthy: " + !needsRestart); + if (needsRestart) + { + _ = CreateConnections(); + } + } + } + + private async Task InitializeData(CancellationToken token) + { + if (_mareHub == null) return; + + Logger.Debug("Initializing data"); + _mareHub.On(Api.OnUserUpdateClientPairs, + UpdateLocalClientPairsCallback); + _mareHub.On(Api.OnUserReceiveCharacterData, + ReceiveCharacterDataCallback); + _mareHub.On(Api.OnUserRemoveOnlinePairedPlayer, + (s) => PairedClientOffline?.Invoke(s)); + _mareHub.On(Api.OnUserAddOnlinePairedPlayer, + (s) => PairedClientOnline?.Invoke(s)); + _mareHub.On(Api.OnAdminForcedReconnect, UserForcedReconnectCallback); + _mareHub.On(Api.OnGroupChange, GroupChangedCallback); + _mareHub.On(Api.OnGroupUserChange, GroupPairChangedCallback); + + PairedClients = + await _mareHub!.InvokeAsync>(Api.InvokeUserGetPairedClients, token).ConfigureAwait(false); + Groups = await GetGroups().ConfigureAwait(false); + GroupPairedClients.Clear(); + foreach (var group in Groups) + { + GroupPairedClients.AddRange(await GetUsersInGroup(group.GID).ConfigureAwait(false)); + } + + if (IsModerator) + { + AdminForbiddenFiles = + await _mareHub.InvokeAsync>(Api.InvokeAdminGetForbiddenFiles, + token).ConfigureAwait(false); + AdminBannedUsers = + await _mareHub.InvokeAsync>(Api.InvokeAdminGetBannedUsers, + token).ConfigureAwait(false); + _mareHub.On(Api.OnAdminUpdateOrAddBannedUser, + UpdateOrAddBannedUserCallback); + _mareHub.On(Api.OnAdminDeleteBannedUser, DeleteBannedUserCallback); + _mareHub.On(Api.OnAdminUpdateOrAddForbiddenFile, + UpdateOrAddForbiddenFileCallback); + _mareHub.On(Api.OnAdminDeleteForbiddenFile, + DeleteForbiddenFileCallback); + } + + _healthCheckTokenSource?.Cancel(); + _healthCheckTokenSource?.Dispose(); + _healthCheckTokenSource = new CancellationTokenSource(); + _ = ClientHealthCheck(_healthCheckTokenSource.Token); + + Connected?.Invoke(); + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(ApiController)); + + _dalamudUtil.LogIn -= DalamudUtilOnLogIn; + _dalamudUtil.LogOut -= DalamudUtilOnLogOut; + + Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token).ConfigureAwait(false)); + _connectionCancellationTokenSource?.Cancel(); + } + + private HubConnection BuildHubConnection(string hubName) + { + return new HubConnectionBuilder() + .WithUrl(ApiUri + hubName, options => + { + options.Headers.Add("Authorization", SecretKey); + options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; + }) + .WithAutomaticReconnect(new ForeverRetryPolicy()) + .ConfigureLogging(a => + { + a.ClearProviders().AddProvider(new DalamudLoggingProvider()); + a.SetMinimumLevel(LogLevel.Warning); + }) + .Build(); + } + + private Task MareHubOnClosed(Exception? arg) + { + CurrentUploads.Clear(); + CurrentDownloads.Clear(); + _uploadCancellationTokenSource?.Cancel(); + Disconnected?.Invoke(); + ServerState = ServerState.Offline; + Logger.Info("Connection closed"); + return Task.CompletedTask; + } + + private Task MareHubOnReconnecting(Exception? arg) + { + _connectionDto = null; + _healthCheckTokenSource?.Cancel(); + ServerState = ServerState.Disconnected; + Logger.Warn("Connection closed... Reconnecting"); + Logger.Warn(arg?.Message ?? string.Empty); + Logger.Warn(arg?.StackTrace ?? string.Empty); + Disconnected?.Invoke(); + ServerState = ServerState.Offline; + return Task.CompletedTask; + } + + private async Task StopConnection(CancellationToken token) + { + if (_mareHub is not null) + { + _uploadCancellationTokenSource?.Cancel(); + Logger.Info("Stopping existing connection"); + _mareHub.Closed -= MareHubOnClosed; + _mareHub.Reconnecting -= MareHubOnReconnecting; + _mareHub.Reconnected -= MareHubOnReconnected; + await _mareHub.StopAsync(token).ConfigureAwait(false); + await _mareHub.DisposeAsync().ConfigureAwait(false); + CurrentUploads.Clear(); + CurrentDownloads.Clear(); + _uploadCancellationTokenSource?.Cancel(); + Disconnected?.Invoke(); + _mareHub = null; + } + + if (ServerState != ServerState.Disconnected) + { + while (ServerState != ServerState.Offline) + { + await Task.Delay(16).ConfigureAwait(false); + } + } + } +} diff --git a/MareSynchronos/WebAPI/ServerState.cs b/MareSynchronos/WebAPI/ServerState.cs new file mode 100644 index 0000000..d146f26 --- /dev/null +++ b/MareSynchronos/WebAPI/ServerState.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.WebAPI; + +public enum ServerState +{ + Offline, + Disconnected, + Connected, + Unauthorized, + VersionMisMatch, + RateLimited +}