[Draft] Update 0.8 (#46)

* move stuff out into file transfer manager

* obnoxious unsupported version text, adjustments to filetransfermanager

* add back file upload transfer progress

* restructure code

* cleanup some more stuff I guess

* downloadids by playername

* individual anim/sound bs

* fix migration stuff, finalize impl of individual sound/anim pause

* fixes with logging stuff

* move download manager to transient

* rework dl ui first iteration

* some refactoring and cleanup

* more code cleanup

* refactoring

* switch to hostbuilder

* some more rework I guess

* more refactoring

* clean up mediator calls and disposal

* fun code cleanup

* push error message when log level is set to anything but information in non-debug builds

* remove notificationservice

* move message to after login

* add download bars to gameworld

* fixes download progress bar

* set gpose ui min and max size

* remove unnecessary usings

* adjustments to reconnection logic

* add options to set visible/offline groups visibility

* add impl of uploading display, transfer list in settings ui

* attempt to fix issues with server selection

* add back download status to compact ui

* make dl bar fixed size based

* some fixes for upload/download handling

* adjust text from Syncing back to Uploading

---------

Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
Co-authored-by: Stanley Dimant <stanley.dimant@varian.com>
This commit is contained in:
rootdarkarchon
2023-03-14 19:48:35 +01:00
committed by GitHub
parent 0824ba434b
commit 0c87e84f25
109 changed files with 7323 additions and 6488 deletions

View File

@@ -239,7 +239,7 @@ dotnet_naming_style.fieldstyle.required_prefix = _
dotnet_naming_style.fieldstyle.required_suffix =
dotnet_naming_style.fieldstyle.word_separator =
dotnet_naming_style.fieldstyle.capitalization = camel_case
dotnet_diagnostic.MA0016.severity = suggestion
dotnet_diagnostic.MA0016.severity = silent
dotnet_diagnostic.MA0026.severity = warning
dotnet_diagnostic.MA0046.severity = suggestion
dotnet_diagnostic.MA0051.severity = suggestion

Submodule MareAPI updated: 85bedb49e3...f8e647af00

View File

@@ -0,0 +1,98 @@
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
[*.cs]
csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
dotnet_diagnostic.MA0076.severity = silent
dotnet_diagnostic.MA0051.severity = silent
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
dotnet_diagnostic.S1075.severity = silent
dotnet_diagnostic.MA0007.severity = silent
dotnet_diagnostic.MA0075.severity = silent

View File

@@ -1,36 +0,0 @@
using MareSynchronos.API.Dto.User;
using MareSynchronos.FileCache;
using MareSynchronos.Managers;
using MareSynchronos.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Factories;
public class CachedPlayerFactory
{
private readonly IpcManager _ipcManager;
private readonly DalamudUtil _dalamudUtil;
private readonly FileCacheManager _fileCacheManager;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly MareMediator _mediator;
private readonly ILoggerFactory _loggerFactory;
public CachedPlayerFactory(IpcManager ipcManager, DalamudUtil dalamudUtil, FileCacheManager fileCacheManager,
GameObjectHandlerFactory gameObjectHandlerFactory,
MareMediator mediator, ILoggerFactory loggerFactory)
{
_ipcManager = ipcManager;
_dalamudUtil = dalamudUtil;
_fileCacheManager = fileCacheManager;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_mediator = mediator;
_loggerFactory = loggerFactory;
}
public CachedPlayer Create(OnlineUserIdentDto dto, ApiController apiController)
{
return new CachedPlayer(_loggerFactory.CreateLogger<CachedPlayer>(), dto, _gameObjectHandlerFactory, _ipcManager, apiController, _dalamudUtil, _fileCacheManager, _mediator);
}
}

View File

@@ -1,28 +0,0 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Factories;
public class GameObjectHandlerFactory
{
private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mediator;
private readonly PerformanceCollector _performanceCollector;
private readonly DalamudUtil _dalamudUtil;
public GameObjectHandlerFactory(ILoggerFactory loggerFactory, MareMediator mediator, PerformanceCollector performanceCollector, DalamudUtil dalamudUtil)
{
_loggerFactory = loggerFactory;
_mediator = mediator;
_performanceCollector = performanceCollector;
_dalamudUtil = dalamudUtil;
}
public GameObjectHandler Create(ObjectKind objectKind, Func<IntPtr> getAddress, bool isWatched)
{
return new GameObjectHandler(_loggerFactory.CreateLogger<GameObjectHandler>(), _performanceCollector, _mediator, _dalamudUtil, objectKind, getAddress, isWatched);
}
}

View File

@@ -1,27 +0,0 @@
using MareSynchronos.Managers;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Models;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Factories;
public class PairFactory
{
private readonly MareConfigService _configService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CachedPlayerFactory _cachedPlayerFactory;
private readonly ILoggerFactory _loggerFactory;
public PairFactory(MareConfigService configService, ServerConfigurationManager serverConfigurationManager, CachedPlayerFactory cachedPlayerFactory, ILoggerFactory loggerFactory)
{
_configService = configService;
_serverConfigurationManager = serverConfigurationManager;
_cachedPlayerFactory = cachedPlayerFactory;
_loggerFactory = loggerFactory;
}
public Pair Create()
{
return new Pair(_loggerFactory.CreateLogger<Pair>(), _cachedPlayerFactory, _configService, _serverConfigurationManager);
}
}

View File

@@ -1,17 +1,9 @@
#nullable disable
using System.Globalization;
namespace MareSynchronos.FileCache;
public class FileCacheEntity
{
public string ResolvedFilepath { get; private set; } = string.Empty;
public string Hash { get; set; }
public string PrefixedFilePath { get; init; }
public string LastModifiedDateTicks { get; set; }
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks)
{
Hash = hash;
@@ -19,10 +11,14 @@ public class FileCacheEntity
LastModifiedDateTicks = lastModifiedDateTicks;
}
public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}";
public string Hash { get; set; }
public string LastModifiedDateTicks { get; set; }
public string PrefixedFilePath { get; init; }
public string ResolvedFilepath { get; private set; } = string.Empty;
public void SetResolvedFilePath(string filePath)
{
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
}
public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks.ToString(CultureInfo.InvariantCulture)}";
}
}

View File

@@ -1,4 +1,4 @@
using MareSynchronos.Managers;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
@@ -8,18 +8,17 @@ using System.Text;
namespace MareSynchronos.FileCache;
public class FileCacheManager : IDisposable
public sealed class FileCacheManager : IDisposable
{
private const string _penumbraPrefix = "{penumbra}";
public const string CsvSplit = "|";
private const string _cachePrefix = "{cache}";
private readonly ILogger<FileCacheManager> _logger;
private readonly IpcManager _ipcManager;
private const string _penumbraPrefix = "{penumbra}";
private readonly MareConfigService _configService;
private readonly string _csvPath;
private string CsvBakPath => _csvPath + ".bak";
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCaches = new(StringComparer.Ordinal);
public const string CsvSplit = "|";
private readonly object _fileWriteLock = new();
private readonly IpcManager _ipcManager;
private readonly ILogger<FileCacheManager> _logger;
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, MareConfigService configService)
{
@@ -51,12 +50,114 @@ public class FileCacheManager : IDisposable
}
catch (Exception)
{
_logger.LogWarning($"Failed to initialize entry {entry}, ignoring");
_logger.LogWarning("Failed to initialize entry {entry}, ignoring", entry);
}
}
}
}
private string CsvBakPath => _csvPath + ".bak";
public FileCacheEntity? CreateCacheEntry(string path)
{
_logger.LogTrace("Creating cache entry for {path}", path);
FileInfo fi = new(path);
if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), _cachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper(CultureInfo.InvariantCulture));
}
public FileCacheEntity? CreateFileEntry(string path)
{
_logger.LogTrace("Creating file entry for {path}", path);
FileInfo fi = new(path);
if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath);
}
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType());
WriteOutFullCsv();
GC.SuppressFinalize(this);
}
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.ToList();
public string GetCacheFilePath(string hash, bool isTemporaryFile)
{
return Path.Combine(_configService.Current.CacheFolder, hash + (isTemporaryFile ? ".tmp" : string.Empty));
}
public FileCacheEntity? GetFileCacheByHash(string hash)
{
if (_fileCaches.Any(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)))
{
return GetValidatedFileCache(_fileCaches.Where(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal))
.OrderByDescending(f => f.Value.PrefixedFilePath.Length)
.FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)).Value);
}
return null;
}
public FileCacheEntity? GetFileCacheByPath(string path)
{
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{
_logger.LogDebug("Found no entries for {path}", cleanedPath);
return CreateFileEntry(path);
}
var validatedCacheEntry = GetValidatedFileCache(entry);
return validatedCacheEntry;
}
public void RemoveHash(FileCacheEntity entity)
{
_logger.LogTrace("Removing {path}", entity.ResolvedFilepath);
_fileCaches.Remove(entity.PrefixedFilePath, out _);
}
public string ResolveFileReplacement(string gamePath)
{
return _ipcManager.PenumbraResolvePath(gamePath);
}
public void UpdateHash(FileCacheEntity fileCache)
{
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
_fileCaches.Remove(fileCache.PrefixedFilePath, out _);
_fileCaches[fileCache.PrefixedFilePath] = fileCache;
}
public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache)
{
fileCache = ReplacePathPrefixes(fileCache);
FileInfo fi = new(fileCache.ResolvedFilepath);
if (!fi.Exists)
{
return (FileState.RequireDeletion, fileCache);
}
if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
{
return (FileState.RequireUpdate, fileCache);
}
return (FileState.Valid, fileCache);
}
public void WriteOutFullCsv()
{
StringBuilder sb = new();
@@ -82,74 +183,6 @@ public class FileCacheManager : IDisposable
}
}
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.ToList();
public FileCacheEntity? GetFileCacheByHash(string hash)
{
if (_fileCaches.Any(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)))
{
return GetValidatedFileCache(_fileCaches.Where(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal))
.OrderByDescending(f => f.Value.PrefixedFilePath.Length)
.FirstOrDefault(f => string.Equals(f.Value.Hash, hash, StringComparison.Ordinal)).Value);
}
return null;
}
public (FileState, FileCacheEntity) ValidateFileCacheEntity(FileCacheEntity fileCache)
{
fileCache = ReplacePathPrefixes(fileCache);
FileInfo fi = new(fileCache.ResolvedFilepath);
if (!fi.Exists)
{
return (FileState.RequireDeletion, fileCache);
}
if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
{
return (FileState.RequireUpdate, fileCache);
}
return (FileState.Valid, fileCache);
}
public FileCacheEntity? GetFileCacheByPath(string path)
{
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
if (entry == null)
{
_logger.LogDebug("Found no entries for " + cleanedPath);
return CreateFileEntry(path);
}
var validatedCacheEntry = GetValidatedFileCache(entry);
return validatedCacheEntry;
}
public FileCacheEntity? CreateCacheEntry(string path)
{
_logger.LogTrace("Creating cache entry for " + path);
FileInfo fi = new(path);
if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), _cachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath, fi.Name.ToUpper(CultureInfo.InvariantCulture));
}
public FileCacheEntity? CreateFileEntry(string path)
{
_logger.LogTrace("Creating file entry for " + path);
FileInfo fi = new(path);
if (!fi.Exists) return null;
var fullName = fi.FullName.ToLowerInvariant();
if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
return CreateFileCacheEntity(fi, prefixedPath);
}
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
{
hash ??= Crypto.GetFileHash(fileInfo.FullName);
@@ -161,7 +194,7 @@ public class FileCacheManager : IDisposable
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
}
var result = GetFileCacheByPath(fileInfo.FullName);
_logger.LogDebug("Creating file cache for " + fileInfo.FullName + " success: " + (result != null));
_logger.LogDebug("Creating file cache for {name} success: {success}", fileInfo.FullName, (result != null));
return result;
}
@@ -172,6 +205,20 @@ public class FileCacheManager : IDisposable
return resulingFileCache;
}
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
{
if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase))
{
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal));
}
else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase))
{
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_cachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal));
}
return fileCache;
}
private FileCacheEntity? Validate(FileCacheEntity fileCache)
{
var file = new FileInfo(fileCache.ResolvedFilepath);
@@ -188,44 +235,4 @@ public class FileCacheManager : IDisposable
return fileCache;
}
public void RemoveHash(FileCacheEntity entity)
{
_logger.LogTrace("Removing " + entity.ResolvedFilepath);
_fileCaches.Remove(entity.PrefixedFilePath, out _);
}
public void UpdateHash(FileCacheEntity fileCache)
{
_logger.LogTrace("Updating hash for " + fileCache.ResolvedFilepath);
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
_fileCaches.Remove(fileCache.PrefixedFilePath, out _);
_fileCaches[fileCache.PrefixedFilePath] = fileCache;
}
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
{
if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase))
{
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal));
}
else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase))
{
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_cachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal));
}
return fileCache;
}
public string ResolveFileReplacement(string gamePath)
{
return _ipcManager.PenumbraResolvePath(gamePath);
}
public void Dispose()
{
_logger.LogTrace($"Disposing {GetType()}");
WriteOutFullCsv();
}
}
}

View File

@@ -1,89 +1,60 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using MareSynchronos.Managers;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace MareSynchronos.FileCache;
public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
public sealed class PeriodicFileScanner : DisposableMediatorSubscriberBase
{
private readonly IpcManager _ipcManager;
private readonly MareConfigService _configService;
private readonly FileCacheManager _fileDbManager;
private readonly PerformanceCollector _performanceCollector;
private readonly IpcManager _ipcManager;
private readonly PerformanceCollectorService _performanceCollector;
private long _currentFileProgress = 0;
private bool _fileScanWasRunning = false;
private CancellationTokenSource? _scanCancellationTokenSource;
private Task? _fileScannerTask = null;
public ConcurrentDictionary<string, int> haltScanLocks = new(StringComparer.Ordinal);
private TimeSpan _timeUntilNextScan = TimeSpan.Zero;
public PeriodicFileScanner(ILogger<PeriodicFileScanner> logger, IpcManager ipcManager, MareConfigService configService,
FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollector performanceCollector) : base(logger, mediator)
FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector) : base(logger, mediator)
{
_logger.LogTrace("Creating " + nameof(PeriodicFileScanner));
_ipcManager = ipcManager;
_configService = configService;
_fileDbManager = fileDbManager;
_performanceCollector = performanceCollector;
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) => StartScan());
Mediator.Subscribe<HaltScanMessage>(this, (msg) => HaltScan(((HaltScanMessage)msg).Source));
Mediator.Subscribe<ResumeScanMessage>(this, (msg) => ResumeScan(((ResumeScanMessage)msg).Source));
Mediator.Subscribe<HaltScanMessage>(this, (msg) => HaltScan(msg.Source));
Mediator.Subscribe<ResumeScanMessage>(this, (msg) => ResumeScan(msg.Source));
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => StartScan());
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => StartScan());
}
public void ResetLocks()
{
haltScanLocks.Clear();
}
public long CurrentFileProgress => _currentFileProgress;
public long FileCacheSize { get; set; }
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
public void ResumeScan(string source)
{
if (!haltScanLocks.ContainsKey(source)) haltScanLocks[source] = 0;
public string TimeUntilNextScan => _timeUntilNextScan.ToString(@"mm\:ss");
haltScanLocks[source]--;
if (haltScanLocks[source] < 0) haltScanLocks[source] = 0;
public long TotalFiles { get; private set; }
if (_fileScanWasRunning && haltScanLocks.All(f => f.Value == 0))
{
_fileScanWasRunning = false;
InvokeScan(forced: true);
}
}
private int TimeBetweenScans => _configService.Current.TimeSpanBetweenScansInSeconds;
public void HaltScan(string source)
{
if (!haltScanLocks.ContainsKey(source)) haltScanLocks[source] = 0;
haltScanLocks[source]++;
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
HaltScanLocks[source]++;
if (IsScanRunning && haltScanLocks.Any(f => f.Value > 0))
if (IsScanRunning && HaltScanLocks.Any(f => f.Value > 0))
{
_scanCancellationTokenSource?.Cancel();
_fileScanWasRunning = true;
}
}
private bool _fileScanWasRunning = false;
private long _currentFileProgress = 0;
public long CurrentFileProgress => _currentFileProgress;
public long FileCacheSize { get; set; }
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
public long TotalFiles { get; private set; }
public string TimeUntilNextScan => _timeUntilNextScan.ToString(@"mm\:ss");
private TimeSpan _timeUntilNextScan = TimeSpan.Zero;
private int TimeBetweenScans => _configService.Current.TimeSpanBetweenScansInSeconds;
public override void Dispose()
{
base.Dispose();
_scanCancellationTokenSource?.Cancel();
}
public void InvokeScan(bool forced = false)
{
bool isForced = forced;
@@ -92,11 +63,11 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
_scanCancellationTokenSource?.Cancel();
_scanCancellationTokenSource = new CancellationTokenSource();
var token = _scanCancellationTokenSource.Token;
_fileScannerTask = Task.Run(async () =>
Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
while (haltScanLocks.Any(f => f.Value > 0) || !_ipcManager.CheckPenumbraApi())
while (HaltScanLocks.Any(f => f.Value > 0) || !_ipcManager.CheckPenumbraApi())
{
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
@@ -150,6 +121,38 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
return true;
}
public void ResetLocks()
{
HaltScanLocks.Clear();
}
public void ResumeScan(string source)
{
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
HaltScanLocks[source]--;
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
if (_fileScanWasRunning && HaltScanLocks.All(f => f.Value == 0))
{
_fileScanWasRunning = false;
InvokeScan(forced: true);
}
}
public void StartScan()
{
if (!_ipcManager.Initialized || !_configService.Current.HasValidSetup()) return;
Logger.LogTrace("Penumbra is active, configuration is valid, scan");
InvokeScan(forced: true);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_scanCancellationTokenSource?.Cancel();
}
private void PeriodicFileScan(CancellationToken ct)
{
TotalFiles = 1;
@@ -159,19 +162,19 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
{
penDirExists = false;
_logger.LogWarning("Penumbra directory is not set or does not exist.");
Logger.LogWarning("Penumbra directory is not set or does not exist.");
}
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
{
cacheDirExists = false;
_logger.LogWarning("Mare Cache directory is not set or does not exist.");
Logger.LogWarning("Mare Cache directory is not set or does not exist.");
}
if (!penDirExists || !cacheDirExists)
{
return;
}
_logger.LogDebug("Getting files from " + penumbraDir + " and " + _configService.Current.CacheFolder);
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder);
string[] ext = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".scd", ".skp", ".shpk" };
var scannedFiles = new ConcurrentDictionary<string, bool>(Directory.EnumerateFiles(penumbraDir!, "*.*", SearchOption.AllDirectories)
@@ -207,18 +210,18 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
scannedFiles[validatedCacheResult.Item2.ResolvedFilepath] = true;
if (validatedCacheResult.Item1 == FileState.RequireUpdate)
{
_logger.LogTrace("To update: {path}", validatedCacheResult.Item2.ResolvedFilepath);
Logger.LogTrace("To update: {path}", validatedCacheResult.Item2.ResolvedFilepath);
entitiesToUpdate.Add(validatedCacheResult.Item2);
}
else if (validatedCacheResult.Item1 == FileState.RequireDeletion)
{
_logger.LogTrace("To delete: {path}", validatedCacheResult.Item2.ResolvedFilepath);
Logger.LogTrace("To delete: {path}", validatedCacheResult.Item2.ResolvedFilepath);
entitiesToRemove.Add(validatedCacheResult.Item2);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed validating {path}", cache.ResolvedFilepath);
Logger.LogWarning(ex, "Failed validating {path}", cache.ResolvedFilepath);
}
Interlocked.Increment(ref _currentFileProgress);
@@ -227,7 +230,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
if (!_ipcManager.CheckPenumbraApi())
{
_logger.LogWarning("Penumbra not available");
Logger.LogWarning("Penumbra not available");
return;
}
@@ -240,14 +243,14 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during enumerating FileCaches");
Logger.LogWarning(ex, "Error during enumerating FileCaches");
}
Task.WaitAll(dbTasks);
if (!_ipcManager.CheckPenumbraApi())
{
_logger.LogWarning("Penumbra not available");
Logger.LogWarning("Penumbra not available");
return;
}
@@ -266,18 +269,18 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
_fileDbManager.WriteOutFullCsv();
}
_logger.LogTrace("Scanner validated existing db files");
Logger.LogTrace("Scanner validated existing db files");
if (!_ipcManager.CheckPenumbraApi())
{
_logger.LogWarning("Penumbra not available");
Logger.LogWarning("Penumbra not available");
return;
}
if (ct.IsCancellationRequested) return;
// scan new files
foreach (var c in scannedFiles.Where(c => c.Value == false))
foreach (var c in scannedFiles.Where(c => !c.Value))
{
var idx = Task.WaitAny(dbTasks, ct);
dbTasks[idx] = Task.Run(() =>
@@ -289,7 +292,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed adding {file}", c.Key);
Logger.LogWarning(ex, "Failed adding {file}", c.Key);
}
Interlocked.Increment(ref _currentFileProgress);
@@ -298,7 +301,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
if (!_ipcManager.CheckPenumbraApi())
{
_logger.LogWarning("Penumbra not available");
Logger.LogWarning("Penumbra not available");
return;
}
@@ -307,9 +310,9 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
Task.WaitAll(dbTasks);
_logger.LogTrace("Scanner added new files to db");
Logger.LogTrace("Scanner added new files to db");
_logger.LogDebug("Scan complete");
Logger.LogDebug("Scan complete");
TotalFiles = 0;
_currentFileProgress = 0;
entitiesToRemove.Clear();
@@ -322,11 +325,4 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable
_configService.Save();
}
}
public void StartScan()
{
if (!_ipcManager.Initialized || !_configService.Current.HasValidSetup()) return;
_logger.LogTrace("Penumbra is active, configuration is valid, scan");
InvokeScan(forced: true);
}
}
}

View File

@@ -1,27 +1,24 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using MareSynchronos.PlayerData.Data;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace MareSynchronos.Managers;
namespace MareSynchronos.FileCache;
public class TransientResourceManager : MediatorSubscriberBase, IDisposable
public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{
private readonly TransientConfigService _configurationService;
private readonly DalamudUtil _dalamudUtil;
private readonly DalamudUtilService _dalamudUtil;
public HashSet<GameObjectHandler> PlayerRelatedPointers = new();
private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" };
private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId;
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = new();
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources { get; } = new();
public TransientResourceManager(ILogger<TransientResourceManager> logger, TransientConfigService configurationService,
DalamudUtil dalamudUtil, MareMediator mediator) : base(logger, mediator)
DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator)
{
_configurationService = configurationService;
_dalamudUtil = dalamudUtil;
@@ -36,66 +33,36 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
try
{
_logger.LogDebug("Loaded persistent transient resource {path}", gamePath);
Logger.LogDebug("Loaded persistent transient resource {path}", gamePath);
SemiTransientResources[ObjectKind.Player].Add(gamePath);
restored++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath);
Logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath);
}
}
_logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count());
Logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count);
}
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, (msg) => Manager_PenumbraResourceLoadEvent((PenumbraResourceLoadMessage)msg));
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (_) => Manager_PenumbraModSettingChanged());
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
Mediator.Subscribe<ClassJobChangedMessage>(this, (_) => DalamudUtil_ClassJobChanged());
Mediator.Subscribe<AddWatchedGameObjectHandler>(this, (msg) =>
{
var actualMsg = ((AddWatchedGameObjectHandler)msg);
PlayerRelatedPointers.Add(actualMsg.Handler);
_playerRelatedPointers.Add(msg.Handler);
});
Mediator.Subscribe<RemoveWatchedGameObjectHandler>(this, (msg) =>
{
var actualMsg = ((RemoveWatchedGameObjectHandler)msg);
PlayerRelatedPointers.Remove(actualMsg.Handler);
_playerRelatedPointers.Remove(msg.Handler);
});
}
private void Manager_PenumbraModSettingChanged()
{
Task.Run(() =>
{
_logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
foreach (var item in SemiTransientResources)
{
Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer));
}
});
}
private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId;
private void DalamudUtil_ClassJobChanged()
{
if (SemiTransientResources.ContainsKey(ObjectKind.Pet))
{
SemiTransientResources[ObjectKind.Pet].Clear();
}
}
private void DalamudUtil_FrameworkUpdate()
{
foreach (var item in TransientResources.ToList())
{
if (!_dalamudUtil.IsGameObjectPresent(item.Key))
{
_logger.LogDebug("Object not present anymore: " + item.Key.ToString("X"));
TransientResources.TryRemove(item.Key, out _);
}
}
}
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources { get; } = new();
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
public void CleanUpSemiTransientResources(ObjectKind objectKind, List<FileReplacement>? fileReplacement = null)
{
@@ -114,6 +81,16 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
}
}
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
{
if (SemiTransientResources.TryGetValue(objectKind, out var result))
{
return result ?? new HashSet<string>(StringComparer.Ordinal);
}
return new HashSet<string>(StringComparer.Ordinal);
}
public List<string> GetTransientResources(IntPtr gameObject)
{
if (TransientResources.TryGetValue(gameObject, out var result))
@@ -124,14 +101,91 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
return new List<string>();
}
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind)
{
if (SemiTransientResources.TryGetValue(objectKind, out var result))
if (!SemiTransientResources.ContainsKey(objectKind))
{
return result ?? new HashSet<string>(StringComparer.Ordinal);
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
return new HashSet<string>(StringComparer.Ordinal);
if (!TransientResources.TryGetValue(gameObject, out var resources))
{
return;
}
var transientResources = resources.ToList();
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
foreach (var gamePath in transientResources)
{
SemiTransientResources[objectKind].Add(gamePath);
}
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal);
_configurationService.Save();
}
TransientResources[gameObject].Clear();
}
internal void AddSemiTransientResource(ObjectKind objectKind, string item)
{
if (!SemiTransientResources.ContainsKey(objectKind))
{
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
SemiTransientResources[objectKind].Add(item.ToLowerInvariant());
}
internal void ClearTransientPaths(IntPtr ptr, List<string> list)
{
if (TransientResources.TryGetValue(ptr, out var set))
{
set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
TransientResources.Clear();
SemiTransientResources.Clear();
if (SemiTransientResources.ContainsKey(ObjectKind.Player))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player];
_configurationService.Save();
}
}
private void DalamudUtil_ClassJobChanged()
{
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
{
value?.Clear();
}
}
private void DalamudUtil_FrameworkUpdate()
{
foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList())
{
Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X"));
TransientResources.TryRemove(item, out _);
}
}
private void Manager_PenumbraModSettingChanged()
{
Task.Run(() =>
{
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
foreach (var item in SemiTransientResources)
{
Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer));
}
});
}
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
@@ -143,7 +197,7 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
{
return;
}
if (!PlayerRelatedPointers.Select(p => p.CurrentAddress).Contains(gameObject))
if (!_playerRelatedPointers.Select(p => p.CurrentAddress).Contains(gameObject))
{
//_logger.LogDebug("Got resource " + gamePath + " for ptr " + gameObject.ToString("X"));
return;
@@ -167,70 +221,13 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable
if (TransientResources[gameObject].Contains(replacedGamePath) ||
SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))))
{
_logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath);
Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath);
}
else
{
TransientResources[gameObject].Add(replacedGamePath);
_logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath);
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, gameObject.ToString("X"), filePath);
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
}
}
public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind)
{
if (!SemiTransientResources.ContainsKey(objectKind))
{
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
if (!TransientResources.TryGetValue(gameObject, out var resources))
{
return;
}
var transientResources = resources.ToList();
_logger.LogDebug("Persisting " + transientResources.Count + " transient resources");
foreach (var gamePath in transientResources)
{
SemiTransientResources[objectKind].Add(gamePath);
}
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal);
_configurationService.Save();
}
TransientResources[gameObject].Clear();
}
public override void Dispose()
{
base.Dispose();
TransientResources.Clear();
SemiTransientResources.Clear();
if (SemiTransientResources.ContainsKey(ObjectKind.Player))
{
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player];
_configurationService.Save();
}
}
internal void AddSemiTransientResource(ObjectKind objectKind, string item)
{
if (!SemiTransientResources.ContainsKey(objectKind))
{
SemiTransientResources[objectKind] = new HashSet<string>(StringComparer.Ordinal);
}
SemiTransientResources[objectKind].Add(item.ToLowerInvariant());
}
internal void ClearTransientPaths(IntPtr ptr, List<string> list)
{
if (TransientResources.TryGetValue(ptr, out var set))
{
set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
}
}
}
}

View File

@@ -3,19 +3,26 @@ using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.Logging;
using System.Text;
namespace MareSynchronos.Utils;
namespace MareSynchronos.Interop;
internal class DalamudLogger : ILogger
internal sealed class DalamudLogger : ILogger
{
private readonly string _name;
private readonly MareConfigService _mareConfigService;
private readonly string _name;
public DalamudLogger(string name, MareConfigService mareConfigService)
{
this._name = name;
_name = name;
_mareConfigService = mareConfigService;
}
public IDisposable BeginScope<TState>(TState state) => default!;
public bool IsEnabled(LogLevel logLevel)
{
return (int)_mareConfigService.Current.LogLevel <= (int)logLevel;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
@@ -35,11 +42,4 @@ internal class DalamudLogger : ILogger
PluginLog.Fatal(sb.ToString());
}
}
public bool IsEnabled(LogLevel logLevel)
{
return (int)_mareConfigService.Current.LogLevel <= (int)logLevel;
}
public IDisposable BeginScope<TState>(TState state) => default!;
}
}

View File

@@ -2,13 +2,14 @@
using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Utils;
namespace MareSynchronos.Interop;
[ProviderAlias("Dalamud")]
public class DalamudLoggingProvider : ILoggerProvider
public sealed class DalamudLoggingProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers =
new(StringComparer.OrdinalIgnoreCase);
private readonly MareConfigService _mareConfigService;
public DalamudLoggingProvider(MareConfigService mareConfigService)
@@ -34,5 +35,6 @@ public class DalamudLoggingProvider : ILoggerProvider
public void Dispose()
{
_loggers.Clear();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -2,7 +2,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Utils;
namespace MareSynchronos.Interop;
public static class DalamudLoggingProviderExtensions
{

View File

@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharaExt

View File

@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct HumanExt

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct Material

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct MaterialData

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct MtrlResource

View File

@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct RenderModel
@@ -24,14 +24,6 @@ public unsafe struct RenderModel
[FieldOffset(0x60)]
public int BoneListCount;
[FieldOffset(0x70)]
private void* UnkDXBuffer1;
[FieldOffset(0x78)]
private void* UnkDXBuffer2;
[FieldOffset(0x80)]
private void* UnkDXBuffer3;
[FieldOffset(0x98)]
public void** Materials;

View File

@@ -1,7 +1,7 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct ResourceHandle

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct Weapon

View File

@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace MareSynchronos.Interop;
namespace MareSynchronos.Interop.FFXIV;
[StructLayout(LayoutKind.Explicit)]
public unsafe struct WeaponDrawObject

View File

@@ -1,95 +1,88 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Game.ClientState.Objects.Types;
using MareSynchronos.Utils;
using Action = System.Action;
using System.Collections.Concurrent;
using System.Text;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using MareSynchronos.Mediator;
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.Models;
using Microsoft.Extensions.Logging;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services;
namespace MareSynchronos.Managers;
namespace MareSynchronos.Interop;
public class IpcManager : MediatorSubscriberBase, IDisposable
public sealed class IpcManager : DisposableMediatorSubscriberBase
{
private readonly ICallGateSubscriber<string> _customizePlusApiVersion;
private readonly ICallGateSubscriber<string, string> _customizePlusGetBodyScale;
private readonly ICallGateSubscriber<string?, object> _customizePlusOnScaleUpdate;
private readonly ICallGateSubscriber<Character?, object> _customizePlusRevert;
private readonly ICallGateSubscriber<string, Character?, object> _customizePlusSetBodyScaleToCharacter;
private readonly DalamudUtilService _dalamudUtil;
private readonly ICallGateSubscriber<int> _glamourerApiVersion;
private readonly ICallGateSubscriber<string, GameObject?, object>? _glamourerApplyAll;
private readonly ICallGateSubscriber<string, GameObject?, object>? _glamourerApplyOnlyCustomization;
private readonly ICallGateSubscriber<string, GameObject?, object>? _glamourerApplyOnlyEquipment;
private readonly ICallGateSubscriber<GameObject?, string>? _glamourerGetAllCustomization;
private readonly ICallGateSubscriber<GameObject?, object> _glamourerRevertCustomization;
private readonly ICallGateSubscriber<string, GameObject?, object>? _glamourerApplyOnlyEquipment;
private readonly ICallGateSubscriber<string, GameObject?, object>? _glamourerApplyOnlyCustomization;
private readonly FuncSubscriber<(int, int)> _penumbraApiVersion;
private readonly FuncSubscriber<string, PenumbraApiEc> _penumbraCreateNamedTemporaryCollection;
private readonly FuncSubscriber<string> _penumbraGetMetaManipulations;
private readonly EventSubscriber _penumbraInit;
private readonly EventSubscriber _penumbraDispose;
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
private readonly ActionSubscriber<string, RedrawType> _penumbraRedraw;
private readonly ActionSubscriber<GameObject, RedrawType> _penumbraRedrawObject;
private readonly FuncSubscriber<string, PenumbraApiEc> _penumbraRemoveTemporaryCollection;
private readonly FuncSubscriber<string, string, int, PenumbraApiEc> _penumbraRemoveTemporaryMod;
private readonly FuncSubscriber<string, int, bool, PenumbraApiEc> _penumbraAssignTemporaryCollection;
private readonly FuncSubscriber<string> _penumbraResolveModDir;
private readonly FuncSubscriber<string, string> _penumbraResolvePlayer;
private readonly FuncSubscriber<string, string[]> _reverseResolvePlayer;
private readonly FuncSubscriber<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> _penumbraAddTemporaryMod;
private readonly FuncSubscriber<string[], string[], (string[], string[][])> _penumbraResolvePaths;
private readonly FuncSubscriber<bool> _penumbraEnabled;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _penumbraModSettingChanged;
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
private readonly ConcurrentQueue<Action> _gposeActionQueue = new();
private readonly ICallGateSubscriber<string> _heelsGetApiVersion;
private readonly ICallGateSubscriber<float> _heelsGetOffset;
private readonly ICallGateSubscriber<float, object?> _heelsOffsetUpdate;
private readonly ICallGateSubscriber<GameObject, float, object?> _heelsRegisterPlayer;
private readonly ICallGateSubscriber<GameObject, object?> _heelsUnregisterPlayer;
private readonly ICallGateSubscriber<string> _customizePlusApiVersion;
private readonly ICallGateSubscriber<string, string> _customizePlusGetBodyScale;
private readonly ICallGateSubscriber<string, Character?, object> _customizePlusSetBodyScaleToCharacter;
private readonly ICallGateSubscriber<Character?, object> _customizePlusRevert;
private readonly ICallGateSubscriber<string?, object> _customizePlusOnScaleUpdate;
private readonly ConcurrentQueue<Action> _normalQueue = new();
private readonly ICallGateSubscriber<string> _palettePlusApiVersion;
private readonly ICallGateSubscriber<Character, string> _palettePlusBuildCharaPalette;
private readonly ICallGateSubscriber<Character, string, object> _palettePlusSetCharaPalette;
private readonly ICallGateSubscriber<Character, object> _palettePlusRemoveCharaPalette;
private readonly ICallGateSubscriber<Character, string, object> _palettePlusPaletteChanged;
private readonly DalamudUtil _dalamudUtil;
private bool _inGposeQueueMode = false;
private ConcurrentQueue<Action> ActionQueue => _inGposeQueueMode ? _gposeActionQueue : _normalQueue;
private readonly ConcurrentQueue<Action> _normalQueue = new();
private readonly ConcurrentQueue<Action> _gposeActionQueue = new();
private readonly ICallGateSubscriber<Character, object> _palettePlusRemoveCharaPalette;
private readonly ICallGateSubscriber<Character, string, object> _palettePlusSetCharaPalette;
private readonly FuncSubscriber<string, string, Dictionary<string, string>, string, int, PenumbraApiEc> _penumbraAddTemporaryMod;
private readonly FuncSubscriber<(int, int)> _penumbraApiVersion;
private readonly FuncSubscriber<string, int, bool, PenumbraApiEc> _penumbraAssignTemporaryCollection;
private readonly FuncSubscriber<string, PenumbraApiEc> _penumbraCreateNamedTemporaryCollection;
private readonly EventSubscriber _penumbraDispose;
private readonly FuncSubscriber<bool> _penumbraEnabled;
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
private readonly FuncSubscriber<string> _penumbraGetMetaManipulations;
private readonly EventSubscriber _penumbraInit;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _penumbraModSettingChanged;
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
private readonly ActionSubscriber<string, RedrawType> _penumbraRedraw;
private readonly ActionSubscriber<GameObject, RedrawType> _penumbraRedrawObject;
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
private CancellationTokenSource _disposalCts = new();
private bool _penumbraAvailable = false;
private bool _glamourerAvailable = false;
private readonly FuncSubscriber<string, PenumbraApiEc> _penumbraRemoveTemporaryCollection;
private readonly FuncSubscriber<string, string, int, PenumbraApiEc> _penumbraRemoveTemporaryMod;
private readonly FuncSubscriber<string> _penumbraResolveModDir;
private readonly FuncSubscriber<string[], string[], (string[], string[][])> _penumbraResolvePaths;
private readonly FuncSubscriber<string, string> _penumbraResolvePlayer;
private readonly FuncSubscriber<string, string[]> _reverseResolvePlayer;
private bool _customizePlusAvailable = false;
private CancellationTokenSource _disposalCts = new();
private bool _glamourerAvailable = false;
private bool _heelsAvailable = false;
private bool _inGposeQueueMode = false;
private bool _palettePlusAvailable = false;
private bool _penumbraAvailable = false;
private bool _shownGlamourerUnavailable = false;
private bool _shownPenumbraUnavailable = false;
public IpcManager(ILogger<IpcManager> logger, DalamudPluginInterface pi, DalamudUtil dalamudUtil, MareMediator mediator) : base(logger, mediator)
public IpcManager(ILogger<IpcManager> logger, DalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator)
{
_dalamudUtil = dalamudUtil;
_logger.LogTrace("Creating " + nameof(IpcManager));
_penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, () => PenumbraInit());
_penumbraDispose = Penumbra.Api.Ipc.Disposed.Subscriber(pi, () => PenumbraDispose());
_penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, PenumbraInit);
_penumbraDispose = Penumbra.Api.Ipc.Disposed.Subscriber(pi, PenumbraDispose);
_penumbraResolvePlayer = Penumbra.Api.Ipc.ResolvePlayerPath.Subscriber(pi);
_penumbraResolveModDir = Penumbra.Api.Ipc.GetModDirectory.Subscriber(pi);
_penumbraRedraw = Penumbra.Api.Ipc.RedrawObjectByName.Subscriber(pi);
_penumbraRedrawObject = Penumbra.Api.Ipc.RedrawObject.Subscriber(pi);
_reverseResolvePlayer = Penumbra.Api.Ipc.ReverseResolvePlayerPath.Subscriber(pi);
_penumbraApiVersion = Penumbra.Api.Ipc.ApiVersions.Subscriber(pi);
_penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, (ptr, idx) => RedrawEvent((IntPtr)ptr, idx));
_penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, RedrawEvent);
_penumbraGetMetaManipulations = Penumbra.Api.Ipc.GetPlayerMetaManipulations.Subscriber(pi);
_penumbraAddTemporaryMod = Penumbra.Api.Ipc.AddTemporaryMod.Subscriber(pi);
_penumbraCreateNamedTemporaryCollection = Penumbra.Api.Ipc.CreateNamedTemporaryCollection.Subscriber(pi);
@@ -104,7 +97,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
Mediator.Publish(new PenumbraModSettingChangedMessage());
});
_penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2));
_penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
_glamourerApiVersion = pi.GetIpcSubscriber<int>("Glamourer.ApiVersion");
_glamourerGetAllCustomization = pi.GetIpcSubscriber<GameObject?, string>("Glamourer.GetAllCustomizationFromCharacter");
@@ -148,64 +141,26 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => PeriodicApiStateCheck());
}
private void PeriodicApiStateCheck()
{
_glamourerAvailable = CheckGlamourerApiInternal();
_penumbraAvailable = CheckPenumbraApiInternal();
_heelsAvailable = CheckHeelsApiInternal();
_customizePlusAvailable = CheckCustomizePlusApiInternal();
_palettePlusAvailable = CheckPalettePlusApiInternal();
PenumbraModDirectory = GetPenumbraModDirectory();
}
private void HandleGposeActionQueue()
{
if (_gposeActionQueue.TryDequeue(out var action))
{
if (action == null) return;
_logger.LogDebug("Execution action in gpose queue: {method}", action.Method);
action();
}
}
public void ToggleGposeQueueMode(bool on)
{
_inGposeQueueMode = on;
}
private void ClearActionQueue()
{
ActionQueue.Clear();
_gposeActionQueue.Clear();
}
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
{
Task.Run(() =>
{
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
{
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
}
});
}
private void HandleActionQueue()
{
if (ActionQueue.TryDequeue(out var action))
{
if (action == null) return;
_logger.LogDebug("Execution action in queue: {method}", action.Method);
action();
}
}
public bool Initialized => CheckPenumbraApiInternal() && CheckGlamourerApiInternal();
public string? PenumbraModDirectory { get; private set; }
private ConcurrentQueue<Action> ActionQueue => _inGposeQueueMode ? _gposeActionQueue : _normalQueue;
public bool CheckCustomizePlusApi() => _customizePlusAvailable;
public bool CheckCustomizePlusApiInternal()
{
try
{
return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public bool CheckGlamourerApi() => _glamourerAvailable;
private bool _shownGlamourerUnavailable = false;
public bool CheckGlamourerApiInternal()
{
bool apiAvailable = false;
@@ -229,9 +184,36 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
}
}
public bool CheckHeelsApi() => _heelsAvailable;
public bool CheckHeelsApiInternal()
{
try
{
return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public bool CheckPalettePlusApi() => _palettePlusAvailable;
public bool CheckPalettePlusApiInternal()
{
try
{
return string.Equals(_palettePlusApiVersion.InvokeFunc(), "1.1.0", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public bool CheckPenumbraApi() => _penumbraAvailable;
private bool _shownPenumbraUnavailable = false;
public bool CheckPenumbraApiInternal()
{
bool apiAvailable = false;
@@ -255,108 +237,31 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
}
}
public bool CheckHeelsApi() => _heelsAvailable;
public bool CheckHeelsApiInternal()
public async Task CustomizePlusRevert(IntPtr character)
{
try
{
return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public bool CheckCustomizePlusApi() => _customizePlusAvailable;
public bool CheckCustomizePlusApiInternal()
{
try
{
return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public bool CheckPalettePlusApi() => _palettePlusAvailable;
public bool CheckPalettePlusApiInternal()
{
try
{
return string.Equals(_palettePlusApiVersion.InvokeFunc(), "1.1.0", StringComparison.Ordinal);
}
catch
{
return false;
}
}
public override void Dispose()
{
base.Dispose();
_disposalCts.Cancel();
int totalSleepTime = 0;
while (!ActionQueue.IsEmpty && totalSleepTime < 2000)
{
_logger.LogTrace("Waiting for actionqueue to clear...");
HandleActionQueue();
Thread.Sleep(16);
totalSleepTime += 16;
}
if (totalSleepTime >= 2000)
{
_logger.LogTrace("Action queue clear or not, disposing");
}
ActionQueue.Clear();
_penumbraModSettingChanged.Dispose();
_penumbraGameObjectResourcePathResolved.Dispose();
_penumbraDispose.Dispose();
_penumbraInit.Dispose();
_penumbraObjectIsRedrawn.Dispose();
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
}
public float GetHeelsOffset()
{
if (!CheckHeelsApi()) return 0.0f;
return _heelsGetOffset.InvokeFunc();
}
public async Task HeelsSetOffsetForPlayer(IntPtr character, float offset)
{
if (!CheckHeelsApi()) return;
if (!CheckCustomizePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
if (gameObj is Character c)
{
_logger.LogTrace("Applying Heels data to {chara}", character.ToString("X"));
_heelsRegisterPlayer.InvokeAction(gameObj, offset);
Logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X"));
_customizePlusRevert!.InvokeAction(c);
}
}).ConfigureAwait(false);
}
public async Task HeelsRestoreOffsetForPlayer(IntPtr character)
public async Task CustomizePlusSetBodyScale(IntPtr character, string scale)
{
if (!CheckHeelsApi()) return;
if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
if (gameObj is Character c)
{
_logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X"));
_heelsUnregisterPlayer.InvokeAction(gameObj);
string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale));
Logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X"));
_customizePlusSetBodyScaleToCharacter!.InvokeAction(decodedScale, c);
}
}).ConfigureAwait(false);
}
@@ -369,57 +274,16 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale));
}
public async Task CustomizePlusSetBodyScale(IntPtr character, string scale)
public float GetHeelsOffset()
{
if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale));
_logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X"));
_customizePlusSetBodyScaleToCharacter!.InvokeAction(decodedScale, c);
}
}).ConfigureAwait(false);
if (!CheckHeelsApi()) return 0.0f;
return _heelsGetOffset.InvokeFunc();
}
public async Task CustomizePlusRevert(IntPtr character)
public string? GetPenumbraModDirectory()
{
if (!CheckCustomizePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
_logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X"));
_customizePlusRevert!.InvokeAction(c);
}
}).ConfigureAwait(false);
}
private async Task PenumbraRedrawAction(ILogger logger, GameObjectHandler obj, Guid applicationId, Action action, bool fireAndForget, CancellationToken token)
{
Mediator.Publish(new PenumbraStartRedrawMessage(obj.Address));
_penumbraRedrawRequests[obj.Address] = !fireAndForget;
ActionQueue.Enqueue(action);
if (!fireAndForget)
{
var disposeToken = _disposalCts.Token;
var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(disposeToken, token).Token;
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
if (!combinedToken.IsCancellationRequested)
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, obj, applicationId, 30000, combinedToken).ConfigureAwait(false);
_penumbraRedrawRequests[obj.Address] = false;
}
Mediator.Publish(new PenumbraEndRedrawMessage(obj.Address));
if (!CheckPenumbraApi()) return null;
return _penumbraResolveModDir!.Invoke().ToLowerInvariant();
}
public async Task GlamourerApplyAll(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
@@ -428,17 +292,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
var gameObj = _dalamudUtil.CreateGameObject(handler.Address);
if (gameObj is Character c)
{
await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyAll!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
}
}
public async Task GlamourerApplyOnlyEquipment(ILogger logger, GameObjectHandler handler, string customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
{
if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
var gameObj = _dalamudUtil.CreateGameObject(handler.Address);
if (gameObj is Character c)
{
await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyOnlyEquipment!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyAll!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
}
}
@@ -448,7 +302,17 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
var gameObj = _dalamudUtil.CreateGameObject(handler.Address);
if (gameObj is Character c)
{
await PenumbraRedrawAction(logger, handler, applicationId, () => _glamourerApplyOnlyCustomization!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyOnlyCustomization!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
}
}
public async Task GlamourerApplyOnlyEquipment(ILogger logger, GameObjectHandler handler, string customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
{
if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
var gameObj = _dalamudUtil.CreateGameObject(handler.Address);
if (gameObj is Character c)
{
await PenumbraRedrawAsync(logger, handler, applicationId, () => _glamourerApplyOnlyEquipment!.InvokeAction(customization, c), fireAndForget, token).ConfigureAwait(false);
}
}
@@ -481,27 +345,93 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
ActionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character));
}
public async Task HeelsRestoreOffsetForPlayer(IntPtr character)
{
if (!CheckHeelsApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
{
Logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X"));
_heelsUnregisterPlayer.InvokeAction(gameObj);
}
}).ConfigureAwait(false);
}
public async Task HeelsSetOffsetForPlayer(IntPtr character, float offset)
{
if (!CheckHeelsApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
{
Logger.LogTrace("Applying Heels data to {chara}", character.ToString("X"));
_heelsRegisterPlayer.InvokeAction(gameObj, offset);
}
}).ConfigureAwait(false);
}
public string PalettePlusBuildPalette()
{
if (!CheckPalettePlusApi()) return string.Empty;
var palette = _palettePlusBuildCharaPalette.InvokeFunc(_dalamudUtil.PlayerCharacter);
if (string.IsNullOrEmpty(palette)) return string.Empty;
return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette));
}
public async Task PalettePlusRemovePalette(IntPtr character)
{
if (!CheckPalettePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
Logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X"));
_palettePlusRemoveCharaPalette!.InvokeAction(c);
}
}).ConfigureAwait(false);
}
public async Task PalettePlusSetPalette(IntPtr character, string palette)
{
if (!CheckPalettePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette));
if (string.IsNullOrEmpty(decodedPalette))
{
Logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X"));
_palettePlusRemoveCharaPalette!.InvokeAction(c);
}
else
{
Logger.LogTrace("PalettePlus applying for {addr}", c.Address.ToString("X"));
_palettePlusSetCharaPalette!.InvokeAction(c, decodedPalette);
}
}
}).ConfigureAwait(false);
}
public string PenumbraGetMetaManipulations()
{
if (!CheckPenumbraApi()) return string.Empty;
return _penumbraGetMetaManipulations.Invoke();
}
public string? PenumbraModDirectory;
public string? GetPenumbraModDirectory()
{
if (!CheckPenumbraApi()) return null;
return _penumbraResolveModDir!.Invoke().ToLowerInvariant();
}
public async Task PenumbraRedraw(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token, bool fireAndForget = false)
{
if (!CheckPenumbraApi() || _dalamudUtil.IsZoning) return;
var gameObj = _dalamudUtil.CreateGameObject(handler.Address);
if (gameObj is Character c)
{
await PenumbraRedrawAction(logger, handler, applicationId, () => _penumbraRedrawObject!.Invoke(c, RedrawType.Redraw), fireAndForget, token).ConfigureAwait(false);
await PenumbraRedrawAsync(logger, handler, applicationId, () => _penumbraRedrawObject!.Invoke(c, RedrawType.Redraw), fireAndForget, token).ConfigureAwait(false);
}
}
@@ -523,6 +453,11 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
return resolvedPath ?? path;
}
public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse)
{
return _penumbraResolvePaths.Invoke(forward, reverse);
}
public string[] PenumbraReverseResolvePlayer(string path)
{
if (!CheckPenumbraApi()) return new[] { path };
@@ -557,28 +492,81 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
logger.LogTrace("[{applicationId}] Setting temp mods for {collName}, Success: {ret2}", applicationId, collName, ret2);
}
public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse)
public void ToggleGposeQueueMode(bool on)
{
return _penumbraResolvePaths.Invoke(forward, reverse);
_inGposeQueueMode = on;
}
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
internal bool RequestedRedraw(nint address)
{
bool wasRequested = false;
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
if (_penumbraRedrawRequests.TryGetValue(address, out var requested))
{
_penumbraRedrawRequests[objectAddress] = false;
return requested;
}
else
return false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_disposalCts.Cancel();
int totalSleepTime = 0;
while (!ActionQueue.IsEmpty && totalSleepTime < 2000)
{
Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
Logger.LogTrace("Waiting for actionqueue to clear...");
PeriodicApiStateCheck();
if (CheckPenumbraApi())
{
HandleActionQueue();
}
Thread.Sleep(16);
totalSleepTime += 16;
}
if (totalSleepTime >= 2000)
{
Logger.LogTrace("Action queue clear or not, disposing");
}
ActionQueue.Clear();
_penumbraModSettingChanged.Dispose();
_penumbraGameObjectResourcePathResolved.Dispose();
_penumbraDispose.Dispose();
_penumbraInit.Dispose();
_penumbraObjectIsRedrawn.Dispose();
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
_palettePlusPaletteChanged.Unsubscribe(OnPalettePlusPaletteChange);
_customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
}
private void ClearActionQueue()
{
ActionQueue.Clear();
_gposeActionQueue.Clear();
}
private void HandleActionQueue()
{
if (ActionQueue.TryDequeue(out var action))
{
if (action == null) return;
Logger.LogDebug("Execution action in queue: {method}", action.Method);
action();
}
}
private void PenumbraInit()
private void HandleGposeActionQueue()
{
Mediator.Publish(new PenumbraInitializedMessage());
_penumbraRedraw!.Invoke("self", RedrawType.Redraw);
if (_gposeActionQueue.TryDequeue(out var action))
{
if (action == null) return;
Logger.LogDebug("Execution action in gpose queue: {method}", action.Method);
action();
}
}
private void HeelsOffsetChange(float offset)
@@ -596,52 +584,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
Mediator.Publish(new PalettePlusMessage());
}
public async Task PalettePlusSetPalette(IntPtr character, string palette)
{
if (!CheckPalettePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette));
if (string.IsNullOrEmpty(decodedPalette))
{
_logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X"));
_palettePlusRemoveCharaPalette!.InvokeAction(c);
}
else
{
_logger.LogTrace("PalettePlus applying for {addr}", c.Address.ToString("X"));
_palettePlusSetCharaPalette!.InvokeAction(c, decodedPalette);
}
}
}).ConfigureAwait(false);
}
public string PalettePlusBuildPalette()
{
if (!CheckPalettePlusApi()) return string.Empty;
var palette = _palettePlusBuildCharaPalette.InvokeFunc(_dalamudUtil.PlayerCharacter);
if (string.IsNullOrEmpty(palette)) return string.Empty;
return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette));
}
public async Task PalettePlusRemovePalette(IntPtr character)
{
if (!CheckPalettePlusApi()) return;
await _dalamudUtil.RunOnFrameworkThread(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj is Character c)
{
_logger.LogTrace("PalettePlus removing for {addr}", c.Address.ToString("X"));
_palettePlusRemoveCharaPalette!.InvokeAction(c);
}
}).ConfigureAwait(false);
}
private void PenumbraDispose()
{
_disposalCts.Cancel();
@@ -651,13 +593,66 @@ public class IpcManager : MediatorSubscriberBase, IDisposable
_disposalCts = new();
}
internal bool RequestedRedraw(nint address)
private void PenumbraInit()
{
if (_penumbraRedrawRequests.TryGetValue(address, out var requested))
{
return requested;
}
return false;
Mediator.Publish(new PenumbraInitializedMessage());
_penumbraRedraw!.Invoke("self", RedrawType.Redraw);
}
}
private async Task PenumbraRedrawAsync(ILogger logger, GameObjectHandler obj, Guid applicationId, Action action, bool fireAndForget, CancellationToken token)
{
Mediator.Publish(new PenumbraStartRedrawMessage(obj.Address));
_penumbraRedrawRequests[obj.Address] = !fireAndForget;
ActionQueue.Enqueue(action);
if (!fireAndForget)
{
var disposeToken = _disposalCts.Token;
var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(disposeToken, token).Token;
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
if (!combinedToken.IsCancellationRequested)
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, obj, applicationId, 30000, combinedToken).ConfigureAwait(false);
_penumbraRedrawRequests[obj.Address] = false;
}
Mediator.Publish(new PenumbraEndRedrawMessage(obj.Address));
}
private void PeriodicApiStateCheck()
{
_glamourerAvailable = CheckGlamourerApiInternal();
_penumbraAvailable = CheckPenumbraApiInternal();
_heelsAvailable = CheckHeelsApiInternal();
_customizePlusAvailable = CheckCustomizePlusApiInternal();
_palettePlusAvailable = CheckPalettePlusApiInternal();
PenumbraModDirectory = GetPenumbraModDirectory();
}
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
{
bool wasRequested = false;
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
{
_penumbraRedrawRequests[objectAddress] = false;
}
else
{
Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
}
}
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
{
Task.Run(() =>
{
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
{
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
}
});
}
}

View File

@@ -10,4 +10,4 @@ public static class ConfigurationExtensions
&& !string.IsNullOrEmpty(configuration.CacheFolder)
&& Directory.Exists(configuration.CacheFolder);
}
}
}

View File

@@ -2,12 +2,14 @@
using MareSynchronos.MareConfiguration.Configurations;
using MareSynchronos.MareConfiguration.Configurations.Obsolete;
using MareSynchronos.MareConfiguration.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace MareSynchronos.MareConfiguration;
#pragma warning disable CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones
public class ConfigurationMigrator
public class ConfigurationMigrator : IHostedService
{
private readonly ILogger<ConfigurationMigrator> _logger;
private readonly DalamudPluginInterface _pi;
@@ -20,7 +22,6 @@ public class ConfigurationMigrator
public void Migrate()
{
#pragma warning disable CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones
if (_pi.GetPluginConfig() is Configuration oldConfig)
{
_logger.LogInformation("Migrating Configuration from old config style to 1");
@@ -41,6 +42,24 @@ public class ConfigurationMigrator
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
Migrate();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private static void SaveConfig(IMareConfiguration config, string path)
{
File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented));
}
private string ConfigurationPath(string configName) => Path.Combine(_pi.ConfigDirectory.FullName, configName);
private void MigrateMareConfigV0ToV1(MareConfigV0 mareConfigV0)
{
_logger.LogInformation("Migrating Configuration from version 0 to 1");
@@ -82,13 +101,6 @@ public class ConfigurationMigrator
SaveConfig(tagConfig, ConfigurationPath(ServerTagConfigService.ConfigName));
SaveConfig(notesConfig, ConfigurationPath(NotesConfigService.ConfigName));
}
#pragma warning restore CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones
}
private string ConfigurationPath(string configName) => Path.Combine(_pi.ConfigDirectory.FullName, configName);
private void SaveConfig(IMareConfiguration config, string path)
{
File.WriteAllText(path, JsonConvert.SerializeObject(config, Formatting.Indented));
}
}
#pragma warning restore CS0618 // ignore Obsolete tag, the point of this migrator is to migrate obsolete configs to new ones

View File

@@ -1,26 +1,18 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MareSynchronos.MareConfiguration.Configurations;
using System.Text.Json;
namespace MareSynchronos.MareConfiguration;
public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareConfiguration
{
protected abstract string ConfigurationName { get; }
public string ConfigurationDirectory => _pluginInterface.ConfigDirectory.FullName;
public T Current => _currentConfigInternal.Value;
protected readonly DalamudPluginInterface _pluginInterface;
private readonly CancellationTokenSource _periodicCheckCts = new();
private DateTime _configLastWriteTime;
private bool _configIsDirty = false;
private DateTime _configLastWriteTime;
private Lazy<T> _currentConfigInternal;
protected string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName);
protected ConfigurationServiceBase(DalamudPluginInterface pluginInterface)
protected ConfigurationServiceBase(string configurationDirectory)
{
_pluginInterface = pluginInterface;
ConfigurationDirectory = configurationDirectory;
Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token);
Task.Run(CheckForDirtyConfigInternal, _periodicCheckCts.Token);
@@ -28,39 +20,26 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
_currentConfigInternal = LazyConfig();
}
private Lazy<T> LazyConfig()
{
_configLastWriteTime = GetConfigLastWriteTime();
return new Lazy<T>(() => LoadConfig());
}
private DateTime GetConfigLastWriteTime() => new FileInfo(ConfigurationPath).LastWriteTimeUtc;
public string ConfigurationDirectory { get; init; }
public T Current => _currentConfigInternal.Value;
protected abstract string ConfigurationName { get; }
protected string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName);
private async Task CheckForConfigUpdatesInternal()
public void Dispose()
{
while (!_periodicCheckCts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false);
var lastWriteTime = GetConfigLastWriteTime();
if (lastWriteTime != _configLastWriteTime)
{
//_logger.LogDebug($"Config {ConfigurationName} changed, reloading config");
_currentConfigInternal = LazyConfig();
}
}
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private async Task CheckForDirtyConfigInternal()
public void Save()
{
while (!_periodicCheckCts.IsCancellationRequested)
{
if (_configIsDirty)
{
SaveDirtyConfig();
}
_configIsDirty = true;
}
await Task.Delay(TimeSpan.FromSeconds(1), _periodicCheckCts.Token).ConfigureAwait(false);
}
protected virtual void Dispose(bool disposing)
{
_periodicCheckCts.Cancel();
_periodicCheckCts.Dispose();
}
protected T LoadConfig()
@@ -73,7 +52,7 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
}
else
{
config = JsonConvert.DeserializeObject<T>(File.ReadAllText(ConfigurationPath));
config = JsonSerializer.Deserialize<T>(File.ReadAllText(ConfigurationPath));
if (config == null)
{
config = (T)Activator.CreateInstance(typeof(T))!;
@@ -98,26 +77,54 @@ public abstract class ConfigurationServiceBase<T> : IDisposable where T : IMareC
}
}
//_logger.LogDebug("Saving dirty config " + ConfigurationName);
try
{
File.Copy(ConfigurationPath, ConfigurationPath + ".bak." + DateTime.Now.ToString("yyyyMMddHHmmss"), overwrite: true);
}
catch { }
catch
{
// ignore if file cannot be backupped once
}
File.WriteAllText(ConfigurationPath, JsonConvert.SerializeObject(Current, Formatting.Indented));
File.WriteAllText(ConfigurationPath, JsonSerializer.Serialize(Current, new JsonSerializerOptions()
{
WriteIndented = true
}));
_configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc;
}
public void Save()
private async Task CheckForConfigUpdatesInternal()
{
_configIsDirty = true;
while (!_periodicCheckCts.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false);
var lastWriteTime = GetConfigLastWriteTime();
if (lastWriteTime != _configLastWriteTime)
{
_currentConfigInternal = LazyConfig();
}
}
}
public void Dispose()
private async Task CheckForDirtyConfigInternal()
{
//_logger.LogTrace($"Disposing {GetType()}");
_periodicCheckCts.Cancel();
while (!_periodicCheckCts.IsCancellationRequested)
{
if (_configIsDirty)
{
SaveDirtyConfig();
}
await Task.Delay(TimeSpan.FromSeconds(1), _periodicCheckCts.Token).ConfigureAwait(false);
}
}
}
private DateTime GetConfigLastWriteTime() => new FileInfo(ConfigurationPath).LastWriteTimeUtc;
private Lazy<T> LazyConfig()
{
_configLastWriteTime = GetConfigLastWriteTime();
return new Lazy<T>(LoadConfig);
}
}

View File

@@ -6,25 +6,31 @@ namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable]
public class MareConfig : IMareConfiguration
{
public int Version { get; set; } = 1;
public bool AcceptedAgreement { get; set; } = false;
public string CacheFolder { get; set; } = string.Empty;
public double MaxLocalCacheInGiB { get; set; } = 20;
public bool ReverseUserSort { get; set; } = false;
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
public bool FileScanPaused { get; set; } = false;
public bool InitialScanComplete { get; set; } = false;
public bool DisableOptionalPluginWarnings { get; set; } = false;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
public bool FileScanPaused { get; set; } = false;
public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast;
public bool InitialScanComplete { get; set; } = false;
public LogLevel LogLevel { get; set; } = LogLevel.Information;
public bool LogPerformance { get; set; } = false;
public double MaxLocalCacheInGiB { get; set; } = 20;
public bool OpenGposeImportOnGposeStart { get; set; } = false;
public bool ShowTransferWindow { get; set; } = true;
public bool OpenPopupOnAdd { get; set; } = true;
public int ParallelDownloads { get; set; } = 10;
public bool ReverseUserSort { get; set; } = false;
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
public bool ShowOfflineUsersSeparately { get; set; } = true;
public bool ShowOnlineNotifications { get; set; } = false;
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast;
public bool ShowTransferBars { get; set; } = true;
public bool ShowTransferWindow { get; set; } = false;
public bool ShowUploading { get; set; } = true;
public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true;
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
public LogLevel LogLevel { get; set; } = LogLevel.Information;
public bool LogPerformance { get; set; } = false;
}
}

View File

@@ -1,6 +1,6 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.MareConfiguration.Models.Obsolete;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
@@ -46,12 +46,12 @@ public class Configuration : IPluginConfiguration
/// The dictionary first maps a server URL to a dictionary, and that
/// dictionary maps the OtherUID of the <see cref="ClientPairDto"/> to a list of tags.
/// </summary>
public Dictionary<string, Dictionary<string, List<string>>> UidServerPairedUserTags = new(StringComparer.Ordinal);
public Dictionary<string, Dictionary<string, List<string>>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
/// <summary>
/// A dictionary that maps a server URL to the tags the user has added for that server.
/// </summary>
public Dictionary<string, HashSet<string>> ServerAvailablePairTags = new(StringComparer.Ordinal);
public HashSet<string> OpenPairTags = new(StringComparer.Ordinal);
public Dictionary<string, HashSet<string>> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
public MareConfigV0 ToMareConfig(ILogger logger)
{
@@ -75,7 +75,7 @@ public class Configuration : IPluginConfiguration
// create all server storage based on current clientsecret
foreach (var secret in ClientSecret)
{
logger.LogDebug("Migrating " + secret.Key);
logger.LogDebug("Migrating {key}", secret.Key);
var apiuri = secret.Key;
var secretkey = secret.Value;
ServerStorageV0 toAdd = new();
@@ -121,9 +121,4 @@ public class Configuration : IPluginConfiguration
return newConfig;
}
public void Migrate()
{
}
}

View File

@@ -1,4 +1,5 @@
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.MareConfiguration.Models.Obsolete;
namespace MareSynchronos.MareConfiguration.Configurations.Obsolete;

View File

@@ -5,5 +5,5 @@ namespace MareSynchronos.MareConfiguration.Configurations;
public class UidNotesConfig : IMareConfiguration
{
public int Version { get; set; } = 0;
public Dictionary<string, ServerNotesStorage> ServerNotes = new(StringComparer.Ordinal);
public Dictionary<string, ServerNotesStorage> ServerNotes { get; set; } = new(StringComparer.Ordinal);
}

View File

@@ -1,13 +1,14 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class MareConfigService : ConfigurationServiceBase<MareConfig>
{
public const string ConfigName = "config.json";
protected override string ConfigurationName => ConfigName;
public MareConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { }
}
public MareConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
}

View File

@@ -1,4 +1,4 @@
namespace MareSynchronos.MareConfiguration.Models;
namespace MareSynchronos.MareConfiguration.Models.Obsolete;
[Serializable]
[Obsolete("Deprecated, use ServerStorage")]
@@ -9,7 +9,7 @@ public class ServerStorageV0
public List<Authentication> Authentications { get; set; } = new();
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
public Dictionary<string, List<string>> UidServerPairedUserTags = new(StringComparer.Ordinal);
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
public Dictionary<int, SecretKey> SecretKeys { get; set; } = new();
@@ -19,11 +19,11 @@ public class ServerStorageV0
{
return new ServerStorage()
{
ServerUri = this.ServerUri,
ServerName = this.ServerName,
Authentications = this.Authentications.ToList(),
FullPause = this.FullPause,
SecretKeys = this.SecretKeys.ToDictionary(p => p.Key, p => p.Value)
ServerUri = ServerUri,
ServerName = ServerName,
Authentications = Authentications.ToList(),
FullPause = FullPause,
SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value)
};
}
}

View File

@@ -3,7 +3,7 @@
[Serializable]
public class ServerTagStorage
{
public Dictionary<string, List<string>> UidServerPairedUserTags = new(StringComparer.Ordinal);
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
}

View File

@@ -1,12 +1,14 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class NotesConfigService : ConfigurationServiceBase<UidNotesConfig>
{
public const string ConfigName = "notes.json";
public NotesConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
public NotesConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { }
}

View File

@@ -1,12 +1,14 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class ServerConfigService : ConfigurationServiceBase<ServerConfig>
{
public const string ConfigName = "server.json";
public ServerConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
public ServerConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { }
}
}

View File

@@ -1,12 +1,14 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
{
public const string ConfigName = "servertags.json";
public ServerTagConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
public ServerTagConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { }
}

View File

@@ -1,12 +1,14 @@
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Configurations;
using Microsoft.Extensions.Logging;
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class TransientConfigService : ConfigurationServiceBase<TransientConfig>
{
public const string ConfigName = "transient.json";
public TransientConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
public TransientConfigService(DalamudPluginInterface pluginInterface) : base(pluginInterface) { }
}
}

View File

@@ -1,28 +1,27 @@
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Dalamud.Interface.ImGuiFileDialog;
using MareSynchronos.Managers;
using MareSynchronos.WebAPI;
using Dalamud.Interface.Windowing;
using MareSynchronos.UI;
using MareSynchronos.Utils;
using MareSynchronos.FileCache;
using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.PlayerData.Services;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;
namespace MareSynchronos;
#pragma warning disable S125 // Sections of code should not be commented out
/*
(..,,...,,,,,+/, ,,.....,,+
..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+.
...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./
,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,.
,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,..,
+..////////////+++++++... .../##(,,////////////////++,,,+/(((+,
+,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,,
/+.+//////////++++/++++++++++,,...,++///////////////////////////((((##,
(..,,...,,,,,+/, ,,.....,,+
..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+.
...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./
,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,.
,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,..,
+..////////////+++++++... .../##(,,////////////////++,,,+/(((+,
+,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,,
/+.+//////////++++/++++++++++,,...,++///////////////////////////((((##,
/,.////////+++++++++++++++++++++////////+++//////++/+++++//////////((((#(+,
/+.+////////+++++++++++++++++++++++++++++++++++++++++++++++++++++/////((((##+
+,.///////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///((((%/
@@ -30,202 +29,137 @@ namespace MareSynchronos;
+,./////////////////+++++++++++++++++++++++++++++++++++++++++++++++,,+++++///((,
...////////++/++++++++++++++++++++++++,,++++++++++++++++++++++++++++++++++++//(,,
..//+,+///++++++++++++++++++,,,,+++,,,,,,,,,,,,++++++++,,+++++++++++++++++++//,,+
..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++...
..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,.
..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+
....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,,
.....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/
.....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+
...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,,
....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,..
...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++......
...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,..
...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,..
..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,..
...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,...
..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,...
..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,...
..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++...
..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,.
..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+
....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,,
.....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/
.....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+
...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,,
....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,..
...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++......
...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,..
...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,..
..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,..
...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,...
..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,...
..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,...
...+//(((((//////////++++++..+++++++++++++++,......,,,,++++++,,,..+++////////////////////////+,.... ...,,,,,,,...
..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,..
...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,...
..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,...
.,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........
.+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++..
.,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++..
.++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++...
.++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ .
.++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,..
.++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++..
.+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,..
..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,...
..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,...
...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,..
...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,..
...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,...
,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,..
..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,..
...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,...
..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,...
.,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........
.+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++..
.,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++..
.++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++...
.++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ .
.++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,..
.++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++..
.+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,..
..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,...
..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,...
...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,..
...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,..
...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,...
,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,..
*/
#pragma warning restore S125 // Sections of code should not be commented out
public class MarePlugin : MediatorSubscriberBase, IDisposable
public class MarePlugin : MediatorSubscriberBase, IHostedService
{
private readonly ServiceProvider _serviceProvider;
private const string _commandName = "/mare";
private readonly DalamudUtilService _dalamudUtil;
private readonly MareConfigService _mareConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly IServiceScopeFactory _serviceScopeFactory;
private IServiceScope? _runtimeServiceScope;
public MarePlugin(ILogger<MarePlugin> logger, ServiceProvider serviceProvider, MareMediator mediator) : base(logger, mediator)
public MarePlugin(ILogger<MarePlugin> logger, MareConfigService mareConfigService,
ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtil,
IServiceScopeFactory serviceScopeFactory, MareMediator mediator) : base(logger, mediator)
{
_serviceProvider = serviceProvider;
_serviceProvider.GetRequiredService<ConfigurationMigrator>().Migrate();
mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => Task.Run(WaitForPlayerAndLaunchCharacterManager));
mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
serviceProvider.GetRequiredService<SettingsUi>();
serviceProvider.GetRequiredService<CompactUi>();
serviceProvider.GetRequiredService<GposeUi>();
serviceProvider.GetRequiredService<IntroUi>();
serviceProvider.GetRequiredService<DownloadUi>();
serviceProvider.GetRequiredService<NotificationService>();
_mareConfigService = mareConfigService;
_serverConfigurationManager = serverConfigurationManager;
_dalamudUtil = dalamudUtil;
_serviceScopeFactory = serviceScopeFactory;
}
public override void Dispose()
public Task StartAsync(CancellationToken cancellationToken)
{
base.Dispose();
var version = Assembly.GetExecutingAssembly().GetName().Version!;
Logger.LogInformation("Launching {name} {major}.{minor}.{build}", "Mare Synchronos", version.Major, version.Minor, version.Build);
_serviceProvider.GetRequiredService<CommandManager>().RemoveHandler(_commandName);
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => Task.Run(WaitForPlayerAndLaunchCharacterManager));
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
_runtimeServiceScope?.Dispose();
_serviceProvider.Dispose();
return Task.CompletedTask;
}
_logger.LogDebug("Shut down");
public Task StopAsync(CancellationToken cancellationToken)
{
UnsubscribeAll();
DalamudUtilOnLogOut();
Logger.LogDebug("Halting MarePlugin");
return Task.CompletedTask;
}
private void DalamudUtilOnLogIn()
{
_logger?.LogDebug("Client login");
Logger?.LogDebug("Client login");
var pi = _serviceProvider.GetRequiredService<DalamudPluginInterface>();
pi.UiBuilder.Draw += Draw;
pi.UiBuilder.OpenConfigUi += OpenUi;
_serviceProvider.GetRequiredService<CommandManager>().AddHandler(_commandName, new CommandInfo(OnCommand)
{
HelpMessage = "Opens the Mare Synchronos UI",
});
if (!_serviceProvider.GetRequiredService<MareConfigService>().Current.HasValidSetup()
|| !_serviceProvider.GetRequiredService<ServerConfigurationManager>().HasValidConfig())
{
_serviceProvider.GetRequiredService<MareMediator>().Publish(new SwitchToIntroUiMessage());
return;
}
_serviceProvider.GetRequiredService<PeriodicFileScanner>().StartScan();
Task.Run(WaitForPlayerAndLaunchCharacterManager);
}
private void DalamudUtilOnLogOut()
{
_logger?.LogDebug("Client logout");
Logger?.LogDebug("Client logout");
_runtimeServiceScope?.Dispose();
var pi = _serviceProvider.GetRequiredService<DalamudPluginInterface>();
pi.UiBuilder.Draw -= Draw;
pi.UiBuilder.OpenConfigUi -= OpenUi;
_serviceProvider.GetRequiredService<CommandManager>().RemoveHandler(_commandName);
}
private async Task WaitForPlayerAndLaunchCharacterManager()
{
var dalamudUtil = _serviceProvider.GetRequiredService<DalamudUtil>();
while (!dalamudUtil.IsPlayerPresent)
while (!_dalamudUtil.IsPlayerPresent)
{
await Task.Delay(100).ConfigureAwait(false);
}
try
{
_logger?.LogDebug("Launching Managers");
Logger?.LogDebug("Launching Managers");
_runtimeServiceScope?.Dispose();
_runtimeServiceScope = _serviceProvider.CreateScope();
_runtimeServiceScope = _serviceScopeFactory.CreateScope();
_runtimeServiceScope.ServiceProvider.GetRequiredService<UiService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<CommandManagerService>();
if (!_mareConfigService.Current.HasValidSetup() || !_serverConfigurationManager.HasValidConfig())
{
Mediator.Publish(new SwitchToIntroUiMessage());
return;
}
_runtimeServiceScope.ServiceProvider.GetRequiredService<PeriodicFileScanner>().StartScan();
_runtimeServiceScope.ServiceProvider.GetRequiredService<CacheCreationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
#if !DEBUG
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
{
Mediator.Publish(new NotificationMessage("Abnormal Log Level",
$"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Mare Settings -> Debug\" unless instructed otherwise.",
Dalamud.Interface.Internal.Notifications.NotificationType.Error, 15000));
}
#endif
}
catch (Exception ex)
{
_logger?.LogCritical(ex, "Error during launch of managers");
Logger?.LogCritical(ex, "Error during launch of managers");
}
}
private void Draw()
{
_serviceProvider.GetRequiredService<WindowSystem>().Draw();
_serviceProvider.GetRequiredService<FileDialogManager>().Draw();
}
private void OnCommand(string command, string args)
{
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (splitArgs == null || splitArgs.Length == 0)
{
// Interpret this as toggling the UI
OpenUi();
return;
}
if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase))
{
var serverConfigurationManager = _serviceProvider.GetRequiredService<ServerConfigurationManager>();
if (serverConfigurationManager.CurrentServer == null) return;
var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch
{
"on" => false,
"off" => true,
_ => !serverConfigurationManager.CurrentServer.FullPause,
} : !serverConfigurationManager.CurrentServer.FullPause;
if (fullPause != serverConfigurationManager.CurrentServer.FullPause)
{
serverConfigurationManager.CurrentServer.FullPause = fullPause;
serverConfigurationManager.Save();
_ = _serviceProvider.GetRequiredService<ApiController>().CreateConnections();
}
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))
{
_serviceProvider.GetRequiredService<GposeUi>().Toggle();
}
else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{
_serviceProvider.GetRequiredService<PeriodicFileScanner>().InvokeScan(forced: true);
}
else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase))
{
if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds))
{
_serviceProvider.GetRequiredService<PerformanceCollector>().PrintPerformanceStats(limitBySeconds);
}
else
{
_serviceProvider.GetRequiredService<PerformanceCollector>().PrintPerformanceStats();
}
}
else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase))
{
_serviceProvider.GetRequiredService<MareMediator>().PrintSubscriberInfo();
}
}
private void OpenUi()
{
if (_serviceProvider.GetRequiredService<MareConfigService>().Current.HasValidSetup())
_serviceProvider.GetRequiredService<CompactUi>().Toggle();
else
_serviceProvider.GetRequiredService<IntroUi>().Toggle();
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>0.7.42</Version>
<Version>0.8.0</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>
@@ -26,7 +26,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.10" />
<PackageReference Include="DalamudPackager" Version="2.1.11" />
<PackageReference Include="lz4net" Version="1.0.15.93" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.19">
<PrivateAssets>all</PrivateAssets>
@@ -34,8 +34,13 @@
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Penumbra.Api" Version="1.0.7" />
<PackageReference Include="Penumbra.String" Version="1.0.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.54.0.64047">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>

View File

@@ -10,4 +10,4 @@
],
"IconUrl": "https://raw.githubusercontent.com/Penumbra-Sync/client/main/MareSynchronos/images/logo.png",
"RepoUrl": "https://github.com/Penumbra-Sync/client"
}
}

View File

@@ -1,6 +0,0 @@
namespace MareSynchronos.Mediator;
public interface IMediatorSubscriber : IDisposable
{
MareMediator Mediator { get; }
}

View File

@@ -1,3 +0,0 @@
namespace MareSynchronos.Mediator;
public interface IMessage { }

View File

@@ -1,22 +0,0 @@
using Dalamud.Interface.Windowing;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Mediator;
public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
{
protected readonly ILogger _logger;
public MareMediator Mediator { get; }
protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name) : base(name)
{
_logger = logger;
Mediator = mediator;
}
public virtual void Dispose()
{
_logger.LogTrace($"Disposing {GetType()}");
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -2,21 +2,19 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data;
namespace MareSynchronos.Models;
namespace MareSynchronos.PlayerData.Data;
public class CharacterData
{
public string CustomizePlusScale { get; set; } = string.Empty;
public Dictionary<ObjectKind, HashSet<FileReplacement>> FileReplacements { get; set; } = new();
public Dictionary<ObjectKind, string> GlamourerString { get; set; } = new();
public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed);
public string ManipulationString { get; set; } = string.Empty;
public float HeelsOffset { get; set; } = 0f;
public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed);
public string CustomizePlusScale { get; set; } = string.Empty;
public string ManipulationString { get; set; } = string.Empty;
public string PalettePlusPalette { get; set; } = string.Empty;
public API.Data.CharacterData ToAPI()
@@ -52,8 +50,8 @@ public class CharacterData
StringBuilder stringBuilder = new();
foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths.First(), StringComparer.Ordinal))
{
stringBuilder.AppendLine(fileReplacement.ToString());
stringBuilder.Append(fileReplacement).AppendLine();
}
return stringBuilder.ToString();
}
}
}

View File

@@ -2,30 +2,28 @@
using MareSynchronos.FileCache;
using MareSynchronos.API.Data;
namespace MareSynchronos.Models;
namespace MareSynchronos.PlayerData.Data;
public class FileReplacement
public partial class FileReplacement
{
private readonly Lazy<string> _hashLazy;
public FileReplacement(List<string> gamePaths, string filePath, FileCacheManager fileDbManager)
{
GamePaths = gamePaths.Select(g => g.Replace('\\', '/')).ToHashSet(StringComparer.Ordinal);
ResolvedPath = filePath.Replace('\\', '/');
HashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty);
_hashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty);
}
public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash);
public HashSet<string> GamePaths { get; init; } = new();
public HashSet<string> GamePaths { get; init; }
public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal));
public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths.First(), ResolvedPath, StringComparison.Ordinal);
public string Hash => HashLazy.Value;
private Lazy<string> HashLazy;
public string ResolvedPath { get; init; } = string.Empty;
public string Hash => _hashLazy.Value;
public bool IsFileSwap => !LocalPathRegex().IsMatch(ResolvedPath) && !string.Equals(GamePaths.First(), ResolvedPath, StringComparison.Ordinal);
public string ResolvedPath { get; init; }
public FileReplacementData ToFileReplacementDto()
{
@@ -41,4 +39,7 @@ public class FileReplacement
{
return $"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}";
}
}
[GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)]
private static partial Regex LocalPathRegex();
}

View File

@@ -1,12 +1,14 @@
using MareSynchronos.Models;
namespace MareSynchronos.Utils;
namespace MareSynchronos.PlayerData.Data;
public class FileReplacementComparer : IEqualityComparer<FileReplacement>
{
private static readonly FileReplacementComparer _instance = new();
private FileReplacementComparer()
{ }
public static FileReplacementComparer Instance => _instance;
private static FileReplacementComparer _instance = new();
private FileReplacementComparer() { }
public bool Equals(FileReplacement? x, FileReplacement? y)
{
if (x == null || y == null) return false;
@@ -18,18 +20,7 @@ public class FileReplacementComparer : IEqualityComparer<FileReplacement>
return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths));
}
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
private bool CompareLists(HashSet<string> list1, HashSet<string> list2)
private static bool CompareLists(HashSet<string> list1, HashSet<string> list2)
{
if (list1.Count != list2.Count)
return false;
@@ -42,4 +33,15 @@ public class FileReplacementComparer : IEqualityComparer<FileReplacement>
return true;
}
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
}

View File

@@ -1,16 +1,20 @@
using MareSynchronos.API.Data;
namespace MareSynchronos.Utils;
namespace MareSynchronos.PlayerData.Data;
public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData>
{
private static readonly FileReplacementDataComparer _instance = new();
private FileReplacementDataComparer()
{ }
public static FileReplacementDataComparer Instance => _instance;
private static FileReplacementDataComparer _instance = new();
private FileReplacementDataComparer() { }
public bool Equals(FileReplacementData? x, FileReplacementData? y)
{
if (x == null || y == null) return false;
return x.Hash.Equals(y.Hash) && CompareLists(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
}
public int GetHashCode(FileReplacementData obj)
@@ -18,18 +22,7 @@ public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
}
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
private bool CompareLists(HashSet<string> list1, HashSet<string> list2)
private static bool CompareHashSets(HashSet<string> list1, HashSet<string> list2)
{
if (list1.Count != list2.Count)
return false;
@@ -42,4 +35,15 @@ public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData
return true;
}
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
}

View File

@@ -1,10 +1,10 @@
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache;
using Newtonsoft.Json;
using System.Text;
using System.Text.Json;
namespace MareSynchronos.Export;
namespace MareSynchronos.PlayerData.Export;
public record MareCharaFileData
{
@@ -52,15 +52,15 @@ public record MareCharaFileData
public byte[] ToByteArray()
{
return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this));
return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(this));
}
public static MareCharaFileData FromByteArray(byte[] data)
{
return JsonConvert.DeserializeObject<MareCharaFileData>(Encoding.UTF8.GetString(data))!;
return JsonSerializer.Deserialize<MareCharaFileData>(Encoding.UTF8.GetString(data))!;
}
public record FileSwap(IEnumerable<string> GamePaths, string FileSwapPath);
public record FileData(IEnumerable<string> GamePaths, long Length);
}
}

View File

@@ -1,9 +1,9 @@
using MareSynchronos.API.Data;
using MareSynchronos.FileCache;
namespace MareSynchronos.Export;
namespace MareSynchronos.PlayerData.Export;
internal class MareCharaFileDataFactory
internal sealed class MareCharaFileDataFactory
{
private readonly FileCacheManager _fileCacheManager;
@@ -16,4 +16,4 @@ internal class MareCharaFileDataFactory
{
return new MareCharaFileData(_fileCacheManager, description, characterCacheDto);
}
}
}

View File

@@ -1,4 +1,4 @@
namespace MareSynchronos.Export;
namespace MareSynchronos.PlayerData.Export;
public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
{
@@ -23,7 +23,7 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
public static MareCharaFileHeader? FromBinaryReader(string path, BinaryReader reader)
{
var chars = new string(reader.ReadChars(4));
if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new System.Exception("Not a Mare Chara File");
if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new InvalidDataException("Not a Mare Chara File");
MareCharaFileHeader? decoded = null;
@@ -41,7 +41,7 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
return decoded;
}
public void AdvanceReaderToData(BinaryReader reader)
public static void AdvanceReaderToData(BinaryReader reader)
{
reader.ReadChars(4);
var version = reader.ReadByte();
@@ -51,4 +51,4 @@ public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData)
_ = reader.ReadBytes(length);
}
}
}
}

View File

@@ -1,37 +1,32 @@
using Dalamud.Game.ClientState.Objects.Types;
using LZ4;
using MareSynchronos.FileCache;
using MareSynchronos.Managers;
using MareSynchronos.Utils;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using CharacterData = MareSynchronos.API.Data.CharacterData;
using Microsoft.Extensions.Logging;
using MareSynchronos.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Interop;
using MareSynchronos.Services;
namespace MareSynchronos.PlayerData.Export;
namespace MareSynchronos.Export;
public class MareCharaFileManager
{
private readonly ILogger<MareCharaFileManager> _logger;
private readonly MareMediator _mediator;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly FileCacheManager _manager;
private readonly IpcManager _ipcManager;
private readonly MareConfigService _configService;
private readonly DalamudUtil _dalamudUtil;
private readonly DalamudUtilService _dalamudUtil;
private readonly MareCharaFileDataFactory _factory;
public MareCharaFileHeader? LoadedCharaFile { get; private set; }
public bool CurrentlyWorking { get; private set; } = false;
private static int GlobalFileCounter = 0;
private readonly Func<ObjectKind, Func<IntPtr>, bool, GameObjectHandler> _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager;
private readonly ILogger<MareCharaFileManager> _logger;
private readonly FileCacheManager _manager;
private int _globalFileCounter = 0;
public MareCharaFileManager(ILogger<MareCharaFileManager> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
FileCacheManager manager, IpcManager ipcManager, MareConfigService configService, DalamudUtil dalamudUtil)
public MareCharaFileManager(ILogger<MareCharaFileManager> logger, Func<ObjectKind, Func<IntPtr>, bool, GameObjectHandler> gameObjectHandlerFactory,
FileCacheManager manager, IpcManager ipcManager, MareConfigService configService, DalamudUtilService dalamudUtil)
{
_factory = new(manager);
_logger = logger;
_mediator = mediator;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_manager = manager;
_ipcManager = ipcManager;
@@ -39,6 +34,58 @@ public class MareCharaFileManager
_dalamudUtil = dalamudUtil;
}
public bool CurrentlyWorking { get; private set; } = false;
public MareCharaFileHeader? LoadedCharaFile { get; private set; }
public async Task ApplyMareCharaFile(GameObject? charaTarget)
{
Dictionary<string, string> extractedFiles = new(StringComparer.Ordinal);
CurrentlyWorking = true;
try
{
if (LoadedCharaFile == null || charaTarget == null || !File.Exists(LoadedCharaFile.FilePath)) return;
var unwrapped = File.OpenRead(LoadedCharaFile.FilePath);
await using (unwrapped.ConfigureAwait(false))
{
CancellationTokenSource disposeCts = new();
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
MareCharaFileHeader.AdvanceReaderToData(reader);
_logger.LogDebug("Applying to {chara}", charaTarget.Name.TextValue);
extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader);
Dictionary<string, string> fileSwaps = new(StringComparer.Ordinal);
foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps)
{
foreach (var path in fileSwap.GamePaths)
{
fileSwaps.Add(path, fileSwap.FileSwapPath);
}
}
var applicationId = Guid.NewGuid();
_ipcManager.ToggleGposeQueueMode(on: true);
_ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue);
_ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, charaTarget.Name.TextValue,
extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal),
LoadedCharaFile.CharaFileData.ManipulationData);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => charaTarget.Address, false);
await _ipcManager.GlamourerApplyAll(_logger, tempHandler, LoadedCharaFile.CharaFileData.GlamourerData, applicationId, disposeCts.Token).ConfigureAwait(false);
_dalamudUtil.WaitWhileGposeCharacterIsDrawing(charaTarget.Address, 30000);
_ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue);
_ipcManager.ToggleGposeQueueMode(on: false);
}
}
finally
{
CurrentlyWorking = false;
_logger.LogDebug("Clearing local files");
foreach (var file in extractedFiles)
{
File.Delete(file.Value);
}
}
}
public void ClearMareCharaFile()
{
LoadedCharaFile = null;
@@ -69,7 +116,7 @@ public class MareCharaFileManager
wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray());
}*/
_logger.LogInformation("Read Mare Chara File");
_logger.LogInformation("Version: " + (LoadedCharaFile?.Version ?? -1));
_logger.LogInformation("Version: {ver}", (LoadedCharaFile?.Version ?? -1));
long expectedLength = 0;
if (LoadedCharaFile != null)
{
@@ -78,7 +125,7 @@ public class MareCharaFileManager
{
foreach (var gamePath in item.GamePaths)
{
_logger.LogTrace("Swap: " + gamePath + " => " + item.FileSwapPath);
_logger.LogTrace("Swap: {gamePath} => {fileSwapPath}", gamePath, item.FileSwapPath);
}
}
@@ -89,99 +136,16 @@ public class MareCharaFileManager
expectedLength += item.Length;
foreach (var gamePath in item.GamePaths)
{
_logger.LogTrace($"File {itemNr}: " + gamePath + " = " + item.Length);
_logger.LogTrace("File {itemNr}: {gamePath} = {len}", itemNr, gamePath, item.Length);
}
}
_logger.LogInformation("Expected length: " + expectedLength);
_logger.LogInformation("Expected length: {expected}", expectedLength);
}
}
catch { throw; }
finally { CurrentlyWorking = false; }
}
public async Task ApplyMareCharaFile(GameObject? charaTarget)
{
Dictionary<string, string> extractedFiles = new(StringComparer.Ordinal);
CurrentlyWorking = true;
try
{
if (LoadedCharaFile == null || charaTarget == null || !File.Exists(LoadedCharaFile.FilePath)) return;
var unwrapped = File.OpenRead(LoadedCharaFile.FilePath);
await using (unwrapped.ConfigureAwait(false))
{
CancellationTokenSource disposeCts = new();
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
using var reader = new BinaryReader(lz4Stream);
LoadedCharaFile.AdvanceReaderToData(reader);
_logger.LogDebug("Applying to " + charaTarget.Name.TextValue);
extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader);
Dictionary<string, string> fileSwaps = new(StringComparer.Ordinal);
foreach (var fileSwap in LoadedCharaFile.CharaFileData.FileSwaps)
{
foreach (var path in fileSwap.GamePaths)
{
fileSwaps.Add(path, fileSwap.FileSwapPath);
}
}
var applicationId = Guid.NewGuid();
_ipcManager.ToggleGposeQueueMode(on: true);
_ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue);
_ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, charaTarget.Name.TextValue,
extractedFiles.Union(fileSwaps).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal),
LoadedCharaFile.CharaFileData.ManipulationData);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => charaTarget.Address, isWatched: false);
await _ipcManager.GlamourerApplyAll(_logger, tempHandler, LoadedCharaFile.CharaFileData.GlamourerData, applicationId, disposeCts.Token).ConfigureAwait(false);
_dalamudUtil.WaitWhileGposeCharacterIsDrawing(charaTarget.Address, 30000);
_ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, charaTarget.Name.TextValue);
_ipcManager.ToggleGposeQueueMode(on: false);
}
}
catch { throw; }
finally
{
CurrentlyWorking = false;
_logger.LogDebug("Clearing local files");
foreach (var file in extractedFiles)
{
File.Delete(file.Value);
}
}
}
private Dictionary<string, string> ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader)
{
Dictionary<string, string> gamePathToFilePath = new(StringComparer.Ordinal);
foreach (var fileData in charaFileHeader.CharaFileData.Files)
{
var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + (GlobalFileCounter++) + ".tmp");
var length = fileData.Length;
var bufferSize = 4 * 1024 * 1024;
var buffer = new byte[bufferSize];
using var fs = File.OpenWrite(fileName);
using var wr = new BinaryWriter(fs);
int chunk = 0;
while (length > 0)
{
if (length < bufferSize) bufferSize = (int)length;
_logger.LogTrace($"Reading chunk {chunk++} {bufferSize}/{length} of {fileName}");
buffer = reader.ReadBytes(bufferSize);
wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray());
length -= bufferSize;
}
wr.Flush();
foreach (var path in fileData.GamePaths)
{
gamePathToFilePath[path] = fileName;
_logger.LogTrace(path + " => " + fileName);
}
}
return gamePathToFilePath;
}
public void SaveMareCharaFile(CharacterData? dto, string description, string filePath)
{
CurrentlyWorking = true;
@@ -204,7 +168,6 @@ public class MareCharaFileManager
{
var itemFromData = playerReplacements.First(f => f.GamePaths.Any(p => item.GamePaths.Contains(p, StringComparer.OrdinalIgnoreCase)));
var file = _manager.GetFileCacheByHash(itemFromData.Hash)!;
var length = new FileInfo(file!.ResolvedFilepath).Length;
using var fsRead = File.OpenRead(file.ResolvedFilepath);
using var br = new BinaryReader(fsRead);
int readBytes = 0;
@@ -214,7 +177,36 @@ public class MareCharaFileManager
}
}
}
catch { throw; }
finally { CurrentlyWorking = false; }
}
}
private Dictionary<string, string> ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader)
{
Dictionary<string, string> gamePathToFilePath = new(StringComparer.Ordinal);
foreach (var fileData in charaFileHeader.CharaFileData.Files)
{
var fileName = Path.Combine(_configService.Current.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp");
var length = fileData.Length;
var bufferSize = 4 * 1024 * 1024;
using var fs = File.OpenWrite(fileName);
using var wr = new BinaryWriter(fs);
int chunk = 0;
while (length > 0)
{
if (length < bufferSize) bufferSize = (int)length;
_logger.LogTrace("Reading chunk {chunk} {bufferSize}/{length} of {fileName}", chunk++, bufferSize, length, fileName);
var buffer = reader.ReadBytes(bufferSize);
wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray());
length -= bufferSize;
}
wr.Flush();
foreach (var path in fileData.GamePaths)
{
gamePathToFilePath[path] = fileName;
_logger.LogTrace("{path} => {fileName}", path, fileName);
}
}
return gamePathToFilePath;
}
}

View File

@@ -4,43 +4,40 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Interop;
using MareSynchronos.Managers;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
using Penumbra.String;
using Weapon = MareSynchronos.Interop.Weapon;
using Weapon = MareSynchronos.Interop.FFXIV.Weapon;
using MareSynchronos.FileCache;
using MareSynchronos.Mediator;
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using System.Globalization;
using MareSynchronos.PlayerData.Data;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Interop.FFXIV;
using MareSynchronos.Services;
namespace MareSynchronos.Factories;
namespace MareSynchronos.PlayerData.Factories;
public class CharacterDataFactory : MediatorSubscriberBase
public class PlayerDataFactory
{
private readonly DalamudUtil _dalamudUtil;
private readonly IpcManager _ipcManager;
private readonly TransientResourceManager _transientResourceManager;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileCacheManager _fileCacheManager;
private readonly PerformanceCollector _performanceCollector;
private readonly IpcManager _ipcManager;
private readonly ILogger<PlayerDataFactory> _logger;
private readonly PerformanceCollectorService _performanceCollector;
private readonly TransientResourceManager _transientResourceManager;
public CharacterDataFactory(ILogger<CharacterDataFactory> logger, DalamudUtil dalamudUtil, IpcManager ipcManager,
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, MareMediator mediator,
PerformanceCollector performanceCollector) : base(logger, mediator)
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector)
{
_logger.LogTrace("Creating " + nameof(CharacterDataFactory));
_logger = logger;
_dalamudUtil = dalamudUtil;
_ipcManager = ipcManager;
_transientResourceManager = transientResourceManager;
_fileCacheManager = fileReplacementFactory;
_performanceCollector = performanceCollector;
}
private unsafe bool CheckForNullDrawObject(IntPtr playerPointer)
{
return ((Character*)playerPointer)->GameObject.DrawObject == null;
_logger.LogTrace("Creating " + nameof(PlayerDataFactory));
}
public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
@@ -102,131 +99,14 @@ public class CharacterDataFactory : MediatorSubscriberBase
previousData.FileReplacements = previousFileReplacements;
previousData.GlamourerString = previousGlamourerData;
return;
}
private async Task<CharacterData> CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
private static unsafe bool CheckForNullDrawObject(IntPtr playerPointer)
{
var objectKind = playerRelatedObject.ObjectKind;
var charaPointer = playerRelatedObject.Address;
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
if (!previousData.FileReplacements.ContainsKey(objectKind))
{
previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
}
else
{
previousData.FileReplacements[objectKind].Clear();
}
// wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false);
int totalWaitTime = 10000;
while (!DalamudUtil.IsObjectPresent(_dalamudUtil.CreateGameObject(charaPointer)) && totalWaitTime > 0)
{
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50).ConfigureAwait(false);
totalWaitTime -= 50;
}
Stopwatch st = Stopwatch.StartNew();
// gather up data from ipc
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
Task<string> getGlamourerData = Task.Run(() => _ipcManager.GlamourerGetCharacterCustomization(playerRelatedObject.Address));
Task<string> getCustomizeData = Task.Run(() => _ipcManager.GetCustomizePlusScale());
Task<string> getPalettePlusData = Task.Run(() => _ipcManager.PalettePlusBuildPalette());
previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]);
previousData.CustomizePlusScale = await getCustomizeData.ConfigureAwait(false);
_logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale);
previousData.PalettePlusPalette = await getPalettePlusData.ConfigureAwait(false);
_logger.LogDebug("Palette is now: {data}", previousData.PalettePlusPalette);
// gather static replacements from render model
var (forwardResolve, reverseResolve) = BuildDataFromModel(objectKind, charaPointer, token);
Dictionary<string, List<string>> resolvedPaths = GetFileReplacementsFromPaths(forwardResolve, reverseResolve);
previousData.FileReplacements[objectKind] =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
_logger.LogDebug("== Static Replacements ==");
foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("=> {repl}", replacement);
}
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
// or we get into redraw city for every change and nothing works properly
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
_transientResourceManager.AddSemiTransientResource(objectKind, item);
}
}
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
_transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind, charaPointer);
var resolvedTransientPaths = GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal));
_logger.LogDebug("== Transient Replacements ==");
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{
_logger.LogDebug("=> {repl}", replacement);
previousData.FileReplacements[objectKind].Add(replacement);
}
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList());
// make sure we only return data that actually has file replacements
foreach (var item in previousData.FileReplacements)
{
previousData.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
}
st.Stop();
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(st.ElapsedTicks).TotalMilliseconds);
return previousData;
return ((Character*)playerPointer)->GameObject.DrawObject == null;
}
private unsafe (HashSet<string> forwardResolve, HashSet<string> reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token)
{
HashSet<string> forwardResolve = new(StringComparer.Ordinal);
HashSet<string> reverseResolve = new(StringComparer.Ordinal);
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
token.ThrowIfCancellationRequested();
AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve);
}
if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(objectKind, charaPointer, human, forwardResolve, reverseResolve);
}
return (forwardResolve, reverseResolve);
}
private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
private unsafe void AddPlayerSpecificReplacements(Human* human, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
var weaponObject = (Weapon*)((Object*)human)->ChildObject;
@@ -275,44 +155,12 @@ public class CharacterDataFactory : MediatorSubscriberBase
}
}
private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
return;
}
string mdlPath;
try
{
mdlPath = new ByteString(mdl->ResourceHandle->FileName()).ToString();
}
catch
{
_logger.LogWarning("Could not get model data");
return;
}
_logger.LogTrace("Checking File Replacement for Model {path}", mdlPath);
reverseResolve.Add(mdlPath);
for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
{
var mtrl = (Material*)mdl->Materials[mtrlIdx];
if (mtrl == null) continue;
AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve);
}
}
private unsafe void AddReplacementsFromMaterial(Material* mtrl, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
string fileName;
try
{
fileName = new ByteString(mtrl->ResourceHandle->FileName()).ToString();
}
catch
{
@@ -357,6 +205,36 @@ public class CharacterDataFactory : MediatorSubscriberBase
}
}
private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
return;
}
string mdlPath;
try
{
mdlPath = new ByteString(mdl->ResourceHandle->FileName()).ToString();
}
catch
{
_logger.LogWarning("Could not get model data");
return;
}
_logger.LogTrace("Checking File Replacement for Model {path}", mdlPath);
reverseResolve.Add(mdlPath);
for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
{
var mtrl = (Material*)mdl->Materials[mtrlIdx];
if (mtrl == null) continue;
AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve);
}
}
private void AddReplacementsFromTexture(string texPath, HashSet<string> forwardResolve, HashSet<string> reverseResolve, bool doNotReverseResolve = true)
{
if (string.IsNullOrEmpty(texPath)) return;
@@ -382,20 +260,131 @@ public class CharacterDataFactory : MediatorSubscriberBase
string raceSexIdString = raceSexId.ToString("0000", CultureInfo.InvariantCulture);
string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb";
_logger.LogTrace("Checking skeleton {path}", skeletonPath);
forwardResolve.Add(skeletonPath);
}
private HashSet<string> ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer)
private unsafe (HashSet<string> forwardResolve, HashSet<string> reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token)
{
_transientResourceManager.PersistTransientResources(charaPointer, objectKind);
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
HashSet<string> forwardResolve = new(StringComparer.Ordinal);
HashSet<string> reverseResolve = new(StringComparer.Ordinal);
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{
pathsToResolve.Add(path);
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
token.ThrowIfCancellationRequested();
AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve);
}
return pathsToResolve;
if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(human, forwardResolve, reverseResolve);
}
return (forwardResolve, reverseResolve);
}
private async Task<CharacterData> CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
{
var objectKind = playerRelatedObject.ObjectKind;
var charaPointer = playerRelatedObject.Address;
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
if (!previousData.FileReplacements.ContainsKey(objectKind))
{
previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
}
else
{
previousData.FileReplacements[objectKind].Clear();
}
// wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false);
int totalWaitTime = 10000;
while (!DalamudUtilService.IsObjectPresent(_dalamudUtil.CreateGameObject(charaPointer)) && totalWaitTime > 0)
{
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, token).ConfigureAwait(false);
totalWaitTime -= 50;
}
Stopwatch st = Stopwatch.StartNew();
// gather up data from ipc
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
Task<string> getGlamourerData = Task.Run(() => _ipcManager.GlamourerGetCharacterCustomization(playerRelatedObject.Address));
Task<string> getCustomizeData = Task.Run(_ipcManager.GetCustomizePlusScale);
Task<string> getPalettePlusData = Task.Run(_ipcManager.PalettePlusBuildPalette);
previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]);
previousData.CustomizePlusScale = await getCustomizeData.ConfigureAwait(false);
_logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale);
previousData.PalettePlusPalette = await getPalettePlusData.ConfigureAwait(false);
_logger.LogDebug("Palette is now: {data}", previousData.PalettePlusPalette);
// gather static replacements from render model
var (forwardResolve, reverseResolve) = BuildDataFromModel(objectKind, charaPointer, token);
Dictionary<string, List<string>> resolvedPaths = GetFileReplacementsFromPaths(forwardResolve, reverseResolve);
previousData.FileReplacements[objectKind] =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
_logger.LogDebug("== Static Replacements ==");
foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("=> {repl}", replacement);
}
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
// or we get into redraw city for every change and nothing works properly
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
_transientResourceManager.AddSemiTransientResource(objectKind, item);
}
}
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
_transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind, charaPointer);
var resolvedTransientPaths = GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal));
_logger.LogDebug("== Transient Replacements ==");
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{
_logger.LogDebug("=> {repl}", replacement);
previousData.FileReplacements[objectKind].Add(replacement);
}
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList());
// make sure we only return data that actually has file replacements
foreach (var item in previousData.FileReplacements)
{
previousData.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
}
st.Stop();
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(st.ElapsedTicks).TotalMilliseconds);
return previousData;
}
private Dictionary<string, List<string>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
@@ -432,4 +421,17 @@ public class CharacterDataFactory : MediatorSubscriberBase
return resolvedPaths;
}
}
private HashSet<string> ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer)
{
_transientResourceManager.PersistTransientResources(charaPointer, objectKind);
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
{
pathsToResolve.Add(path);
}
return pathsToResolve;
}
}

View File

@@ -1,33 +1,31 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using Penumbra.String;
using System.Runtime.InteropServices;
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
namespace MareSynchronos.Models;
namespace MareSynchronos.PlayerData.Handlers;
public class GameObjectHandler : MediatorSubscriberBase
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtil;
private readonly Func<IntPtr> _getAddress;
private readonly bool _isOwnedObject;
private readonly MareMediator _mediator;
private readonly DalamudUtil _dalamudUtil;
private readonly PerformanceCollector _performanceCollector;
private readonly PerformanceCollectorService _performanceCollector;
private CancellationTokenSource? _clearCts = new();
private Task? _clearTask;
private Task? _delayedZoningTask;
private bool _haltProcessing = false;
private bool _ignoreSendAfterRedraw = false;
private CancellationTokenSource _zoningCts = new();
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollector performanceCollector,
MareMediator mediator, DalamudUtil dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool watchedObject = true) : base(logger, mediator)
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool watchedObject = true) : base(logger, mediator)
{
_performanceCollector = performanceCollector;
_mediator = mediator;
ObjectKind = objectKind;
_dalamudUtil = dalamudUtil;
_getAddress = getAddress;
@@ -40,8 +38,7 @@ public class GameObjectHandler : MediatorSubscriberBase
{
if (_delayedZoningTask?.IsCompleted ?? true)
{
var actualMsg = (TransientResourceChangedMessage)msg;
if (actualMsg.Address != Address) return;
if (msg.Address != Address) return;
Mediator.Publish(new CreateCacheForObjectMessage(this));
}
});
@@ -63,14 +60,14 @@ public class GameObjectHandler : MediatorSubscriberBase
});
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, (msg) =>
{
if (((PenumbraStartRedrawMessage)msg).Address == Address)
if (msg.Address == Address)
{
_haltProcessing = true;
}
});
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
{
if (((PenumbraEndRedrawMessage)msg).Address == Address)
if (msg.Address == Address)
{
_haltProcessing = false;
Task.Run(async () =>
@@ -87,7 +84,7 @@ public class GameObjectHandler : MediatorSubscriberBase
public IntPtr Address { get; set; }
public unsafe Character* Character => (Character*)Address;
public Lazy<Dalamud.Game.ClientState.Objects.Types.GameObject?> GameObjectLazy { get; private set; }
public IntPtr CurrentAddress => _getAddress.Invoke();
public string Name { get; private set; }
public ObjectKind ObjectKind { get; }
@@ -95,37 +92,6 @@ public class GameObjectHandler : MediatorSubscriberBase
private IntPtr DrawObjectAddress { get; set; }
private byte[] EquipSlotData { get; set; } = new byte[40];
private byte? HatState { get; set; }
private byte? VisorWeaponState { get; set; }
public override void Dispose()
{
base.Dispose();
if (_isOwnedObject)
Mediator.Publish(new RemoveWatchedGameObjectHandler(this));
}
public override string ToString()
{
var owned = (_isOwnedObject ? "Self" : "Other");
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
}
private unsafe IntPtr GetDrawObj()
{
return (IntPtr)((GameObject*)_getAddress.Invoke())->GetDrawObject();
}
private unsafe bool IsBeingDrawn(IntPtr drawObj, IntPtr curPtr)
{
_logger.LogTrace("IsBeingDrawn for ptr {curPtr} : {drawObj}", curPtr.ToString("X"), drawObj.ToString("X"));
return drawObj == IntPtr.Zero
|| (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0)
|| (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0)
|| (((GameObject*)curPtr)->RenderFlags & 0b100000000000) == 0b100000000000;
}
public async Task<bool> IsBeingDrawnRunOnFramework()
{
return await _dalamudUtil.RunOnFrameworkThread(() =>
@@ -137,7 +103,7 @@ public class GameObjectHandler : MediatorSubscriberBase
var drawObj = GetDrawObj();
return IsBeingDrawn(drawObj, curPtr);
}
catch (Exception ex)
catch (Exception)
{
if (curPtr != IntPtr.Zero)
{
@@ -149,6 +115,20 @@ public class GameObjectHandler : MediatorSubscriberBase
}).ConfigureAwait(false);
}
public override string ToString()
{
var owned = _isOwnedObject ? "Self" : "Other";
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_isOwnedObject)
Mediator.Publish(new RemoveWatchedGameObjectHandler(this));
}
private unsafe void CheckAndUpdateObject()
{
if (_haltProcessing) return;
@@ -166,69 +146,66 @@ public class GameObjectHandler : MediatorSubscriberBase
}
catch (Exception ex)
{
var name = new ByteString(((Character*)curPtr)->GameObject.Name).ToString();
_logger.LogError(ex, "Error during checking for draw object for {name}", this);
Logger.LogError(ex, "Error during checking for draw object for {name}", this);
}
if (curPtr != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
{
if (_clearCts != null)
{
_logger.LogDebug("[{this}] Cancelling Clear Task", this);
Logger.LogDebug("[{this}] Cancelling Clear Task", this);
_clearCts?.Cancel();
_clearCts = null;
}
bool addrDiff = Address != curPtr;
Address = curPtr;
if (addrDiff)
{
GameObjectLazy = new(() => _dalamudUtil.CreateGameObject(curPtr));
}
var chara = (Character*)curPtr;
var name = new ByteString(chara->GameObject.Name).ToString();
bool nameChange = (!string.Equals(name, Name, StringComparison.Ordinal));
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
Name = name;
bool equipDiff = CompareAndUpdateEquipByteData(chara->EquipSlotData);
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
if (equipDiff && !_isOwnedObject && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self
{
if (!_ignoreSendAfterRedraw)
{
_logger.LogTrace("[{this}] Changed", this);
Mediator.Publish(new CharacterChangedMessage(this));
return;
}
Logger.LogTrace("[{this}] Changed", this);
Mediator.Publish(new CharacterChangedMessage(this));
return;
}
var customizeDiff = CompareAndUpdateCustomizeData(chara->CustomizeData);
if (addrDiff || equipDiff || customizeDiff || drawObjDiff || nameChange)
if ((addrDiff || equipDiff || customizeDiff || drawObjDiff || nameChange) && _isOwnedObject)
{
if (_isOwnedObject)
{
_logger.LogTrace("[{this}] Changed", this);
Logger.LogTrace("[{this}] Changed", this);
_logger.LogDebug("[{this}] Sending CreateCacheObjectMessage", this);
Mediator.Publish(new CreateCacheForObjectMessage(this));
}
Logger.LogDebug("[{this}] Sending CreateCacheObjectMessage", this);
Mediator.Publish(new CreateCacheForObjectMessage(this));
}
}
else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero)
{
Address = IntPtr.Zero;
DrawObjectAddress = IntPtr.Zero;
_logger.LogTrace("[{this}] Changed -> Null", this);
Logger.LogTrace("[{this}] Changed -> Null", this);
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
{
_clearCts?.Cancel();
_clearCts?.Dispose();
_clearCts = new();
var token = _clearCts.Token;
_clearTask = Task.Run(() => ClearTask(token), token);
_ = Task.Run(() => ClearTask(token), token);
}
}
}
private async Task ClearTask(CancellationToken token)
{
_logger.LogDebug("[{this}] Running Clear Task", this);
Logger.LogDebug("[{this}] Running Clear Task", this);
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
_logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this);
Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this);
Mediator.Publish(new ClearCacheForObjectMessage(this));
_clearCts = null;
}
@@ -277,10 +254,24 @@ public class GameObjectHandler : MediatorSubscriberBase
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this);
Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this);
}
}
private unsafe IntPtr GetDrawObj()
{
return (IntPtr)((GameObject*)_getAddress.Invoke())->GetDrawObject();
}
private unsafe bool IsBeingDrawn(IntPtr drawObj, IntPtr curPtr)
{
Logger.LogTrace("IsBeingDrawn for ptr {curPtr} : {drawObj}", curPtr.ToString("X"), drawObj.ToString("X"));
return drawObj == IntPtr.Zero
|| (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0)
|| (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0)
|| (((GameObject*)curPtr)->RenderFlags & 0b100000000000) == 0b100000000000;
}
private void ZoneSwitchEnd()
{
if (!_isOwnedObject || _haltProcessing) return;
@@ -296,17 +287,20 @@ public class GameObjectHandler : MediatorSubscriberBase
if (!_isOwnedObject || _haltProcessing) return;
_zoningCts = new();
_logger.LogDebug("[{obj}] Starting Delay After Zoning", this);
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);
_delayedZoningTask = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false);
}
catch { }
catch
{
// ignore cancelled
}
finally
{
_logger.LogDebug("[{this}] Delay after zoning complete", this);
Logger.LogDebug("[{this}] Delay after zoning complete", this);
_zoningCts.Dispose();
}
});

View File

@@ -3,52 +3,71 @@ using Dalamud.Logging;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Factories;
using MareSynchronos.FileCache;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Interop;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Managers;
namespace MareSynchronos.PlayerData.Pairs;
public class CachedPlayer : MediatorSubscriberBase, IDisposable
public sealed class CachedPlayer : DisposableMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly DalamudUtil _dalamudUtil;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager;
private API.Data.CharacterData _cachedData = new();
private GameObjectHandler? _currentOtherChara;
private readonly Func<ObjectKind, Func<nint>, bool, GameObjectHandler> _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager;
private readonly IHostApplicationLifetime _lifetime;
private Guid _applicationId;
private Task? _applicationTask;
private CharacterData _cachedData = new();
private GameObjectHandler? _charaHandler;
private CancellationTokenSource? _downloadCancellationTokenSource = new();
private string _lastGlamourerData = string.Empty;
private string _originalGlamourerData = string.Empty;
public CachedPlayer(ILogger<CachedPlayer> logger, OnlineUserIdentDto onlineUser, GameObjectHandlerFactory gameObjectHandlerFactory,
IpcManager ipcManager, ApiController apiController,
DalamudUtil dalamudUtil, FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator)
private CancellationTokenSource _redrawCts = new();
public CachedPlayer(ILogger<CachedPlayer> logger, OnlineUserIdentDto onlineUser,
Func<ObjectKind, Func<nint>, bool, GameObjectHandler> gameObjectHandlerFactory,
IpcManager ipcManager, FileDownloadManager transferManager,
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator)
{
OnlineUser = onlineUser;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_ipcManager = ipcManager;
_apiController = apiController;
_downloadManager = transferManager;
_dalamudUtil = dalamudUtil;
_lifetime = lifetime;
_fileDbManager = fileDbManager;
}
private OnlineUserIdentDto OnlineUser { get; set; }
private IntPtr PlayerCharacter => _currentOtherChara?.Address ?? IntPtr.Zero;
private enum PlayerChanges
{
Heels = 1,
Customize = 2,
Palette = 3,
Mods = 4
}
public string? PlayerName { get; private set; }
public string PlayerNameHash => OnlineUser.Ident;
private OnlineUserIdentDto OnlineUser { get; set; }
private IntPtr PlayerCharacter => _charaHandler?.Address ?? IntPtr.Zero;
public void ApplyCharacterData(API.Data.CharacterData characterData, OptionalPluginWarning warning, bool forced = false)
public void ApplyCharacterData(CharacterData characterData, OptionalPluginWarning warning, bool forced = false)
{
_logger.LogDebug("Received data for {player}", this);
SetUploading(false);
_logger.LogDebug("Checking for files to download for player {name}", this);
_logger.LogDebug("Hash for data is {newHash}, current cache hash is {oldHash}", characterData.DataHash.Value, _cachedData.DataHash.Value);
Logger.LogDebug("Received data for {player}", this);
Logger.LogDebug("Checking for files to download for player {name}", this);
Logger.LogDebug("Hash for data is {newHash}, current cache hash is {oldHash}", characterData.DataHash.Value, _cachedData.DataHash.Value);
if (!_ipcManager.CheckPenumbraApi()) return;
if (!_ipcManager.CheckGlamourerApi()) return;
@@ -62,12 +81,167 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
NotifyForMissingPlugins(playerChanges, warning);
}
Logger.LogDebug("Downloading and applying character for {name}", this);
DownloadAndApplyCharacter(characterData, charaDataToUpdate);
_cachedData = characterData;
}
private Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(API.Data.CharacterData oldData, API.Data.CharacterData newData, bool forced)
public bool CheckExistence()
{
if (PlayerName == null || _charaHandler == null
|| !string.Equals(PlayerName, _charaHandler.Name, StringComparison.Ordinal)
|| _charaHandler.CurrentAddress == IntPtr.Zero)
{
return false;
}
return true;
}
public void Initialize(string name)
{
PlayerName = name;
_charaHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, false);
_originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
_lastGlamourerData = _originalGlamourerData;
Mediator.Subscribe<PenumbraRedrawMessage>(this, IpcManagerOnPenumbraRedrawEvent);
Mediator.Subscribe<CharacterChangedMessage>(this, (msg) =>
{
if (msg.GameObjectHandler == _charaHandler && (_applicationTask?.IsCompleted ?? true))
{
Logger.LogTrace("Saving new Glamourer Data for {this}", this);
_lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
}
});
Logger.LogDebug("Initializing Player {obj}", this);
}
public override string ToString()
{
return OnlineUser == null
? (base.ToString() ?? string.Empty)
: (OnlineUser.User.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != IntPtr.Zero ? "HasChar" : "NoChar"));
}
internal void SetUploading(bool isUploading = true)
{
Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading);
if (_charaHandler != null)
{
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
}
}
protected override void Dispose(bool disposing)
{
if (string.IsNullOrEmpty(PlayerName)) return; // already disposed
base.Dispose(disposing);
SetUploading(false);
_downloadManager.Dispose();
var name = PlayerName;
Logger.LogDebug("Disposing {name} ({user})", name, OnlineUser);
try
{
Guid applicationId = Guid.NewGuid();
_downloadCancellationTokenSource?.Cancel();
_downloadCancellationTokenSource?.Dispose();
_downloadCancellationTokenSource = null;
nint ptr = PlayerCharacter;
_charaHandler?.Dispose();
_charaHandler = null;
if (!_lifetime.ApplicationStopping.IsCancellationRequested && ptr != IntPtr.Zero && !_dalamudUtil.IsZoning)
{
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser);
_ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name);
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData.FileReplacements)
{
RevertCustomizationData(ptr, item.Key, name, applicationId).GetAwaiter().GetResult();
}
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error on disposal of {name}", name);
}
finally
{
PlayerName = null;
_cachedData = new();
Logger.LogDebug("Disposing {name} complete", name);
}
}
private async Task ApplyBaseData(Guid applicationId, Dictionary<string, string> moddedPaths, string manipulationData)
{
await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, PlayerName!)).ConfigureAwait(false);
await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraSetTemporaryMods(Logger, applicationId, PlayerName!, moddedPaths, manipulationData)).ConfigureAwait(false);
}
private async Task ApplyCustomizationData(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, API.Data.CharacterData charaData)
{
if (PlayerCharacter == IntPtr.Zero) return;
var ptr = PlayerCharacter;
var handler = changes.Key switch
{
ObjectKind.Player => _charaHandler!,
ObjectKind.Companion => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetCompanion(ptr), false),
ObjectKind.MinionOrMount => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), false),
ObjectKind.Pet => _gameObjectHandlerFactory(changes.Key, () => _dalamudUtil.GetPet(ptr), false),
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
};
CancellationTokenSource applicationTokenSource = new();
applicationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
if (handler.Address == IntPtr.Zero)
{
if (handler != _charaHandler) handler.Dispose();
return;
}
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000).ConfigureAwait(false);
foreach (var change in changes.Value)
{
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
switch (change)
{
case PlayerChanges.Palette:
await _ipcManager.PalettePlusSetPalette(handler.Address, charaData.PalettePlusData).ConfigureAwait(false);
break;
case PlayerChanges.Customize:
await _ipcManager.CustomizePlusSetBodyScale(handler.Address, charaData.CustomizePlusData).ConfigureAwait(false);
break;
case PlayerChanges.Heels:
await _ipcManager.HeelsSetOffsetForPlayer(handler.Address, charaData.HeelsOffset).ConfigureAwait(false);
break;
case PlayerChanges.Mods:
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
{
await _ipcManager.GlamourerApplyAll(Logger, handler, glamourerData, applicationId, applicationTokenSource.Token).ConfigureAwait(false);
}
else
{
await _ipcManager.PenumbraRedraw(Logger, handler, applicationId, applicationTokenSource.Token).ConfigureAwait(false);
}
break;
}
}
if (handler != _charaHandler) handler.Dispose();
}
private Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(CharacterData oldData, CharacterData newData, bool forced)
{
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
@@ -89,7 +263,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
{
_logger.LogDebug("Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," +
Logger.LogDebug("Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," +
" OldButNotNewFiles:{hasOldButNotNewFileReplacements}, NewButNotOldGlam:{hasNewButNotOldGlamourerData}, OldButNotNewGlam:{hasOldButNotNewGlamourerData}) => {change}",
this, objectKind, hasNewButNotOldFileReplacements, hasOldButNotNewFileReplacements, hasNewButNotOldGlamourerData, hasOldButNotNewGlamourerData, PlayerChanges.Mods);
charaDataToUpdate[objectKind].Add(PlayerChanges.Mods);
@@ -98,10 +272,10 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
{
if (hasNewAndOldFileReplacements)
{
bool listsAreEqual = Enumerable.SequenceEqual(oldData.FileReplacements[objectKind], newData.FileReplacements[objectKind], FileReplacementDataComparer.Instance);
bool listsAreEqual = oldData.FileReplacements[objectKind].SequenceEqual(newData.FileReplacements[objectKind], Data.FileReplacementDataComparer.Instance);
if (!listsAreEqual || forced)
{
_logger.LogDebug("Updating {object}/{kind} (FileReplacements not equal) => {change}", this, objectKind, PlayerChanges.Mods);
Logger.LogDebug("Updating {object}/{kind} (FileReplacements not equal) => {change}", this, objectKind, PlayerChanges.Mods);
charaDataToUpdate[objectKind].Add(PlayerChanges.Mods);
}
}
@@ -111,7 +285,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
bool glamourerDataDifferent = !string.Equals(oldData.GlamourerData[objectKind], newData.GlamourerData[objectKind], StringComparison.Ordinal);
if (glamourerDataDifferent || forced)
{
_logger.LogDebug("Updating {object}/{kind} (Glamourer different) => {change}", this, objectKind, PlayerChanges.Mods);
Logger.LogDebug("Updating {object}/{kind} (Glamourer different) => {change}", this, objectKind, PlayerChanges.Mods);
charaDataToUpdate[objectKind].Add(PlayerChanges.Mods);
}
}
@@ -122,28 +296,28 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
if (manipDataDifferent || forced)
{
_logger.LogDebug("Updating {object}/{kind} (Diff manip data) => {change}", this, objectKind, PlayerChanges.Mods);
Logger.LogDebug("Updating {object}/{kind} (Diff manip data) => {change}", this, objectKind, PlayerChanges.Mods);
charaDataToUpdate[objectKind].Add(PlayerChanges.Mods);
}
bool heelsOffsetDifferent = oldData.HeelsOffset != newData.HeelsOffset;
if (heelsOffsetDifferent || forced)
{
_logger.LogDebug("Updating {object}/{kind} (Diff heels data) => {change}", this, objectKind, PlayerChanges.Heels);
Logger.LogDebug("Updating {object}/{kind} (Diff heels data) => {change}", this, objectKind, PlayerChanges.Heels);
charaDataToUpdate[objectKind].Add(PlayerChanges.Heels);
}
bool customizeDataDifferent = !string.Equals(oldData.CustomizePlusData, newData.CustomizePlusData, StringComparison.Ordinal);
if (customizeDataDifferent || forced)
{
_logger.LogDebug("Updating {object}/{kind} (Diff customize data) => {change}", this, objectKind, PlayerChanges.Customize);
Logger.LogDebug("Updating {object}/{kind} (Diff customize data) => {change}", this, objectKind, PlayerChanges.Customize);
charaDataToUpdate[objectKind].Add(PlayerChanges.Customize);
}
bool palettePlusDataDifferent = !string.Equals(oldData.PalettePlusData, newData.PalettePlusData, StringComparison.Ordinal);
if (palettePlusDataDifferent || forced)
{
_logger.LogDebug("Updating {object}/{kind} (Diff palette data) => {change}", this, objectKind, PlayerChanges.Palette);
Logger.LogDebug("Updating {object}/{kind} (Diff palette data) => {change}", this, objectKind, PlayerChanges.Palette);
charaDataToUpdate[objectKind].Add(PlayerChanges.Palette);
}
}
@@ -157,195 +331,11 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
return charaDataToUpdate;
}
private enum PlayerChanges
{
Heels = 1,
Customize = 2,
Palette = 3,
Mods = 4
}
private void NotifyForMissingPlugins(HashSet<PlayerChanges> changes, OptionalPluginWarning warning)
{
List<string> missingPluginsForData = new();
if (changes.Contains(PlayerChanges.Heels))
{
if (!warning.ShownHeelsWarning && !_ipcManager.CheckHeelsApi())
{
missingPluginsForData.Add("Heels");
warning.ShownHeelsWarning = true;
}
}
if (changes.Contains(PlayerChanges.Customize))
{
if (!warning.ShownCustomizePlusWarning && !_ipcManager.CheckCustomizePlusApi())
{
missingPluginsForData.Add("Customize+");
warning.ShownCustomizePlusWarning = true;
}
}
if (changes.Contains(PlayerChanges.Palette))
{
if (!warning.ShownPalettePlusWarning && !_ipcManager.CheckPalettePlusApi())
{
missingPluginsForData.Add("Palette+");
warning.ShownPalettePlusWarning = true;
}
}
if (missingPluginsForData.Any())
{
Mediator.Publish(new NotificationMessage("Missing plugins for " + PlayerName,
$"Received data for {PlayerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.",
NotificationType.Warning, 10000));
}
}
public bool CheckExistence()
{
if (PlayerName == null || _currentOtherChara == null
|| !string.Equals(PlayerName, _currentOtherChara.Name, StringComparison.Ordinal)
|| _currentOtherChara.CurrentAddress == IntPtr.Zero)
{
return false;
}
return true;
}
public override void Dispose()
{
if (string.IsNullOrEmpty(PlayerName)) return; // already disposed
base.Dispose();
var name = PlayerName;
PlayerName = null;
_logger.LogDebug("Disposing {name} ({user})", name, OnlineUser);
try
{
Guid applicationId = Guid.NewGuid();
_logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser);
_ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, name);
_downloadCancellationTokenSource?.Cancel();
_downloadCancellationTokenSource?.Dispose();
_downloadCancellationTokenSource = null;
nint ptr = PlayerCharacter;
_currentOtherChara?.Dispose();
if (ptr != IntPtr.Zero && !_dalamudUtil.IsZoning)
{
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData.FileReplacements)
{
Task.Run(async () => await RevertCustomizationData(ptr, item.Key, name, applicationId).ConfigureAwait(false));
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error on disposal of {name}", name);
}
finally
{
_currentOtherChara = null;
_cachedData = new();
_logger.LogDebug("Disposing {name} complete", name);
PlayerName = null;
}
}
public void Initialize(string name)
{
PlayerName = name;
_currentOtherChara = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, isWatched: false);
_originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
_lastGlamourerData = _originalGlamourerData;
Mediator.Subscribe<PenumbraRedrawMessage>(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg)));
Mediator.Subscribe<CharacterChangedMessage>(this, (msg) =>
{
var actualMsg = (CharacterChangedMessage)msg;
if (actualMsg.GameObjectHandler == _currentOtherChara && (_applicationTask?.IsCompleted ?? true))
{
_logger.LogTrace("Saving new Glamourer Data for {this}", this);
_lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter);
}
});
_logger.LogDebug("Initializing Player {obj}", this);
}
public override string ToString()
{
return OnlineUser.User.AliasOrUID + ":" + PlayerName + ":" + ((PlayerCharacter != IntPtr.Zero) ? "HasChar" : "NoChar");
}
private async Task ApplyBaseData(Guid applicationId, Dictionary<string, string> moddedPaths, string manipulationData)
{
await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraRemoveTemporaryCollection(_logger, applicationId, PlayerName!)).ConfigureAwait(false);
await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.PenumbraSetTemporaryMods(_logger, applicationId, PlayerName!, moddedPaths, manipulationData)).ConfigureAwait(false);
}
private async Task ApplyCustomizationData(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, API.Data.CharacterData charaData)
{
if (PlayerCharacter == IntPtr.Zero) return;
var ptr = PlayerCharacter;
var handler = changes.Key switch
{
ObjectKind.Player => _currentOtherChara!,
ObjectKind.Companion => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanion(ptr), isWatched: false),
ObjectKind.MinionOrMount => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false),
ObjectKind.Pet => _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false),
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
};
CancellationTokenSource applicationTokenSource = new();
applicationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
if (handler.Address == IntPtr.Zero)
{
if (handler != _currentOtherChara) handler.Dispose();
return;
}
_logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, handler, applicationId, 30000).ConfigureAwait(false);
foreach (var change in changes.Value)
{
_logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
switch (change)
{
case PlayerChanges.Palette:
await _ipcManager.PalettePlusSetPalette(handler.Address, charaData.PalettePlusData).ConfigureAwait(false);
break;
case PlayerChanges.Customize:
await _ipcManager.CustomizePlusSetBodyScale(handler.Address, charaData.CustomizePlusData).ConfigureAwait(false);
break;
case PlayerChanges.Heels:
await _ipcManager.HeelsSetOffsetForPlayer(handler.Address, charaData.HeelsOffset).ConfigureAwait(false);
break;
case PlayerChanges.Mods:
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
{
await _ipcManager.GlamourerApplyAll(_logger, handler, glamourerData, applicationId, applicationTokenSource.Token).ConfigureAwait(false);
}
else
{
await _ipcManager.PenumbraRedraw(_logger, handler, applicationId, applicationTokenSource.Token).ConfigureAwait(false);
}
break;
}
}
if (handler != _currentOtherChara) handler.Dispose();
}
private void DownloadAndApplyCharacter(API.Data.CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
private void DownloadAndApplyCharacter(CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
{
if (!updatedData.Any())
{
_logger.LogDebug("Nothing to update for {obj}", this);
Logger.LogDebug("Nothing to update for {obj}", this);
}
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.Mods));
@@ -355,7 +345,6 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
_downloadCancellationTokenSource = new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token;
var downloadId = _apiController.GetDownloadId();
Task.Run(async () =>
{
List<FileReplacementData> toDownloadReplacements;
@@ -367,22 +356,22 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
int attempts = 0;
while ((toDownloadReplacements = TryCalculateModdedDictionary(charaData, out moddedPaths)).Count > 0 && attempts++ <= 10)
{
downloadId = _apiController.GetDownloadId();
_logger.LogDebug("Downloading missing files for player {name}, {kind}", PlayerName, updatedData);
_downloadManager.CancelDownload();
Logger.LogDebug("Downloading missing files for player {name}, {kind}", PlayerName, updatedData);
if (toDownloadReplacements.Any())
{
await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken).ConfigureAwait(false);
_apiController.CancelDownload(downloadId);
await _downloadManager.DownloadFiles(_charaHandler, toDownloadReplacements, downloadToken).ConfigureAwait(false);
_downloadManager.CancelDownload();
}
if (downloadToken.IsCancellationRequested)
{
_logger.LogTrace("Detected cancellation");
_apiController.CancelDownload(downloadId);
Logger.LogTrace("Detected cancellation");
_downloadManager.CancelDownload();
return;
}
if ((TryCalculateModdedDictionary(charaData, out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
if (TryCalculateModdedDictionary(charaData, out moddedPaths).All(c => _downloadManager.ForbiddenTransfers.Any(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
{
break;
}
@@ -391,10 +380,10 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
}
}
while (!_applicationTask?.IsCompleted ?? false && !downloadToken.IsCancellationRequested)
while ((!_applicationTask?.IsCompleted ?? false) && !downloadToken.IsCancellationRequested)
{
// block until current application is done
_logger.LogDebug("Waiting for current data application (Id: {id}) to finish", _applicationId);
Logger.LogDebug("Waiting for current data application (Id: {id}) to finish", _applicationId);
await Task.Delay(250).ConfigureAwait(false);
}
@@ -403,7 +392,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
_applicationTask = Task.Run(async () =>
{
_applicationId = Guid.NewGuid();
_logger.LogDebug("[{applicationId}] Starting application task", _applicationId);
Logger.LogDebug("[{applicationId}] Starting application task", _applicationId);
if (updateModdedPaths && (moddedPaths.Any() || !string.IsNullOrEmpty(charaData.ManipulationData)))
{
@@ -415,16 +404,11 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
await ApplyCustomizationData(_applicationId, kind, charaData).ConfigureAwait(false);
}
_logger.LogDebug("[{applicationId}] Application finished", _applicationId);
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
});
}, downloadToken);
}
private Task? _applicationTask;
private CancellationTokenSource _redrawCts = new();
private Guid _applicationId;
private void IpcManagerOnPenumbraRedrawEvent(PenumbraRedrawMessage msg)
{
var player = _dalamudUtil.GetCharacterFromObjectTableByIndex(msg.ObjTblIdx);
@@ -438,14 +422,42 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
Task.Run(async () =>
{
var applicationId = Guid.NewGuid();
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, _currentOtherChara!, applicationId, ct: token).ConfigureAwait(false);
_logger.LogDebug("Unauthorized character change detected");
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, applicationId, ct: token).ConfigureAwait(false);
Logger.LogDebug("Unauthorized character change detected");
await ApplyCustomizationData(applicationId, new(ObjectKind.Player,
new HashSet<PlayerChanges>(new[] { PlayerChanges.Palette, PlayerChanges.Customize, PlayerChanges.Heels, PlayerChanges.Mods })),
_cachedData).ConfigureAwait(false);
}, token);
}
private void NotifyForMissingPlugins(HashSet<PlayerChanges> changes, OptionalPluginWarning warning)
{
List<string> missingPluginsForData = new();
if (changes.Contains(PlayerChanges.Heels) && !warning.ShownHeelsWarning && !_ipcManager.CheckHeelsApi())
{
missingPluginsForData.Add("Heels");
warning.ShownHeelsWarning = true;
}
if (changes.Contains(PlayerChanges.Customize) && !warning.ShownCustomizePlusWarning && !_ipcManager.CheckCustomizePlusApi())
{
missingPluginsForData.Add("Customize+");
warning.ShownCustomizePlusWarning = true;
}
if (changes.Contains(PlayerChanges.Palette) && !warning.ShownPalettePlusWarning && !_ipcManager.CheckPalettePlusApi())
{
missingPluginsForData.Add("Palette+");
warning.ShownPalettePlusWarning = true;
}
if (missingPluginsForData.Any())
{
Mediator.Publish(new NotificationMessage("Missing plugins for " + PlayerName,
$"Received data for {PlayerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.",
NotificationType.Warning, 10000));
}
}
private async Task RevertCustomizationData(IntPtr address, ObjectKind objectKind, string name, Guid applicationId)
{
if (address == IntPtr.Zero) return;
@@ -453,20 +465,20 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
var cancelToken = new CancellationTokenSource();
cancelToken.CancelAfter(TimeSpan.FromSeconds(10));
_logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind);
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind);
if (objectKind == ObjectKind.Player)
{
using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, false);
_logger.LogDebug("[{applicationId}] Restoring Customization for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _originalGlamourerData);
await _ipcManager.GlamourerApplyOnlyCustomization(_logger, tempHandler, _originalGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
_logger.LogDebug("[{applicationId}] Restoring Equipment for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _lastGlamourerData);
await _ipcManager.GlamourerApplyOnlyEquipment(_logger, tempHandler, _lastGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
_logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Player, () => address, false);
Logger.LogDebug("[{applicationId}] Restoring Customization for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _originalGlamourerData);
await _ipcManager.GlamourerApplyOnlyCustomization(Logger, tempHandler, _originalGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
Logger.LogDebug("[{applicationId}] Restoring Equipment for {alias}/{name}: {data}", applicationId, OnlineUser.User.AliasOrUID, name, _lastGlamourerData);
await _ipcManager.GlamourerApplyOnlyEquipment(Logger, tempHandler, _lastGlamourerData, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
await _ipcManager.HeelsRestoreOffsetForPlayer(address).ConfigureAwait(false);
_logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
await _ipcManager.CustomizePlusRevert(address).ConfigureAwait(false);
_logger.LogDebug("[{applicationId}] Restoring Palette+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
Logger.LogDebug("[{applicationId}] Restoring Palette+ for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name);
await _ipcManager.PalettePlusRemovePalette(address).ConfigureAwait(false);
}
else if (objectKind == ObjectKind.MinionOrMount)
@@ -474,8 +486,8 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
var minionOrMount = _dalamudUtil.GetMinionOrMount(address);
if (minionOrMount != IntPtr.Zero)
{
using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false);
await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.MinionOrMount, () => minionOrMount, false);
await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
}
}
else if (objectKind == ObjectKind.Pet)
@@ -483,8 +495,8 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
var pet = _dalamudUtil.GetPet(address);
if (pet != IntPtr.Zero)
{
using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false);
await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Pet, () => pet, false);
await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
}
}
else if (objectKind == ObjectKind.Companion)
@@ -492,13 +504,13 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
var companion = _dalamudUtil.GetCompanion(address);
if (companion != IntPtr.Zero)
{
using GameObjectHandler tempHandler = _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false);
await _ipcManager.PenumbraRedraw(_logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
using GameObjectHandler tempHandler = _gameObjectHandlerFactory(ObjectKind.Pet, () => companion, false);
await _ipcManager.PenumbraRedraw(Logger, tempHandler, applicationId, cancelToken.Token, fireAndForget: false).ConfigureAwait(false);
}
}
}
private List<FileReplacementData> TryCalculateModdedDictionary(API.Data.CharacterData charaData, out Dictionary<string, string> moddedDictionary)
private List<FileReplacementData> TryCalculateModdedDictionary(CharacterData charaData, out Dictionary<string, string> moddedDictionary)
{
List<FileReplacementData> missingFiles = new();
moddedDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
@@ -515,7 +527,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
}
else
{
_logger.LogTrace("Missing file: {hash}", item.Hash);
Logger.LogTrace("Missing file: {hash}", item.Hash);
missingFiles.Add(item);
}
}
@@ -525,7 +537,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
{
foreach (var gamePath in item.GamePaths)
{
_logger.LogTrace("Adding file swap for {path}: {fileSwap}", gamePath, item.FileSwapPath);
Logger.LogTrace("Adding file swap for {path}: {fileSwap}", gamePath, item.FileSwapPath);
moddedDictionary[gamePath] = item.FileSwapPath;
}
}
@@ -534,7 +546,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable
{
PluginLog.Error(ex, "Something went wrong during calculation replacements");
}
_logger.LogDebug("ModdedPaths calculated, missing files: {count}", missingFiles.Count);
Logger.LogDebug("ModdedPaths calculated, missing files: {count}", missingFiles.Count);
return missingFiles;
}
}

View File

@@ -1,57 +1,46 @@
using MareSynchronos.API.Data;
using MareSynchronos.FileCache;
using MareSynchronos.Mediator;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Managers;
namespace MareSynchronos.PlayerData.Pairs;
public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
public class OnlinePlayerManager : DisposableMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly DalamudUtil _dalamudUtil;
private readonly FileCacheManager _fileDbManager;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileUploadManager _fileTransferManager;
private readonly PairManager _pairManager;
private CharacterData? _lastSentData;
public OnlinePlayerManager(ILogger<OnlinePlayerManager> logger, ApiController apiController, DalamudUtil dalamudUtil,
FileCacheManager fileDbManager, PairManager pairManager, MareMediator mediator) : base(logger, mediator)
public OnlinePlayerManager(ILogger<OnlinePlayerManager> logger, ApiController apiController, DalamudUtilService dalamudUtil,
PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
{
_logger.LogTrace("Creating " + nameof(OnlinePlayerManager));
_apiController = apiController;
_dalamudUtil = dalamudUtil;
_fileDbManager = fileDbManager;
_pairManager = pairManager;
Mediator.Subscribe<PlayerChangedMessage>(this, (msg) => PlayerManagerOnPlayerHasChanged((PlayerChangedMessage)msg));
_fileTransferManager = fileTransferManager;
Mediator.Subscribe<PlayerChangedMessage>(this, (_) => PlayerManagerOnPlayerHasChanged());
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
{
var newData = ((CharacterDataCreatedMessage)msg).CharacterData;
if (_lastSentData == null || _lastSentData != null && !string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal))
var newData = msg.CharacterData;
if (_lastSentData == null || (!string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal)))
{
_logger.LogDebug("Pushing data for visible players");
Logger.LogDebug("Pushing data for visible players");
_lastSentData = newData;
PushCharacterData(_pairManager.VisibleUsers);
PushCharacterData(_pairManager.GetVisibleUsers());
}
else
{
_logger.LogDebug("Not sending data for " + newData.DataHash.Value);
Logger.LogDebug("Not sending data for {hash}", newData.DataHash.Value);
}
});
}
private void PlayerManagerOnPlayerHasChanged(PlayerChangedMessage msg)
{
PushCharacterData(_pairManager.VisibleUsers);
}
public override void Dispose()
{
base.Dispose();
}
private void FrameworkOnUpdate()
{
if (!_dalamudUtil.IsPlayerPresent || !_apiController.IsConnected) return;
@@ -71,18 +60,24 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable
if (newVisiblePlayers.Any())
{
_logger.LogTrace("Has new visible players, pushing character data");
Logger.LogTrace("Has new visible players, pushing character data");
PushCharacterData(newVisiblePlayers);
}
}
private void PlayerManagerOnPlayerHasChanged()
{
PushCharacterData(_pairManager.GetVisibleUsers());
}
private void PushCharacterData(List<UserData> visiblePlayers)
{
if (visiblePlayers.Any() && _lastSentData != null)
{
Task.Run(async () =>
{
await _apiController.PushCharacterData(_lastSentData, visiblePlayers).ConfigureAwait(false);
var dataToSend = await _fileTransferManager.UploadFiles(_lastSentData.DeepClone(), visiblePlayers).ConfigureAwait(false);
await _apiController.PushCharacterData(dataToSend, visiblePlayers).ConfigureAwait(false);
});
}
}

View File

@@ -1,4 +1,4 @@
namespace MareSynchronos.Models;
namespace MareSynchronos.PlayerData.Pairs;
public record OptionalPluginWarning
{

View File

@@ -4,24 +4,23 @@ using MareSynchronos.API.Data.Comparer;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Factories;
using MareSynchronos.Managers;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Models;
namespace MareSynchronos.PlayerData.Pairs;
public class Pair
{
private readonly ILogger<Pair> _logger;
private readonly CachedPlayerFactory _cachedPlayerFactory;
private readonly Func<OnlineUserIdentDto, CachedPlayer> _cachedPlayerFactory;
private readonly MareConfigService _configService;
private readonly ILogger<Pair> _logger;
private readonly ServerConfigurationManager _serverConfigurationManager;
private OnlineUserIdentDto? _onlineUserIdentDto = null;
private OptionalPluginWarning? _pluginWarnings;
public Pair(ILogger<Pair> logger, CachedPlayerFactory cachedPlayerFactory, MareConfigService configService, ServerConfigurationManager serverConfigurationManager)
public Pair(ILogger<Pair> logger, Func<OnlineUserIdentDto, CachedPlayer> cachedPlayerFactory, MareConfigService configService, ServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_cachedPlayerFactory = cachedPlayerFactory;
@@ -29,52 +28,21 @@ public class Pair
_serverConfigurationManager = serverConfigurationManager;
}
public UserPairDto? UserPair { get; set; }
private CachedPlayer? CachedPlayer { get; set; }
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName);
public API.Data.CharacterData? LastReceivedCharacterData { get; set; }
public bool CachedPlayerExists => CachedPlayer?.CheckExistence() ?? false;
public Dictionary<GroupFullInfoDto, GroupPairFullInfoDto> GroupPair { get; set; } = new(GroupDtoComparer.Instance);
public string PlayerNameHash => CachedPlayer?.PlayerNameHash ?? string.Empty;
public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty;
public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User;
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName);
public bool IsOnline => CachedPlayer != null;
public bool IsVisible => CachedPlayer?.PlayerName != null;
public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? (UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused())
public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused()
: GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused());
public string? GetNote()
{
return _serverConfigurationManager.GetNoteForUid(UserData.UID);
}
public void SetNote(string note)
{
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
}
public bool HasAnyConnection()
{
return UserPair != null || GroupPair.Any();
}
public bool InitializePair(string name)
{
if (!PlayerName.IsNullOrEmpty()) return false;
if (CachedPlayer == null) throw new InvalidOperationException("CachedPlayer not initialized");
_pluginWarnings ??= new()
{
ShownCustomizePlusWarning = _configService.Current.DisableOptionalPluginWarnings,
ShownHeelsWarning = _configService.Current.DisableOptionalPluginWarnings,
ShownPalettePlusWarning = _configService.Current.DisableOptionalPluginWarnings,
};
CachedPlayer.Initialize(name);
ApplyLastReceivedData();
return true;
}
public bool IsVisible => CachedPlayer?.PlayerName != null;
public CharacterData? LastReceivedCharacterData { get; set; }
public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty;
public string PlayerNameHash => CachedPlayer?.PlayerNameHash ?? string.Empty;
public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User;
public UserPairDto? UserPair { get; set; }
private CachedPlayer? CachedPlayer { get; set; }
public void ApplyData(OnlineUserCharaDataDto data)
{
@@ -102,21 +70,91 @@ public class Pair
CachedPlayer.ApplyCharacterData(RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, _pluginWarnings, forced);
}
private API.Data.CharacterData? RemoveNotSyncedFiles(API.Data.CharacterData? data)
public string? GetNote()
{
return _serverConfigurationManager.GetNoteForUid(UserData.UID);
}
public bool HasAnyConnection()
{
return UserPair != null || GroupPair.Any();
}
public bool InitializePair(string name)
{
if (!PlayerName.IsNullOrEmpty()) return false;
if (CachedPlayer == null) throw new InvalidOperationException("CachedPlayer not initialized");
_pluginWarnings ??= new()
{
ShownCustomizePlusWarning = _configService.Current.DisableOptionalPluginWarnings,
ShownHeelsWarning = _configService.Current.DisableOptionalPluginWarnings,
ShownPalettePlusWarning = _configService.Current.DisableOptionalPluginWarnings,
};
CachedPlayer.Initialize(name);
ApplyLastReceivedData();
return true;
}
public void MarkOffline()
{
_onlineUserIdentDto = null;
LastReceivedCharacterData = null;
CachedPlayer?.Dispose();
CachedPlayer = null;
}
public void RecreateCachedPlayer(OnlineUserIdentDto? dto = null)
{
if (dto == null && _onlineUserIdentDto == null) return;
if (dto != null)
{
_onlineUserIdentDto = dto;
}
CachedPlayer?.Dispose();
CachedPlayer = _cachedPlayerFactory(_onlineUserIdentDto!);
}
public void SetNote(string note)
{
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
}
internal void SetIsUploading()
{
CachedPlayer?.SetUploading();
}
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
{
_logger.LogTrace("Removing not synced files");
if (data == null || (UserPair != null && UserPair.OtherPermissions.IsPaired()))
if (data == null)
{
_logger.LogTrace("Nothing to remove or user is paired directly");
_logger.LogTrace("Nothing to remove");
return data;
}
bool disableAnimations = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations());
bool disableSounds = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds());
bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
bool disableGroupAnimations = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations());
bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations);
bool disableIndividualSounds = UserPair != null && (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
bool disableGroupSounds = GroupPair.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds());
bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds);
_logger.LogTrace("Individual Sounds: {disableIndividualSounds}, Individual Anims: {disableIndividualAnims}; " +
"Group Sounds: {disableGroupSounds}, Group Anims: {disableGroupAnims} => Disable Sounds: {disableSounds}, Disable Anims: {disableAnims}",
disableIndividualSounds, disableIndividualAnimations, disableGroupSounds, disableGroupAnimations, disableSounds, disableAnimations);
if (disableAnimations || disableSounds)
{
_logger.LogTrace($"Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}");
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}", disableAnimations, disableSounds);
foreach (var kvp in data.FileReplacements)
{
if (disableSounds)
@@ -132,30 +170,4 @@ public class Pair
return data;
}
public bool CachedPlayerExists => CachedPlayer?.CheckExistence() ?? false;
private OnlineUserIdentDto? _onlineUserIdentDto = null;
private ApiController? _apiController = null;
public void RecreateCachedPlayer(OnlineUserIdentDto? dto = null, ApiController? controller = null)
{
if ((dto == null && _onlineUserIdentDto == null) || (_apiController == null && controller == null)) return;
if (dto != null || controller != null)
{
_onlineUserIdentDto = dto;
_apiController = controller;
}
CachedPlayer?.Dispose();
CachedPlayer = null;
CachedPlayer = _cachedPlayerFactory.Create(_onlineUserIdentDto!, _apiController!);
}
public void MarkOffline()
{
_onlineUserIdentDto = null;
LastReceivedCharacterData = null;
CachedPlayer?.Dispose();
CachedPlayer = null;
}
}
}

View File

@@ -6,26 +6,26 @@ using MareSynchronos.API.Data.Comparer;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Factories;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace MareSynchronos.Managers;
namespace MareSynchronos.PlayerData.Pairs;
public class PairManager : MediatorSubscriberBase, IDisposable
public sealed class PairManager : DisposableMediatorSubscriberBase
{
private readonly ConcurrentDictionary<UserData, Pair> _allClientPairs = new(UserDataComparer.Instance);
private readonly ConcurrentDictionary<GroupData, GroupFullInfoDto> _allGroups = new(GroupDataComparer.Instance);
private readonly PairFactory _pairFactory;
private readonly MareConfigService _configurationService;
private readonly Func<Pair> _pairFactory;
private Lazy<List<Pair>> _directPairsInternal;
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
MareConfigService configurationService, MareMediator mediator) : base(logger, mediator)
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
public PairManager(ILogger<PairManager> logger, Func<Pair> pairFactory,
MareConfigService configurationService, MareMediator mediator) : base(logger, mediator)
{
_pairFactory = pairFactory;
_configurationService = configurationService;
@@ -36,33 +36,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable
_groupPairsInternal = GroupPairsLazy();
}
private void RecreateLazy()
{
_directPairsInternal = DirectPairsLazy();
_groupPairsInternal = GroupPairsLazy();
}
private Lazy<List<Pair>> _directPairsInternal;
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
public Dictionary<GroupFullInfoDto, List<Pair>> GroupPairs => _groupPairsInternal.Value;
public List<Pair> DirectPairs => _directPairsInternal.Value;
private Lazy<List<Pair>> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value).Where(k => k.UserPair != null).ToList());
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> GroupPairsLazy()
{
return new Lazy<Dictionary<GroupFullInfoDto, List<Pair>>>(() =>
{
Dictionary<GroupFullInfoDto, List<Pair>> outDict = new();
foreach (var group in _allGroups)
{
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList();
}
return outDict;
});
}
public List<Pair> OnlineUserPairs => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.PlayerNameHash)).Select(p => p.Value).ToList();
public List<UserData> VisibleUsers => _allClientPairs.Where(p => p.Value.HasCachedPlayer).Select(p => p.Key).ToList();
public Dictionary<GroupFullInfoDto, List<Pair>> GroupPairs => _groupPairsInternal.Value;
public Pair? LastAddedUser { get; internal set; }
@@ -72,33 +48,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable
RecreateLazy();
}
public void RemoveGroup(GroupData data)
{
_allGroups.TryRemove(data, out _);
foreach (var item in _allClientPairs.ToList())
{
foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).ToList())
{
if (GroupDataComparer.Instance.Equals(grpPair.Group, data))
{
_allClientPairs[item.Key].GroupPair.Remove(grpPair);
}
}
if (!_allClientPairs[item.Key].HasAnyConnection())
{
if (_allClientPairs.TryRemove(item.Key, out var pair))
{
pair.MarkOffline();
}
}
}
RecreateLazy();
}
public void AddGroupPair(GroupPairFullInfoDto dto)
{
if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Create();
if (!_allClientPairs.ContainsKey(dto.User)) _allClientPairs[dto.User] = _pairFactory.Invoke();
var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group] = dto;
@@ -109,7 +61,7 @@ public class PairManager : MediatorSubscriberBase, IDisposable
{
if (!_allClientPairs.ContainsKey(dto.User))
{
_allClientPairs[dto.User] = _pairFactory.Create();
_allClientPairs[dto.User] = _pairFactory.Invoke();
}
else
{
@@ -125,39 +77,24 @@ public class PairManager : MediatorSubscriberBase, IDisposable
public void ClearPairs()
{
_logger.LogDebug("Clearing all Pairs");
Logger.LogDebug("Clearing all Pairs");
DisposePairs();
_allClientPairs.Clear();
_allGroups.Clear();
RecreateLazy();
}
public override void Dispose()
{
base.Dispose();
DisposePairs();
}
private void DisposePairs(bool recreate = false)
{
_logger.LogDebug("Disposing all Pairs");
foreach (var item in _allClientPairs)
{
if (recreate)
item.Value.RecreateCachedPlayer();
else
item.Value.MarkOffline();
}
RecreateLazy();
}
public Pair? FindPair(PlayerCharacter? pChar)
{
if (pChar == null) return null;
var hash = pChar.GetHash256();
return OnlineUserPairs.Find(p => string.Equals(p.PlayerNameHash, hash, StringComparison.Ordinal));
return GetOnlineUserPairs().Find(p => string.Equals(p.PlayerNameHash, hash, StringComparison.Ordinal));
}
public List<Pair> GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.PlayerNameHash)).Select(p => p.Value).ToList();
public List<UserData> GetVisibleUsers() => _allClientPairs.Where(p => p.Value.HasCachedPlayer).Select(p => p.Key).ToList();
public void MarkPairOffline(UserData user)
{
if (_allClientPairs.TryGetValue(user, out var pair))
@@ -167,14 +104,14 @@ public class PairManager : MediatorSubscriberBase, IDisposable
}
}
public void MarkPairOnline(OnlineUserIdentDto dto, ApiController controller, bool sendNotif = true)
public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true)
{
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
var pair = _allClientPairs[dto.User];
if (pair.HasCachedPlayer) return;
if (sendNotif && _configurationService.Current.ShowOnlineNotifications
&& ((_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null)
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs)
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote())
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs))
@@ -186,7 +123,7 @@ public class PairManager : MediatorSubscriberBase, IDisposable
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, 5000));
}
pair.RecreateCachedPlayer(dto, controller);
pair.RecreateCachedPlayer(dto);
RecreateLazy();
}
@@ -205,6 +142,24 @@ public class PairManager : MediatorSubscriberBase, IDisposable
}
}
public void RemoveGroup(GroupData data)
{
_allGroups.TryRemove(data, out _);
foreach (var item in _allClientPairs.ToList())
{
foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList())
{
_allClientPairs[item.Key].GroupPair.Remove(grpPair);
}
if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair))
{
pair.MarkOffline();
}
}
RecreateLazy();
}
public void RemoveGroupPair(GroupPairDto dto)
{
if (_allClientPairs.TryGetValue(dto.User, out var pair))
@@ -241,6 +196,14 @@ public class PairManager : MediatorSubscriberBase, IDisposable
}
}
public void SetGroupInfo(GroupInfoDto dto)
{
_allGroups[dto.Group].Group = dto.Group;
_allGroups[dto.Group].Owner = dto.Owner;
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
RecreateLazy();
}
public void UpdatePairPermissions(UserPermissionsDto dto)
{
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
@@ -251,10 +214,10 @@ public class PairManager : MediatorSubscriberBase, IDisposable
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
pair.UserPair.OtherPermissions = dto.Permissions;
if (!pair.UserPair.OtherPermissions.IsPaired())
{
pair.ApplyLastReceivedData();
}
Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}",
pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds());
pair.ApplyLastReceivedData();
}
public void UpdateSelfPairPermissions(UserPermissionsDto dto)
@@ -267,29 +230,38 @@ public class PairManager : MediatorSubscriberBase, IDisposable
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
pair.UserPair.OwnPermissions = dto.Permissions;
Logger.LogTrace("Paired: {synced}, Paused: {paused}, Anims: {anims}, Sounds: {sounds}",
pair.UserPair.OwnPermissions.IsPaired(), pair.UserPair.OwnPermissions.IsPaused(), pair.UserPair.OwnPermissions.IsDisableAnimations(), pair.UserPair.OwnPermissions.IsDisableSounds());
pair.ApplyLastReceivedData();
}
private void DalamudUtilOnDelayedFrameworkUpdate()
internal void ReceiveUploadStatus(UserDto dto)
{
foreach (Pair pair in _allClientPairs.Select(p => p.Value).Where(p => p.HasCachedPlayer).ToList())
if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible)
{
if (!pair.CachedPlayerExists)
{
pair.RecreateCachedPlayer();
}
existingPair.SetIsUploading();
}
}
private void DalamudUtilOnZoneSwitched()
internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto)
{
DisposePairs(true);
var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo;
RecreateLazy();
}
public void SetGroupInfo(GroupInfoDto dto)
internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto)
{
_allGroups[dto.Group].Group = dto.Group;
_allGroups[dto.Group].Owner = dto.Owner;
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
var group = _allGroups[dto.Group];
var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions;
_allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions;
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds())
{
_allClientPairs[dto.User].ApplyLastReceivedData();
}
RecreateLazy();
}
@@ -307,17 +279,9 @@ public class PairManager : MediatorSubscriberBase, IDisposable
RecreateLazy();
}
internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto)
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
{
var group = _allGroups[dto.Group];
var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions;
_allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions;
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds())
{
_allClientPairs[dto.User].ApplyLastReceivedData();
}
RecreateLazy();
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
}
internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto)
@@ -334,15 +298,60 @@ public class PairManager : MediatorSubscriberBase, IDisposable
RecreateLazy();
}
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
protected override void Dispose(bool disposing)
{
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
base.Dispose(disposing);
DisposePairs();
}
internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto)
private void DalamudUtilOnDelayedFrameworkUpdate()
{
var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo;
foreach (Pair pair in _allClientPairs.Select(p => p.Value).Where(p => p.HasCachedPlayer).ToList())
{
if (!pair.CachedPlayerExists)
{
pair.RecreateCachedPlayer();
}
}
}
private void DalamudUtilOnZoneSwitched()
{
DisposePairs(recreate: true);
}
private Lazy<List<Pair>> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value).Where(k => k.UserPair != null).ToList());
private void DisposePairs(bool recreate = false)
{
Logger.LogDebug("Disposing all Pairs");
foreach (var item in _allClientPairs)
{
if (recreate)
item.Value.RecreateCachedPlayer();
else
item.Value.MarkOffline();
}
RecreateLazy();
}
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> GroupPairsLazy()
{
return new Lazy<Dictionary<GroupFullInfoDto, List<Pair>>>(() =>
{
Dictionary<GroupFullInfoDto, List<Pair>> outDict = new();
foreach (var group in _allGroups)
{
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList();
}
return outDict;
});
}
private void RecreateLazy()
{
_directPairsInternal = DirectPairsLazy();
_groupPairsInternal = GroupPairsLazy();
}
}

View File

@@ -1,51 +1,50 @@
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Factories;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using MareSynchronos.PlayerData.Data;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Managers;
namespace MareSynchronos.PlayerData.Services;
public class CacheCreationService : MediatorSubscriberBase, IDisposable
public sealed class CacheCreationService : DisposableMediatorSubscriberBase
{
private readonly CharacterDataFactory _characterDataFactory;
private Task? _cacheCreationTask;
private readonly SemaphoreSlim _cacheCreateLock = new(1);
private readonly Dictionary<ObjectKind, GameObjectHandler> _cachesToCreate = new();
private readonly CharacterData _playerData = new();
private readonly PlayerDataFactory _characterDataFactory;
private readonly CancellationTokenSource _cts = new();
private readonly CharacterData _playerData = new();
private readonly List<GameObjectHandler> _playerRelatedObjects = new();
private Task? _cacheCreationTask;
private CancellationTokenSource _palettePlusCts = new();
private SemaphoreSlim _cacheCreateLock = new(1);
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) : base(logger, mediator)
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, Func<ObjectKind, Func<IntPtr>, bool, GameObjectHandler> gameObjectHandlerFactory,
PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator)
{
_characterDataFactory = characterDataFactory;
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
{
var actualMsg = (CreateCacheForObjectMessage)msg;
_cacheCreateLock.Wait();
_cachesToCreate[actualMsg.ObjectToCreateFor.ObjectKind] = actualMsg.ObjectToCreateFor;
_cachesToCreate[msg.ObjectToCreateFor.ObjectKind] = msg.ObjectToCreateFor;
_cacheCreateLock.Release();
});
_playerRelatedObjects.AddRange(new List<GameObjectHandler>()
{
gameObjectHandlerFactory.Create(ObjectKind.Player, () => dalamudUtil.PlayerPointer, isWatched: true),
gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true),
gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), isWatched: true),
gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true),
gameObjectHandlerFactory(ObjectKind.Player, () => dalamudUtil.PlayerPointer, true),
gameObjectHandlerFactory(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), true),
gameObjectHandlerFactory(ObjectKind.Pet, () => dalamudUtil.GetPet(), true),
gameObjectHandlerFactory(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), true),
});
Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) =>
{
Task.Run(() =>
{
var actualMsg = (ClearCacheForObjectMessage)msg;
_playerData.FileReplacements.Remove(actualMsg.ObjectToCreateFor.ObjectKind);
_playerData.GlamourerString.Remove(actualMsg.ObjectToCreateFor.ObjectKind);
_playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind);
_playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind);
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
});
});
@@ -57,6 +56,14 @@ public class CacheCreationService : MediatorSubscriberBase, IDisposable
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, async (msg) => await AddPlayerCacheToCreate().ConfigureAwait(false));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_playerRelatedObjects.ForEach(p => p.Dispose());
_cts.Dispose();
}
private async Task AddPlayerCacheToCreate()
{
await _cacheCreateLock.WaitAsync().ConfigureAwait(false);
@@ -101,31 +108,24 @@ public class CacheCreationService : MediatorSubscriberBase, IDisposable
{
await Task.Delay(100).ConfigureAwait(false);
maxWaitingTime -= 100;
_logger.LogTrace("Waiting for Cache to be ready");
Logger.LogTrace("Waiting for Cache to be ready");
}
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error during Cache Creation Processing");
Logger.LogCritical(ex, "Error during Cache Creation Processing");
}
finally
{
_logger.LogDebug("Cache Creation complete");
Logger.LogDebug("Cache Creation complete");
}
}, _cts.Token);
}
else if (_cachesToCreate.Any())
{
_logger.LogDebug("Cache Creation stored until previous creation finished");
Logger.LogDebug("Cache Creation stored until previous creation finished");
}
}
public override void Dispose()
{
base.Dispose();
_playerRelatedObjects.ForEach(p => p.Dispose());
_cts.Dispose();
}
}
}

View File

@@ -1,109 +1,150 @@
using Dalamud.Game.Command;
using Dalamud.Plugin;
using MareSynchronos.Factories;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState;
using Dalamud.Interface.ImGuiFileDialog;
using MareSynchronos.Managers;
using MareSynchronos.WebAPI;
using Dalamud.Interface.Windowing;
using MareSynchronos.UI;
using MareSynchronos.Utils;
using Dalamud.Game.ClientState.Conditions;
using MareSynchronos.FileCache;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using MareSynchronos.Export;
using Dalamud.Data;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto.User;
using MareSynchronos.FileCache;
using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.PlayerData.Factories;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.PlayerData.Services;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI;
using MareSynchronos.WebAPI.Files;
using MareSynchronos.WebAPI.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos;
public sealed class Plugin : IDalamudPlugin
{
private readonly MarePlugin _plugin;
public string Name => "Mare Synchronos";
private readonly ILogger<Plugin> _pluginLogger;
private readonly CancellationTokenSource _pluginCts = new();
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, DataManager gameData,
Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition, ChatGui chatGui)
Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition, ChatGui chatGui,
GameGui gameGui)
{
IServiceCollection collection = new ServiceCollection();
collection.AddLogging(o =>
new HostBuilder()
.UseContentRoot(pluginInterface.ConfigDirectory.FullName)
.ConfigureLogging(lb =>
{
o.AddDalamudLogging();
o.SetMinimumLevel(LogLevel.Trace);
});
lb.ClearProviders();
lb.AddDalamudLogging();
lb.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureServices(collection =>
{
collection.AddSingleton(new WindowSystem("MareSynchronos"));
collection.AddSingleton<FileDialogManager>();
collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true));
// inject dalamud stuff
collection.AddSingleton(pluginInterface);
collection.AddSingleton(commandManager);
collection.AddSingleton(gameData);
collection.AddSingleton(framework);
collection.AddSingleton(objectTable);
collection.AddSingleton(clientState);
collection.AddSingleton(condition);
collection.AddSingleton(chatGui);
collection.AddSingleton(pluginInterface.UiBuilder);
collection.AddSingleton(new WindowSystem("MareSynchronos"));
collection.AddSingleton<FileDialogManager>();
// add mare related singletons
collection.AddSingleton<MareMediator>();
collection.AddSingleton<FileCacheManager>();
collection.AddSingleton<ServerConfigurationManager>();
collection.AddSingleton<PairManager>();
collection.AddSingleton<ApiController>();
collection.AddSingleton<MareCharaFileManager>();
collection.AddSingleton<PerformanceCollectorService>();
collection.AddSingleton<HubFactory>();
collection.AddSingleton<FileUploadManager>();
collection.AddSingleton<FileTransferOrchestrator>();
collection.AddSingleton<MarePlugin>();
collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService<ILogger<DalamudUtilService>>(),
clientState, objectTable, framework, gameGui, condition, gameData,
s.GetRequiredService<MareMediator>(), s.GetRequiredService<PerformanceCollectorService>()));
collection.AddSingleton((s) => new IpcManager(s.GetRequiredService<ILogger<IpcManager>>(),
pluginInterface, s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
// add mare related stuff
collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true));
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), pluginInterface));
collection.AddSingleton<ConfigurationMigrator>();
collection.AddSingleton<MareConfigService>();
collection.AddSingleton<ServerTagConfigService>();
collection.AddSingleton<TransientConfigService>();
collection.AddSingleton<NotesConfigService>();
collection.AddSingleton<ServerConfigService>();
collection.AddSingleton<MareMediator>();
collection.AddSingleton<DalamudUtil>();
collection.AddSingleton<IpcManager>();
collection.AddSingleton<FileCacheManager>();
collection.AddSingleton<CachedPlayerFactory>();
collection.AddSingleton<PairFactory>();
collection.AddSingleton<ServerConfigurationManager>();
collection.AddSingleton<PairManager>();
collection.AddSingleton<ApiController>();
collection.AddSingleton<PeriodicFileScanner>();
collection.AddSingleton<MareCharaFileManager>();
collection.AddSingleton<NotificationService>();
collection.AddSingleton<GameObjectHandlerFactory>();
collection.AddSingleton<PerformanceCollector>();
collection.AddSingleton<HubFactory>();
// func factory method singletons
collection.AddSingleton(s =>
new Func<ObjectKind, Func<nint>, bool, GameObjectHandler>((o, f, b)
=> new GameObjectHandler(s.GetRequiredService<ILogger<GameObjectHandler>>(),
s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<MareMediator>(),
s.GetRequiredService<DalamudUtilService>(),
o, f, b)));
collection.AddSingleton(s =>
new Func<OnlineUserIdentDto, CachedPlayer>((o)
=> new CachedPlayer(s.GetRequiredService<ILogger<CachedPlayer>>(),
o,
s.GetRequiredService<Func<ObjectKind, Func<nint>, bool, GameObjectHandler>>(),
s.GetRequiredService<IpcManager>(),
s.GetRequiredService<Func<FileDownloadManager>>().Invoke(),
s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<IHostApplicationLifetime>(),
s.GetRequiredService<FileCacheManager>(),
s.GetRequiredService<MareMediator>())));
collection.AddSingleton(s =>
new Func<Pair>(()
=> new Pair(s.GetRequiredService<ILogger<Pair>>(),
s.GetRequiredService<Func<OnlineUserIdentDto, CachedPlayer>>(),
s.GetRequiredService<MareConfigService>(),
s.GetRequiredService<ServerConfigurationManager>())));
collection.AddSingleton(s =>
new Func<FileDownloadManager>(()
=> new FileDownloadManager(s.GetRequiredService<ILogger<FileDownloadManager>>(),
s.GetRequiredService<MareMediator>(),
s.GetRequiredService<FileTransferOrchestrator>(),
s.GetRequiredService<FileCacheManager>())));
collection.AddSingleton<UiShared>();
collection.AddSingleton<SettingsUi>();
collection.AddSingleton<CompactUi>();
collection.AddSingleton<GposeUi>();
collection.AddSingleton<IntroUi>();
collection.AddSingleton<DownloadUi>();
// add scoped services
collection.AddScoped<PeriodicFileScanner>();
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
collection.AddScoped<WindowMediatorSubscriberBase, GposeUi>();
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
collection.AddScoped<CacheCreationService>();
collection.AddScoped<TransientResourceManager>();
collection.AddScoped<PlayerDataFactory>();
collection.AddScoped<OnlinePlayerManager>();
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface, s.GetRequiredService<MareConfigService>(),
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<UiService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<MareMediator>(), pluginInterface.UiBuilder, chatGui, s.GetRequiredService<MareConfigService>()));
collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareConfigService>(), s.GetRequiredService<DalamudUtilService>(),
pluginInterface, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped<CacheCreationService>();
collection.AddScoped<TransientResourceManager>();
collection.AddScoped<CharacterDataFactory>();
collection.AddScoped<OnlinePlayerManager>();
var serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true, ValidateScopes = true });
_pluginLogger = serviceProvider.GetRequiredService<ILogger<Plugin>>();
_pluginLogger.LogDebug("Launching " + Name);
serviceProvider.GetRequiredService<Dalamud.Localization>().SetupWithLangCode("en");
serviceProvider.GetRequiredService<DalamudPluginInterface>().UiBuilder.DisableGposeUiHide = true;
var mediator = serviceProvider.GetRequiredService<MareMediator>();
var logger = serviceProvider.GetRequiredService<ILogger<MarePlugin>>();
_plugin = new MarePlugin(logger, serviceProvider, mediator);
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
collection.AddHostedService(p => p.GetRequiredService<PerformanceCollectorService>());
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
})
.Build()
.RunAsync(_pluginCts.Token);
}
public string Name => "Mare Synchronos";
public void Dispose()
{
_pluginLogger.LogTrace($"Disposing {GetType()}");
_plugin.Dispose();
_pluginCts.Cancel();
_pluginCts.Dispose();
}
}
}

View File

@@ -0,0 +1,97 @@
using Dalamud.Game.Command;
using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI;
namespace MareSynchronos.Services;
public sealed class CommandManagerService : IDisposable
{
private const string _commandName = "/mare";
private readonly ApiController _apiController;
private readonly CommandManager _commandManager;
private readonly MareMediator _mediator;
private readonly PerformanceCollectorService _performanceCollectorService;
private readonly PeriodicFileScanner _periodicFileScanner;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiService _uiService;
public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService,
UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner,
ApiController apiController, MareMediator mediator)
{
_commandManager = commandManager;
_performanceCollectorService = performanceCollectorService;
_uiService = uiService;
_serverConfigurationManager = serverConfigurationManager;
_periodicFileScanner = periodicFileScanner;
_apiController = apiController;
_mediator = mediator;
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
{
HelpMessage = "Opens the Mare Synchronos UI"
});
}
public void Dispose()
{
_commandManager.RemoveHandler(_commandName);
}
private void OnCommand(string command, string args)
{
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (splitArgs == null || splitArgs.Length == 0)
{
// Interpret this as toggling the UI
_uiService.ToggleUi();
return;
}
if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase))
{
if (_serverConfigurationManager.CurrentServer == null) return;
var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch
{
"on" => false,
"off" => true,
_ => !_serverConfigurationManager.CurrentServer.FullPause,
} : !_serverConfigurationManager.CurrentServer.FullPause;
if (fullPause != _serverConfigurationManager.CurrentServer.FullPause)
{
_serverConfigurationManager.CurrentServer.FullPause = fullPause;
_serverConfigurationManager.Save();
_ = _apiController.CreateConnections();
}
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(GposeUi)));
}
else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{
_periodicFileScanner.InvokeScan(forced: true);
}
else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase))
{
if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds))
{
_performanceCollectorService.PrintPerformanceStats(limitBySeconds);
}
else
{
_performanceCollectorService.PrintPerformanceStats();
}
}
else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase))
{
_mediator.PrintSubscriberInfo();
}
}
}

View File

@@ -3,31 +3,144 @@ using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Numerics;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace MareSynchronos.Utils;
namespace MareSynchronos.Services;
public class DalamudUtil : IDisposable
public class DalamudUtilService : IHostedService
{
private readonly ILogger<DalamudUtil> _logger;
private readonly ClientState _clientState;
private readonly ObjectTable _objectTable;
private readonly Framework _framework;
private readonly Condition _condition;
private readonly Framework _framework;
private readonly GameGui _gameGui;
private readonly ILogger<DalamudUtilService> _logger;
private readonly MareMediator _mediator;
private readonly PerformanceCollector _performanceCollector;
private readonly ObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector;
private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.Now;
private bool _sentBetweenAreas = false;
public DalamudUtilService(ILogger<DalamudUtilService> logger, ClientState clientState, ObjectTable objectTable, Framework framework,
GameGui gameGui, Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollectorService performanceCollector)
{
_logger = logger;
_clientState = clientState;
_objectTable = objectTable;
_framework = framework;
_gameGui = gameGui;
_condition = condition;
_mediator = mediator;
_performanceCollector = performanceCollector;
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.World>(Dalamud.ClientLanguage.English)!
.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
});
}
public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget;
public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
public bool IsInCutscene { get; private set; } = false;
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
public bool IsInGpose { get; private set; } = false;
public bool IsLoggedIn { get; private set; }
public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!;
public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--";
public string PlayerNameHashed => (PlayerName + _clientState.LocalPlayer!.HomeWorld.Id).GetHash256();
public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id;
public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
return obj != null && obj.IsValid();
}
public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference)
{
return _objectTable.CreateObjectReference(reference);
}
public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index)
{
var objTableObj = _objectTable[index];
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null;
return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj;
}
public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
}
public int? GetIndexFromObjectTableByName(string characterName)
{
for (int i = 0; i < _objectTable.Length; i++)
{
if (_objectTable[i] == null) continue;
if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i;
}
return null;
}
public unsafe IntPtr GetMinion(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
return (IntPtr)((Character*)playerPointer)->CompanionObject;
}
public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
}
public unsafe IntPtr GetPet(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
}
public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName)
{
foreach (var item in _objectTable)
{
if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item;
}
return null;
}
public List<PlayerCharacter> GetPlayerCharacters()
{
return _objectTable.Where(obj =>
obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player &&
!string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Cast<PlayerCharacter>().ToList();
}
public unsafe bool IsGameObjectPresent(IntPtr key)
{
@@ -42,30 +155,92 @@ public class DalamudUtil : IDisposable
return false;
}
public DalamudUtil(ILogger<DalamudUtil> logger, ClientState clientState, ObjectTable objectTable, Framework framework,
Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollector performanceCollector)
public async Task RunOnFrameworkThread(Action act)
{
_logger.LogTrace("Running Action on framework thread: {act}", act);
await _framework.RunOnFrameworkThread(act).ConfigureAwait(false);
}
public async Task<T> RunOnFrameworkThread<T>(Func<T> func)
{
_logger.LogTrace("Running Func on framework thread: {func}", func);
return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false);
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger = logger;
_clientState = clientState;
_objectTable = objectTable;
_framework = framework;
_condition = condition;
_mediator = mediator;
_performanceCollector = performanceCollector;
_framework.Update += FrameworkOnUpdate;
if (IsLoggedIn)
{
_classJobId = _clientState.LocalPlayer!.ClassJob.Id;
}
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.World>(Dalamud.ClientLanguage.English)!
.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
});
return Task.CompletedTask;
}
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("Stopping {type}", GetType());
_framework.Update -= FrameworkOnUpdate;
return Task.CompletedTask;
}
public Vector2 WorldToScreen(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
if (obj == null) return Vector2.Zero;
return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero;
}
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
{
if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return;
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
const int tick = 250;
int curWaitTime = 0;
try
{
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while ((!ct?.IsCancellationRequested ?? true)
&& curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFramework().ConfigureAwait(true)) // 0b100000000000 is "still rendering" or something
{
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
curWaitTime += tick;
await Task.Delay(tick).ConfigureAwait(true);
}
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
}
catch (NullReferenceException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
catch (AccessViolationException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
}
public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000)
{
Thread.Sleep(500);
var obj = (GameObject*)characterAddress;
const int tick = 250;
int curWaitTime = 0;
_logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X"));
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while (obj->RenderFlags != 0x00 && curWaitTime < timeOut)
{
_logger.LogTrace($"Waiting for gpose actor to finish drawing");
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
private void FrameworkOnUpdate(Framework framework)
{
@@ -93,7 +268,6 @@ public class DalamudUtil : IDisposable
IsInCutscene = true;
_mediator.Publish(new CutsceneStartMessage());
_mediator.Publish(new HaltScanMessage("Cutscene"));
}
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene)
{
@@ -160,163 +334,4 @@ public class DalamudUtil : IDisposable
_delayedFrameworkUpdateCheck = DateTime.Now;
}
public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference)
{
return _objectTable.CreateObjectReference(reference);
}
public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget;
public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
public bool IsLoggedIn { get; private set; }
public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
return obj != null && obj.IsValid();
}
public unsafe IntPtr GetMinion(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
return (IntPtr)((Character*)playerPointer)->CompanionObject;
}
public unsafe IntPtr GetPet(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
}
public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
}
public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
}
public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--";
public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id;
public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!;
public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id);
public List<PlayerCharacter> GetPlayerCharacters()
{
return _objectTable.Where(obj =>
obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player &&
!string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Cast<PlayerCharacter>().ToList();
}
public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index)
{
var objTableObj = _objectTable[index];
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null;
return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj;
}
public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName)
{
foreach (var item in _objectTable)
{
if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item;
}
return null;
}
public int? GetIndexFromObjectTableByName(string characterName)
{
for (int i = 0; i < _objectTable.Length; i++)
{
if (_objectTable[i] == null) continue;
if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i;
}
return null;
}
public async Task RunOnFrameworkThread(Action act)
{
_logger.LogTrace("Running Action on framework thread: {act}", act);
await _framework.RunOnFrameworkThread(act).ConfigureAwait(false);
}
public async Task<T> RunOnFrameworkThread<T>(Func<T> func)
{
_logger.LogTrace("Running Func on framework thread: {func}", func);
return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false);
}
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
{
if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return;
logger.LogTrace($"[{redrawId}] Starting wait for {handler} to draw");
const int tick = 250;
int curWaitTime = 0;
try
{
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while ((!ct?.IsCancellationRequested ?? true)
&& curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFramework().ConfigureAwait(true)) // 0b100000000000 is "still rendering" or something
{
logger.LogTrace($"[{redrawId}] Waiting for {handler} to finish drawing");
curWaitTime += tick;
await Task.Delay(tick).ConfigureAwait(true);
}
logger.LogTrace($"[{redrawId}] Finished drawing after {curWaitTime}ms");
}
catch (NullReferenceException ex)
{
logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?");
}
catch (AccessViolationException ex)
{
logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?");
}
}
public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000)
{
Thread.Sleep(500);
var obj = (GameObject*)characterAddress;
const int tick = 250;
int curWaitTime = 0;
_logger.LogTrace("RenderFlags:" + obj->RenderFlags.ToString("X"));
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while (obj->RenderFlags != 0x00 && curWaitTime < timeOut)
{
_logger.LogTrace($"Waiting for gpose actor to finish drawing");
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
public void Dispose()
{
_logger.LogTrace($"Disposing {GetType()}");
_framework.Update -= FrameworkOnUpdate;
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable
{
protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator)
{
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this);
UnsubscribeAll();
}
}

View File

@@ -0,0 +1,6 @@
namespace MareSynchronos.Services.Mediator;
public interface IMediatorSubscriber
{
MareMediator Mediator { get; }
}

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.Services.Mediator;
public interface IMessage { }

View File

@@ -1,36 +1,78 @@
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using System.Text;
namespace MareSynchronos.Mediator;
namespace MareSynchronos.Services.Mediator;
public class MareMediator : IDisposable
public sealed class MareMediator : IDisposable
{
private class SubscriberAction
{
public IMediatorSubscriber Subscriber { get; }
public Action<IMessage> Action { get; }
public SubscriberAction(IMediatorSubscriber subscriber, Action<IMessage> action)
{
Subscriber = subscriber;
Action = action;
}
}
private readonly Dictionary<Type, HashSet<SubscriberAction>> _subscriberDict = new();
private readonly ILogger<MareMediator> _logger;
private readonly PerformanceCollector _performanceCollector;
private readonly object _addRemoveLock = new();
private readonly Dictionary<object, DateTime> _lastErrorTime = new();
public MareMediator(ILogger<MareMediator> logger, PerformanceCollector performanceCollector)
private readonly ILogger<MareMediator> _logger;
private readonly PerformanceCollectorService _performanceCollector;
private readonly Dictionary<Type, HashSet<SubscriberAction>> _subscriberDict = new();
public MareMediator(ILogger<MareMediator> logger, PerformanceCollectorService performanceCollector)
{
_logger = logger;
_performanceCollector = performanceCollector;
}
public void Subscribe<T>(IMediatorSubscriber subscriber, Action<IMessage> action) where T : IMessage
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType());
_subscriberDict.Clear();
GC.SuppressFinalize(this);
}
public void PrintSubscriberInfo()
{
foreach (var kvp in _subscriberDict.SelectMany(c => c.Value.Select(v => v))
.DistinctBy(p => p.Subscriber).OrderBy(p => p.Subscriber.GetType().FullName, StringComparer.Ordinal).ToList())
{
_logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().Name, kvp.Subscriber.ToString());
StringBuilder sb = new();
sb.Append("=> ");
foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == kvp.Subscriber)).ToList())
{
sb.Append(item.Key.Name).Append(", ");
}
if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal))
_logger.LogInformation("{sb}", sb.ToString());
_logger.LogInformation("---");
}
}
public void Publish<T>(T message) where T : IMessage
{
if (_subscriberDict.TryGetValue(message.GetType(), out HashSet<SubscriberAction>? subscribers) && subscribers != null && subscribers.Any())
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}", () =>
{
foreach (SubscriberAction subscriber in subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet<SubscriberAction>())
{
try
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}", () => ((Action<T>)subscriber.Action).Invoke(message));
}
catch (Exception ex)
{
if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow)
continue;
_logger.LogCritical(ex, "Error executing {type} for subscriber {subscriber}", message.GetType().Name, subscriber.Subscriber.GetType().Name);
_lastErrorTime[subscriber] = DateTime.UtcNow;
}
}
});
}
}
public void Subscribe<T>(IMediatorSubscriber subscriber, Action<T> action) where T : IMessage
{
lock (_addRemoveLock)
{
@@ -41,7 +83,7 @@ public class MareMediator : IDisposable
throw new InvalidOperationException("Already subscribed");
}
_logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T), subscriber);
_logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name);
}
}
@@ -56,33 +98,6 @@ public class MareMediator : IDisposable
}
}
public void Publish(IMessage message)
{
if (_subscriberDict.TryGetValue(message.GetType(), out HashSet<SubscriberAction>? subscribers) && subscribers != null && subscribers.Any())
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}", () =>
{
foreach (SubscriberAction subscriber in subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet<SubscriberAction>())
{
try
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}", () => subscriber.Action.Invoke(message));
}
catch (Exception ex)
{
if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime))
{
if (lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow) continue;
}
_logger.LogCritical(ex, "Error executing {type} for subscriber {subscriber}", message.GetType().Name, subscriber.Subscriber.GetType().Name);
_lastErrorTime[subscriber] = DateTime.UtcNow;
}
}
});
}
}
internal void UnsubscribeAll(IMediatorSubscriber subscriber)
{
lock (_addRemoveLock)
@@ -92,41 +107,25 @@ public class MareMediator : IDisposable
int unSubbed = _subscriberDict[kvp.Key]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0;
if (unSubbed > 0)
{
_logger.LogDebug("{sub} unsubscribed from {msg}", subscriber, kvp.Key.Name);
_logger.LogTrace("Remaining Subscribers:");
foreach (var item in _subscriberDict[kvp.Key])
_logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Key.Name);
if (_subscriberDict[kvp.Key].Any())
{
_logger.LogTrace("{Key}: {item}", kvp.Key, item.Subscriber);
_logger.LogTrace("Remaining Subscribers: {item}", string.Join(", ", _subscriberDict[kvp.Key].Select(k => k.Subscriber.GetType().Name)));
}
}
}
}
}
public void PrintSubscriberInfo()
private sealed class SubscriberAction
{
foreach (var kvp in _subscriberDict.ToList().SelectMany(c => c.Value.Select(v => v))
.DistinctBy(p => p.Subscriber).OrderBy(p => p.Subscriber.GetType().FullName, StringComparer.Ordinal))
public SubscriberAction(IMediatorSubscriber subscriber, object action)
{
_logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().FullName, kvp.Subscriber.ToString());
StringBuilder sb = new();
sb.Append("=> ");
foreach (var item in _subscriberDict.ToList())
{
if (item.Value.Any(v => v.Subscriber == kvp.Subscriber))
{
sb.Append(item.Key.Name + ", ");
}
}
if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal))
_logger.LogInformation("{sb}", sb.ToString());
_logger.LogInformation("---");
Subscriber = subscriber;
Action = action;
}
}
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType());
_subscriberDict.Clear();
public object Action { get; }
public IMediatorSubscriber Subscriber { get; }
}
}

View File

@@ -1,20 +1,23 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Mediator;
namespace MareSynchronos.Services.Mediator;
public abstract class MediatorSubscriberBase : IMediatorSubscriber
{
protected ILogger _logger { get; }
public MareMediator Mediator { get; }
protected MediatorSubscriberBase(ILogger logger, MareMediator mediator)
{
_logger = logger;
Logger = logger;
Logger.LogTrace("Creating {type} ({this})", GetType().Name, this);
Mediator = mediator;
}
public virtual void Dispose()
public MareMediator Mediator { get; }
protected ILogger Logger { get; }
protected void UnsubscribeAll()
{
_logger.LogTrace("Disposing {type} ({this})", GetType(), this);
Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this);
Mediator.UnsubscribeAll(this);
}
}
}

View File

@@ -1,7 +1,9 @@
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.Models;
using MareSynchronos.API.Dto;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.WebAPI.Files.Models;
namespace MareSynchronos.Mediator;
namespace MareSynchronos.Services.Mediator;
#pragma warning disable MA0048 // File name must match type name
public record SwitchToIntroUiMessage : IMessage;
@@ -19,16 +21,16 @@ public record GposeStartMessage : IMessage;
public record GposeEndMessage : IMessage;
public record CutsceneEndMessage : IMessage;
public record CutsceneFrameworkUpdateMessage : IMessage;
public record ConnectedMessage : IMessage;
public record ConnectedMessage(ConnectionDto Connection) : IMessage;
public record DisconnectedMessage : IMessage;
public record PenumbraModSettingChangedMessage : IMessage;
public record PenumbraInitializedMessage : IMessage;
public record PenumbraDisposedMessage : IMessage;
public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : IMessage;
public record HeelsOffsetMessage() : IMessage;
public record HeelsOffsetMessage : IMessage;
public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : IMessage;
public record CustomizePlusMessage() : IMessage;
public record PalettePlusMessage() : IMessage;
public record CustomizePlusMessage : IMessage;
public record PalettePlusMessage : IMessage;
public record PlayerChangedMessage(API.Data.CharacterData Data) : IMessage;
public record CharacterChangedMessage(GameObjectHandler GameObjectHandler) : IMessage;
public record TransientResourceChangedMessage(IntPtr Address) : IMessage;
@@ -46,4 +48,10 @@ public record PenumbraEndRedrawMessage(IntPtr Address) : IMessage;
public record HubReconnectingMessage(Exception? Exception) : IMessage;
public record HubReconnectedMessage(string? Arg) : IMessage;
public record HubClosedMessage(Exception? Exception) : IMessage;
public record DownloadReadyMessage(Guid RequestId) : IMessage;
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : IMessage;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : IMessage;
public record UiToggleMessage(Type UiType) : IMessage;
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : IMessage;
#pragma warning restore MA0048 // File name must match type name

View File

@@ -0,0 +1,45 @@
using Dalamud.Interface.Windowing;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable
{
protected readonly ILogger _logger;
protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name) : base(name)
{
_logger = logger;
Mediator = mediator;
_logger.LogTrace("Creating {type}", GetType());
Mediator.Subscribe<UiToggleMessage>(this, (msg) =>
{
if (msg.UiType == GetType())
{
Toggle();
}
});
}
public MareMediator Mediator { get; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void Dispose(bool disposing)
{
_logger.LogTrace("Disposing {type}", GetType());
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -4,84 +4,30 @@ using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Mediator;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Managers;
public class NotificationService : MediatorSubscriberBase
namespace MareSynchronos.Services;
public class NotificationService : DisposableMediatorSubscriberBase
{
private readonly UiBuilder _uiBuilder;
private readonly ChatGui _chatGui;
private readonly MareConfigService _configurationService;
private readonly UiBuilder _uiBuilder;
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator, UiBuilder uiBuilder, ChatGui chatGui, MareConfigService configurationService) : base(logger, mediator)
{
_uiBuilder = uiBuilder;
_chatGui = chatGui;
_configurationService = configurationService;
mediator.Subscribe<NotificationMessage>(this, (msg) => ShowNotification((NotificationMessage)msg));
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
}
private void ShowNotification(NotificationMessage msg)
private void PrintErrorChat(string? message)
{
_logger.LogInformation(msg.ToString());
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
_uiBuilder.AddNotification(msg.Message ?? string.Empty, "[Mare Synchronos] " + msg.Title, msg.Type, msg.TimeShownOnScreen);
}
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
@@ -96,9 +42,72 @@ public class NotificationService : MediatorSubscriberBase
_chatGui.Print(se.BuiltString);
}
private void PrintErrorChat(string? message)
private void ShowChat(NotificationMessage msg)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message);
_chatGui.PrintError(se.BuiltString);
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
}
}
private void ShowNotification(NotificationMessage msg)
{
Logger.LogInformation("{msg}", msg.ToString());
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
_uiBuilder.AddNotification(msg.Message ?? string.Empty, "[Mare Synchronos] " + msg.Title, msg.Type, msg.TimeShownOnScreen);
}
}

View File

@@ -1,55 +1,26 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace MareSynchronos.Utils;
namespace MareSynchronos.Services;
public class PerformanceCollector : IDisposable
public sealed class PerformanceCollectorService : IHostedService
{
private readonly ConcurrentDictionary<string, RollingList<Tuple<TimeOnly, long>>> _performanceCounters = new(StringComparer.Ordinal);
private readonly ILogger<PerformanceCollector> _logger;
private readonly MareConfigService _mareConfigService;
private const string _counterSplit = "=>";
private readonly ILogger<PerformanceCollectorService> _logger;
private readonly MareConfigService _mareConfigService;
private readonly ConcurrentDictionary<string, RollingList<Tuple<TimeOnly, long>>> _performanceCounters = new(StringComparer.Ordinal);
private readonly CancellationTokenSource _periodicLogPruneTask = new();
public PerformanceCollector(ILogger<PerformanceCollector> logger, MareConfigService mareConfigService)
public PerformanceCollectorService(ILogger<PerformanceCollectorService> logger, MareConfigService mareConfigService)
{
_logger = logger;
_mareConfigService = mareConfigService;
_ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token);
}
public void Dispose()
{
_logger.LogTrace("Disposing {this}", GetType());
_periodicLogPruneTask.Cancel();
}
private async Task PeriodicLogPrune()
{
while (!_periodicLogPruneTask.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTask.Token).ConfigureAwait(false);
foreach (var entries in _performanceCounters.ToList())
{
try
{
var last = entries.Value.ToList().Last();
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now))
{
_performanceCounters.Remove(entries.Key, out _);
}
}
catch (Exception e)
{
_logger.LogDebug("Error removing performance counter {counter}", entries.Key);
}
}
}
}
public T LogPerformance<T>(object sender, string counterName, Func<T> func)
@@ -68,10 +39,6 @@ public class PerformanceCollector : IDisposable
{
return func.Invoke();
}
catch
{
throw;
}
finally
{
st.Stop();
@@ -95,16 +62,23 @@ public class PerformanceCollector : IDisposable
{
act.Invoke();
}
catch
{
throw;
}
finally
{
st.Stop();
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), st.ElapsedTicks));
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_periodicLogPruneTask.Cancel();
return Task.CompletedTask;
}
internal void PrintPerformanceStats(int limitBySeconds = 0)
@@ -138,7 +112,7 @@ public class PerformanceCollector : IDisposable
sb.Append("-Counter Name".PadRight(longestCounterName, '-'));
sb.AppendLine();
var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList();
var previousCaller = orderedData.First().Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
var previousCaller = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
foreach (var entry in orderedData)
{
var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
@@ -196,4 +170,28 @@ public class PerformanceCollector : IDisposable
sb.Append("".PadRight(longestCounterName, '-'));
sb.AppendLine();
}
}
private async Task PeriodicLogPrune()
{
while (!_periodicLogPruneTask.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTask.Token).ConfigureAwait(false);
foreach (var entries in _performanceCounters.ToList())
{
try
{
var last = entries.Value.ToList()[^1];
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now))
{
_performanceCounters.Remove(entries.Key, out _);
}
}
catch (Exception e)
{
_logger.LogDebug(e, "Error removing performance counter {counter}", entries.Key);
}
}
}
}
}

View File

@@ -1,3 +1,3 @@
namespace MareSynchronos.Models;
namespace MareSynchronos.Services.ServerConfiguration;
public record JwtCache(string ApiUrl, string PlayerName, uint WorldId, string SecretKey);

View File

@@ -1,38 +1,22 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Models;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace MareSynchronos.Managers;
namespace MareSynchronos.Services.ServerConfiguration;
public class ServerConfigurationManager
{
private readonly Dictionary<JwtCache, string> _tokenDictionary = new();
private readonly ILogger<ServerConfigurationManager> _logger;
private readonly ServerConfigService _configService;
private readonly ServerTagConfigService _serverTagConfig;
private readonly DalamudUtilService _dalamudUtil;
private readonly ILogger<ServerConfigurationManager> _logger;
private readonly NotesConfigService _notesConfig;
private readonly DalamudUtil _dalamudUtil;
public string CurrentApiUrl => string.IsNullOrEmpty(_configService.Current.CurrentServer) ? ApiController.MainServiceUri : _configService.Current.CurrentServer;
public ServerStorage? CurrentServer => (_configService.Current.ServerStorage.ContainsKey(CurrentApiUrl) ? _configService.Current.ServerStorage[CurrentApiUrl] : null);
private ServerTagStorage CurrentServerTagStorage()
{
TryCreateCurrentServerTagStorage();
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private ServerNotesStorage CurrentNotesStorage()
{
TryCreateCurrentNotesStorage();
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
}
private readonly ServerTagConfigService _serverTagConfig;
private readonly Dictionary<JwtCache, string> _tokenDictionary = new();
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtil dalamudUtil)
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil)
{
_logger = logger;
_configService = configService;
@@ -41,58 +25,14 @@ public class ServerConfigurationManager
_dalamudUtil = dalamudUtil;
}
public bool HasValidConfig()
{
return CurrentServer != null;
}
public string[] GetServerApiUrls()
{
return _configService.Current.ServerStorage.Keys.ToArray();
}
public string[] GetServerNames()
{
return _configService.Current.ServerStorage.Values.Select(v => v.ServerName).ToArray();
}
public ServerStorage GetServerByIndex(int idx)
{
try
{
return _configService.Current.ServerStorage.ElementAt(idx).Value;
}
catch
{
_configService.Current.CurrentServer = ApiController.MainServiceUri;
if (!_configService.Current.ServerStorage.ContainsKey(ApiController.MainServer))
{
_configService.Current.ServerStorage.Add(_configService.Current.CurrentServer, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer });
}
Save();
return CurrentServer!;
}
}
public string CurrentApiUrl => string.IsNullOrEmpty(_configService.Current.CurrentServer) ? ApiController.MainServiceUri : _configService.Current.CurrentServer;
public ServerStorage? CurrentServer => _configService.Current.ServerStorage.TryGetValue(CurrentApiUrl, out ServerStorage? value) ? value : null;
public int GetCurrentServerIndex()
{
return Array.IndexOf(_configService.Current.ServerStorage.Keys.ToArray(), CurrentApiUrl);
}
public void Save()
{
var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
_logger.LogDebug(caller + " Calling config save");
_configService.Save();
}
public void SelectServer(int idx)
{
_configService.Current.CurrentServer = GetServerByIndex(idx).ServerUri;
CurrentServer!.FullPause = false;
Save();
}
public string? GetSecretKey(int serverIdx = -1)
{
ServerStorage? currentServer;
@@ -128,6 +68,34 @@ public class ServerConfigurationManager
return null;
}
public string[] GetServerApiUrls()
{
return _configService.Current.ServerStorage.Keys.ToArray();
}
public ServerStorage GetServerByIndex(int idx)
{
try
{
return _configService.Current.ServerStorage.ElementAt(idx).Value;
}
catch
{
_configService.Current.CurrentServer = ApiController.MainServiceUri;
if (!_configService.Current.ServerStorage.ContainsKey(ApiController.MainServer))
{
_configService.Current.ServerStorage.Add(_configService.Current.CurrentServer, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer });
}
Save();
return CurrentServer!;
}
}
public string[] GetServerNames()
{
return _configService.Current.ServerStorage.Values.Select(v => v.ServerName).ToArray();
}
public string? GetToken()
{
var charaName = _dalamudUtil.PlayerName;
@@ -142,6 +110,18 @@ public class ServerConfigurationManager
return null;
}
public bool HasValidConfig()
{
return CurrentServer != null;
}
public void Save()
{
var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
_logger.LogDebug(caller + " Calling config save");
_configService.Save();
}
public void SaveToken(string token)
{
var charaName = _dalamudUtil.PlayerName;
@@ -151,6 +131,13 @@ public class ServerConfigurationManager
_tokenDictionary[new JwtCache(CurrentApiUrl, charaName, worldId, secretKey)] = token;
}
public void SelectServer(int idx)
{
_configService.Current.CurrentServer = GetServerByIndex(idx).ServerUri;
CurrentServer!.FullPause = false;
Save();
}
internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1, bool addLastSecretKey = false)
{
if (serverSelectionIndex == -1) serverSelectionIndex = GetCurrentServerIndex();
@@ -171,11 +158,10 @@ public class ServerConfigurationManager
Save();
}
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
internal void AddOpenPairTag(string tag)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Remove(item);
Save();
CurrentServerTagStorage().OpenPairTags.Add(tag);
_serverTagConfig.Save();
}
internal void AddServer(ServerStorage serverStorage)
@@ -184,71 +170,12 @@ public class ServerConfigurationManager
Save();
}
internal void DeleteServer(ServerStorage selectedServer)
{
_configService.Current.ServerStorage.Remove(selectedServer.ServerUri);
Save();
}
internal void AddOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Add(tag);
_serverTagConfig.Save();
}
internal void RemoveOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Remove(tag);
_serverTagConfig.Save();
}
internal bool ContainsOpenPairTag(string tag)
{
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
}
internal HashSet<string> GetServerAvailablePairTags()
{
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal void AddTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Add(tag);
_serverTagConfig.Save();
}
internal void RemoveTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
foreach (var uid in GetUidsForTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
_serverTagConfig.Save();
}
private void TryCreateCurrentServerTagStorage()
{
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentNotesStorage()
{
if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl))
{
_notesConfig.Current.ServerNotes[CurrentApiUrl] = new();
}
}
internal void AddTagForUid(string uid, string tagName)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
@@ -263,14 +190,9 @@ public class ServerConfigurationManager
_serverTagConfig.Save();
}
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
internal bool ContainsOpenPairTag(string tag)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Remove(tagName);
if (save)
_serverTagConfig.Save();
}
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
}
internal bool ContainsTag(string uid, string tag)
@@ -283,41 +205,10 @@ public class ServerConfigurationManager
return false;
}
internal bool HasTags(string uid)
internal void DeleteServer(ServerStorage selectedServer)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Any();
}
return false;
}
internal HashSet<string> GetUidsForTag(string tag)
{
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
}
internal string? GetNoteForUid(string uid)
{
if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal void SetNoteForUid(string uid, string note, bool save = true)
{
CurrentNotesStorage().UidServerComments[uid] = note;
if (save)
_notesConfig.Save();
}
internal void SaveNotes()
{
_notesConfig.Save();
_configService.Current.ServerStorage.Remove(selectedServer.ServerUri);
Save();
}
internal string? GetNoteForGid(string gID)
@@ -331,10 +222,118 @@ public class ServerConfigurationManager
return null;
}
internal string? GetNoteForUid(string uid)
{
if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal HashSet<string> GetServerAvailablePairTags()
{
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
}
internal HashSet<string> GetUidsForTag(string tag)
{
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
}
internal bool HasTags(string uid)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Any();
}
return false;
}
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Remove(item);
Save();
}
internal void RemoveOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Remove(tag);
_serverTagConfig.Save();
}
internal void RemoveTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
foreach (var uid in GetUidsForTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
_serverTagConfig.Save();
}
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Remove(tagName);
if (save)
_serverTagConfig.Save();
}
}
internal void SaveNotes()
{
_notesConfig.Save();
}
internal void SetNoteForGid(string gid, string note, bool save = true)
{
CurrentNotesStorage().GidServerComments[gid] = note;
if (save)
_notesConfig.Save();
}
}
internal void SetNoteForUid(string uid, string note, bool save = true)
{
CurrentNotesStorage().UidServerComments[uid] = note;
if (save)
_notesConfig.Save();
}
private ServerNotesStorage CurrentNotesStorage()
{
TryCreateCurrentNotesStorage();
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
}
private ServerTagStorage CurrentServerTagStorage()
{
TryCreateCurrentServerTagStorage();
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private void TryCreateCurrentNotesStorage()
{
if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl))
{
_notesConfig.Current.ServerNotes[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentServerTagStorage()
{
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
}

View File

@@ -0,0 +1,66 @@
using Dalamud.Plugin;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using MareSynchronos.UI;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class UiService : IDisposable
{
private readonly DalamudPluginInterface _dalamudPluginInterface;
private readonly FileDialogManager _fileDialogManager;
private readonly ILogger<UiService> _logger;
private readonly MareConfigService _mareConfigService;
private readonly MareMediator _mareMediator;
private readonly WindowSystem _windowSystem;
public UiService(ILogger<UiService> logger, DalamudPluginInterface dalamudPluginInterface,
MareConfigService mareConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows,
FileDialogManager fileDialogManager, MareMediator mareMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);
_dalamudPluginInterface = dalamudPluginInterface;
_mareConfigService = mareConfigService;
_windowSystem = windowSystem;
_fileDialogManager = fileDialogManager;
_mareMediator = mareMediator;
_dalamudPluginInterface.UiBuilder.DisableGposeUiHide = true;
_dalamudPluginInterface.UiBuilder.Draw += Draw;
_dalamudPluginInterface.UiBuilder.OpenConfigUi += ToggleUi;
foreach (var window in windows)
{
_windowSystem.AddWindow(window);
}
}
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType().Name);
_windowSystem.RemoveAllWindows();
_dalamudPluginInterface.UiBuilder.Draw -= Draw;
_dalamudPluginInterface.UiBuilder.OpenConfigUi -= ToggleUi;
}
public void ToggleUi()
{
if (_mareConfigService.Current.HasValidSetup())
_mareMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
else
_mareMediator.Publish(new UiToggleMessage(typeof(IntroUi)));
}
private void Draw()
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
}

View File

@@ -1,126 +1,117 @@
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using ImGuiNET;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Managers;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Models;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI;
using MareSynchronos.WebAPI.Files;
using MareSynchronos.WebAPI.Files.Models;
using MareSynchronos.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.UI;
public class CompactUi : WindowMediatorSubscriberBase, IDisposable
public class CompactUi : WindowMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly PairManager _pairManager;
private readonly ServerConfigurationManager _serverManager;
private readonly MareConfigService _configService;
private readonly TagHandler _tagHandler;
public readonly Dictionary<string, bool> ShowUidForEntry = new(StringComparer.Ordinal);
private readonly UiShared _uiShared;
private readonly WindowSystem _windowSystem;
private string _characterOrCommentFilter = string.Empty;
public string EditUserComment = string.Empty;
public string EditNickEntry = string.Empty;
private string _pairToAdd = string.Empty;
private readonly Stopwatch _timeout = new();
private bool _buttonState;
public string EditUserComment = string.Empty;
public float TransferPartHeight;
public float WindowContentWidth;
private bool _showModalForUserAddition;
private bool _wasOpen;
private bool _showSyncShells;
private readonly ApiController _apiController;
private readonly MareConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly FileUploadManager _fileTransferManager;
private readonly GroupPanel _groupPanel;
private Pair? _lastAddedUser;
private string _lastAddedUserComment = string.Empty;
private readonly PairGroupsUi _pairGroupsUi;
private readonly PairManager _pairManager;
private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly SelectPairForGroupUi _selectPairsForGroupUi;
private readonly PairGroupsUi _pairGroupsUi;
private readonly ServerConfigurationManager _serverManager;
private readonly Stopwatch _timeout = new();
private readonly UiSharedService _uiShared;
private bool _buttonState;
private string _characterOrCommentFilter = string.Empty;
private Pair? _lastAddedUser;
private string _lastAddedUserComment = string.Empty;
private string _pairToAdd = string.Empty;
private int _secretKeyIdx = 0;
private bool _showModalForUserAddition;
private bool _showSyncShells;
private bool _wasOpen;
public CompactUi(ILogger<CompactUi> logger, WindowSystem windowSystem,
UiShared uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager,
ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator, "###MareSynchronosMainUI")
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator, "###MareSynchronosMainUI")
{
#if DEBUG
string dev = "Dev Build";
var ver = Assembly.GetExecutingAssembly().GetName().Version;
this.WindowName = $"Mare Synchronos {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###MareSynchronosMainUI";
Toggle();
#else
var ver = Assembly.GetExecutingAssembly().GetName().Version;
this.WindowName = "Mare Synchronos " + ver.Major + "." + ver.Minor + "." + ver.Build + "###MareSynchronosMainUI";
#endif
_logger.LogTrace("Creating " + nameof(CompactUi));
_windowSystem = windowSystem;
_uiShared = uiShared;
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
_serverManager = serverManager;
_tagHandler = new(_serverManager);
_fileTransferManager = fileTransferManager;
var tagHandler = new TagHandler(_serverManager);
_groupPanel = new(this, uiShared, _pairManager, _serverManager, _configService);
_selectGroupForPairUi = new(_tagHandler);
_selectPairsForGroupUi = new(_tagHandler);
_pairGroupsUi = new(_tagHandler, DrawPairedClient, apiController, _selectPairsForGroupUi);
_selectGroupForPairUi = new(tagHandler);
_selectPairsForGroupUi = new(tagHandler);
_pairGroupsUi = new(configService, tagHandler, DrawPairedClient, apiController, _selectPairsForGroupUi);
#if DEBUG
string dev = "Dev Build";
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
WindowName = $"Mare Synchronos {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###MareSynchronosMainUI";
Toggle();
#else
var ver = Assembly.GetExecutingAssembly().GetName().Version;
WindowName = "Mare Synchronos " + ver.Major + "." + ver.Minor + "." + ver.Build + "###MareSynchronosMainUI";
#endif
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiShared_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiShared_GposeEnd());
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(350, 400),
MaximumSize = new Vector2(350, 2000),
};
windowSystem.AddWindow(this);
}
private void UiShared_GposeEnd()
{
IsOpen = _wasOpen;
}
private void UiShared_GposeStart()
{
_wasOpen = IsOpen;
IsOpen = false;
}
public override void Dispose()
{
base.Dispose();
_windowSystem.RemoveWindow(this);
}
public override void Draw()
{
WindowContentWidth = UiShared.GetWindowContentRegionWidth();
UiShared.DrawWithID("header", DrawUIDHeader);
WindowContentWidth = UiSharedService.GetWindowContentRegionWidth();
if (!_apiController.IsCurrentVersion)
{
var ver = _apiController.CurrentClientVersion;
if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont);
var unsupported = "UNSUPPORTED VERSION";
var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported);
if (_uiShared.UidFontBuilt) ImGui.PopFont();
UiSharedService.ColorTextWrapped($"Your Mare Synchronos installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
$"It is highly recommended to keep Mare Synchronos up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed);
}
UiSharedService.DrawWithID("header", DrawUIDHeader);
ImGui.Separator();
UiShared.DrawWithID("serverstatus", DrawServerStatus);
UiSharedService.DrawWithID("serverstatus", DrawServerStatus);
if (_apiController.ServerState is ServerState.Connected)
{
@@ -131,7 +122,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]);
}
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{
_showSyncShells = false;
}
@@ -140,7 +131,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
ImGui.PopStyleColor();
}
ImGui.PopFont();
UiShared.AttachToolTip("Individual pairs");
UiSharedService.AttachToolTip("Individual pairs");
ImGui.SameLine();
@@ -149,7 +140,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]);
}
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{
_showSyncShells = true;
}
@@ -159,23 +150,22 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
}
ImGui.PopFont();
UiShared.AttachToolTip("Syncshells");
UiSharedService.AttachToolTip("Syncshells");
ImGui.Separator();
if (!hasShownSyncShells)
{
UiShared.DrawWithID("pairlist", DrawPairList);
UiSharedService.DrawWithID("pairlist", DrawPairList);
}
else
{
UiShared.DrawWithID("syncshells", _groupPanel.DrawSyncshells);
UiSharedService.DrawWithID("syncshells", _groupPanel.DrawSyncshells);
}
ImGui.Separator();
UiShared.DrawWithID("transfers", DrawTransfers);
UiSharedService.DrawWithID("transfers", DrawTransfers);
TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight;
UiShared.DrawWithID("group-user-popup", () => _selectPairsForGroupUi.Draw(_pairManager.DirectPairs, ShowUidForEntry));
UiShared.DrawWithID("grouping-popup", () => _selectGroupForPairUi.Draw(ShowUidForEntry));
UiSharedService.DrawWithID("group-user-popup", () => _selectPairsForGroupUi.Draw(_pairManager.DirectPairs, ShowUidForEntry));
UiSharedService.DrawWithID("grouping-popup", () => _selectGroupForPairUi.Draw(ShowUidForEntry));
}
if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null)
@@ -187,7 +177,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_lastAddedUserComment = string.Empty;
}
if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiShared.PopupWindowFlags))
if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags))
{
if (_lastAddedUser == null)
{
@@ -195,9 +185,9 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
}
else
{
UiShared.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:");
UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:");
ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100);
if (UiShared.IconTextButton(FontAwesomeIcon.Save, "Save Note"))
if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note"))
{
_serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment);
_lastAddedUser = null;
@@ -205,7 +195,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_showModalForUserAddition = false;
}
}
UiShared.SetScaledWindowSize(275);
UiSharedService.SetScaledWindowSize(275);
ImGui.EndPopup();
}
}
@@ -217,12 +207,42 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
base.OnClose();
}
private void DrawAddCharacter()
{
ImGui.Dummy(new(10));
var keys = _serverManager.CurrentServer!.SecretKeys;
if (keys.TryGetValue(_secretKeyIdx, out var secretKey))
{
var friendlyName = secretKey.FriendlyName;
if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key"))
{
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
{
CharacterName = _uiShared.PlayerName,
WorldId = _uiShared.WorldId,
SecretKeyIdx = _secretKeyIdx
});
_serverManager.Save();
_ = _apiController.CreateConnections(forceGetToken: true);
}
_uiShared.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key);
}
else
{
UiSharedService.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow);
}
}
private void DrawAddPair()
{
var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus);
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
var canAdd = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal));
if (!canAdd)
{
@@ -235,7 +255,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_ = _apiController.UserAddPair(new(new(_pairToAdd)));
_pairToAdd = string.Empty;
}
UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
}
ImGuiHelpers.ScaledDummy(2);
@@ -243,8 +263,8 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
private void DrawFilter()
{
var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp);
var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play);
var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowUp);
var playButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Play);
if (!_configService.Current.ReverseUserSort)
{
if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown))
@@ -252,7 +272,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_configService.Current.ReverseUserSort = true;
_configService.Save();
}
UiShared.AttachToolTip("Sort by name descending");
UiSharedService.AttachToolTip("Sort by name descending");
}
else
{
@@ -261,7 +281,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_configService.Current.ReverseUserSort = false;
_configService.Save();
}
UiShared.AttachToolTip("Sort by name ascending");
UiSharedService.AttachToolTip("Sort by name ascending");
}
ImGui.SameLine();
@@ -288,12 +308,15 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
case true when !pausedUsers.Any():
_buttonState = false;
break;
case false when !resumedUsers.Any():
_buttonState = true;
break;
case true:
users = pausedUsers;
break;
case false:
users = resumedUsers;
break;
@@ -305,28 +328,25 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
_timeout.Reset();
if (ImGuiComponents.IconButton(button))
if (ImGuiComponents.IconButton(button) && UiSharedService.CtrlPressed())
{
if (UiShared.CtrlPressed())
foreach (var entry in users)
{
foreach (var entry in users)
{
var perm = entry.UserPair!.OwnPermissions;
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, perm));
}
_timeout.Start();
_buttonState = !_buttonState;
var perm = entry.UserPair!.OwnPermissions;
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, perm));
}
_timeout.Start();
_buttonState = !_buttonState;
}
UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users.");
UiSharedService.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users.");
}
else
{
var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000;
ImGuiComponents.DisabledButton(button);
UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds");
UiSharedService.AttachToolTip($"Next execution is available at {availableAt} seconds");
}
}
@@ -335,14 +355,14 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (entry.UserPair == null) return;
var pauseIcon = entry.UserPair!.OwnPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseIconSize = UiShared.GetIconButtonSize(pauseIcon);
var barButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars);
var pauseIconSize = UiSharedService.GetIconButtonSize(pauseIcon);
var barButtonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars);
var entryUID = entry.UserData.AliasOrUID;
var textSize = ImGui.CalcTextSize(entryUID);
var originalY = ImGui.GetCursorPosY();
var buttonSizes = pauseIconSize.Y + barButtonSize.Y;
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth();
var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
var textPos = originalY + pauseIconSize.Y / 2 - textSize.Y / 2;
ImGui.SetCursorPosY(textPos);
@@ -369,17 +389,17 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
}
ImGui.PushFont(UiBuilder.IconFont);
UiShared.ColorText(connectionIcon.ToIconString(), connectionColor);
UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor);
ImGui.PopFont();
UiShared.AttachToolTip(connectionText);
UiSharedService.AttachToolTip(connectionText);
ImGui.SameLine();
ImGui.SetCursorPosY(textPos);
if (entry is { IsOnline: true, IsVisible: true })
{
ImGui.PushFont(UiBuilder.IconFont);
UiShared.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen);
UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen);
ImGui.PopFont();
UiShared.AttachToolTip(entryUID + " is visible: " + entry.PlayerName!);
UiSharedService.AttachToolTip(entryUID + " is visible: " + entry.PlayerName!);
}
var textIsUid = true;
@@ -414,7 +434,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(playerText);
if (textIsUid) ImGui.PopFont();
UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine +
UiSharedService.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine +
"Right click to change nick for " + entryUID);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
@@ -429,7 +449,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
var pair = _pairManager.DirectPairs.Find(p => p.UserData.UID == EditNickEntry);
var pair = _pairManager.DirectPairs.Find(p => string.Equals(p.UserData.UID, EditNickEntry, StringComparison.Ordinal));
if (pair != null)
{
pair.SetNote(EditUserComment);
@@ -443,7 +463,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
ImGui.SetCursorPosY(originalY);
ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2);
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2);
if (ImGui.InputTextWithHint("", "Nick/Notes", ref EditUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue))
{
_serverManager.SetNoteForUid(entry.UserPair!.User.UID, EditUserComment);
@@ -454,12 +474,58 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
EditNickEntry = string.Empty;
}
UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel");
UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel");
}
// Pause Button
// Pause Button && sound warnings
if (entry.UserPair!.OwnPermissions.IsPaired() && entry.UserPair!.OtherPermissions.IsPaired())
{
var individualSoundsDisabled = (entry.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (entry.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
var individualAnimDisabled = (entry.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (entry.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
if (individualAnimDisabled || individualSoundsDisabled)
{
var infoIconPosDist = windowEndX - barButtonSize.X - spacingX - pauseIconSize.X - spacingX;
var icon = FontAwesomeIcon.ExclamationTriangle;
var iconwidth = UiSharedService.GetIconSize(icon);
ImGui.SameLine(infoIconPosDist - iconwidth.X);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont);
ImGui.PopStyleColor();
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text("Individual User permissions");
if (individualSoundsDisabled)
{
var userSoundsText = "Sound sync disabled with " + entry.UserData.AliasOrUID;
UiSharedService.FontText(FontAwesomeIcon.VolumeOff.ToIconString(), UiBuilder.IconFont);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.Text(userSoundsText);
ImGui.NewLine();
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.Text("You: " + (entry.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (entry.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled"));
}
if (individualAnimDisabled)
{
var userAnimText = "Animation sync disabled with " + entry.UserData.AliasOrUID;
UiSharedService.FontText(FontAwesomeIcon.Stop.ToIconString(), UiBuilder.IconFont);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.Text(userAnimText);
ImGui.NewLine();
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.Text("You: " + (entry.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (entry.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled"));
}
ImGui.EndTooltip();
}
}
ImGui.SameLine(windowEndX - barButtonSize.X - spacingX - pauseIconSize.X);
ImGui.SetCursorPosY(originalY);
if (ImGuiComponents.IconButton(pauseIcon))
@@ -468,7 +534,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(entry.UserData, perm));
}
UiShared.AttachToolTip(!entry.UserPair!.OwnPermissions.IsPaused()
UiSharedService.AttachToolTip(!entry.UserPair!.OwnPermissions.IsPaused()
? "Pause pairing with " + entryUID
: "Resume pairing with " + entryUID);
}
@@ -483,7 +549,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
}
if (ImGui.BeginPopup("User Flyout Menu"))
{
UiShared.DrawWithID($"buttons-{entry.UserPair!.User.UID}", () => DrawPairedClientMenu(entry));
UiSharedService.DrawWithID($"buttons-{entry.UserPair!.User.UID}", () => DrawPairedClientMenu(entry));
ImGui.EndPopup();
}
}
@@ -492,37 +558,54 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
{
if (entry.IsVisible)
{
if (UiShared.IconTextButton(FontAwesomeIcon.Sync, "Reload last data"))
if (UiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data"))
{
entry.ApplyLastReceivedData(forced: true);
ImGui.CloseCurrentPopup();
}
UiShared.AttachToolTip("This reapplies the last received character data to this character");
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
}
var entryUID = entry.UserData.AliasOrUID;
if (UiShared.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups"))
if (UiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups"))
{
_selectGroupForPairUi.Open(entry);
}
UiShared.AttachToolTip("Choose pair groups for " + entryUID);
UiSharedService.AttachToolTip("Choose pair groups for " + entryUID);
if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently"))
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute;
if (UiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText))
{
if (UiShared.CtrlPressed())
{
_ = _apiController.UserRemovePair(new(entry.UserData));
}
var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableSounds(!isDisableSounds);
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
}
UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations();
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop;
if (UiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText))
{
var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableAnimations(!isDisableAnims);
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
}
if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed())
{
_ = _apiController.UserRemovePair(new(entry.UserData));
}
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
}
private void DrawPairList()
{
UiShared.DrawWithID("addpair", DrawAddPair);
UiShared.DrawWithID("pairs", DrawPairs);
UiSharedService.DrawWithID("addpair", DrawAddPair);
UiSharedService.DrawWithID("pairs", DrawPairs);
TransferPartHeight = ImGui.GetCursorPosY();
UiShared.DrawWithID("filter", DrawFilter);
UiSharedService.DrawWithID("filter", DrawFilter);
}
private void DrawPairs()
@@ -552,20 +635,9 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
ImGui.EndChild();
}
private List<Pair> GetFilteredUsers()
{
return _pairManager.DirectPairs.Where(p =>
{
if (_characterOrCommentFilter.IsNullOrEmpty()) return true;
return p.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ||
(p.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) ||
(p.PlayerName?.Contains(_characterOrCommentFilter) ?? false);
}).ToList();
}
private void DrawServerStatus()
{
var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link);
var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
var userSize = ImGui.CalcTextSize(userCount);
var textSize = ImGui.CalcTextSize("Users Online");
@@ -579,7 +651,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (_apiController.ServerState is ServerState.Connected)
{
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.ParsedGreen, userCount);
ImGui.SameLine();
@@ -595,16 +667,16 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - shardTextSize.X / 2);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X) / 2 - shardTextSize.X / 2);
ImGui.TextUnformatted(shardConnection);
}
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
}
var color = UiShared.GetBoolColor(!_serverManager.CurrentServer!.FullPause);
var color = UiSharedService.GetBoolColor(!_serverManager.CurrentServer!.FullPause);
var connectedIcon = !_serverManager.CurrentServer.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink;
if (_apiController.ServerState != ServerState.Reconnecting)
@@ -617,13 +689,13 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_ = _apiController.CreateConnections();
}
ImGui.PopStyleColor();
UiShared.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
UiSharedService.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
private void DrawTransfers()
{
var currentUploads = _apiController.CurrentUploads.ToList();
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text(FontAwesomeIcon.Upload.ToIconString());
ImGui.PopFont();
@@ -638,7 +710,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
var totalToUpload = currentUploads.Sum(c => c.Total);
ImGui.Text($"{doneUploads}/{totalUploads}");
var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})";
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
var textSize = ImGui.CalcTextSize(uploadText);
ImGui.SameLine(WindowContentWidth - textSize.X);
ImGui.Text(uploadText);
@@ -648,7 +720,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
ImGui.Text("No uploads in progress");
}
var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList();
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text(FontAwesomeIcon.Download.ToIconString());
ImGui.PopFont();
@@ -656,14 +728,14 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (currentDownloads.Any())
{
var totalDownloads = currentDownloads.Count();
var doneDownloads = currentDownloads.Count(c => c.IsTransferred);
var totalDownloaded = currentDownloads.Sum(c => c.Transferred);
var totalToDownload = currentDownloads.Sum(c => c.Total);
var totalDownloads = currentDownloads.Sum(c => c.TotalFiles);
var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles);
var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes);
var totalToDownload = currentDownloads.Sum(c => c.TotalBytes);
ImGui.Text($"{doneDownloads}/{totalDownloads}");
var downloadText =
$"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})";
$"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})";
var textSize = ImGui.CalcTextSize(downloadText);
ImGui.SameLine(WindowContentWidth - textSize.X);
ImGui.Text(downloadText);
@@ -687,28 +759,28 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
var originalPos = ImGui.GetCursorPos();
ImGui.SetWindowFontScale(1.5f);
var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog);
var buttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog);
buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2;
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
{
Mediator.Publish(new OpenSettingsUiMessage());
}
UiShared.AttachToolTip("Open the Mare Synchronos Settings");
UiSharedService.AttachToolTip("Open the Mare Synchronos Settings");
ImGui.SameLine(); //Important to draw the uidText consistently
ImGui.SetCursorPos(originalPos);
if (_apiController.ServerState is ServerState.Connected)
{
buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2;
buttonSizeX += UiSharedService.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2;
ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy))
{
ImGui.SetClipboardText(_apiController.DisplayName);
}
UiShared.AttachToolTip("Copy your UID to clipboard");
UiSharedService.AttachToolTip("Copy your UID to clipboard");
ImGui.SameLine();
}
ImGui.SetWindowFontScale(1f);
@@ -721,7 +793,7 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
if (_apiController.ServerState is not ServerState.Connected)
{
UiShared.ColorTextWrapped(GetServerError(), GetUidColor());
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
if (_apiController.ServerState is ServerState.NoSecretKey)
{
DrawAddCharacter();
@@ -729,38 +801,17 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
}
}
private void DrawAddCharacter()
private List<Pair> GetFilteredUsers()
{
ImGui.Dummy(new(10));
var keys = _serverManager.CurrentServer!.SecretKeys;
if (keys.TryGetValue(secretKeyIdx, out var secretKey))
return _pairManager.DirectPairs.Where(p =>
{
var friendlyName = secretKey.FriendlyName;
if (UiShared.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key"))
{
_serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication()
{
CharacterName = _uiShared.PlayerName,
WorldId = _uiShared.WorldId,
SecretKeyIdx = secretKeyIdx
});
_serverManager.Save();
_ = _apiController.CreateConnections(forceGetToken: true);
}
_uiShared.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => secretKeyIdx = f.Key);
}
else
{
UiShared.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow);
}
if (_characterOrCommentFilter.IsNullOrEmpty()) return true;
return p.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ||
(p.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) ||
(p.PlayerName?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false);
}).ToList();
}
private int secretKeyIdx = 0;
private string GetServerError()
{
return _apiController.ServerState switch
@@ -812,4 +863,15 @@ public class CompactUi : WindowMediatorSubscriberBase, IDisposable
_ => string.Empty
};
}
}
private void UiSharedService_GposeEnd()
{
IsOpen = _wasOpen;
}
private void UiSharedService_GposeStart()
{
_wasOpen = IsOpen;
IsOpen = false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,215 +2,236 @@
using Dalamud.Interface.Components;
using ImGuiNET;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.Models;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI;
namespace MareSynchronos.UI.Components
namespace MareSynchronos.UI.Components;
public class PairGroupsUi
{
public class PairGroupsUi
private readonly ApiController _apiController;
private readonly Action<Pair> _clientRenderFn;
private readonly MareConfigService _mareConfig;
private readonly SelectPairForGroupUi _selectGroupForPairUi;
private readonly TagHandler _tagHandler;
public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, Action<Pair> clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi)
{
private readonly Action<Pair> _clientRenderFn;
private readonly TagHandler _tagHandler;
private readonly ApiController _apiController;
private readonly SelectPairForGroupUi _selectGroupForPairUi;
_clientRenderFn = clientRenderFn;
_mareConfig = mareConfig;
_tagHandler = tagHandler;
_apiController = apiController;
_selectGroupForPairUi = selectGroupForPairUi;
}
public PairGroupsUi(TagHandler tagHandler, Action<Pair> clientRenderFn, ApiController apiController, SelectPairForGroupUi selectGroupForPairUi)
public void Draw(List<Pair> visibleUsers, List<Pair> onlineUsers, List<Pair> offlineUsers)
{
// Only render those tags that actually have pairs in them, otherwise
// we can end up with a bunch of useless pair groups
var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted();
var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList();
if (_mareConfig.Current.ShowVisibleUsersSeparately)
{
_clientRenderFn = clientRenderFn;
_tagHandler = tagHandler;
_apiController = apiController;
_selectGroupForPairUi = selectGroupForPairUi;
UiSharedService.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers));
}
public void Draw(List<Pair> visibleUsers, List<Pair> onlineUsers, List<Pair> offlineUsers)
foreach (var tag in tagsWithPairsInThem)
{
// Only render those tags that actually have pairs in them, otherwise
// we can end up with a bunch of useless pair groups
var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted();
var allUsers = visibleUsers.Concat(onlineUsers).Concat(offlineUsers).ToList();
UiShared.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers));
foreach (var tag in tagsWithPairsInThem)
if (_mareConfig.Current.ShowOfflineUsersSeparately)
{
UiShared.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers));
}
UiShared.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
UiShared.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag, offlineUsers, allUsers));
}
private void DrawCategory(string tag, List<Pair> onlineUsers, List<Pair> allUsers, List<Pair>? visibleUsers = null)
{
List<Pair> usersInThisTag;
HashSet<string>? otherUidsTaggedWithTag = null;
bool isSpecialTag = false;
int visibleInThisTag = 0;
if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag)
{
usersInThisTag = onlineUsers;
isSpecialTag = true;
UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers));
}
else
{
otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag);
usersInThisTag = onlineUsers
.Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID))
.ToList();
visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0;
}
if (isSpecialTag && !usersInThisTag.Any()) return;
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count);
if (!isSpecialTag)
UiShared.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList()));
if (!_tagHandler.IsTagOpen(tag)) return;
ImGui.Indent(20);
DrawPairs(tag, usersInThisTag);
ImGui.Unindent(20);
}
private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total)
{
string displayedName = tag switch
{
TagHandler.CustomOfflineTag => "Offline/Unpaired",
TagHandler.CustomOnlineTag => "Online/Paused",
TagHandler.CustomVisibleTag => "Visible",
_ => tag
};
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
UiShared.FontText(icon.ToIconString(), UiBuilder.IconFont);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ToggleTagOpen(tag);
}
ImGui.SameLine();
UiShared.FontText(resultFolderName, UiBuilder.DefaultFont);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ToggleTagOpen(tag);
}
if (!isSpecialTag && ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted($"Group {tag}");
ImGui.Separator();
ImGui.TextUnformatted($"{visible} Pairs visible");
ImGui.TextUnformatted($"{online} Pairs online/paused");
ImGui.TextUnformatted($"{total} Pairs total");
ImGui.EndTooltip();
UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers));
}
}
private void DrawButtons(string tag, List<Pair> availablePairsInThisTag)
if (_mareConfig.Current.ShowOfflineUsersSeparately)
{
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var flyoutMenuX = UiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X;
var pauseButtonX = UiShared.GetIconButtonSize(pauseButton).X;
var windowX = ImGui.GetWindowContentRegionMin().X;
var windowWidth = UiShared.GetWindowContentRegionWidth();
var spacingX = ImGui.GetStyle().ItemSpacing.X;
UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag,
onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
UiSharedService.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag,
offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers));
}
else
{
UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag,
onlineUsers.Concat(offlineUsers).Where(u => u.UserPair!.OtherPermissions.IsPaired() && !_tagHandler.HasAnyTag(u.UserPair!)).ToList(), allUsers));
}
UiSharedService.DrawWithID($"group-UnpairedCustomTag", () => DrawCategory(TagHandler.CustomUnpairedTag,
offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers));
}
var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX;
ImGui.SameLine(buttonPauseOffset);
if (ImGuiComponents.IconButton(pauseButton))
{
// If all of the currently visible pairs (after applying filters to the pairs)
// are paused we display a resume button to resume all currently visible (after filters)
// pairs. Otherwise, we just pause all the remaining pairs.
if (allArePaused)
{
// If all are paused => resume all
ResumeAllPairs(availablePairsInThisTag);
}
else
{
// otherwise pause all remaining
PauseRemainingPairs(availablePairsInThisTag);
}
}
private void DrawButtons(string tag, List<Pair> availablePairsInThisTag)
{
var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused());
var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var flyoutMenuX = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
var pauseButtonX = UiSharedService.GetIconButtonSize(pauseButton).X;
var windowX = ImGui.GetWindowContentRegionMin().X;
var windowWidth = UiSharedService.GetWindowContentRegionWidth();
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX;
ImGui.SameLine(buttonPauseOffset);
if (ImGuiComponents.IconButton(pauseButton))
{
// If all of the currently visible pairs (after applying filters to the pairs)
// are paused we display a resume button to resume all currently visible (after filters)
// pairs. Otherwise, we just pause all the remaining pairs.
if (allArePaused)
{
UiShared.AttachToolTip($"Resume pairing with all pairs in {tag}");
// If all are paused => resume all
ResumeAllPairs(availablePairsInThisTag);
}
else
{
UiShared.AttachToolTip($"Pause pairing with all pairs in {tag}");
}
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
ImGui.SameLine(buttonDeleteOffset);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars))
{
ImGui.OpenPopup("Group Flyout Menu");
}
if (ImGui.BeginPopup("Group Flyout Menu"))
{
UiShared.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag));
ImGui.EndPopup();
// otherwise pause all remaining
PauseRemainingPairs(availablePairsInThisTag);
}
}
private void DrawGroupMenu(string tag)
if (allArePaused)
{
if (UiShared.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag))
{
_selectGroupForPairUi.Open(tag);
}
UiShared.AttachToolTip($"Add more users to Group {tag}");
if (UiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag))
{
if (UiShared.CtrlPressed())
{
_tagHandler.RemoveTag(tag);
}
}
UiShared.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete");
UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}");
}
else
{
UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}");
}
private void DrawPairs(string tag, List<Pair> availablePairsInThisCategory)
var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX;
ImGui.SameLine(buttonDeleteOffset);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bars))
{
// These are all the OtherUIDs that are tagged with this tag
availablePairsInThisCategory
.ForEach(pair => UiShared.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair)));
ImGui.Separator();
ImGui.OpenPopup("Group Flyout Menu");
}
private void ToggleTagOpen(string tag)
if (ImGui.BeginPopup("Group Flyout Menu"))
{
bool open = !_tagHandler.IsTagOpen(tag);
_tagHandler.SetTagOpen(tag, open);
}
private void PauseRemainingPairs(List<Pair> availablePairs)
{
foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused()))
{
var perm = pairToPause.UserPair!.OwnPermissions;
perm.SetPaused(paused: true);
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
}
}
private void ResumeAllPairs(List<Pair> availablePairs)
{
foreach (var pairToPause in availablePairs)
{
var perm = pairToPause.UserPair!.OwnPermissions;
perm.SetPaused(paused: false);
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
}
UiSharedService.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag));
ImGui.EndPopup();
}
}
private void DrawCategory(string tag, List<Pair> onlineUsers, List<Pair> allUsers, List<Pair>? visibleUsers = null)
{
List<Pair> usersInThisTag;
HashSet<string>? otherUidsTaggedWithTag = null;
bool isSpecialTag = false;
int visibleInThisTag = 0;
if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag or TagHandler.CustomUnpairedTag)
{
usersInThisTag = onlineUsers;
isSpecialTag = true;
}
else
{
otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag);
usersInThisTag = onlineUsers
.Where(pair => otherUidsTaggedWithTag.Contains(pair.UserData.UID))
.ToList();
visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UserData.UID)) ?? 0;
}
if (isSpecialTag && !usersInThisTag.Any()) return;
DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count, otherUidsTaggedWithTag?.Count);
if (!isSpecialTag)
UiSharedService.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Where(p => otherUidsTaggedWithTag!.Contains(p.UserData.UID)).ToList()));
if (!_tagHandler.IsTagOpen(tag)) return;
ImGui.Indent(20);
DrawPairs(tag, usersInThisTag);
ImGui.Unindent(20);
}
private void DrawGroupMenu(string tag)
{
if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag))
{
_selectGroupForPairUi.Open(tag);
}
UiSharedService.AttachToolTip($"Add more users to Group {tag}");
if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed())
{
_tagHandler.RemoveTag(tag);
}
UiSharedService.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete");
}
private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total)
{
string displayedName = tag switch
{
TagHandler.CustomUnpairedTag => "Unpaired",
TagHandler.CustomOfflineTag => "Offline",
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts",
TagHandler.CustomVisibleTag => "Visible",
_ => tag
};
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
UiSharedService.FontText(icon.ToIconString(), UiBuilder.IconFont);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ToggleTagOpen(tag);
}
ImGui.SameLine();
UiSharedService.FontText(resultFolderName, UiBuilder.DefaultFont);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ToggleTagOpen(tag);
}
if (!isSpecialTag && ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.TextUnformatted($"Group {tag}");
ImGui.Separator();
ImGui.TextUnformatted($"{visible} Pairs visible");
ImGui.TextUnformatted($"{online} Pairs online/paused");
ImGui.TextUnformatted($"{total} Pairs total");
ImGui.EndTooltip();
}
}
private void DrawPairs(string tag, List<Pair> availablePairsInThisCategory)
{
// These are all the OtherUIDs that are tagged with this tag
availablePairsInThisCategory
.ForEach(pair => UiSharedService.DrawWithID($"tag-{tag}-pair-${pair.UserData.UID}", () => _clientRenderFn(pair)));
ImGui.Separator();
}
private void PauseRemainingPairs(List<Pair> availablePairs)
{
foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused()))
{
var perm = pairToPause.UserPair!.OwnPermissions;
perm.SetPaused(paused: true);
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
}
}
private void ResumeAllPairs(List<Pair> availablePairs)
{
foreach (var pairToPause in availablePairs)
{
var perm = pairToPause.UserPair!.OwnPermissions;
perm.SetPaused(paused: false);
_ = _apiController.UserSetPairPermissions(new(pairToPause.UserData, perm));
}
}
private void ToggleTagOpen(string tag)
{
bool open = !_tagHandler.IsTagOpen(tag);
_tagHandler.SetTagOpen(tag, open);
}
}

View File

@@ -3,17 +3,14 @@ using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Utility;
using ImGuiNET;
using MareSynchronos.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.UI.Handlers;
namespace MareSynchronos.UI.Components;
public class SelectGroupForPairUi
{
/// <summary>
/// Should the panel show, yes/no
/// </summary>
private bool _show;
private readonly TagHandler _tagHandler;
/// <summary>
/// The group UI is always open for a specific pair. This defines which pair the UI is open for.
@@ -21,13 +18,16 @@ public class SelectGroupForPairUi
/// <returns></returns>
private Pair? _pair;
/// <summary>
/// Should the panel show, yes/no
/// </summary>
private bool _show;
/// <summary>
/// For the add category option, this stores the currently typed in tag name
/// </summary>
private string _tagNameToAdd = "";
private readonly TagHandler _tagHandler;
public SelectGroupForPairUi(TagHandler tagHandler)
{
_show = false;
@@ -35,17 +35,6 @@ public class SelectGroupForPairUi
_tagHandler = tagHandler;
}
public void Open(Pair pair)
{
_pair = pair;
// Using "_show" here to de-couple the opening of the popup
// The popup name is derived from the name the user currently sees, which is
// based on the showUidForEntry dictionary.
// We'd have to derive the name here to open it popup modal here, when the Open() is called
_show = true;
}
public void Draw(Dictionary<string, bool> showUidForEntry)
{
if (_pair == null)
@@ -68,34 +57,53 @@ public class SelectGroupForPairUi
var childHeight = tags.Count != 0 ? tags.Count * 25 : 1;
var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale;
UiShared.FontText($"Select the groups you want {name} to be in.", UiBuilder.DefaultFont);
UiSharedService.FontText($"Select the groups you want {name} to be in.", UiBuilder.DefaultFont);
if (ImGui.BeginChild(name + "##listGroups", childSize))
{
foreach (var tag in tags)
{
UiShared.DrawWithID($"groups-pair-{_pair.UserData.UID}-{tag}", () => DrawGroupName(_pair, tag));
UiSharedService.DrawWithID($"groups-pair-{_pair.UserData.UID}-{tag}", () => DrawGroupName(_pair, tag));
}
ImGui.EndChild();
}
ImGui.Separator();
UiShared.FontText($"Create a new group for {name}.", UiBuilder.DefaultFont);
UiSharedService.FontText($"Create a new group for {name}.", UiBuilder.DefaultFont);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
{
HandleAddTag();
}
ImGui.SameLine();
ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40);
if (ImGui.IsKeyDown(ImGuiKey.Enter))
{
if (ImGui.IsKeyDown(ImGuiKey.Enter))
{
HandleAddTag();
}
HandleAddTag();
}
ImGui.EndPopup();
}
}
public void Open(Pair pair)
{
_pair = pair;
// Using "_show" here to de-couple the opening of the popup
// The popup name is derived from the name the user currently sees, which is
// based on the showUidForEntry dictionary.
// We'd have to derive the name here to open it popup modal here, when the Open() is called
_show = true;
}
private static string PairName(Dictionary<string, bool> showUidForEntry, Pair pair)
{
showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName);
var playerText = pair.GetNote();
if (showUidInsteadOfName || string.IsNullOrEmpty(playerText))
{
playerText = pair.UserData.AliasOrUID;
}
return playerText;
}
private void DrawGroupName(Pair pair, string name)
{
var hasTagBefore = _tagHandler.HasTag(pair.UserPair!, name);
@@ -129,15 +137,4 @@ public class SelectGroupForPairUi
_tagNameToAdd = string.Empty;
}
}
private string PairName(Dictionary<string, bool> showUidForEntry, Pair pair)
{
showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName);
var playerText = pair.GetNote();
if (showUidInsteadOfName || string.IsNullOrEmpty(playerText))
{
playerText = pair.UserData.AliasOrUID;
}
return playerText;
}
}

View File

@@ -1,38 +1,31 @@
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using MareSynchronos.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.UI.Handlers;
namespace MareSynchronos.UI.Components;
public class SelectPairForGroupUi
{
private bool _show = false;
private bool _opened = false;
private HashSet<string> _peopleInGroup = new(StringComparer.Ordinal);
private string _tag = string.Empty;
private readonly TagHandler _tagHandler;
private string _filter = string.Empty;
private bool _opened = false;
private HashSet<string> _peopleInGroup = new(StringComparer.Ordinal);
private bool _show = false;
private string _tag = string.Empty;
public SelectPairForGroupUi(TagHandler tagHandler)
{
_tagHandler = tagHandler;
}
public void Open(string tag)
{
_peopleInGroup = _tagHandler.GetOtherUidsForTag(tag);
_tag = tag;
_show = true;
}
public void Draw(List<Pair> pairs, Dictionary<string, bool> showUidForEntry)
{
var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale;
var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale;
var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale;
var popupName = $"Choose Users for Group {_tag}";
if (!_show)
@@ -43,7 +36,7 @@ public class SelectPairForGroupUi
if (_show && !_opened)
{
ImGui.SetNextWindowSize(minSize);
UiShared.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always);
ImGui.OpenPopup(popupName);
_opened = true;
}
@@ -51,10 +44,12 @@ public class SelectPairForGroupUi
ImGui.SetNextWindowSizeConstraints(minSize, maxSize);
if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal))
{
UiShared.FontText($"Select users for group {_tag}", UiBuilder.DefaultFont);
UiSharedService.FontText($"Select users for group {_tag}", UiBuilder.DefaultFont);
ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None);
foreach (var item in pairs.OrderBy(p => PairName(showUidForEntry, p), StringComparer.OrdinalIgnoreCase)
.Where(p => string.IsNullOrEmpty(_filter) || PairName(showUidForEntry, p).Contains(_filter, StringComparison.OrdinalIgnoreCase)).ToList())
foreach (var item in pairs
.Where(p => string.IsNullOrEmpty(_filter) || PairName(showUidForEntry, p).Contains(_filter, StringComparison.OrdinalIgnoreCase))
.OrderBy(p => PairName(showUidForEntry, p), StringComparer.OrdinalIgnoreCase)
.ToList())
{
var isInGroup = _peopleInGroup.Contains(item.UserData.UID);
if (ImGui.Checkbox(PairName(showUidForEntry, item), ref isInGroup))
@@ -80,7 +75,14 @@ public class SelectPairForGroupUi
}
}
private string PairName(Dictionary<string, bool> showUidForEntry, Pair pair)
public void Open(string tag)
{
_peopleInGroup = _tagHandler.GetOtherUidsForTag(tag);
_tag = tag;
_show = true;
}
private static string PairName(Dictionary<string, bool> showUidForEntry, Pair pair)
{
showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName);
var playerText = pair.GetNote();
@@ -90,4 +92,4 @@ public class SelectPairForGroupUi
}
return playerText;
}
}
}

View File

@@ -1,40 +1,38 @@
using System.Numerics;
using Dalamud.Interface.Windowing;
using Dalamud.Interface.Colors;
using ImGuiNET;
using MareSynchronos.MareConfiguration;
using MareSynchronos.WebAPI;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Numerics;
namespace MareSynchronos.UI;
public class DownloadUi : Window, IDisposable
public class DownloadUi : WindowMediatorSubscriberBase
{
private readonly ILogger<DownloadUi> _logger;
private readonly WindowSystem _windowSystem;
private readonly MareConfigService _configService;
private readonly ApiController _apiController;
private readonly UiShared _uiShared;
private bool _wasOpen = false;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileUploadManager _fileTransferManager;
private readonly UiSharedService _uiShared;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
public void Dispose()
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, MareConfigService configService,
FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared) : base(logger, mediator, "Mare Synchronos Downloads")
{
_logger.LogTrace($"Disposing {GetType()}");
_windowSystem.RemoveWindow(this);
}
public DownloadUi(ILogger<DownloadUi> logger, WindowSystem windowSystem, MareConfigService configService, ApiController apiController, UiShared uiShared) : base("Mare Synchronos Downloads")
{
_logger = logger;
_logger.LogTrace("Creating " + nameof(DownloadUi));
_windowSystem = windowSystem;
_dalamudUtilService = dalamudUtilService;
_configService = configService;
_apiController = apiController;
_fileTransferManager = fileTransferManager;
_uiShared = uiShared;
SizeConstraints = new WindowSizeConstraints()
{
MaximumSize = new Vector2(300, 90),
MinimumSize = new Vector2(300, 90),
MaximumSize = new Vector2(500, 90),
MinimumSize = new Vector2(500, 90),
};
Flags |= ImGuiWindowFlags.NoMove;
@@ -48,20 +46,175 @@ public class DownloadUi : Window, IDisposable
ForceMainWindow = true;
windowSystem.AddWindow(this);
IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
{
if (msg.IsUploading)
{
_uploadingPlayers[msg.Handler] = true;
}
else
{
_uploadingPlayers.TryRemove(msg.Handler, out _);
}
});
}
public override void Draw()
{
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return;
if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return;
if (!IsOpen) return;
if (_configService.Current.ShowTransferWindow)
{
try
{
if (_fileTransferManager.CurrentUploads.Any())
{
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred);
var totalUploaded = currentUploads.Sum(c => c.Transferred);
var totalToUpload = currentUploads.Sum(c => c.Total);
UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
if (_currentDownloads.Any()) ImGui.Separator();
}
}
catch
{
// ignore errors thrown from UI
}
try
{
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
}
catch
{
// ignore errors thrown from UI
}
}
if (_configService.Current.ShowTransferBars)
{
const int transparency = 100;
const int dlBarBorder = 3;
foreach (var transfer in _currentDownloads.ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GameObjectLazy.Value);
if (screenPos == Vector2.Zero) continue;
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);
var downloadText =
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
var textSize = ImGui.CalcTextSize(maxDlText);
int dlBarHeight = (int)textSize.Y + 8;
int dlBarWidth = (int)textSize.X + 150;
var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);
var drawList = ImGui.GetBackgroundDrawList();
drawList.AddRectFilled(
dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 },
UiSharedService.Color(0, 0, 0, transparency), 1);
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
UiSharedService.Color(220, 220, 220, transparency), 1);
drawList.AddRectFilled(dlBarStart, dlBarEnd,
UiSharedService.Color(0, 0, 0, transparency), 1);
var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(50, 205, 50, transparency), 1);
UiSharedService.DrawOutlinedFont(drawList, downloadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 255, transparency),
UiSharedService.Color(0, 0, 0, transparency), 1);
}
if (_configService.Current.ShowUploading)
{
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
{
var screenPos = _dalamudUtilService.WorldToScreen(player.GameObjectLazy.Value);
if (screenPos == Vector2.Zero) continue;
try
{
if (_uiShared.UidFontBuilt && _configService.Current.ShowUploadingBigText) ImGui.PushFont(_uiShared.UidFont);
var uploadText = "Uploading";
var textSize = ImGui.CalcTextSize(uploadText);
var drawList = ImGui.GetBackgroundDrawList();
UiSharedService.DrawOutlinedFont(drawList, uploadText,
screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
UiSharedService.Color(255, 255, 0, transparency),
UiSharedService.Color(0, 0, 0, transparency), 2);
}
catch
{
// ignore errors thrown on UI
}
finally
{
if (_uiShared.UidFontBuilt && _configService.Current.ShowUploadingBigText) ImGui.PopFont();
}
}
}
}
}
public override void PreDraw()
{
if (_uiShared.IsInGpose)
{
_wasOpen = IsOpen;
IsOpen = false;
}
base.PreDraw();
if (_uiShared.EditTrackerPosition)
{
Flags &= ~ImGuiWindowFlags.NoMove;
@@ -76,65 +229,12 @@ public class DownloadUi : Window, IDisposable
Flags |= ImGuiWindowFlags.NoInputs;
Flags |= ImGuiWindowFlags.NoResize;
}
}
public override void Draw()
{
if (!_configService.Current.ShowTransferWindow) return;
if (!_apiController.IsDownloading && !_apiController.IsUploading) return;
var drawList = ImGui.GetWindowDrawList();
var yDistance = 20;
var xDistance = 20;
var basePosition = ImGui.GetWindowPos() + ImGui.GetWindowContentRegionMin();
try
var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3);
SizeConstraints = new()
{
if (_apiController.CurrentUploads.Any())
{
var currentUploads = _apiController.CurrentUploads.ToList();
var totalUploads = currentUploads.Count;
var doneUploads = currentUploads.Count(c => c.IsTransferred);
var totalUploaded = currentUploads.Sum(c => c.Transferred);
var totalToUpload = currentUploads.Sum(c => c.Total);
UiShared.DrawOutlinedFont(drawList, "▲",
new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * 0.5)),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
UiShared.DrawOutlinedFont(drawList, $"Compressing+Uploading {doneUploads}/{totalUploads}",
new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * 0),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
UiShared.DrawOutlinedFont(drawList, $"{UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)}",
new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * 1),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
}
}
catch { }
try
{
if (_apiController.CurrentDownloads.Any())
{
var currentDownloads = _apiController.CurrentDownloads.Where(d => d.Value != null && d.Value.Any()).ToList().SelectMany(k => k.Value).ToList();
var multBase = currentDownloads.Any() ? 0 : 2;
var doneDownloads = currentDownloads.Count(c => c.IsTransferred);
var totalDownloads = currentDownloads.Count;
var totalDownloaded = currentDownloads.Sum(c => c.Transferred);
var totalToDownload = currentDownloads.Sum(c => c.Total);
UiShared.DrawOutlinedFont(drawList, "▼",
new Vector2(basePosition.X + 0, basePosition.Y + (int)(yDistance * multBase + (yDistance * 0.5))),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
UiShared.DrawOutlinedFont(drawList, $"Downloading {doneDownloads}/{totalDownloads}",
new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * multBase),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
UiShared.DrawOutlinedFont(drawList, $"{UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)}",
new Vector2(basePosition.X + xDistance, basePosition.Y + yDistance * (1 + multBase)),
UiShared.Color(255, 255, 255, 255), UiShared.Color(0, 0, 0, 255), 2);
}
}
catch { }
MinimumSize = new Vector2(300, maxHeight),
MaximumSize = new Vector2(300, maxHeight),
};
}
}

View File

@@ -1,38 +1,73 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using MareSynchronos.Export;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.PlayerData.Export;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.UI;
public class GposeUi : WindowMediatorSubscriberBase, IDisposable
public class GposeUi : WindowMediatorSubscriberBase
{
private readonly WindowSystem _windowSystem;
private readonly MareCharaFileManager _mareCharaFileManager;
private readonly DalamudUtil _dalamudUtil;
private readonly FileDialogManager _fileDialogManager;
private readonly MareConfigService _configService;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileDialogManager _fileDialogManager;
private readonly MareCharaFileManager _mareCharaFileManager;
public GposeUi(ILogger<GposeUi> logger, WindowSystem windowSystem, MareCharaFileManager mareCharaFileManager,
DalamudUtil dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService,
public GposeUi(ILogger<GposeUi> logger, MareCharaFileManager mareCharaFileManager,
DalamudUtilService dalamudUtil, FileDialogManager fileDialogManager, MareConfigService configService,
MareMediator mediator) : base(logger, mediator, "Mare Synchronos Gpose Import UI###MareSynchronosGposeUI")
{
_windowSystem = windowSystem;
_mareCharaFileManager = mareCharaFileManager;
_dalamudUtil = dalamudUtil;
_fileDialogManager = fileDialogManager;
_configService = configService;
Mediator.Subscribe<GposeStartMessage>(this, (_) => StartGpose());
Mediator.Subscribe<GposeEndMessage>(this, (_) => EndGpose());
IsOpen = _dalamudUtil.IsInGpose;
Flags = ImGuiWindowFlags.AlwaysAutoResize;
_windowSystem.AddWindow(this);
this.SizeConstraints = new()
{
MinimumSize = new(200, 200),
MaximumSize = new(400, 400)
};
}
public override void Draw()
{
if (!_dalamudUtil.IsInGpose) IsOpen = false;
if (!_mareCharaFileManager.CurrentlyWorking)
{
if (UiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF"))
{
_fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, path) =>
{
if (!success) return;
Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path));
});
}
UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor");
if (_mareCharaFileManager.LoadedCharaFile != null)
{
UiSharedService.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath);
UiSharedService.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description);
if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF"))
{
Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false));
}
UiSharedService.AttachToolTip("Applies it to the currently selected GPose actor");
UiSharedService.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow);
}
}
else
{
UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
}
UiSharedService.TextWrapped("Hint: You can disable the automatic loading of this window in the Mare settings and open it manually with /mare gpose");
}
private void EndGpose()
@@ -45,45 +80,4 @@ public class GposeUi : WindowMediatorSubscriberBase, IDisposable
{
IsOpen = _configService.Current.OpenGposeImportOnGposeStart;
}
public override void Dispose()
{
base.Dispose();
_windowSystem.RemoveWindow(this);
}
public override void Draw()
{
if (!_dalamudUtil.IsInGpose) IsOpen = false;
if (!_mareCharaFileManager.CurrentlyWorking)
{
if (UiShared.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF"))
{
_fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, path) =>
{
if (!success) return;
Task.Run(() => _mareCharaFileManager.LoadMareCharaFile(path));
});
}
UiShared.AttachToolTip("Applies it to the currently selected GPose actor");
if (_mareCharaFileManager.LoadedCharaFile != null)
{
UiShared.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath);
UiShared.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description);
if (UiShared.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF"))
{
Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false));
}
UiShared.AttachToolTip("Applies it to the currently selected GPose actor");
UiShared.ColorTextWrapped("Warning: redrawing or changing the character will revert all applied mods.", ImGuiColors.DalamudYellow);
}
}
else
{
UiShared.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow);
}
UiShared.TextWrapped("Hint: You can disable the automatic loading of this window in the Mare settings and open it manually with /mare gpose");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,83 +1,83 @@
using MareSynchronos.API.Dto.User;
using MareSynchronos.Managers;
using MareSynchronos.Services.ServerConfiguration;
namespace MareSynchronos.UI.Handlers
namespace MareSynchronos.UI.Handlers;
public class TagHandler
{
public class TagHandler
public const string CustomOfflineTag = "Mare_Offline";
public const string CustomOnlineTag = "Mare_Online";
public const string CustomUnpairedTag = "Mare_Unpaired";
public const string CustomVisibleTag = "Mare_Visible";
private readonly ServerConfigurationManager _serverConfigurationManager;
public TagHandler(ServerConfigurationManager serverConfigurationManager)
{
private readonly ServerConfigurationManager _serverConfigurationManager;
public const string CustomVisibleTag = "Mare_Visible";
public const string CustomOnlineTag = "Mare_Online";
public const string CustomOfflineTag = "Mare_Offline";
_serverConfigurationManager = serverConfigurationManager;
}
public TagHandler(ServerConfigurationManager serverConfigurationManager)
public void AddTag(string tag)
{
_serverConfigurationManager.AddTag(tag);
}
public void AddTagToPairedUid(UserPairDto pair, string tagName)
{
_serverConfigurationManager.AddTagForUid(pair.User.UID, tagName);
}
public List<string> GetAllTagsSorted()
{
return _serverConfigurationManager.GetServerAvailablePairTags()
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public HashSet<string> GetOtherUidsForTag(string tag)
{
return _serverConfigurationManager.GetUidsForTag(tag);
}
public bool HasAnyTag(UserPairDto pair)
{
return _serverConfigurationManager.HasTags(pair.User.UID);
}
public bool HasTag(UserPairDto pair, string tagName)
{
return _serverConfigurationManager.ContainsTag(pair.User.UID, tagName);
}
/// <summary>
/// Is this tag opened in the paired clients UI?
/// </summary>
/// <param name="tag">the tag</param>
/// <returns>open true/false</returns>
public bool IsTagOpen(string tag)
{
return _serverConfigurationManager.ContainsOpenPairTag(tag);
}
public void RemoveTag(string tag)
{
// First remove the tag from teh available pair tags
_serverConfigurationManager.RemoveTag(tag);
}
public void RemoveTagFromPairedUid(UserPairDto pair, string tagName)
{
_serverConfigurationManager.RemoveTagForUid(pair.User.UID, tagName);
}
public void SetTagOpen(string tag, bool open)
{
if (open)
{
_serverConfigurationManager = serverConfigurationManager;
_serverConfigurationManager.AddOpenPairTag(tag);
}
public void AddTag(string tag)
else
{
_serverConfigurationManager.AddTag(tag);
}
public void RemoveTag(string tag)
{
// First remove the tag from teh available pair tags
_serverConfigurationManager.RemoveTag(tag);
}
public void SetTagOpen(string tag, bool open)
{
if (open)
{
_serverConfigurationManager.AddOpenPairTag(tag);
}
else
{
_serverConfigurationManager.RemoveOpenPairTag(tag);
}
}
/// <summary>
/// Is this tag opened in the paired clients UI?
/// </summary>
/// <param name="tag">the tag</param>
/// <returns>open true/false</returns>
public bool IsTagOpen(string tag)
{
return _serverConfigurationManager.ContainsOpenPairTag(tag);
}
public List<string> GetAllTagsSorted()
{
return _serverConfigurationManager.GetServerAvailablePairTags()
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public HashSet<string> GetOtherUidsForTag(string tag)
{
return _serverConfigurationManager.GetUidsForTag(tag);
}
public void AddTagToPairedUid(UserPairDto pair, string tagName)
{
_serverConfigurationManager.AddTagForUid(pair.User.UID, tagName);
}
public void RemoveTagFromPairedUid(UserPairDto pair, string tagName)
{
_serverConfigurationManager.RemoveTagForUid(pair.User.UID, tagName);
}
public bool HasTag(UserPairDto pair, string tagName)
{
return _serverConfigurationManager.ContainsTag(pair.User.UID, tagName);
}
public bool HasAnyTag(UserPairDto pair)
{
return _serverConfigurationManager.HasTags(pair.User.UID);
_serverConfigurationManager.RemoveOpenPairTag(tag);
}
}
}

View File

@@ -1,52 +1,41 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using MareSynchronos.Localization;
using Dalamud.Utility;
using ImGuiNET;
using MareSynchronos.FileCache;
using Dalamud.Interface;
using MareSynchronos.Managers;
using MareSynchronos.Localization;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Mediator;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace MareSynchronos.UI;
internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
public class IntroUi : WindowMediatorSubscriberBase
{
private readonly UiShared _uiShared;
private readonly MareConfigService _configService;
private readonly PeriodicFileScanner _fileCacheManager;
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } };
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly WindowSystem _windowSystem;
private readonly UiSharedService _uiShared;
private int _currentLanguage;
private bool _readFirstPage;
private string _secretKey = string.Empty;
private string _timeoutLabel = string.Empty;
private Task? _timeoutTask;
private string[]? _tosParagraphs;
private Task? _timeoutTask;
private string _timeoutLabel = string.Empty;
private readonly Dictionary<string, string> _languages = new(StringComparer.Ordinal) { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } };
private int _currentLanguage;
public override void Dispose()
{
base.Dispose();
_windowSystem.RemoveWindow(this);
}
public IntroUi(ILogger<IntroUi> logger, WindowSystem windowSystem, UiShared uiShared, MareConfigService configService,
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService,
PeriodicFileScanner fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator) : base(logger, mareMediator, "Mare Synchronos Setup")
{
_logger.LogTrace("Creating " + nameof(IntroUi));
_uiShared = uiShared;
_configService = configService;
_fileCacheManager = fileCacheManager;
_serverConfigurationManager = serverConfigurationManager;
_windowSystem = windowSystem;
IsOpen = false;
SizeConstraints = new WindowSizeConstraints()
@@ -59,8 +48,6 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = true);
_windowSystem.AddWindow(this);
}
public override void Draw()
@@ -73,11 +60,11 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
ImGui.TextUnformatted("Welcome to Mare Synchronos");
if (_uiShared.UidFontBuilt) ImGui.PopFont();
ImGui.Separator();
UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " +
UiSharedService.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " +
"Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.");
UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue.");
UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue.");
UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " +
UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " +
"might look broken because of this or others players mods might not apply on your end altogether. " +
"If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow);
if (!_uiShared.DrawOtherPluginState()) return;
@@ -121,17 +108,16 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
string readThis = Strings.ToS.ReadLabel;
textSize = ImGui.CalcTextSize(readThis);
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
UiShared.ColorText(readThis, ImGuiColors.DalamudRed);
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed);
ImGui.SetWindowFontScale(1.0f);
ImGui.Separator();
UiShared.TextWrapped(_tosParagraphs![0]);
UiShared.TextWrapped(_tosParagraphs![1]);
UiShared.TextWrapped(_tosParagraphs![2]);
UiShared.TextWrapped(_tosParagraphs![3]);
UiShared.TextWrapped(_tosParagraphs![4]);
UiShared.TextWrapped(_tosParagraphs![5]);
UiSharedService.TextWrapped(_tosParagraphs![0]);
UiSharedService.TextWrapped(_tosParagraphs![1]);
UiSharedService.TextWrapped(_tosParagraphs![2]);
UiSharedService.TextWrapped(_tosParagraphs![3]);
UiSharedService.TextWrapped(_tosParagraphs![4]);
UiSharedService.TextWrapped(_tosParagraphs![5]);
ImGui.Separator();
if (_timeoutTask?.IsCompleted ?? true)
@@ -144,12 +130,12 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
}
else
{
UiShared.TextWrapped(_timeoutLabel);
UiSharedService.TextWrapped(_timeoutLabel);
}
}
else if (_configService.Current.AcceptedAgreement
&& (string.IsNullOrEmpty(_configService.Current.CacheFolder)
|| _configService.Current.InitialScanComplete == false
|| !_configService.Current.InitialScanComplete
|| !Directory.Exists(_configService.Current.CacheFolder)))
{
if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont);
@@ -159,17 +145,17 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
if (!_uiShared.HasValidPenumbraModPath)
{
UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed);
}
else
{
UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " +
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " +
"Additionally, a local storage folder must be set where Mare Synchronos will download other character files to. " +
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Mare Synchronos in the Plugin Configurations folder of Dalamud. " +
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Mare Synchronos in the Plugin Configurations folder of Dalamud. " +
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow);
UiShared.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
_uiShared.DrawCacheDirectorySetting();
}
@@ -191,22 +177,22 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
ImGui.TextUnformatted("Service Registration");
if (_uiShared.UidFontBuilt) ImGui.PopFont();
ImGui.Separator();
UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account.");
UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise.");
UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #mare-commands.");
UiSharedService.TextWrapped("To be able to use Mare Synchronos you will have to register an account.");
UiSharedService.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise.");
UiSharedService.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #mare-commands.");
if (ImGui.Button("Join the Mare Synchronos Discord"))
{
Util.OpenLink("https://discord.gg/mpNdkrTRjW");
}
UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key.");
UiSharedService.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key.");
ImGui.Separator();
UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below.");
UiSharedService.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below.");
var idx = _uiShared.DrawServiceSelection(selectOnChange: true);
_ = _uiShared.DrawServiceSelection(selectOnChange: true);
var text = "Enter Secret Key";
var buttonText = "Save";
@@ -215,11 +201,11 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
ImGui.AlignTextToFramePadding();
ImGui.Text(text);
ImGui.SameLine();
ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X);
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X);
ImGui.InputText("", ref _secretKey, 64);
if (_secretKey.Length > 0 && _secretKey.Length != 64)
{
UiShared.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed);
}
else if (_secretKey.Length == 64)
{
@@ -245,8 +231,6 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
}
}
private string _secretKey = string.Empty;
private void GetToSLocalization(int changeLanguageTo = -1)
{
if (changeLanguageTo != -1)
@@ -256,4 +240,4 @@ internal class IntroUi : WindowMediatorSubscriberBase, IDisposable
_tosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 };
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ namespace MareSynchronos.Utils;
public static class Crypto
{
#pragma warning disable SYSLIB0021 // Type or member is obsolete
public static string GetFileHash(this string filePath)
{
using SHA1CryptoServiceProvider cryptoProvider = new();
@@ -30,5 +31,6 @@ public static class Crypto
using SHA256CryptoServiceProvider cryptoProvider = new();
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", "", StringComparison.Ordinal);
}
#pragma warning restore SYSLIB0021 // Type or member is obsolete
}
}

View File

@@ -4,8 +4,8 @@ namespace MareSynchronos.Utils;
public class RollingList<T> : IEnumerable<T>
{
private readonly LinkedList<T> _list = new();
private readonly object _addLock = new();
private readonly LinkedList<T> _list = new();
public RollingList(int maximumCount)
{
@@ -15,8 +15,19 @@ public class RollingList<T> : IEnumerable<T>
MaximumCount = maximumCount;
}
public int MaximumCount { get; }
public int Count => _list.Count;
public int MaximumCount { get; }
public T this[int index]
{
get
{
if (index < 0 || index >= Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _list.Skip(index).First();
}
}
public void Add(T value)
{
@@ -30,17 +41,7 @@ public class RollingList<T> : IEnumerable<T>
}
}
public T this[int index]
{
get
{
if (index < 0 || index >= Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _list.Skip(index).First();
}
}
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@@ -1,32 +1,11 @@
using System.Globalization;
using System.Reflection;
using Newtonsoft.Json;
using System.Text.Json;
namespace MareSynchronos.Utils;
public static class VariousExtensions
{
public static DateTime GetLinkerTime(Assembly assembly)
{
const string BuildVersionMetadataPrefix = "+build";
var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (attribute?.InformationalVersion != null)
{
var value = attribute.InformationalVersion;
var index = value.IndexOf(BuildVersionMetadataPrefix, StringComparison.Ordinal);
if (index > 0)
{
value = value[(index + BuildVersionMetadataPrefix.Length)..];
return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture);
}
}
return default;
}
public static T DeepClone<T>(this T obj)
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj))!;
return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;
}
}

View File

@@ -1,540 +0,0 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text;
using Dalamud.Utility;
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.Mediator;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Utils;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes;
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
private bool _currentUploadCancelled = false;
private int _downloadId = 0;
public async Task<bool> CancelUpload()
{
if (CurrentUploads.Any())
{
_logger.LogDebug("Cancelling current upload");
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
await FilesAbortUpload().ConfigureAwait(false);
return true;
}
return false;
}
public async Task FilesAbortUpload()
{
await _mareHub!.SendAsync(nameof(FilesAbortUpload)).ConfigureAwait(false);
}
public async Task FilesDeleteAll()
{
_verifiedUploadedHashes.Clear();
await _mareHub!.SendAsync(nameof(FilesDeleteAll)).ConfigureAwait(false);
}
private async Task<Guid> GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct)
{
var response = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
var requestId = Guid.Parse(responseString.Trim('"'));
if (!_downloadReady.ContainsKey(requestId))
{
_downloadReady[requestId] = false;
}
return requestId;
}
private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
{
bool alreadyCancelled = false;
try
{
CancellationTokenSource localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady)
{
try
{
await Task.Delay(250, composite.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
if (downloadCt.IsCancellationRequested) throw;
var req = await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false);
try
{
req.EnsureSuccessStatusCode();
localTimeoutCts.Dispose();
composite.Dispose();
localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
}
catch (HttpRequestException)
{
throw;
}
}
}
localTimeoutCts.Dispose();
composite.Dispose();
_logger.LogDebug($"Download {requestId} ready");
}
catch (TaskCanceledException)
{
try
{
await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
alreadyCancelled = true;
}
catch { }
throw;
}
finally
{
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
{
try
{
await SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
}
catch { }
}
_downloadReady.Remove(requestId, out _);
}
}
private async Task DownloadFileHttpClient(DownloadFileTransfer fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
{
var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false);
_logger.LogDebug($"GUID {requestId} for file {fileTransfer.Hash} on server {fileTransfer.DownloadUri}");
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
HttpResponseMessage response = null!;
var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId);
_logger.LogDebug($"Downloading {requestUrl} for file {fileTransfer.Hash}");
try
{
response = await SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, $"Error during download of {requestUrl}, HttpStatusCode: {ex.StatusCode}");
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
{
throw new Exception($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
}
}
try
{
var fileStream = File.Create(tempPath);
await using (fileStream.ConfigureAwait(false))
{
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024;
var buffer = new byte[bufferSize];
var bytesRead = 0;
while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{
ct.ThrowIfCancellationRequested();
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
progress.Report(bytesRead);
}
_logger.LogDebug($"{requestUrl} downloaded to {tempPath}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error during file download of {requestUrl}");
try
{
if (!tempPath.IsNullOrEmpty())
File.Delete(tempPath);
}
catch { }
throw;
}
}
public int GetDownloadId() => _downloadId++;
public async Task DownloadFiles(int currentDownloadId, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
{
Mediator.Publish(new HaltScanMessage("Download"));
try
{
await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct).ConfigureAwait(false);
}
catch
{
CancelDownload(currentDownloadId);
}
finally
{
Mediator.Publish(new ResumeScanMessage("Download"));
}
}
private async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null)
{
using var requestMessage = new HttpRequestMessage(method, uri);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null)
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._serverManager.GetToken());
if (requestMessage.Content != null)
{
_logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri + " (Content: " + await (((JsonContent)requestMessage.Content).ReadAsStringAsync()) + ")");
}
else
{
_logger.LogDebug("Sending " + requestMessage.Method + " to " + requestMessage.RequestUri);
}
try
{
if (ct != null)
return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error during SendRequestInternal for " + requestMessage.RequestUri);
throw;
}
}
private async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = JsonContent.Create(content);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
private async Task DownloadFilesInternal(int currentDownloadId, List<FileReplacementData> fileReplacement, CancellationToken ct)
{
_logger.LogDebug("Downloading files (Download ID " + currentDownloadId + ")");
List<DownloadFileDto> downloadFileInfoFromService = new();
downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList()).ConfigureAwait(false));
_logger.LogDebug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred).ToList();
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{
if (!ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
{
ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
}
}
var downloadGroups = CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + f.DownloadUri.Port, StringComparer.Ordinal);
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
{
MaxDegreeOfParallelism = downloadGroups.Count(),
CancellationToken = ct,
},
async (fileGroup, token) =>
{
// let server predownload files
await SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
foreach (var file in fileGroup)
{
var hash = file.Hash;
Progress<long> progress = new((bytesDownloaded) =>
{
file.Transferred += bytesDownloaded;
});
var tempPath = Path.Combine(_configService.Current.CacheFolder, file.Hash + ".tmp");
try
{
await DownloadFileHttpClient(file, tempPath, progress, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
File.Delete(tempPath);
_logger.LogDebug("Detected cancellation, removing " + currentDownloadId);
CancelDownload(currentDownloadId);
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during download of " + file.Hash);
return;
}
var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false);
var extratokenedFile = LZ4Codec.Unwrap(tempFileData);
File.Delete(tempPath);
var filePath = Path.Combine(_configService.Current.CacheFolder, file.Hash);
await File.WriteAllBytesAsync(filePath, extratokenedFile, token).ConfigureAwait(false);
var fi = new FileInfo(filePath);
Func<DateTime> RandomDayInThePast()
{
DateTime start = new(1995, 1, 1);
Random gen = new();
int range = (DateTime.Today - start).Days;
return () => start.AddDays(gen.Next(range));
}
fi.CreationTime = RandomDayInThePast().Invoke();
fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke();
try
{
_ = _fileDbManager.CreateCacheEntry(filePath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Issue creating cache entry");
}
}
}).ConfigureAwait(false);
_logger.LogDebug("Download complete, removing " + currentDownloadId);
CancelDownload(currentDownloadId);
}
public async Task PushCharacterData(CharacterData data, List<UserData> visibleCharacters)
{
if (!IsConnected) return;
try
{
_currentUploadCancelled = await CancelUpload().ConfigureAwait(false);
_uploadCancellationTokenSource = new CancellationTokenSource();
var uploadToken = _uploadCancellationTokenSource.Token;
_logger.LogDebug($"Sending Character data {data.DataHash.Value} to service {_serverManager.CurrentApiUrl}");
HashSet<string> unverifiedUploads = VerifyFiles(data);
if (unverifiedUploads.Any())
{
await UploadMissingFiles(unverifiedUploads, uploadToken).ConfigureAwait(false);
_logger.LogInformation("Upload complete for " + data.DataHash.Value);
}
await PushCharacterDataInternal(data, visibleCharacters.ToList()).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Upload operation was cancelled");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error during upload of files");
}
finally
{
if (!_currentUploadCancelled)
_currentUploadCancelled = await CancelUpload().ConfigureAwait(false);
}
}
private HashSet<string> VerifyFiles(CharacterData data)
{
HashSet<string> unverifiedUploadHashes = new(StringComparer.Ordinal);
foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList())
{
if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime))
{
verifiedTime = DateTime.MinValue;
}
if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10)))
{
_logger.LogTrace("Verifying " + item + ", last verified: " + verifiedTime);
unverifiedUploadHashes.Add(item);
}
}
return unverifiedUploadHashes;
}
private async Task UploadMissingFiles(HashSet<string> unverifiedUploadHashes, CancellationToken uploadToken)
{
unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
_logger.LogDebug("Verifying " + unverifiedUploadHashes.Count + " files");
var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList()).ConfigureAwait(false);
foreach (var file in filesToUpload.Where(f => !f.IsForbidden))
{
try
{
CurrentUploads.Add(new UploadFileTransfer(file)
{
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Tried to request file " + file.Hash + " but file was not present");
}
}
foreach (var file in filesToUpload.Where(c => c.IsForbidden))
{
if (ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
{
ForbiddenTransfers.Add(new UploadFileTransfer(file)
{
LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty,
});
}
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
var totalSize = CurrentUploads.Sum(c => c.Total);
_logger.LogDebug("Compressing and uploading files");
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
{
_logger.LogDebug("Compressing and uploading " + file);
var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
await UploadFile(data.Item2, file.Hash, uploadToken).ConfigureAwait(false);
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
uploadToken.ThrowIfCancellationRequested();
}
if (CurrentUploads.Any())
{
var compressedSize = CurrentUploads.Sum(c => c.Total);
_logger.LogDebug($"Compressed {UiShared.ByteToString(totalSize)} to {UiShared.ByteToString(compressedSize)} ({(compressedSize / (double)totalSize):P2})");
_logger.LogDebug("Upload tasks complete, waiting for server to confirm");
_logger.LogDebug("Uploads open: " + CurrentUploads.Any(c => c.IsInTransfer));
const double waitStep = 1.0d;
while (CurrentUploads.Any(c => c.IsInTransfer) && !uploadToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(waitStep), uploadToken).ConfigureAwait(false);
_logger.LogDebug("Waiting for uploads to finish");
}
}
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
{
_verifiedUploadedHashes[file] = DateTime.UtcNow;
}
CurrentUploads.Clear();
}
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{
_logger.LogInformation("Pushing character data for " + character.DataHash.Value + " to " + string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));
StringBuilder sb = new();
foreach (var kvp in character.FileReplacements.ToList())
{
sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}");
character.FileReplacements[kvp.Key].RemoveAll(i => ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
}
foreach (var item in character.GlamourerData)
{
sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}");
}
_logger.LogDebug("Chara data contained: " + Environment.NewLine + sb.ToString());
await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false);
}
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
{
var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath;
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
(int)new FileInfo(fileCache).Length));
}
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken)
{
if (uploadToken.IsCancellationRequested) return;
async IAsyncEnumerable<byte[]> AsyncFileData([EnumeratorCancellation] CancellationToken token)
{
var chunkSize = 1024 * 512; // 512kb
using var ms = new MemoryStream(compressedFile);
var buffer = new byte[chunkSize];
int bytesRead;
while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token).ConfigureAwait(false)) > 0 && !token.IsCancellationRequested)
{
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred += bytesRead;
token.ThrowIfCancellationRequested();
yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray();
}
}
await FilesUploadStreamAsync(fileHash, AsyncFileData(uploadToken)).ConfigureAwait(false);
}
public async Task FilesUploadStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
{
await _mareHub!.InvokeAsync(nameof(FilesUploadStreamAsync), hash, fileContent).ConfigureAwait(false);
}
public async Task<bool> FilesIsUploadFinished()
{
return await _mareHub!.InvokeAsync<bool>(nameof(FilesIsUploadFinished)).ConfigureAwait(false);
}
public async Task<List<DownloadFileDto>> FilesGetSizes(List<string> hashes)
{
return await _mareHub!.InvokeAsync<List<DownloadFileDto>>(nameof(FilesGetSizes), hashes).ConfigureAwait(false);
}
public async Task<List<UploadFileDto>> FilesSend(List<string> fileListHashes)
{
return await _mareHub!.InvokeAsync<List<UploadFileDto>>(nameof(FilesSend), fileListHashes).ConfigureAwait(false);
}
public void CancelDownload(int downloadId)
{
while (CurrentDownloads.ContainsKey(downloadId))
{
CurrentDownloads.TryRemove(downloadId, out _);
}
}
}

View File

@@ -1,55 +0,0 @@
using MareSynchronos.API.Dto.User;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.WebAPI;
public partial class ApiController
{
public async Task UserDelete()
{
CheckConnection();
await FilesDeleteAll().ConfigureAwait(false);
await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false);
await CreateConnections().ConfigureAwait(false);
}
public async Task UserPushData(UserCharaDataMessageDto dto)
{
try
{
await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to Push character data");
}
}
public async Task<List<UserPairDto>> UserGetPairedClients()
{
return await _mareHub!.InvokeAsync<List<UserPairDto>>(nameof(UserGetPairedClients)).ConfigureAwait(false);
}
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs()
{
return await _mareHub!.InvokeAsync<List<OnlineUserIdentDto>>(nameof(UserGetOnlinePairs)).ConfigureAwait(false);
}
public async Task UserSetPairPermissions(UserPermissionsDto dto)
{
await _mareHub!.SendAsync(nameof(UserSetPairPermissions), dto).ConfigureAwait(false);
}
public async Task UserAddPair(UserDto dto)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserAddPair), dto).ConfigureAwait(false);
}
public async Task UserRemovePair(UserDto dto)
{
if (!IsConnected) return;
await _mareHub!.SendAsync(nameof(UserRemovePair), dto).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,341 @@
using Dalamud.Utility;
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{
private readonly ConcurrentDictionary<Guid, bool> _downloadReady = new();
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
private readonly FileCacheManager _fileDbManager;
private readonly FileTransferOrchestrator _orchestrator;
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
FileCacheManager fileCacheManager) : base(logger, mediator)
{
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator;
_fileDbManager = fileCacheManager;
Mediator.Subscribe<DownloadReadyMessage>(this, (msg) =>
{
if (_downloadReady.ContainsKey(msg.RequestId))
{
_downloadReady[msg.RequestId] = true;
}
});
}
public List<DownloadFileTransfer> CurrentDownloads { get; private set; } = new();
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
public bool IsDownloading => !CurrentDownloads.Any();
public void CancelDownload()
{
CurrentDownloads.Clear();
_downloadStatus.Clear();
}
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
{
Mediator.Publish(new HaltScanMessage("Download"));
try
{
await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false);
}
catch
{
CancelDownload();
}
finally
{
Mediator.Publish(new DownloadFinishedMessage(gameObject));
Mediator.Publish(new ResumeScanMessage("Download"));
}
}
protected override void Dispose(bool disposing)
{
CancelDownload();
base.Dispose(disposing);
}
private async Task DownloadFileHttpClient(string downloadGroup, DownloadFileTransfer fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
{
var requestId = await GetQueueRequest(fileTransfer, ct).ConfigureAwait(false);
Logger.LogDebug("GUID {requestId} for file {hash} on server {uri}", requestId, fileTransfer.Hash, fileTransfer.DownloadUri);
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
_downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
HttpResponseMessage response = null!;
var requestUrl = MareFiles.CacheGetFullPath(fileTransfer.DownloadUri, requestId);
Logger.LogDebug("Downloading {requestUrl} for file {hash}", requestUrl, fileTransfer.Hash);
try
{
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
{
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
}
}
try
{
var fileStream = File.Create(tempPath);
await using (fileStream.ConfigureAwait(false))
{
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 4096 : 1024;
var buffer = new byte[bufferSize];
var bytesRead = 0;
while ((bytesRead = await (await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false)).ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
{
ct.ThrowIfCancellationRequested();
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
progress.Report(bytesRead);
}
Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error during file download of {requestUrl}", requestUrl);
try
{
if (!tempPath.IsNullOrEmpty())
File.Delete(tempPath);
}
catch
{
// ignore if file deletion fails
}
throw;
}
}
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
{
Logger.LogDebug("Downloading files for {id}", gameObjectHandler.Name);
// force create lazy
_ = gameObjectHandler.GameObjectLazy.Value;
List<DownloadFileDto> downloadFileInfoFromService = new();
downloadFileInfoFromService.AddRange(await FilesGetSizes(fileReplacement.Select(f => f.Hash).ToList(), ct).ConfigureAwait(false));
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred).ToList();
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{
if (!_orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
{
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
}
}
var downloadGroups = CurrentDownloads.Where(f => f.CanBeTransferred).GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal);
foreach (var downloadGroup in downloadGroups)
{
_downloadStatus[downloadGroup.Key] = new FileDownloadStatus()
{
DownloadStatus = DownloadStatus.Initializing,
TotalBytes = downloadGroup.Sum(c => c.Total),
TotalFiles = downloadGroup.Count(),
TransferredBytes = 0,
TransferredFiles = 0
};
}
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
{
MaxDegreeOfParallelism = downloadGroups.Count(),
CancellationToken = ct,
},
async (fileGroup, token) =>
{
// let server predownload files
await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
foreach (var file in fileGroup)
{
var tempPath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: true);
Progress<long> progress = new((bytesDownloaded) =>
{
if (!_downloadStatus.ContainsKey(fileGroup.Key)) return;
_downloadStatus[fileGroup.Key].TransferredBytes += bytesDownloaded;
file.Transferred += bytesDownloaded;
});
try
{
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
await DownloadFileHttpClient(fileGroup.Key, file, tempPath, progress, token).ConfigureAwait(false);
_downloadStatus[fileGroup.Key].TransferredFiles += 1;
}
catch (OperationCanceledException)
{
File.Delete(tempPath);
Logger.LogDebug("Detected cancellation, removing {id}", gameObjectHandler);
CancelDownload();
return;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error during download of {hash}", file.Hash);
continue;
}
finally
{
_orchestrator.ReleaseDownloadSlot();
}
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.Decompressing;
var tempFileData = await File.ReadAllBytesAsync(tempPath, token).ConfigureAwait(false);
var extractedFile = LZ4Codec.Unwrap(tempFileData);
File.Delete(tempPath);
var filePath = _fileDbManager.GetCacheFilePath(file.Hash, isTemporaryFile: false);
await File.WriteAllBytesAsync(filePath, extractedFile, token).ConfigureAwait(false);
var fi = new FileInfo(filePath);
Func<DateTime> RandomDayInThePast()
{
DateTime start = new(1995, 1, 1);
Random gen = new();
int range = (DateTime.Today - start).Days;
return () => start.AddDays(gen.Next(range));
}
fi.CreationTime = RandomDayInThePast().Invoke();
fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke();
try
{
_ = _fileDbManager.CreateCacheEntry(filePath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Issue creating cache entry");
}
}
}).ConfigureAwait(false);
Logger.LogDebug("Download for {id} complete", gameObjectHandler);
CancelDownload();
}
private async Task<List<DownloadFileDto>> FilesGetSizes(List<string> hashes, CancellationToken ct)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), hashes, ct).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<DownloadFileDto>();
}
private async Task<Guid> GetQueueRequest(DownloadFileTransfer downloadFileTransfer, CancellationToken ct)
{
var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestRequestFileFullPath(downloadFileTransfer.DownloadUri, downloadFileTransfer.Hash), ct).ConfigureAwait(false);
var responseString = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
var requestId = Guid.Parse(responseString.Trim('"'));
if (!_downloadReady.ContainsKey(requestId))
{
_downloadReady[requestId] = false;
}
return requestId;
}
private async Task WaitForDownloadReady(DownloadFileTransfer downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
{
bool alreadyCancelled = false;
try
{
CancellationTokenSource localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
while (_downloadReady.TryGetValue(requestId, out bool isReady) && !isReady)
{
try
{
await Task.Delay(250, composite.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
if (downloadCt.IsCancellationRequested) throw;
var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer.DownloadUri, requestId, downloadFileTransfer.Hash), downloadCt).ConfigureAwait(false);
req.EnsureSuccessStatusCode();
localTimeoutCts.Dispose();
composite.Dispose();
localTimeoutCts = new();
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
}
}
localTimeoutCts.Dispose();
composite.Dispose();
Logger.LogDebug("Download {requestId} ready", requestId);
}
catch (TaskCanceledException)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
alreadyCancelled = true;
}
catch
{
// ignore whatever happens here
}
throw;
}
finally
{
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
{
try
{
await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer.DownloadUri, requestId)).ConfigureAwait(false);
}
catch
{
// ignore whatever happens here
}
}
_downloadReady.Remove(requestId, out _);
}
}
}

View File

@@ -0,0 +1,109 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
{
private readonly HttpClient _httpClient;
private readonly MareConfigService _mareConfig;
private readonly object _semaphoreModificationLock = new();
private readonly ServerConfigurationManager _serverManager;
private int _availableDownloadSlots;
private SemaphoreSlim _downloadSemaphore;
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, MareConfigService mareConfig, ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator)
{
_mareConfig = mareConfig;
_serverManager = serverManager;
_httpClient = new();
_availableDownloadSlots = mareConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots);
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress;
});
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
FilesCdnUri = null;
});
}
public Uri? FilesCdnUri { private set; get; }
public List<FileTransfer> ForbiddenTransfers { get; } = new();
public bool IsInitialized => FilesCdnUri != null;
public void ReleaseDownloadSlot()
{
_downloadSemaphore.Release();
}
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri, CancellationToken? ct = null)
{
using var requestMessage = new HttpRequestMessage(method, uri);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = JsonContent.Create(content);
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct)
{
using var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Content = content;
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
}
public async Task WaitForDownloadSlotAsync(CancellationToken token)
{
lock (_semaphoreModificationLock)
{
if (_availableDownloadSlots != _mareConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount)
{
_availableDownloadSlots = _mareConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots);
}
}
await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false);
}
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null)
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _serverManager.GetToken());
if (requestMessage.Content != null && requestMessage.Content is not StreamContent)
{
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
}
else
{
Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri);
}
try
{
if (ct != null)
return await _httpClient.SendAsync(requestMessage, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogCritical(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
throw;
}
}
}

View File

@@ -0,0 +1,226 @@
using LZ4;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI.Files.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace MareSynchronos.WebAPI.Files;
public sealed class FileUploadManager : DisposableMediatorSubscriberBase
{
private readonly FileCacheManager _fileDbManager;
private readonly FileTransferOrchestrator _orchestrator;
private readonly ServerConfigurationManager _serverManager;
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
private CancellationTokenSource? _uploadCancellationTokenSource = new();
public FileUploadManager(ILogger<FileUploadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
FileCacheManager fileDbManager,
ServerConfigurationManager serverManager) : base(logger, mediator)
{
_orchestrator = orchestrator;
_fileDbManager = fileDbManager;
_serverManager = serverManager;
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
Reset();
});
}
public List<FileTransfer> CurrentUploads { get; } = new();
public bool IsUploading => CurrentUploads.Count > 0;
public bool CancelUpload()
{
if (CurrentUploads.Any())
{
Logger.LogDebug("Cancelling current upload");
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
return true;
}
return false;
}
public async Task DeleteAllFiles()
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false);
}
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
{
CancelUpload();
_uploadCancellationTokenSource = new CancellationTokenSource();
var uploadToken = _uploadCancellationTokenSource.Token;
Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentApiUrl);
HashSet<string> unverifiedUploads = GetUnverifiedFiles(data);
if (unverifiedUploads.Any())
{
await UploadUnverifiedFiles(unverifiedUploads, visiblePlayers, uploadToken).ConfigureAwait(false);
Logger.LogInformation("Upload complete for {hash}", data.DataHash.Value);
}
foreach (var kvp in data.FileReplacements)
{
data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Any(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase)));
}
return data;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Reset();
}
private async Task<List<UploadFileDto>> FilesSend(List<string> hashes, List<string> uids, CancellationToken ct)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
FilesSendDto filesSendDto = new()
{
FileHashes = hashes,
UIDs = uids
};
var response = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesFilesSendFullPath(_orchestrator.FilesCdnUri!), filesSendDto, ct).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<List<UploadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? new List<UploadFileDto>();
}
private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
{
var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath;
return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
(int)new FileInfo(fileCache).Length));
}
private HashSet<string> GetUnverifiedFiles(CharacterData data)
{
HashSet<string> unverifiedUploadHashes = new(StringComparer.Ordinal);
foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList())
{
if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime))
{
verifiedTime = DateTime.MinValue;
}
if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10)))
{
Logger.LogTrace("Verifying {item}, last verified: {date}", item, verifiedTime);
unverifiedUploadHashes.Add(item);
}
}
return unverifiedUploadHashes;
}
private void Reset()
{
_uploadCancellationTokenSource?.Cancel();
_uploadCancellationTokenSource?.Dispose();
_uploadCancellationTokenSource = null;
CurrentUploads.Clear();
_verifiedUploadedHashes.Clear();
}
private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken)
{
if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized");
Logger.LogInformation("Uploading {file}, {size}", fileHash, UiSharedService.ByteToString(compressedFile.Length));
if (uploadToken.IsCancellationRequested) return;
using var ms = new MemoryStream(compressedFile);
Progress<UploadProgress> prog = new((prog) =>
{
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
});
var streamContent = new ProgressableStreamContent(ms, prog);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = await _orchestrator.SendRequestStreamAsync(HttpMethod.Post, MareFiles.ServerFilesUploadFullPath(_orchestrator.FilesCdnUri!, fileHash), streamContent, uploadToken).ConfigureAwait(false);
Logger.LogDebug("Upload Status: {status}", response.StatusCode);
}
private async Task UploadUnverifiedFiles(HashSet<string> unverifiedUploadHashes, List<UserData> visiblePlayers, CancellationToken uploadToken)
{
unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal);
Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count);
var filesToUpload = await FilesSend(unverifiedUploadHashes.ToList(), visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false);
foreach (var file in filesToUpload.Where(f => !f.IsForbidden))
{
try
{
CurrentUploads.Add(new UploadFileTransfer(file)
{
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
});
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Tried to request file {hash} but file was not present", file.Hash);
}
}
foreach (var file in filesToUpload.Where(c => c.IsForbidden))
{
if (_orchestrator.ForbiddenTransfers.All(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal)))
{
_orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file)
{
LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty,
});
}
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
var totalSize = CurrentUploads.Sum(c => c.Total);
Logger.LogDebug("Compressing and uploading files");
Task uploadTask = Task.CompletedTask;
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
{
Logger.LogDebug("Compressing {file}", file);
var data = await GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
await uploadTask.ConfigureAwait(false);
uploadTask = UploadFile(data.Item2, file.Hash, uploadToken);
uploadToken.ThrowIfCancellationRequested();
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
}
if (CurrentUploads.Any())
{
await uploadTask.ConfigureAwait(false);
var compressedSize = CurrentUploads.Sum(c => c.Total);
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
}
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Any(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
{
_verifiedUploadedHashes[file] = DateTime.UtcNow;
}
CurrentUploads.Clear();
}
}

View File

@@ -1,6 +1,6 @@
using MareSynchronos.API.Dto.Files;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.Files.Models;
public class DownloadFileTransfer : FileTransfer
{
@@ -9,7 +9,10 @@ public class DownloadFileTransfer : FileTransfer
public Uri DownloadUri => new(Dto.Url);
public override long Total
{
set { }
set
{
// nothing to set
}
get => Dto.Size;
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronos.WebAPI.Files.Models;
public enum DownloadStatus
{
Initializing,
WaitingForSlot,
WaitingForQueue,
Downloading,
Decompressing
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronos.WebAPI.Files.Models;
public class FileDownloadStatus
{
public DownloadStatus DownloadStatus { get; set; }
public int TotalFiles { get; set; }
public int TransferredFiles { get; set; }
public long TotalBytes { get; set; }
public long TransferredBytes { get; set; }
}

View File

@@ -1,6 +1,6 @@
using MareSynchronos.API.Dto.Files;
namespace MareSynchronos.WebAPI.Utils;
namespace MareSynchronos.WebAPI.Files.Models;
public abstract class FileTransfer
{

View File

@@ -0,0 +1,93 @@
using System.Net;
namespace MareSynchronos.WebAPI.Files.Models;
public class ProgressableStreamContent : StreamContent
{
private const int _defaultBufferSize = 4096;
private readonly int _bufferSize;
private readonly IProgress<UploadProgress> _progress;
private readonly Stream _streamToWrite;
private bool _contentConsumed;
public ProgressableStreamContent(Stream streamToWrite, IProgress<UploadProgress> downloader)
: this(streamToWrite, _defaultBufferSize, downloader)
{
}
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<UploadProgress> progress)
: base(streamToWrite, bufferSize)
{
if (streamToWrite == null)
{
throw new ArgumentNullException(nameof(streamToWrite));
}
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
_streamToWrite = streamToWrite;
_bufferSize = bufferSize;
_progress = progress;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_streamToWrite.Dispose();
}
base.Dispose(disposing);
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
PrepareContent();
var buffer = new byte[_bufferSize];
var size = _streamToWrite.Length;
var uploaded = 0;
using (_streamToWrite)
{
while (true)
{
var length = _streamToWrite.Read(buffer, 0, buffer.Length);
if (length <= 0)
{
break;
}
uploaded += length;
_progress.Report(new UploadProgress(uploaded, size));
await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false);
}
}
}
protected override bool TryComputeLength(out long length)
{
length = _streamToWrite.Length;
return true;
}
private void PrepareContent()
{
if (_contentConsumed)
{
if (_streamToWrite.CanSeek)
{
_streamToWrite.Position = 0;
}
else
{
throw new InvalidOperationException("The stream has already been read.");
}
}
_contentConsumed = true;
}
}

Some files were not shown because too many files have changed in this diff Show More