diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index 9f5aed2..6d56416 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -336,7 +336,7 @@ public sealed class FileCacheManager : IDisposable private void AddHashedFile(FileCacheEntity fileCache) { - if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries)) + if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null) { _fileCaches[fileCache.Hash] = entries = []; } diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 644f612..00329f6 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -24,34 +24,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase _configurationService = configurationService; _dalamudUtil = dalamudUtil; - PlayerPersistentDataKey = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetWorldIdAsync().GetAwaiter().GetResult(); - - SemiTransientResources.TryAdd(ObjectKind.Player, new HashSet(StringComparer.Ordinal)); - if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var gamePaths)) - { - int restored = 0; - foreach (var gamePath in gamePaths) - { - if (string.IsNullOrEmpty(gamePath)) continue; - - try - { - 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.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count); - } - Mediator.Subscribe(this, Manager_PenumbraResourceLoadEvent); Mediator.Subscribe(this, (_) => Manager_PenumbraModSettingChanged()); Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); - Mediator.Subscribe(this, (_) => DalamudUtil_ClassJobChanged()); + Mediator.Subscribe(this, (msg) => + { + if (_playerRelatedPointers.Contains(msg.gameObjectHandler)) + { + DalamudUtil_ClassJobChanged(); + } + }); Mediator.Subscribe(this, (msg) => { _playerRelatedPointers.Add(msg.Handler); @@ -62,8 +44,41 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase }); } - private string PlayerPersistentDataKey { get; } - private ConcurrentDictionary> SemiTransientResources { get; } = new(); + private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); + private ConcurrentDictionary>? _semiTransientResources = null; + private ConcurrentDictionary> SemiTransientResources + { + get + { + if (_semiTransientResources == null) + { + _semiTransientResources = new(); + _semiTransientResources.TryAdd(ObjectKind.Player, new HashSet(StringComparer.Ordinal)); + if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var gamePaths)) + { + int restored = 0; + foreach (var gamePath in gamePaths) + { + if (string.IsNullOrEmpty(gamePath)) continue; + + try + { + 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.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count); + } + } + + return _semiTransientResources; + } + } private ConcurrentDictionary> TransientResources { get; } = new(); public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) diff --git a/MareSynchronos/Interop/DalamudLogger.cs b/MareSynchronos/Interop/DalamudLogger.cs index 2f1d117..b244d7f 100644 --- a/MareSynchronos/Interop/DalamudLogger.cs +++ b/MareSynchronos/Interop/DalamudLogger.cs @@ -36,6 +36,13 @@ internal sealed class DalamudLogger : ILogger StringBuilder sb = new(); sb.AppendLine($"[{_name}]{{{(int)logLevel}}} {state}: {exception?.Message}"); sb.AppendLine(exception?.StackTrace); + var innerException = exception?.InnerException; + while (innerException != null) + { + sb.AppendLine($"InnerException {innerException}: {innerException.Message}"); + sb.AppendLine(innerException.StackTrace); + innerException = innerException.InnerException; + } if (logLevel == LogLevel.Warning) _pluginLog.Warning(sb.ToString()); else if (logLevel == LogLevel.Error) diff --git a/MareSynchronos/Interop/IpcManager.cs b/MareSynchronos/Interop/IpcManager.cs index 64dd28c..9dbad06 100644 --- a/MareSynchronos/Interop/IpcManager.cs +++ b/MareSynchronos/Interop/IpcManager.cs @@ -22,6 +22,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; private readonly ICallGateSubscriber _customizePlusRevertCharacter; private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; + private readonly DalamudPluginInterface _pi; private readonly DalamudUtilService _dalamudUtil; private readonly ICallGateSubscriber<(int, int)> _glamourerApiVersions; private readonly ICallGateSubscriber? _glamourerApplyAll; @@ -47,7 +48,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly ICallGateSubscriber _palettePlusRemoveCharaPalette; private readonly ICallGateSubscriber _palettePlusSetCharaPalette; private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; - private readonly FuncSubscriber<(int, int)> _penumbraApiVersion; private readonly FuncSubscriber _penumbraAssignTemporaryCollection; private readonly FuncSubscriber _penumbraConvertTextureFile; private readonly FuncSubscriber _penumbraCreateNamedTemporaryCollection; @@ -64,7 +64,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly FuncSubscriber _penumbraRemoveTemporaryCollection; private readonly FuncSubscriber _penumbraRemoveTemporaryMod; private readonly FuncSubscriber _penumbraResolveModDir; - private readonly FuncSubscriber _penumbraResolvePaths; + private readonly FuncSubscriber> _penumbraResolvePaths; private readonly ParamsFuncSubscriber?[]> _penumbraResourcePaths; private readonly SemaphoreSlim _redrawSemaphore = new(2); private readonly uint LockCode = 0x6D617265; @@ -80,6 +80,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase public IpcManager(ILogger logger, DalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) { + _pi = pi; _dalamudUtil = dalamudUtil; _penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, PenumbraInit); @@ -87,7 +88,6 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase _penumbraResolveModDir = Penumbra.Api.Ipc.GetModDirectory.Subscriber(pi); _penumbraRedraw = Penumbra.Api.Ipc.RedrawObjectByName.Subscriber(pi); _penumbraRedrawObject = Penumbra.Api.Ipc.RedrawObject.Subscriber(pi); - _penumbraApiVersion = Penumbra.Api.Ipc.ApiVersions.Subscriber(pi); _penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, RedrawEvent); _penumbraGetMetaManipulations = Penumbra.Api.Ipc.GetPlayerMetaManipulations.Subscriber(pi); _penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi); @@ -95,7 +95,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase _penumbraCreateNamedTemporaryCollection = Penumbra.Api.Ipc.CreateNamedTemporaryCollection.Subscriber(pi); _penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi); _penumbraAssignTemporaryCollection = Penumbra.Api.Ipc.AssignTemporaryCollection.Subscriber(pi); - _penumbraResolvePaths = Penumbra.Api.Ipc.ResolvePlayerPaths.Subscriber(pi); + _penumbraResolvePaths = Penumbra.Api.Ipc.ResolvePlayerPathsAsync.Subscriber(pi); _penumbraEnabled = Penumbra.Api.Ipc.GetEnabledState.Subscriber(pi); _penumbraModSettingChanged = Penumbra.Api.Ipc.ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) => { @@ -157,7 +157,14 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (_) => PeriodicApiStateCheck()); - PeriodicApiStateCheck(); + try + { + PeriodicApiStateCheck(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to check for some IPC, plugin not installed?"); + } } public bool Initialized => CheckPenumbraApiInternal() && CheckGlamourerApiInternal(); @@ -286,7 +293,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase } } - public async Task GlamourerRevert(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + public async Task GlamourerRevert(ILogger logger, string name, GameObjectHandler handler, Guid applicationId, CancellationToken token) { if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; try @@ -297,7 +304,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase try { logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); - _glamourerUnlock.InvokeFunc(handler.Name, LockCode); + _glamourerUnlock.InvokeFunc(name, LockCode); logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId); _glamourerRevert.InvokeAction(chara, LockCode); logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId); @@ -315,6 +322,26 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase } } + public async Task GlamourerRevertByNameAsync(ILogger logger, string name, Guid applicationId) + { + if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; + + await _dalamudUtil.RunOnFrameworkThread(() => + { + try + { + logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId); + _glamourerRevertByName.InvokeAction(name, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); + _glamourerUnlock.InvokeFunc(name, LockCode); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during Glamourer RevertByName"); + } + }).ConfigureAwait(false); + } + public void GlamourerRevertByName(ILogger logger, string name, Guid applicationId) { if ((!CheckGlamourerApi()) || _dalamudUtil.IsZoning) return; @@ -569,7 +596,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase public async Task<(string[] forward, string[][] reverse)> PenumbraResolvePathsAsync(string[] forward, string[] reverse) { - return await _dalamudUtil.RunOnFrameworkThread(() => _penumbraResolvePaths.Invoke(forward, reverse)).ConfigureAwait(false); + return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false); } public async Task PenumbraSetManipulationDataAsync(ILogger logger, Guid applicationId, string collName, string manipulationData) @@ -700,20 +727,23 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private bool CheckPenumbraApiInternal() { - bool apiAvailable = false; + bool penumbraAvailable = false; try { - apiAvailable = _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 21 } && _penumbraEnabled.Invoke(); - _shownPenumbraUnavailable = _shownPenumbraUnavailable && !apiAvailable; - return apiAvailable; + penumbraAvailable = (_pi.InstalledPlugins + .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) + ?.Version ?? new Version(0, 0, 0, 0)) >= new Version(0, 8, 1, 6); + penumbraAvailable &= _penumbraEnabled.Invoke(); + _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; + return penumbraAvailable; } catch { - return apiAvailable; + return penumbraAvailable; } finally { - if (!apiAvailable && !_shownPenumbraUnavailable) + if (!penumbraAvailable && !_shownPenumbraUnavailable) { _shownPenumbraUnavailable = true; Mediator.Publish(new NotificationMessage("Penumbra inactive", "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Mare.", NotificationType.Error)); diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 61cd581..8392327 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -10,6 +10,8 @@ public class MareConfig : IMareConfiguration public string CacheFolder { get; set; } = string.Empty; public bool DisableOptionalPluginWarnings { get; set; } = false; public bool EnableDtrEntry { get; set; } = false; + public bool ShowUidInDtrTooltip { get; set; } = true; + public bool PreferNoteInDtrTooltip { get; set; } = false; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public string ExportFolder { get; set; } = string.Empty; @@ -22,13 +24,17 @@ public class MareConfig : IMareConfiguration public bool OpenGposeImportOnGposeStart { get; set; } = false; public bool OpenPopupOnAdd { get; set; } = true; public int ParallelDownloads { get; set; } = 10; + public int DownloadSpeedLimitInBytes { get; set; } = 0; + public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; public bool PreferNotesOverNamesForVisible { get; set; } = false; public float ProfileDelay { get; set; } = 1.5f; public bool ProfilePopoutRight { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false; public bool ProfilesShow { get; set; } = true; + public bool ShowSyncshellUsersInVisible { get; set; } = true; public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; public bool ShowOfflineUsersSeparately { get; set; } = true; + public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true; public bool GroupUpSyncshells { get; set; } = true; public bool ShowOnlineNotifications { get; set; } = false; public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true; diff --git a/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs b/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs new file mode 100644 index 0000000..815da1f --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.MareConfiguration.Models; + +public enum DownloadSpeeds +{ + Bps, + KBps, + MBps +} \ No newline at end of file diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index a378a2d..3a86763 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -31,6 +31,7 @@ + all @@ -39,7 +40,7 @@ - + diff --git a/MareSynchronos/PlayerData/Data/PlayerChanges.cs b/MareSynchronos/PlayerData/Data/PlayerChanges.cs index f33729c..bd1beb9 100644 --- a/MareSynchronos/PlayerData/Data/PlayerChanges.cs +++ b/MareSynchronos/PlayerData/Data/PlayerChanges.cs @@ -2,11 +2,12 @@ public enum PlayerChanges { - Heels = 1, - Customize = 2, - Palette = 3, - Honorific = 4, - ModFiles = 5, - ModManip = 6, - Glamourer = 7 + ModFiles = 1, + ModManip = 2, + Glamourer = 3, + Customize = 4, + Heels = 5, + Palette = 6, + Honorific = 7, + ForcedRedraw = 8, } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs index 1025f72..b5bdd82 100644 --- a/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs +++ b/MareSynchronos/PlayerData/Export/MareCharaFileManager.cs @@ -48,7 +48,7 @@ public class MareCharaFileManager : DisposableMediatorSubscriberBase { if ((await dalamudUtil.RunOnFrameworkThread(() => item.Value.CurrentAddress()).ConfigureAwait(false)) != nint.Zero) { - await _ipcManager.GlamourerRevert(logger, item.Value, Guid.NewGuid(), cts.Token).ConfigureAwait(false); + await _ipcManager.GlamourerRevert(logger, item.Value.Name, item.Value, Guid.NewGuid(), cts.Token).ConfigureAwait(false); } else { diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index f9ee5f6..73bad33 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -1,6 +1,7 @@ using MareSynchronos.API.Dto.User; using MareSynchronos.FileCache; using MareSynchronos.Interop; +using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -20,12 +21,13 @@ public class PairHandlerFactory private readonly IpcManager _ipcManager; private readonly ILoggerFactory _loggerFactory; private readonly MareMediator _mareMediator; + private readonly MareConfigService _mareConfigService; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, - FileCacheManager fileCacheManager, MareMediator mareMediator) + FileCacheManager fileCacheManager, MareMediator mareMediator, MareConfigService mareConfigService) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -36,12 +38,13 @@ public class PairHandlerFactory _hostApplicationLifetime = hostApplicationLifetime; _fileCacheManager = fileCacheManager; _mareMediator = mareMediator; + _mareConfigService = mareConfigService; } public PairHandler Create(OnlineUserIdentDto onlineUserIdentDto) { return new PairHandler(_loggerFactory.CreateLogger(), onlineUserIdentDto, _gameObjectHandlerFactory, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, - _fileCacheManager, _mareMediator); + _fileCacheManager, _mareMediator, _mareConfigService); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs index 84f979f..ee2e71f 100644 --- a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -22,6 +22,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase private bool _haltProcessing = false; private bool _ignoreSendAfterRedraw = false; private int _ptrNullCounter = 0; + private byte _classJob = 0; private CancellationTokenSource _zoningCts = new(); public GameObjectHandler(ILogger logger, PerformanceCollectorService performanceCollector, @@ -98,6 +99,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase ModelFilesInSlotLoaded } + public byte RaceId { get; private set; } + public byte Gender { get; private set; } + public byte TribeId { get; private set; } + public IntPtr Address { get; private set; } public string Name { get; private set; } public ObjectKind ObjectKind { get; } @@ -215,6 +220,14 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase && ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human) { + var classJob = chara->CharacterData.ClassJob; + if (classJob != _classJob) + { + Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob); + _classJob = classJob; + Mediator.Publish(new ClassJobChangedMessage(this)); + } + equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head); ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand); @@ -244,6 +257,19 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase && ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human) { + var gender = ((Human*)DrawObjectAddress)->Customize.Sex; + var raceId = ((Human*)DrawObjectAddress)->Customize.Race; + var tribeId = ((Human*)DrawObjectAddress)->Customize.Clan; + + if (_isOwnedObject && ObjectKind == ObjectKind.Player + && (gender != Gender || raceId != RaceId || tribeId != TribeId)) + { + Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId)); + Gender = gender; + RaceId = raceId; + TribeId = tribeId; + } + customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data); if (customizeDiff) Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff); diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index 9884ed5..917dad8 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -2,6 +2,7 @@ using MareSynchronos.API.Dto.User; using MareSynchronos.FileCache; using MareSynchronos.Interop; +using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -21,6 +22,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly FileDownloadManager _downloadManager; private readonly FileCacheManager _fileDbManager; + private readonly MareConfigService _mareConfigService; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; private readonly IHostApplicationLifetime _lifetime; @@ -34,13 +36,16 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private bool _forceApplyMods = false; private bool _isVisible; private string _penumbraCollection; + private bool _redrawOnNextApplication = false; + public long LastAppliedDataSize { get; private set; } public PairHandler(ILogger logger, OnlineUserIdentDto onlineUser, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManager transferManager, PluginWarningNotificationService pluginWarningNotificationManager, DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, - FileCacheManager fileDbManager, MareMediator mediator) : base(logger, mediator) + FileCacheManager fileDbManager, MareMediator mediator, + MareConfigService mareConfigService) : base(logger, mediator) { OnlineUser = onlineUser; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -50,7 +55,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _dalamudUtil = dalamudUtil; _lifetime = lifetime; _fileDbManager = fileDbManager; - + _mareConfigService = mareConfigService; _penumbraCollection = _ipcManager.PenumbraCreateTemporaryCollectionAsync(logger, OnlineUser.User.UID).ConfigureAwait(false).GetAwaiter().GetResult(); Mediator.Subscribe(this, (_) => FrameworkUpdate()); @@ -70,6 +75,15 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _charaHandler = null; } }); + Mediator.Subscribe(this, (msg) => + { + if (msg.gameObjectHandler == _charaHandler) + { + _redrawOnNextApplication = true; + } + }); + + LastAppliedDataSize = -1; } public bool IsVisible @@ -127,6 +141,12 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _forceApplyMods = false; } + if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player)) + { + player.Add(PlayerChanges.ForcedRedraw); + _redrawOnNextApplication = false; + } + if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges)) { _pluginWarningNotificationManager.NotifyForMissingPlugins(OnlineUser.User, PlayerName!, playerChanges); @@ -173,22 +193,35 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (_lifetime.ApplicationStopping.IsCancellationRequested) return; - if (_dalamudUtil is { IsZoning: false, IsInCutscene: false }) + if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) { Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); + Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, OnlineUser); _ipcManager.PenumbraRemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult(); - - foreach (KeyValuePair> item in _cachedData?.FileReplacements ?? []) + if (!IsVisible) { - try + Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, OnlineUser); + _ipcManager.GlamourerRevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult(); + } + else + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(60)); + + foreach (KeyValuePair> item in _cachedData?.FileReplacements ?? []) { - RevertCustomizationDataAsync(item.Key, name, applicationId).GetAwaiter().GetResult(); - } - catch (InvalidOperationException ex) - { - Logger.LogWarning(ex, "Failed disposing player (not present anymore?)"); - break; + try + { + RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult(); + } + catch (InvalidOperationException ex) + { + Logger.LogWarning(ex, "Failed disposing player (not present anymore?)"); + break; + } } + + cts.CancelDispose(); } } } @@ -263,15 +296,15 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase } break; - case PlayerChanges.ModFiles: - case PlayerChanges.ModManip: + case PlayerChanges.ForcedRedraw: + await _ipcManager.PenumbraRedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); + break; + + default: break; } token.ThrowIfCancellationRequested(); } - - if (changes.Value.Contains(PlayerChanges.ModFiles) || changes.Value.Contains(PlayerChanges.ModManip) || changes.Value.Contains(PlayerChanges.Glamourer)) - await _ipcManager.PenumbraRedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); } finally { @@ -361,6 +394,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase if (updateModdedPaths) { await _ipcManager.PenumbraSetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection, moddedPaths).ConfigureAwait(false); + LastAppliedDataSize = -1; + foreach (var path in moddedPaths.Select(v => new FileInfo(v.Value)).Where(p => p.Exists)) + { + LastAppliedDataSize += path.Length; + } } if (updateManip) @@ -453,14 +491,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _ipcManager.PenumbraAssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult(); } - private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId) + private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken) { nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(OnlineUser.Ident); if (address == nint.Zero) return; - var cancelToken = new CancellationTokenSource(); - cancelToken.CancelAfter(TimeSpan.FromSeconds(60)); - Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, OnlineUser.User.AliasOrUID, name, objectKind); if (objectKind == ObjectKind.Player) @@ -468,7 +503,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); - await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); + await _ipcManager.GlamourerRevert(Logger, name, tempHandler, applicationId, cancelToken).ConfigureAwait(false); tempHandler.CompareNameAndThrow(name); Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, OnlineUser.User.AliasOrUID, name); await _ipcManager.HeelsRestoreOffsetForPlayerAsync(address).ConfigureAwait(false); @@ -489,8 +524,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { await _ipcManager.CustomizePlusRevertAsync(minionOrMount).ConfigureAwait(false); using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false); - await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); - await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); + await _ipcManager.GlamourerRevert(Logger, tempHandler.Name, tempHandler, applicationId, cancelToken).ConfigureAwait(false); + await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false); } } else if (objectKind == ObjectKind.Pet) @@ -500,8 +535,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { await _ipcManager.CustomizePlusRevertAsync(pet).ConfigureAwait(false); using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false); - await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); - await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); + await _ipcManager.GlamourerRevert(Logger, tempHandler.Name, tempHandler, applicationId, cancelToken).ConfigureAwait(false); + await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false); } } else if (objectKind == ObjectKind.Companion) @@ -511,12 +546,10 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { await _ipcManager.CustomizePlusRevertAsync(companion).ConfigureAwait(false); using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false); - await _ipcManager.GlamourerRevert(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); - await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken.Token).ConfigureAwait(false); + await _ipcManager.GlamourerRevert(Logger, tempHandler.Name, tempHandler, applicationId, cancelToken).ConfigureAwait(false); + await _ipcManager.PenumbraRedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false); } } - - cancelToken.CancelDispose(); } private List TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary moddedDictionary, CancellationToken token) diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs index e6c4df9..930ce72 100644 --- a/MareSynchronos/PlayerData/Pairs/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -44,6 +44,7 @@ public class Pair public bool IsVisible => CachedPlayer?.IsVisible ?? false; public CharacterData? LastReceivedCharacterData { get; set; } public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; + public long LastAppliedDataSize => CachedPlayer?.LastAppliedDataSize ?? -1; public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; @@ -58,9 +59,11 @@ public class Pair SeStringBuilder seStringBuilder = new(); SeStringBuilder seStringBuilder2 = new(); SeStringBuilder seStringBuilder3 = new(); + SeStringBuilder seStringBuilder4 = new(); var openProfileSeString = seStringBuilder.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Open Profile").Build(); var reapplyDataSeString = seStringBuilder2.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Reapply last data").Build(); var cyclePauseState = seStringBuilder3.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Cycle pause state").Build(); + var changePermissions = seStringBuilder4.AddUiForeground(526).AddText(" ").AddUiForegroundOff().AddText("Change Permissions").Build(); args.AddCustomItem(new GameObjectContextMenuItem(openProfileSeString, (a) => { _mediator.Publish(new ProfileOpenStandaloneMessage(this)); @@ -69,6 +72,10 @@ public class Pair { ApplyLastReceivedData(forced: true); }, useDalamudIndicator: false)); + args.AddCustomItem(new GameObjectContextMenuItem(changePermissions, (a) => + { + _mediator.Publish(new OpenPermissionWindow(this)); + }, useDalamudIndicator: false)); args.AddCustomItem(new GameObjectContextMenuItem(cyclePauseState, (a) => { _mediator.Publish(new CyclePauseMessage(UserData)); diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index df56ff7..2951485 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -29,12 +29,13 @@ namespace MareSynchronos; public sealed class Plugin : IDalamudPlugin { private readonly CancellationTokenSource _pluginCts = new(); + private readonly Task _hostBuilderRunTask; public Plugin(DalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData, IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog) { - _ = new HostBuilder() + _hostBuilderRunTask = new HostBuilder() .UseContentRoot(pluginInterface.ConfigDirectory.FullName) .ConfigureLogging(lb => { @@ -86,22 +87,11 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService>(), pluginInterface)); - collection.AddSingleton((s) => new HubFactory(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), pluginLog)); - - // func factory method singletons - collection.AddSingleton(s => - new Func((pair) => - new StandaloneProfileUi(s.GetRequiredService>(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), pair))); + collection.AddSingleton(); // add scoped services collection.AddScoped(); + collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); @@ -109,23 +99,24 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); + collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface.UiBuilder, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); - collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); - collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), - s.GetRequiredService(), s.GetServices(), s.GetRequiredService>(), + collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface.UiBuilder, s.GetRequiredService(), + s.GetRequiredService(), s.GetServices(), + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new NotificationService(s.GetRequiredService>(), s.GetRequiredService(), pluginInterface.UiBuilder, chatGui, s.GetRequiredService())); collection.AddScoped((s) => new UiSharedService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), @@ -147,5 +138,6 @@ public sealed class Plugin : IDalamudPlugin { _pluginCts.Cancel(); _pluginCts.Dispose(); + Task.WaitAny(_hostBuilderRunTask); } } \ No newline at end of file diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs index db9285e..6e4034f 100644 --- a/MareSynchronos/Services/CommandManagerService.cs +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -1,6 +1,7 @@ using Dalamud.Game.Command; using Dalamud.Plugin.Services; using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.UI; @@ -16,13 +17,14 @@ public sealed class CommandManagerService : IDisposable private readonly ApiController _apiController; private readonly ICommandManager _commandManager; private readonly MareMediator _mediator; + private readonly MareConfigService _mareConfigService; private readonly PerformanceCollectorService _performanceCollectorService; private readonly PeriodicFileScanner _periodicFileScanner; private readonly ServerConfigurationManager _serverConfigurationManager; public CommandManagerService(ICommandManager commandManager, PerformanceCollectorService performanceCollectorService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, - ApiController apiController, MareMediator mediator) + ApiController apiController, MareMediator mediator, MareConfigService mareConfigService) { _commandManager = commandManager; _performanceCollectorService = performanceCollectorService; @@ -30,6 +32,7 @@ public sealed class CommandManagerService : IDisposable _periodicFileScanner = periodicFileScanner; _apiController = apiController; _mediator = mediator; + _mareConfigService = mareConfigService; _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) { HelpMessage = "Opens the Mare Synchronos UI" @@ -48,7 +51,10 @@ public sealed class CommandManagerService : IDisposable if (splitArgs == null || splitArgs.Length == 0) { // Interpret this as toggling the UI - _mediator.Publish(new UiToggleMessage(typeof(CompactUi))); + if (_mareConfigService.Current.HasValidSetup()) + _mediator.Publish(new UiToggleMessage(typeof(CompactUi))); + else + _mediator.Publish(new UiToggleMessage(typeof(IntroUi))); return; } @@ -100,7 +106,7 @@ public sealed class CommandManagerService : IDisposable } else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase)) { - _mediator.Publish(new OpenDataAnalysisUiMessage()); + _mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } } } \ No newline at end of file diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs index 9a662ed..0306e1c 100644 --- a/MareSynchronos/Services/DalamudUtilService.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -173,7 +173,7 @@ public class DalamudUtilService : IHostedService public async Task GetPlayerNameHashedAsync() { - return await RunOnFrameworkThread(() => (GetPlayerName() + GetWorldId()).GetHash256()).ConfigureAwait(false); + return await RunOnFrameworkThread(() => (GetPlayerName() + GetHomeWorldId()).GetHash256()).ConfigureAwait(false); } public IntPtr GetPlayerPointer() @@ -187,17 +187,28 @@ public class DalamudUtilService : IHostedService return await RunOnFrameworkThread(GetPlayerPointer).ConfigureAwait(false); } - public uint GetWorldId() + public uint GetHomeWorldId() { EnsureIsOnFramework(); return _clientState.LocalPlayer!.HomeWorld.Id; } + public uint GetWorldId() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer!.CurrentWorld.Id; + } + public async Task GetWorldIdAsync() { return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false); } + public async Task GetHomeWorldIdAsync() + { + return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false); + } + public unsafe bool IsGameObjectPresent(IntPtr key) { return _objectTable.Any(f => f.Address == key); @@ -494,17 +505,6 @@ public class DalamudUtilService : IHostedService _mediator.Publish(new DalamudLogoutMessage()); } - if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) - { - var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; - - if (_classJobId != newclassJobId) - { - _classJobId = newclassJobId; - _mediator.Publish(new ClassJobChangedMessage(_classJobId)); - } - } - _mediator.Publish(new DelayedFrameworkUpdateMessage()); _delayedFrameworkUpdateCheck = DateTime.Now; diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 75c2d0e..5c795d1 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -15,11 +15,10 @@ namespace MareSynchronos.Services.Mediator; public record SwitchToIntroUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase; -public record OpenDataAnalysisUiMessage : MessageBase; public record DalamudLoginMessage : MessageBase; public record DalamudLogoutMessage : MessageBase; public record FrameworkUpdateMessage : SameThreadMessage; -public record ClassJobChangedMessage(uint? ClassJob) : MessageBase; +public record ClassJobChangedMessage(GameObjectHandler gameObjectHandler) : MessageBase; public record DelayedFrameworkUpdateMessage : SameThreadMessage; public record ZoneSwitchStartMessage : MessageBase; public record ZoneSwitchEndMessage : MessageBase; @@ -55,9 +54,9 @@ public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThr public record CharacterDataAnalyzedMessage : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; -public record HubReconnectingMessage(Exception? Exception) : MessageBase; -public record HubReconnectedMessage(string? Arg) : MessageBase; -public record HubClosedMessage(Exception? Exception) : MessageBase; +public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage; +public record HubReconnectedMessage(string? Arg) : SameThreadMessage; +public record HubClosedMessage(Exception? Exception) : SameThreadMessage; public record DownloadReadyMessage(Guid RequestId) : MessageBase; public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary DownloadStatus) : MessageBase; public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; @@ -72,9 +71,10 @@ public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : Message public record PairHandlerVisibleMessage(PairHandler Player) : MessageBase; public record OpenReportPopupMessage(Pair PairToReport) : MessageBase; public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase; -public record JoinSyncshellPopupMessage() : MessageBase; -public record OpenCreateSyncshellPopupMessage() : MessageBase; -public record OpenSyncshellAdminPanelPopupMessage(GroupFullInfoDto GroupInfo) : MessageBase; +public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase; +public record OpenPermissionWindow(Pair Pair) : MessageBase; +public record DownloadLimitChangedMessage() : SameThreadMessage; +public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index 77e4a1e..24bcf7b 100644 --- a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -62,7 +62,7 @@ public class ServerConfigurationManager } var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); - var worldId = _dalamudUtil.GetWorldIdAsync().GetAwaiter().GetResult(); + var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any()) { currentServer.Authentications.Add(new Authentication() @@ -136,7 +136,7 @@ public class ServerConfigurationManager server.Authentications.Add(new Authentication() { CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), - WorldId = _dalamudUtil.GetWorldIdAsync().GetAwaiter().GetResult(), + WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(), SecretKeyIdx = addLastSecretKey ? server.SecretKeys.Last().Key : -1, }); Save(); diff --git a/MareSynchronos/Services/UiFactory.cs b/MareSynchronos/Services/UiFactory.cs new file mode 100644 index 0000000..7db5232 --- /dev/null +++ b/MareSynchronos/Services/UiFactory.cs @@ -0,0 +1,52 @@ +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.UI.Components.Popup; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class UiFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverConfigManager; + private readonly MareProfileManager _mareProfileManager; + + public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController, + UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager, + MareProfileManager mareProfileManager) + { + _loggerFactory = loggerFactory; + _mareMediator = mareMediator; + _apiController = apiController; + _uiSharedService = uiSharedService; + _pairManager = pairManager; + _serverConfigManager = serverConfigManager; + _mareProfileManager = mareProfileManager; + } + + public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) + { + return new SyncshellAdminUI(_loggerFactory.CreateLogger(), _mareMediator, + _apiController, _uiSharedService, _pairManager, dto); + } + + public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) + { + return new StandaloneProfileUi(_loggerFactory.CreateLogger(), _mareMediator, + _uiSharedService, _serverConfigManager, _mareProfileManager, _pairManager, pair); + } + + public PermissionWindowUI CreatePermissionPopupUi(Pair pair) + { + return new PermissionWindowUI(_loggerFactory.CreateLogger(), pair, + _mareMediator, _uiSharedService, _apiController); + } +} diff --git a/MareSynchronos/Services/UiService.cs b/MareSynchronos/Services/UiService.cs index 60565a7..54a8053 100644 --- a/MareSynchronos/Services/UiService.cs +++ b/MareSynchronos/Services/UiService.cs @@ -1,10 +1,10 @@ -using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; -using Dalamud.Plugin; using MareSynchronos.MareConfiguration; -using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; using MareSynchronos.UI; +using MareSynchronos.UI.Components.Popup; using Microsoft.Extensions.Logging; namespace MareSynchronos.Services; @@ -12,28 +12,31 @@ namespace MareSynchronos.Services; public sealed class UiService : DisposableMediatorSubscriberBase { private readonly List _createdWindows = []; - private readonly DalamudPluginInterface _dalamudPluginInterface; + private readonly UiBuilder _uiBuilder; private readonly FileDialogManager _fileDialogManager; private readonly ILogger _logger; private readonly MareConfigService _mareConfigService; private readonly WindowSystem _windowSystem; + private readonly UiFactory _uiFactory; - public UiService(ILogger logger, DalamudPluginInterface dalamudPluginInterface, + public UiService(ILogger logger, UiBuilder uiBuilder, MareConfigService mareConfigService, WindowSystem windowSystem, - IEnumerable windows, Func standaloneProfileUiFactory, - FileDialogManager fileDialogManager, MareMediator mareMediator) : base(logger, mareMediator) + IEnumerable windows, + UiFactory uiFactory, FileDialogManager fileDialogManager, + MareMediator mareMediator) : base(logger, mareMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); - _dalamudPluginInterface = dalamudPluginInterface; + _uiBuilder = uiBuilder; _mareConfigService = mareConfigService; _windowSystem = windowSystem; + _uiFactory = uiFactory; _fileDialogManager = fileDialogManager; - _dalamudPluginInterface.UiBuilder.DisableGposeUiHide = true; - _dalamudPluginInterface.UiBuilder.Draw += Draw; - _dalamudPluginInterface.UiBuilder.OpenConfigUi += ToggleUi; - _dalamudPluginInterface.UiBuilder.OpenMainUi += ToggleMainUi; + _uiBuilder.DisableGposeUiHide = true; + _uiBuilder.Draw += Draw; + _uiBuilder.OpenConfigUi += ToggleUi; + _uiBuilder.OpenMainUi += ToggleMainUi; foreach (var window in windows) { @@ -45,7 +48,29 @@ public sealed class UiService : DisposableMediatorSubscriberBase if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui && string.Equals(ui.Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal))) { - var window = standaloneProfileUiFactory(msg.Pair); + var window = _uiFactory.CreateStandaloneProfileUi(msg.Pair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is SyncshellAdminUI ui + && string.Equals(ui.GroupFullInfo.GID, msg.GroupInfo.GID, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateSyncshellAdminUi(msg.GroupInfo); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is PermissionWindowUI ui + && msg.Pair == ui.Pair)) + { + var window = _uiFactory.CreatePermissionPopupUi(msg.Pair); _createdWindows.Add(window); _windowSystem.AddWindow(window); } @@ -88,8 +113,9 @@ public sealed class UiService : DisposableMediatorSubscriberBase window.Dispose(); } - _dalamudPluginInterface.UiBuilder.Draw -= Draw; - _dalamudPluginInterface.UiBuilder.OpenConfigUi -= ToggleUi; + _uiBuilder.Draw -= Draw; + _uiBuilder.OpenConfigUi -= ToggleUi; + _uiBuilder.OpenMainUi -= ToggleMainUi; } private void Draw() diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 7788d99..5d850bc 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -7,6 +7,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.API.Data.Extensions; @@ -113,9 +114,9 @@ public class CompactUi : WindowMediatorSubscriberBase $"It is highly recommended to keep Mare Synchronos up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed); } - UiSharedService.DrawWithID("header", DrawUIDHeader); + using (ImRaii.PushId("header")) DrawUIDHeader(); ImGui.Separator(); - UiSharedService.DrawWithID("serverstatus", DrawServerStatus); + using (ImRaii.PushId("serverstatus")) DrawServerStatus(); if (_apiController.ServerState is ServerState.Connected) { @@ -159,17 +160,17 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.Separator(); if (!hasShownSyncShells) { - UiSharedService.DrawWithID("pairlist", DrawPairList); + using (ImRaii.PushId("pairlist")) DrawPairList(); } else { - UiSharedService.DrawWithID("syncshells", _groupPanel.DrawSyncshells); + using (ImRaii.PushId("syncshells")) _groupPanel.DrawSyncshells(); } ImGui.Separator(); - UiSharedService.DrawWithID("transfers", DrawTransfers); + using (ImRaii.PushId("transfers")) DrawTransfers(); TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; - UiSharedService.DrawWithID("group-user-popup", () => _selectPairsForGroupUi.Draw(_pairManager.DirectPairs)); - UiSharedService.DrawWithID("grouping-popup", () => _selectGroupForPairUi.Draw()); + using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs); + using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw(); } if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) @@ -191,7 +192,7 @@ public class CompactUi : WindowMediatorSubscriberBase { 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 (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Save, "Save Note")) { _serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment); _lastAddedUser = null; @@ -226,7 +227,7 @@ public class CompactUi : WindowMediatorSubscriberBase if (keys.Any()) { if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key; - if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) { _serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication() { @@ -344,10 +345,10 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawPairList() { - UiSharedService.DrawWithID("addpair", DrawAddPair); - UiSharedService.DrawWithID("pairs", DrawPairs); + using (ImRaii.PushId("addpair")) DrawAddPair(); + using (ImRaii.PushId("pairs")) DrawPairs(); TransferPartHeight = ImGui.GetCursorPosY(); - UiSharedService.DrawWithID("filter", DrawFilter); + using (ImRaii.PushId("filter")) DrawFilter(); } private void DrawPairs() @@ -498,9 +499,9 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("No downloads in progress"); } - if (UiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Mare Character Data Analysis", WindowContentWidth)) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Mare Character Data Analysis", WindowContentWidth)) { - Mediator.Publish(new OpenDataAnalysisUiMessage()); + Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } ImGui.SameLine(); diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs index 2203ea5..ecd2f1a 100644 --- a/MareSynchronos/UI/Components/DrawGroupPair.cs +++ b/MareSynchronos/UI/Components/DrawGroupPair.cs @@ -18,7 +18,6 @@ namespace MareSynchronos.UI.Components; public class DrawGroupPair : DrawPairBase { protected readonly MareMediator _mediator; - private static string _banReason = string.Empty; private readonly GroupPairFullInfoDto _fullInfoDto; private readonly GroupFullInfoDto _group; @@ -248,7 +247,7 @@ public class DrawGroupPair : DrawPairBase if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner)) { var pinText = entryIsPinned ? "Unpin user" : "Pin user"; - if (UiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText)) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Thumbtack, pinText)) { ImGui.CloseCurrentPopup(); var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; @@ -256,14 +255,14 @@ public class DrawGroupPair : DrawPairBase } UiSharedService.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove user") && UiSharedService.CtrlPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Remove user") && UiSharedService.CtrlPressed()) { ImGui.CloseCurrentPopup(); _ = _apiController.GroupRemoveUser(_fullInfoDto); } UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) { ImGui.CloseCurrentPopup(); _mediator.Publish(new OpenBanUserPopupMessage(_pair, _group)); @@ -274,7 +273,7 @@ public class DrawGroupPair : DrawPairBase if (userIsOwner) { string modText = entryIsMod ? "Demod user" : "Mod user"; - if (UiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText) && UiSharedService.CtrlPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.UserShield, modText) && UiSharedService.CtrlPressed()) { ImGui.CloseCurrentPopup(); var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; @@ -282,7 +281,7 @@ public class DrawGroupPair : DrawPairBase } UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) { ImGui.CloseCurrentPopup(); _ = _apiController.GroupChangeOwnership(_fullInfoDto); @@ -293,13 +292,13 @@ public class DrawGroupPair : DrawPairBase ImGui.Separator(); if (!_pair.IsPaused) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.User, "Open Profile")) { _displayHandler.OpenProfile(_pair); ImGui.CloseCurrentPopup(); } UiSharedService.AttachToolTip("Opens the profile for this user in a new window"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) { ImGui.CloseCurrentPopup(); _mediator.Publish(new OpenReportPopupMessage(_pair)); diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 6596053..4322e68 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface; using ImGuiNET; using MareSynchronos.PlayerData.Pairs; @@ -66,7 +67,13 @@ public class DrawUserPair : DrawPairBase ImGui.PushFont(UiBuilder.IconFont); UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); ImGui.PopFont(); - UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName!); + var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName!; + if (_pair.LastAppliedDataSize >= 0) + { + visibleTooltip += UiSharedService.TooltipSeparator + + "Loaded Mods Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataSize, true); + } + UiSharedService.AttachToolTip(visibleTooltip); } } @@ -90,7 +97,7 @@ public class DrawUserPair : DrawPairBase { var infoIconPosDist = windowEndX - barButtonSize.X - spacingX - pauseIconSize.X - spacingX; var icon = FontAwesomeIcon.ExclamationTriangle; - var iconwidth = UiSharedService.GetIconSize(icon); + var iconwidth = UiSharedService.GetIconButtonSize(icon); rightSideStart = infoIconPosDist - iconwidth.X; ImGui.SameLine(infoIconPosDist - iconwidth.X); @@ -172,7 +179,7 @@ public class DrawUserPair : DrawPairBase } if (ImGui.BeginPopup("User Flyout Menu")) { - UiSharedService.DrawWithID($"buttons-{_pair.UserData.UID}", () => DrawPairedClientMenu(_pair)); + using (ImRaii.PushId($"buttons-{_pair.UserData.UID}")) DrawPairedClientMenu(_pair); ImGui.EndPopup(); } @@ -183,7 +190,7 @@ public class DrawUserPair : DrawPairBase { if (!entry.IsPaused) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.User, "Open Profile")) { _displayHandler.OpenProfile(entry); ImGui.CloseCurrentPopup(); @@ -192,7 +199,7 @@ public class DrawUserPair : DrawPairBase } if (entry.IsVisible) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Sync, "Reload last data")) { entry.ApplyLastReceivedData(forced: true); ImGui.CloseCurrentPopup(); @@ -200,13 +207,13 @@ public class DrawUserPair : DrawPairBase UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); } - if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state")) { _ = _apiController.CyclePause(entry.UserData); ImGui.CloseCurrentPopup(); } var entryUID = entry.UserData.AliasOrUID; - if (UiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) { _selectGroupForPairUi.Open(entry); } @@ -215,7 +222,7 @@ public class DrawUserPair : DrawPairBase 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 (UiSharedService.NormalizedIconTextButton(disableSoundsIcon, disableSoundsText)) { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableSounds(!isDisableSounds); @@ -225,7 +232,7 @@ public class DrawUserPair : DrawPairBase 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)) + if (UiSharedService.NormalizedIconTextButton(disableAnimsIcon, disableAnimsText)) { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableAnimations(!isDisableAnims); @@ -235,14 +242,14 @@ public class DrawUserPair : DrawPairBase var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX(); string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync"; var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; - if (UiSharedService.IconTextButton(disableVFXIcon, disableVFXText)) + if (UiSharedService.NormalizedIconTextButton(disableVFXIcon, disableVFXText)) { var permissions = entry.UserPair.OwnPermissions; permissions.SetDisableVFX(!isDisableVFX); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); } - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed()) { _ = _apiController.UserRemovePair(new(entry.UserData)); } @@ -251,7 +258,7 @@ public class DrawUserPair : DrawPairBase ImGui.Separator(); if (!entry.IsPaused) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Mare Profile")) { ImGui.CloseCurrentPopup(); _mediator.Publish(new OpenReportPopupMessage(_pair)); diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs index bb6f7ff..96ca49b 100644 --- a/MareSynchronos/UI/Components/GroupPanel.cs +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface; using Dalamud.Utility; using ImGuiNET; @@ -62,8 +63,8 @@ internal sealed class GroupPanel public void DrawSyncshells() { - UiSharedService.DrawWithID("addsyncshell", DrawAddSyncshell); - UiSharedService.DrawWithID("syncshelllist", DrawSyncshellList); + using (ImRaii.PushId("addsyncshell")) DrawAddSyncshell(); + using (ImRaii.PushId("syncshelllist")) DrawSyncshellList(); _mainUi.TransferPartHeight = ImGui.GetCursorPosY(); } @@ -265,7 +266,7 @@ internal sealed class GroupPanel } else { - var buttonSizes = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X + UiSharedService.GetIconSize(FontAwesomeIcon.LockOpen).X; + var buttonSizes = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X + UiSharedService.GetIconButtonSize(FontAwesomeIcon.LockOpen).X; ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) { @@ -280,7 +281,8 @@ internal sealed class GroupPanel UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); } - UiSharedService.DrawWithID(groupDto.GID + "settings", () => DrawSyncShellButtons(groupDto, pairsInGroup)); + + using (ImRaii.PushId(groupDto.GID + "settings")) DrawSyncShellButtons(groupDto, pairsInGroup); if (_showModalBanList && !_modalBanListOpened) { @@ -292,7 +294,7 @@ internal sealed class GroupPanel if (ImGui.BeginPopupModal("Manage Banlist for " + groupDto.GID, ref _showModalBanList, UiSharedService.PopupWindowFlags)) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) { _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; } @@ -321,7 +323,7 @@ internal sealed class GroupPanel ImGui.TableNextColumn(); UiSharedService.TextWrapped(bannedUser.Reason); ImGui.TableNextColumn(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban#" + bannedUser.UID)) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Check, "Unban#" + bannedUser.UID)) { _ = ApiController.GroupUnbanUser(bannedUser); _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); @@ -382,7 +384,7 @@ internal sealed class GroupPanel { ImGui.SetNextItemWidth(-1); ImGui.SliderInt("Amount##bulkinvites", ref _bulkInviteCount, 1, 100); - if (UiSharedService.IconTextButton(FontAwesomeIcon.MailBulk, "Create invites")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.MailBulk, "Create invites")) { _bulkOneTimeInvites = ApiController.GroupCreateTempInvite(groupDto, _bulkInviteCount).Result; } @@ -390,7 +392,7 @@ internal sealed class GroupPanel else { UiSharedService.TextWrapped("A total of " + _bulkOneTimeInvites.Count + " invites have been created."); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard")) { ImGui.SetClipboardText(string.Join(Environment.NewLine, _bulkOneTimeInvites)); } @@ -434,7 +436,7 @@ internal sealed class GroupPanel ImGui.Separator(); foreach (var entry in visibleUsers) { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); + using (ImRaii.PushId(groupDto.GID + entry.UID)) entry.DrawPairedClient(); } } @@ -444,7 +446,7 @@ internal sealed class GroupPanel ImGui.Separator(); foreach (var entry in onlineUsers) { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); + using (ImRaii.PushId(groupDto.GID + entry.UID)) entry.DrawPairedClient(); } } @@ -454,7 +456,7 @@ internal sealed class GroupPanel ImGui.Separator(); foreach (var entry in offlineUsers) { - UiSharedService.DrawWithID(groupDto.GID + entry.UID, () => entry.DrawPairedClient()); + using (ImRaii.PushId(groupDto.GID + entry.UID)) entry.DrawPairedClient(); } } @@ -487,8 +489,8 @@ internal sealed class GroupPanel var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; - var iconSize = UiSharedService.GetIconSize(infoIcon); - var diffLockUnlockIcons = showInfoIcon ? (UiSharedService.GetIconSize(infoIcon).X - iconSize.X) / 2 : 0; + var iconSize = UiSharedService.GetIconButtonSize(infoIcon); + var diffLockUnlockIcons = showInfoIcon ? (UiSharedService.GetIconButtonSize(infoIcon).X - iconSize.X) / 2 : 0; var barbuttonSize = UiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal); @@ -583,21 +585,21 @@ internal sealed class GroupPanel if (ImGui.BeginPopup("ShellPopup")) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell") && UiSharedService.CtrlPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell") && UiSharedService.CtrlPressed()) { _ = ApiController.GroupLeave(groupDto); } UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine + "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Copy, "Copy ID")) { ImGui.CloseCurrentPopup(); ImGui.SetClipboardText(groupDto.GroupAliasOrGID); } UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes")) { ImGui.CloseCurrentPopup(); ImGui.SetClipboardText(UiSharedService.GetNotes(groupPairs)); @@ -605,7 +607,7 @@ internal sealed class GroupPanel UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> Privacy -> Import Notes from Clipboard"); var soundsText = userSoundsDisabled ? "Enable sound sync" : "Disable sound sync"; - if (UiSharedService.IconTextButton(userSoundsIcon, soundsText)) + if (UiSharedService.NormalizedIconTextButton(userSoundsIcon, soundsText)) { ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; @@ -618,7 +620,7 @@ internal sealed class GroupPanel + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); var animText = userAnimDisabled ? "Enable animations sync" : "Disable animations sync"; - if (UiSharedService.IconTextButton(userAnimIcon, animText)) + if (UiSharedService.NormalizedIconTextButton(userAnimIcon, animText)) { ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; @@ -632,7 +634,7 @@ internal sealed class GroupPanel + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); var vfxText = userVFXDisabled ? "Enable VFX sync" : "Disable VFX sync"; - if (UiSharedService.IconTextButton(userVFXIcon, vfxText)) + if (UiSharedService.NormalizedIconTextButton(userVFXIcon, vfxText)) { ImGui.CloseCurrentPopup(); var perm = groupDto.GroupUserPermissions; @@ -648,10 +650,10 @@ internal sealed class GroupPanel if (isOwner || groupDto.GroupUserInfo.IsModerator()) { ImGui.Separator(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel")) { ImGui.CloseCurrentPopup(); - _mainUi.Mediator.Publish(new OpenSyncshellAdminPanelPopupMessage(groupDto)); + _mainUi.Mediator.Publish(new OpenSyncshellAdminPanel(groupDto)); } } @@ -667,7 +669,7 @@ internal sealed class GroupPanel ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false); foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList()) { - UiSharedService.DrawWithID(entry.Key.Group.GID, () => DrawSyncshell(entry.Key, entry.Value)); + using (ImRaii.PushId(entry.Key.Group.GID)) DrawSyncshell(entry.Key, entry.Value); } ImGui.EndChild(); } diff --git a/MareSynchronos/UI/Components/PairGroupsUi.cs b/MareSynchronos/UI/Components/PairGroupsUi.cs index 4527077..690e066 100644 --- a/MareSynchronos/UI/Components/PairGroupsUi.cs +++ b/MareSynchronos/UI/Components/PairGroupsUi.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using MareSynchronos.API.Data.Extensions; using MareSynchronos.MareConfiguration; @@ -81,7 +82,7 @@ public class PairGroupsUi if (ImGui.BeginPopup("Group Flyout Menu")) { - UiSharedService.DrawWithID($"buttons-{tag}", () => DrawGroupMenu(tag)); + using (ImRaii.PushId($"buttons-{tag}")) DrawGroupMenu(tag); ImGui.EndPopup(); } } @@ -113,7 +114,7 @@ public class PairGroupsUi { if (onlineUsers.Any() && onlineUsers.First() is DrawUserPair) { - UiSharedService.DrawWithID($"group-{tag}-buttons", () => DrawButtons(tag, allUsers.Cast().Where(p => otherUidsTaggedWithTag!.Contains(p.UID)).ToList())); + using (ImRaii.PushId($"group-{tag}-buttons")) DrawButtons(tag, allUsers.Cast().Where(p => otherUidsTaggedWithTag!.Contains(p.UID)).ToList()); } } @@ -126,13 +127,13 @@ public class PairGroupsUi private void DrawGroupMenu(string tag) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag)) + if (UiSharedService.NormalizedIconTextButton(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()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed()) { _tagHandler.RemoveTag(tag); } @@ -183,7 +184,7 @@ public class PairGroupsUi // These are all the OtherUIDs that are tagged with this tag foreach (var pair in availablePairsInThisCategory) { - UiSharedService.DrawWithID($"tag-{tag}-pair-${pair.UID}", () => pair.DrawPairedClient()); + using (ImRaii.PushId($"tag-{tag}-pair-${pair.UID}")) pair.DrawPairedClient(); } ImGui.Separator(); } @@ -192,33 +193,33 @@ public class PairGroupsUi { if (_mareConfig.Current.ShowVisibleUsersSeparately) { - UiSharedService.DrawWithID("$group-VisibleCustomTag", () => DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers)); + using (ImRaii.PushId("$group-VisibleCustomTag")) DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers); } foreach (var tag in tagsWithPairsInThem) { if (_mareConfig.Current.ShowOfflineUsersSeparately) { - UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers, allUsers, visibleUsers)); + using (ImRaii.PushId($"group-{tag}")) DrawCategory(tag, onlineUsers, allUsers, visibleUsers); } else { - UiSharedService.DrawWithID($"group-{tag}", () => DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers)); + using (ImRaii.PushId($"group-{tag}")) DrawCategory(tag, onlineUsers.Concat(offlineUsers).ToList(), allUsers, visibleUsers); } } if (_mareConfig.Current.ShowOfflineUsersSeparately) { - UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, - onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers)); - UiSharedService.DrawWithID($"group-OfflineCustomTag", () => DrawCategory(TagHandler.CustomOfflineTag, - offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers)); + using (ImRaii.PushId($"group-OnlineCustomTag")) DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers); + using (ImRaii.PushId($"group-OfflineCustomTag")) DrawCategory(TagHandler.CustomOfflineTag, + offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers); } else { - UiSharedService.DrawWithID($"group-OnlineCustomTag", () => DrawCategory(TagHandler.CustomOnlineTag, - onlineUsers.Concat(offlineUsers).Where(u => u.UserPair!.OtherPermissions.IsPaired() && !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers)); + using (ImRaii.PushId($"group-OnlineCustomTag")) DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Concat(offlineUsers).Where(u => u.UserPair!.OtherPermissions.IsPaired() && !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers); } - UiSharedService.DrawWithID($"group-UnpairedCustomTag", () => DrawCategory(TagHandler.CustomUnpairedTag, - offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers)); + using (ImRaii.PushId($"group-UnpairedCustomTag")) DrawCategory(TagHandler.CustomUnpairedTag, + offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers); } private void PauseRemainingPairs(List availablePairs) diff --git a/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs index 97a633d..8a2f69f 100644 --- a/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs @@ -27,7 +27,7 @@ public class BanUserPopupHandler : IPopupHandler UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell."); ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255); - if (UiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) { ImGui.CloseCurrentPopup(); var reason = _banReason; @@ -37,6 +37,10 @@ public class BanUserPopupHandler : IPopupHandler UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."); } + public void OnClose() + { + } + public void Open(OpenBanUserPopupMessage message) { _reportedPair = message.PairToBan; diff --git a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs index c29d64e..aa649b4 100644 --- a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs @@ -7,4 +7,6 @@ public interface IPopupHandler Vector2 PopupSize { get; } void DrawContent(); + + void OnClose(); } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/PopupHandler.cs b/MareSynchronos/UI/Components/Popup/PopupHandler.cs index 8359e97..46aa6e1 100644 --- a/MareSynchronos/UI/Components/Popup/PopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/PopupHandler.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using MareSynchronos.Services.Mediator; @@ -43,15 +44,6 @@ public class PopupHandler : WindowMediatorSubscriberBase ((BanUserPopupHandler)_currentHandler).Open(msg); IsOpen = true; }); - - Mediator.Subscribe(this, (msg) => - { - IsOpen = true; - _openPopup = true; - _currentHandler = _handlers.OfType().Single(); - ((SyncshellAdminPopupHandler)_currentHandler).Open(msg.GroupInfo); - IsOpen = true; - }); } public override void Draw() @@ -65,15 +57,22 @@ public class PopupHandler : WindowMediatorSubscriberBase } var viewportSize = ImGui.GetWindowViewport().Size; - ImGui.SetNextWindowSize(_currentHandler!.PopupSize); + ImGui.SetNextWindowSize(_currentHandler!.PopupSize * ImGuiHelpers.GlobalScale); ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); using var popup = ImRaii.Popup(WindowName, ImGuiWindowFlags.Modal); if (!popup) return; _currentHandler.DrawContent(); ImGui.Separator(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Times, "Close")) { ImGui.CloseCurrentPopup(); + _currentHandler.OnClose(); } } + + public override void OnClose() + { + base.OnClose(); + _currentHandler?.OnClose(); + } } \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs index c72146f..3f58dd2 100644 --- a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs +++ b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs @@ -40,7 +40,7 @@ internal class ReportPopupHandler : IPopupHandler using (ImRaii.Disabled(string.IsNullOrEmpty(_reportReason))) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Send Report")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.ExclamationTriangle, "Send Report")) { ImGui.CloseCurrentPopup(); var reason = _reportReason; @@ -49,6 +49,10 @@ internal class ReportPopupHandler : IPopupHandler } } + public void OnClose() + { + } + public void Open(OpenReportPopupMessage msg) { _reportedPair = msg.PairToReport; diff --git a/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs b/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs deleted file mode 100644 index 6fdad18..0000000 --- a/MareSynchronos/UI/Components/Popup/SyncshellAdminPopupHandler.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility.Raii; -using ImGuiNET; -using MareSynchronos.API.Data.Extensions; -using MareSynchronos.API.Dto.Group; -using MareSynchronos.PlayerData.Pairs; -using MareSynchronos.WebAPI; -using System.Globalization; -using System.Numerics; - -namespace MareSynchronos.UI.Components.Popup; - -internal class SyncshellAdminPopupHandler : IPopupHandler -{ - private readonly ApiController _apiController; - private readonly List _oneTimeInvites = []; - private readonly PairManager _pairManager; - private readonly UiSharedService _uiSharedService; - private List _bannedUsers = []; - private GroupFullInfoDto _groupFullInfo = null!; - private bool _isModerator = false; - private bool _isOwner = false; - private int _multiInvites = 30; - private string _newPassword = string.Empty; - private bool _pwChangeSuccess = true; - - public SyncshellAdminPopupHandler(ApiController apiController, UiSharedService uiSharedService, PairManager pairManager) - { - _apiController = apiController; - _uiSharedService = uiSharedService; - _pairManager = pairManager; - } - - public Vector2 PopupSize => new(700, 500); - - public void DrawContent() - { - if (!_isModerator && !_isOwner) return; - - _groupFullInfo = _pairManager.Groups[_groupFullInfo.Group]; - - using (ImRaii.PushFont(_uiSharedService.UidFont)) - ImGui.TextUnformatted(_groupFullInfo.GroupAliasOrGID + " Administrative Panel"); - - ImGui.Separator(); - var perm = _groupFullInfo.GroupPermissions; - - var inviteNode = ImRaii.TreeNode("Invites"); - if (inviteNode) - { - bool isInvitesDisabled = perm.IsDisableInvites(); - - if (UiSharedService.IconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock, - isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell")) - { - perm.SetDisableInvites(!isInvitesDisabled); - _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); - } - - ImGui.Dummy(new(2f)); - - UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password."); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) - { - ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(_groupFullInfo.Group), 1).Result.FirstOrDefault() ?? string.Empty); - } - UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); - ImGui.InputInt("##amountofinvites", ref _multiInvites); - ImGui.SameLine(); - using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100)) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites")) - { - _oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(_groupFullInfo.Group), _multiInvites).Result); - } - } - - if (_oneTimeInvites.Any()) - { - var invites = string.Join(Environment.NewLine, _oneTimeInvites); - ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard")) - { - ImGui.SetClipboardText(invites); - } - } - } - inviteNode.Dispose(); - - var mgmtNode = ImRaii.TreeNode("User Management"); - if (mgmtNode) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) - { - _ = _apiController.GroupClear(new(_groupFullInfo.Group)); - } - UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell"); - - ImGui.Dummy(new(2f)); - - if (UiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) - { - _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(_groupFullInfo.Group)).Result; - } - - if (ImGui.BeginTable("bannedusertable" + _groupFullInfo.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) - { - ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); - ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); - - ImGui.TableHeadersRow(); - - foreach (var bannedUser in _bannedUsers.ToList()) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UID); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedBy); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); - ImGui.TableNextColumn(); - UiSharedService.TextWrapped(bannedUser.Reason); - ImGui.TableNextColumn(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban#" + bannedUser.UID)) - { - _ = _apiController.GroupUnbanUser(bannedUser); - _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); - } - } - - ImGui.EndTable(); - } - } - mgmtNode.Dispose(); - - var permNode = ImRaii.TreeNode("Permissions"); - if (permNode) - { - bool isDisableAnimations = perm.IsDisableAnimations(); - bool isDisableSounds = perm.IsDisableSounds(); - bool isDisableVfx = perm.IsDisableVFX(); - - ImGui.AlignTextToFramePadding(); - ImGui.Text("Sound Sync"); - UiSharedService.BooleanToColoredIcon(!isDisableSounds); - ImGui.SameLine(230); - if (UiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, - isDisableSounds ? "Enable sound sync" : "Disable sound sync")) - { - perm.SetDisableSounds(!perm.IsDisableSounds()); - _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); - } - - ImGui.AlignTextToFramePadding(); - ImGui.Text("Animation Sync"); - UiSharedService.BooleanToColoredIcon(!isDisableAnimations); - ImGui.SameLine(230); - if (UiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, - isDisableAnimations ? "Enable animation sync" : "Disable animation sync")) - { - perm.SetDisableAnimations(!perm.IsDisableAnimations()); - _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); - } - - ImGui.AlignTextToFramePadding(); - ImGui.Text("VFX Sync"); - UiSharedService.BooleanToColoredIcon(!isDisableVfx); - ImGui.SameLine(230); - if (UiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, - isDisableVfx ? "Enable VFX sync" : "Disable VFX sync")) - { - perm.SetDisableVFX(!perm.IsDisableVFX()); - _ = _apiController.GroupChangeGroupPermissionState(new(_groupFullInfo.Group, perm)); - } - } - permNode.Dispose(); - - if (_isOwner) - { - var ownerNode = ImRaii.TreeNode("Owner Settings"); - if (ownerNode) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("New Password"); - ImGui.SameLine(); - ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50); - ImGui.SameLine(); - using (ImRaii.Disabled(_newPassword.Length < 10)) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) - { - _pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(_groupFullInfo.Group, _newPassword)).Result; - _newPassword = string.Empty; - } - } - UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible."); - - if (!_pwChangeSuccess) - { - UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow); - } - - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) - { - ImGui.CloseCurrentPopup(); - _ = _apiController.GroupDelete(new(_groupFullInfo.Group)); - } - UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); - } - ownerNode.Dispose(); - } - } - - public void Open(GroupFullInfoDto groupFullInfo) - { - _groupFullInfo = groupFullInfo; - _isOwner = string.Equals(_groupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); - _isModerator = _groupFullInfo.GroupUserInfo.IsModerator(); - _newPassword = string.Empty; - _bannedUsers.Clear(); - _oneTimeInvites.Clear(); - _multiInvites = 30; - _pwChangeSuccess = true; - } -} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs index 4be2e70..2a814bb 100644 --- a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs +++ b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs @@ -1,7 +1,7 @@ using System.Numerics; using Dalamud.Interface; -using Dalamud.Interface.Components; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.PlayerData.Pairs; @@ -65,14 +65,14 @@ public class SelectGroupForPairUi { foreach (var tag in tags) { - UiSharedService.DrawWithID($"groups-pair-{_pair.UserData.UID}-{tag}", () => DrawGroupName(_pair, tag)); + using (ImRaii.PushId($"groups-pair-{_pair.UserData.UID}-{tag}")) DrawGroupName(_pair, tag); } ImGui.EndChild(); } ImGui.Separator(); UiSharedService.FontText($"Create a new group for {name}.", UiBuilder.DefaultFont); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + if (UiSharedService.NormalizedIconButton(FontAwesomeIcon.Plus)) { HandleAddTag(); } diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs index 10cf97a..28a527e 100644 --- a/MareSynchronos/UI/DataAnalysisUi.cs +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -39,7 +39,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _hasUpdate = true; }); - Mediator.Subscribe(this, (_) => Toggle()); SizeConstraints = new() { MinimumSize = new() @@ -66,7 +65,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count); UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) { _conversionCancellationTokenSource.Cancel(); } @@ -108,7 +107,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", ImGuiColors.DalamudYellow); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) { _characterAnalyzer.CancelAnalyze(); } @@ -119,14 +118,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", ImGuiColors.DalamudYellow); - if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) { _ = _characterAnalyzer.ComputeAnalysis(print: false); } } else { - if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)")) { _ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true); } @@ -260,7 +259,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." + Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete." , ImGuiColors.DalamudYellow); - if (_texturesToConvert.Count > 0 && UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)")) + if (_texturesToConvert.Count > 0 && UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)")) { _conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate(); _conversionTask = _ipcManager.PenumbraConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token); diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs index 4cb46bf..18ca0ba 100644 --- a/MareSynchronos/UI/DtrEntry.cs +++ b/MareSynchronos/UI/DtrEntry.cs @@ -122,9 +122,20 @@ public sealed class DtrEntry : IDisposable, IHostedService text = $"\uE044 {pairCount}"; if (pairCount > 0) { - var visiblePairs = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible) - .Select(x => string.Format("{0} ({1})", x.PlayerName, x.UserData.AliasOrUID)); + IEnumerable visiblePairs; + if (_configService.Current.ShowUidInDtrTooltip) + { + visiblePairs = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible) + .Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID )); + } + else + { + visiblePairs = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible) + .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName)); + } + tooltip = $"Mare Synchronos: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; } else diff --git a/MareSynchronos/UI/EditProfileUi.cs b/MareSynchronos/UI/EditProfileUi.cs index ae83cb9..d3d03dc 100644 --- a/MareSynchronos/UI/EditProfileUi.cs +++ b/MareSynchronos/UI/EditProfileUi.cs @@ -131,7 +131,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.Separator(); _uiSharedService.BigText("Profile Settings"); - if (UiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) { _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => { @@ -162,7 +162,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("Select and upload a new profile picture"); ImGui.SameLine(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null)); } @@ -211,13 +211,13 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.EndChildFrame(); ImGui.PopFont(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Save, "Save Description")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText)); } UiSharedService.AttachToolTip("Sets your profile description text"); ImGui.SameLine(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Clear Description")) { _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "")); } diff --git a/MareSynchronos/UI/GposeUi.cs b/MareSynchronos/UI/GposeUi.cs index ac7b5ab..e90fedf 100644 --- a/MareSynchronos/UI/GposeUi.cs +++ b/MareSynchronos/UI/GposeUi.cs @@ -41,7 +41,7 @@ public class GposeUi : WindowMediatorSubscriberBase if (!_mareCharaFileManager.CurrentlyWorking) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF")) { _fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, paths) => { @@ -59,7 +59,7 @@ public class GposeUi : WindowMediatorSubscriberBase { UiSharedService.TextWrapped("Loaded file: " + _mareCharaFileManager.LoadedCharaFile.FilePath); UiSharedService.TextWrapped("File Description: " + _mareCharaFileManager.LoadedCharaFile.CharaFileData.Description); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Check, "Apply loaded MCDF")) { _ = Task.Run(async () => await _mareCharaFileManager.ApplyMareCharaFile(_dalamudUtil.GposeTargetGameObject).ConfigureAwait(false)); } diff --git a/MareSynchronos/UI/Handlers/TagHandler.cs b/MareSynchronos/UI/Handlers/TagHandler.cs index 7979861..11f2d41 100644 --- a/MareSynchronos/UI/Handlers/TagHandler.cs +++ b/MareSynchronos/UI/Handlers/TagHandler.cs @@ -5,6 +5,7 @@ namespace MareSynchronos.UI.Handlers; public class TagHandler { public const string CustomOfflineTag = "Mare_Offline"; + public const string CustomOfflineSyncshellTag = "Mare_OfflineSyncshell"; public const string CustomOnlineTag = "Mare_Online"; public const string CustomUnpairedTag = "Mare_Unpaired"; public const string CustomVisibleTag = "Mare_Visible"; diff --git a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs index 75f1295..7b57907 100644 --- a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs +++ b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.ServerConfiguration; @@ -38,9 +39,9 @@ public class UidDisplayHandler if (!string.Equals(_editNickEntry, pair.UserData.UID, StringComparison.Ordinal)) { ImGui.SetCursorPosY(originalY); - if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(playerText); - if (textIsUid) ImGui.PopFont(); + + using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText); + if (ImGui.IsItemHovered()) { if (!string.Equals(_lastMouseOverUid, id)) diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 96d5247..3fceced 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -37,6 +37,8 @@ public class IntroUi : WindowMediatorSubscriberBase _serverConfigurationManager = serverConfigurationManager; IsOpen = false; + ShowCloseButton = false; + RespectCloseHotkey = false; SizeConstraints = new WindowSizeConstraints() { @@ -60,9 +62,7 @@ public class IntroUi : WindowMediatorSubscriberBase if (!_configService.Current.AcceptedAgreement && !_readFirstPage) { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("Welcome to Mare Synchronos"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); + _uiShared.BigText("Welcome to Mare Synchronos"); ImGui.Separator(); 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."); diff --git a/MareSynchronos/UI/PermissionWindowUI.cs b/MareSynchronos/UI/PermissionWindowUI.cs new file mode 100644 index 0000000..a84aaf9 --- /dev/null +++ b/MareSynchronos/UI/PermissionWindowUI.cs @@ -0,0 +1,162 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +public class PermissionWindowUI : WindowMediatorSubscriberBase +{ + public Pair Pair { get; init; } + + private readonly UiSharedService _uiSharedService; + private readonly ApiController _apiController; + private UserPermissions _ownPermissions; + + public PermissionWindowUI(ILogger logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, + ApiController apiController) : base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###MareSynchronosPermissions" + pair.UserData.UID) + { + Pair = pair; + _uiSharedService = uiSharedService; + _apiController = apiController; + _ownPermissions = pair.UserPair.OwnPermissions.DeepClone(); + Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; + SizeConstraints = new() + { + MinimumSize = new(450, 100), + MaximumSize = new(450, 500) + }; + IsOpen = true; + } + + public override void Draw() + { + var paused = _ownPermissions.IsPaused(); + var disableSounds = _ownPermissions.IsDisableSounds(); + var disableAnimations = _ownPermissions.IsDisableAnimations(); + var disableVfx = _ownPermissions.IsDisableVFX(); + var style = ImGui.GetStyle(); + var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X; + + _uiSharedService.BigText("Permissions for " + Pair.UserData.AliasOrUID); + ImGuiHelpers.ScaledDummy(1f); + + if (ImGui.Checkbox("Pause Sync", ref paused)) + { + _ownPermissions.SetPaused(paused); + } + UiSharedService.DrawHelpText("Pausing will completely cease any sync with this user." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user pausing will cease sync completely."); + var otherPerms = Pair.UserPair.OtherPermissions; + + var otherIsPaused = otherPerms.IsPaused(); + var otherDisableSounds = otherPerms.IsDisableSounds(); + var otherDisableAnimations = otherPerms.IsDisableAnimations(); + var otherDisableVFX = otherPerms.IsDisableVFX(); + + using (ImRaii.PushIndent(indentSize, false)) + { + UiSharedService.BooleanToColoredIcon(!otherIsPaused, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherIsPaused ? "not " : string.Empty) + "paused you"); + } + + ImGuiHelpers.ScaledDummy(0.5f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(0.5f); + + if (ImGui.Checkbox("Disable Sounds", ref disableSounds)) + { + _ownPermissions.SetDisableSounds(disableSounds); + } + UiSharedService.DrawHelpText("Disabling sounds will remove all sounds synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + UiSharedService.BooleanToColoredIcon(!otherDisableSounds, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableSounds ? "not " : string.Empty) + "disabled sound sync with you"); + } + + if (ImGui.Checkbox("Disable Animations", ref disableAnimations)) + { + _ownPermissions.SetDisableAnimations(disableAnimations); + } + UiSharedService.DrawHelpText("Disabling sounds will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + UiSharedService.BooleanToColoredIcon(!otherDisableAnimations, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableAnimations ? "not " : string.Empty) + "disabled animation sync with you"); + } + + if (ImGui.Checkbox("Disable VFX", ref disableVfx)) + { + _ownPermissions.SetDisableVFX(disableVfx); + } + UiSharedService.DrawHelpText("Disabling sounds will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + UiSharedService.BooleanToColoredIcon(!otherDisableVFX, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you"); + } + + ImGuiHelpers.ScaledDummy(0.5f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(0.5f); + + bool hasChanges = _ownPermissions != Pair.UserPair.OwnPermissions; + + using (ImRaii.Disabled(!hasChanges)) + if (UiSharedService.NormalizedIconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save")) + { + _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); + } + UiSharedService.AttachToolTip("Save and apply all changes"); + + var rightSideButtons = UiSharedService.GetNormalizedIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert").X + + UiSharedService.GetNormalizedIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default").X; + var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + + ImGui.SameLine(availableWidth - rightSideButtons); + + using (ImRaii.Disabled(!hasChanges)) + if (UiSharedService.NormalizedIconTextButton(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert")) + { + _ownPermissions = Pair.UserPair.OwnPermissions.DeepClone(); + } + UiSharedService.AttachToolTip("Revert all changes"); + + ImGui.SameLine(); + if (UiSharedService.NormalizedIconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default")) + { + _ownPermissions.SetPaused(false); + _ownPermissions.SetDisableVFX(false); + _ownPermissions.SetDisableSounds(false); + _ownPermissions.SetDisableAnimations(false); + _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); + } + UiSharedService.AttachToolTip("This will set all permissions to their default setting"); + + var ySize = ImGui.GetCursorPosY() + style.FramePadding.Y * ImGuiHelpers.GlobalScale + style.FrameBorderSize; + ImGui.SetWindowSize(new(400, ySize)); + } + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } +} diff --git a/MareSynchronos/UI/PopoutProfileUi.cs b/MareSynchronos/UI/PopoutProfileUi.cs index b25506f..449a1e0 100644 --- a/MareSynchronos/UI/PopoutProfileUi.cs +++ b/MareSynchronos/UI/PopoutProfileUi.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using MareSynchronos.API.Data.Extensions; using MareSynchronos.MareConfiguration; @@ -110,14 +111,14 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase var rectMin = drawList.GetClipRectMin(); var rectMax = drawList.GetClipRectMax(); - if (_uiSharedService.UidFontBuilt) ImGui.PushFont(_uiSharedService.UidFont); - UiSharedService.ColorText(_pair.UserData.AliasOrUID, ImGuiColors.HealerGreen); - if (_uiSharedService.UidFontBuilt) ImGui.PopFont(); - ImGui.Dummy(new(spacing.Y, spacing.Y)); + using (ImRaii.PushFont(_uiSharedService.UidFont, _uiSharedService.UidFontBuilt)) + UiSharedService.ColorText(_pair.UserData.AliasOrUID, ImGuiColors.HealerGreen); + + ImGuiHelpers.ScaledDummy(spacing.Y, spacing.Y); var textPos = ImGui.GetCursorPosY(); ImGui.Separator(); var imagePos = ImGui.GetCursorPos(); - ImGui.Dummy(new(256, 256 * ImGuiHelpers.GlobalScale + spacing.Y)); + ImGuiHelpers.ScaledDummy(256, 256 * ImGuiHelpers.GlobalScale + spacing.Y); var note = _serverManager.GetNoteForUid(_pair.UserData.UID); if (!string.IsNullOrEmpty(note)) { diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index a7e54a2..67325eb 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -1,5 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; using MareSynchronos.API.Data; @@ -19,6 +21,7 @@ using MareSynchronos.WebAPI.Files.Models; using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using System.Globalization; using System.Numerics; using System.Text.Json; @@ -66,6 +69,8 @@ public class SettingsUi : WindowMediatorSubscriberBase _apiController = apiController; _fileCompactor = fileCompactor; _uiShared = uiShared; + AllowClickthrough = false; + AllowPinning = false; SizeConstraints = new WindowSizeConstraints() { @@ -139,6 +144,37 @@ public class SettingsUi : WindowMediatorSubscriberBase int maxParallelDownloads = _configService.Current.ParallelDownloads; bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload; + int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Global Download Speed Limit"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100); + if (ImGui.InputInt("###speedlimit", ref downloadSpeedLimit)) + { + _configService.Current.DownloadSpeedLimitInBytes = downloadSpeedLimit; + _configService.Save(); + Mediator.Publish(new DownloadLimitChangedMessage()); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100); + _uiShared.DrawCombo("###speed", new[] { DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps }, + (s) => s switch + { + DownloadSpeeds.Bps => "Byte/s", + DownloadSpeeds.KBps => "KB/s", + DownloadSpeeds.MBps => "MB/s", + _ => throw new NotSupportedException() + }, (s) => + { + _configService.Current.DownloadSpeedType = s; + _configService.Save(); + Mediator.Publish(new DownloadLimitChangedMessage()); + }, _configService.Current.DownloadSpeedType); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("0 = No limit/infinite"); + if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10)) { _configService.Current.ParallelDownloads = maxParallelDownloads; @@ -248,14 +284,14 @@ public class SettingsUi : WindowMediatorSubscriberBase foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray()) { var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); + var col = ImRaii.PushColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); ImGui.TextUnformatted(transfer.Hash); ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); + col.Dispose(); ImGui.TableNextRow(); } @@ -281,13 +317,13 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TextUnformatted(userName); ImGui.TableNextColumn(); ImGui.TextUnformatted(entry.Key); - ImGui.PushStyleColor(ImGuiCol.Text, color); + var col = ImRaii.PushColor(ImGuiCol.Text, color); ImGui.TableNextColumn(); ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + UiSharedService.ByteToString(entry.Value.TotalBytes)); ImGui.TableNextColumn(); - ImGui.PopStyleColor(); + col.Dispose(); ImGui.TableNextRow(); } } @@ -324,7 +360,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TreePop(); } #endif - if (UiSharedService.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard")) { if (LastCreatedCharacterData != null) { @@ -351,17 +387,16 @@ public class SettingsUi : WindowMediatorSubscriberBase } UiSharedService.DrawHelpText("Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); - if (!logPerformance) ImGui.BeginDisabled(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog")) + using var disabled = ImRaii.Disabled(!logPerformance); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog")) { _performanceCollector.PrintPerformanceStats(); } ImGui.SameLine(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) { _performanceCollector.PrintPerformanceStats(60); } - if (!logPerformance) ImGui.EndDisabled(); } private void DrawFileStorageSettings() @@ -384,7 +419,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (!_mareCharaFileManager.CurrentlyWorking) { ImGui.InputTextWithHint("Export Descriptor", "This description will be shown on loading the data", ref _exportDescription, 255); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF")) { string defaultFileName = string.IsNullOrEmpty(_exportDescription) ? "export.mcdf" @@ -452,14 +487,14 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SameLine(); if (!_fileCompactor.MassCompactRunning) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.FileArchive, "Compact all files in storage")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.FileArchive, "Compact all files in storage")) { _ = Task.Run(() => _fileCompactor.CompactStorage(compress: true)); } UiSharedService.AttachToolTip("This will run compression on all files in your current Mare Storage." + Environment.NewLine + "You do not need to run this manually if you keep the file compactor enabled."); ImGui.SameLine(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.File, "Decompact all files in storage")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.File, "Decompact all files in storage")) { _ = Task.Run(() => _fileCompactor.CompactStorage(compress: false)); } @@ -475,7 +510,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TextUnformatted("The file compactor is only available on Windows."); } - ImGui.Dummy(new Vector2(10, 10)); + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); ImGui.TextUnformatted("To clear the local storage accept the following disclaimer"); ImGui.Indent(); ImGui.Checkbox("##readClearCache", ref _readClearCache); @@ -485,7 +520,7 @@ public class SettingsUi : WindowMediatorSubscriberBase + Environment.NewLine + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); if (!_readClearCache) ImGui.BeginDisabled(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && UiSharedService.CtrlPressed() && _readClearCache) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && UiSharedService.CtrlPressed() && _readClearCache) { _ = Task.Run(() => { @@ -515,11 +550,11 @@ public class SettingsUi : WindowMediatorSubscriberBase _lastTab = "General"; UiSharedService.FontText("Notes", _uiShared.UidFont); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) { ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs.UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, UserDataComparer.Instance).ToList())); } - if (UiSharedService.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) { _notesSuccessfullyApplied = null; var notes = ImGui.GetClipboardText(); @@ -558,6 +593,8 @@ public class SettingsUi : WindowMediatorSubscriberBase var profileOnRight = _configService.Current.ProfilePopoutRight; var enableRightClickMenu = _configService.Current.EnableRightClickMenus; var enableDtrEntry = _configService.Current.EnableDtrEntry; + var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip; + var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip; var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible; if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) @@ -574,6 +611,22 @@ public class SettingsUi : WindowMediatorSubscriberBase } UiSharedService.DrawHelpText("This will add Mare connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); + using (ImRaii.Disabled(!enableDtrEntry)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip)) + { + _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Save(); + } + + if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + { + _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Save(); + } + } + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) { _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; @@ -622,13 +675,6 @@ public class SettingsUi : WindowMediatorSubscriberBase Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); } UiSharedService.DrawHelpText("Will show profiles on the right side of the main UI"); - if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) - { - Mediator.Publish(new ClearProfileDataMessage()); - _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; - _configService.Save(); - } - UiSharedService.DrawHelpText("Will show profiles that have the NSFW tag enabled"); if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) { _configService.Current.ProfileDelay = profileDelay; @@ -637,6 +683,13 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.DrawHelpText("Delay until the profile should be displayed"); if (!showProfiles) ImGui.EndDisabled(); ImGui.Unindent(); + if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) + { + Mediator.Publish(new ClearProfileDataMessage()); + _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; + _configService.Save(); + } + UiSharedService.DrawHelpText("Will show profiles that have the NSFW tag enabled"); ImGui.Separator(); @@ -695,7 +748,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } UiSharedService.DrawHelpText("Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online."); - if (!onlineNotifs) ImGui.BeginDisabled(); + using var disabled = ImRaii.Disabled(!onlineNotifs); if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) { _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; @@ -708,7 +761,6 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); } UiSharedService.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note."); - if (!onlineNotifs) ImGui.EndDisabled(); } private void DrawServerConfiguration() @@ -717,7 +769,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (ApiController.ServerAlive) { UiSharedService.FontText("Service Actions", _uiShared.UidFont); - + ImGuiHelpers.ScaledDummy(new Vector2(5, 5)); if (ImGui.Button("Delete all my files")) { _deleteFilesPopupModalShown = true; @@ -798,7 +850,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var idx = _uiShared.DrawServiceSelection(); - ImGui.Dummy(new Vector2(10, 10)); + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); var selectedServer = _serverConfigurationManager.GetServerByIndex(idx); if (selectedServer == _serverConfigurationManager.CurrentServer) @@ -817,59 +869,58 @@ public class SettingsUi : WindowMediatorSubscriberBase int i = 0; foreach (var item in selectedServer.Authentications.ToList()) { - UiSharedService.DrawWithID("selectedChara" + i, () => + using var charaId = ImRaii.PushId("selectedChara" + i); + + var worldIdx = (ushort)item.WorldId; + var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value); + if (!data.TryGetValue(worldIdx, out string? worldPreview)) { - var worldIdx = (ushort)item.WorldId; - var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value); - if (!data.TryGetValue(worldIdx, out string? worldPreview)) + worldPreview = data.First().Value; + } + + var secretKeyIdx = item.SecretKeyIdx; + var keys = selectedServer.SecretKeys; + if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) + { + secretKey = new(); + } + var friendlyName = secretKey.FriendlyName; + + if (ImGui.TreeNode($"chara", $"Character: {item.CharacterName}, World: {worldPreview}, Secret Key: {friendlyName}")) + { + var charaName = item.CharacterName; + if (ImGui.InputText("Character Name", ref charaName, 64)) { - worldPreview = data.First().Value; + item.CharacterName = charaName; + _serverConfigurationManager.Save(); } - var secretKeyIdx = item.SecretKeyIdx; - var keys = selectedServer.SecretKeys; - if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) - { - secretKey = new(); - } - var friendlyName = secretKey.FriendlyName; - - if (ImGui.TreeNode($"chara", $"Character: {item.CharacterName}, World: {worldPreview}, Secret Key: {friendlyName}")) - { - var charaName = item.CharacterName; - if (ImGui.InputText("Character Name", ref charaName, 64)) + _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, + (w) => { - item.CharacterName = charaName; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, - (w) => + if (item.WorldId != w.Key) { - if (item.WorldId != w.Key) - { - item.WorldId = w.Key; - _serverConfigurationManager.Save(); - } - }, EqualityComparer>.Default.Equals(data.FirstOrDefault(f => f.Key == worldIdx), default) ? data.First() : data.First(f => f.Key == worldIdx)); + item.WorldId = w.Key; + _serverConfigurationManager.Save(); + } + }, EqualityComparer>.Default.Equals(data.FirstOrDefault(f => f.Key == worldIdx), default) ? data.First() : data.First(f => f.Key == worldIdx)); - _uiShared.DrawCombo("Secret Key##" + item.CharacterName + i, keys, (w) => w.Value.FriendlyName, - (w) => + _uiShared.DrawCombo("Secret Key##" + item.CharacterName + i, keys, (w) => w.Value.FriendlyName, + (w) => + { + if (w.Key != item.SecretKeyIdx) { - if (w.Key != item.SecretKeyIdx) - { - item.SecretKeyIdx = w.Key; - _serverConfigurationManager.Save(); - } - }, EqualityComparer>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx)); + item.SecretKeyIdx = w.Key; + _serverConfigurationManager.Save(); + } + }, EqualityComparer>.Default.Equals(keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) ? keys.First() : keys.First(f => f.Key == item.SecretKeyIdx)); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed()) - _serverConfigurationManager.RemoveCharacterFromServer(idx, item); - UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Delete Character") && UiSharedService.CtrlPressed()) + _serverConfigurationManager.RemoveCharacterFromServer(idx, item); + UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); - ImGui.TreePop(); - } - }); + ImGui.TreePop(); + } i++; } @@ -878,14 +929,14 @@ public class SettingsUi : WindowMediatorSubscriberBase if (!selectedServer.Authentications.Exists(c => string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal) && c.WorldId == _uiShared.WorldId)) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.User, "Add current character")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.User, "Add current character")) { _serverConfigurationManager.AddCurrentCharacterToServer(idx); } ImGui.SameLine(); } - if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Plus, "Add new character")) { _serverConfigurationManager.AddEmptyCharacterToServer(idx); } @@ -902,41 +953,39 @@ public class SettingsUi : WindowMediatorSubscriberBase { foreach (var item in selectedServer.SecretKeys.ToList()) { - UiSharedService.DrawWithID("key" + item.Key, () => + using var id = ImRaii.PushId("key" + item.Key); + var friendlyName = item.Value.FriendlyName; + if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) { - var friendlyName = item.Value.FriendlyName; - if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) + item.Value.FriendlyName = friendlyName; + _serverConfigurationManager.Save(); + } + var key = item.Value.Key; + if (ImGui.InputText("Secret Key", ref key, 64)) + { + item.Value.Key = key; + _serverConfigurationManager.Save(); + } + if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) + { + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) { - item.Value.FriendlyName = friendlyName; + selectedServer.SecretKeys.Remove(item.Key); _serverConfigurationManager.Save(); } - var key = item.Value.Key; - if (ImGui.InputText("Secret Key", ref key, 64)) - { - item.Value.Key = key; - _serverConfigurationManager.Save(); - } - if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) - { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) - { - selectedServer.SecretKeys.Remove(item.Key); - _serverConfigurationManager.Save(); - } - UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); - } - else - { - UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", ImGuiColors.DalamudYellow); - } - }); + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); + } + else + { + UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", ImGuiColors.DalamudYellow); + } if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) ImGui.Separator(); } ImGui.Separator(); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) { selectedServer.SecretKeys.Add(selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, new SecretKey() { @@ -976,7 +1025,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) { - if (UiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed()) + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed()) { _serverConfigurationManager.DeleteServer(selectedServer); } @@ -990,7 +1039,21 @@ public class SettingsUi : WindowMediatorSubscriberBase private void DrawSettingsContent() { - _uiShared.PrintServerState(); + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.SameLine(); + ImGui.TextUnformatted("Users Online"); + ImGui.SameLine(); + ImGui.TextUnformatted(")"); + } + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Community and Support:"); ImGui.SameLine(); diff --git a/MareSynchronos/UI/StandaloneProfileUi.cs b/MareSynchronos/UI/StandaloneProfileUi.cs index 0a4f200..5160bea 100644 --- a/MareSynchronos/UI/StandaloneProfileUi.cs +++ b/MareSynchronos/UI/StandaloneProfileUi.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using MareSynchronos.API.Data.Extensions; using MareSynchronos.PlayerData.Pairs; @@ -76,9 +77,9 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase var rectMax = drawList.GetClipRectMax(); var headerSize = ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y; - if (_uiSharedService.UidFontBuilt) ImGui.PushFont(_uiSharedService.UidFont); - UiSharedService.ColorText(Pair.UserData.AliasOrUID, ImGuiColors.HealerGreen); - if (_uiSharedService.UidFontBuilt) ImGui.PopFont(); + using (ImRaii.PushFont(_uiSharedService.UidFont, _uiSharedService.UidFontBuilt)) + UiSharedService.ColorText(Pair.UserData.AliasOrUID, ImGuiColors.HealerGreen); + ImGuiHelpers.ScaledDummy(new Vector2(spacing.Y, spacing.Y)); var textPos = ImGui.GetCursorPosY() - headerSize; ImGui.Separator(); diff --git a/MareSynchronos/UI/SyncshellAdminUI.cs b/MareSynchronos/UI/SyncshellAdminUI.cs new file mode 100644 index 0000000..3c53997 --- /dev/null +++ b/MareSynchronos/UI/SyncshellAdminUI.cs @@ -0,0 +1,252 @@ +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Globalization; + +namespace MareSynchronos.UI.Components.Popup; + +public class SyncshellAdminUI : WindowMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly bool _isModerator = false; + private readonly bool _isOwner = false; + private readonly List _oneTimeInvites = []; + private readonly PairManager _pairManager; + private readonly UiSharedService _uiSharedService; + private List _bannedUsers = []; + private int _multiInvites; + private string _newPassword; + private bool _pwChangeSuccess; + public SyncshellAdminUI(ILogger logger, MareMediator mediator, ApiController apiController, + UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo) + : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GID + ")") + { + GroupFullInfo = groupFullInfo; + _apiController = apiController; + _uiSharedService = uiSharedService; + _pairManager = pairManager; + _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); + _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); + _newPassword = string.Empty; + _multiInvites = 30; + _pwChangeSuccess = true; + IsOpen = true; + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new(700, 500), + MaximumSize = new(700, 2000), + }; + } + + public GroupFullInfoDto GroupFullInfo { get; private set; } + + public override void Draw() + { + if (!_isModerator && !_isOwner) return; + + GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; + + using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); + + using (ImRaii.PushFont(_uiSharedService.UidFont)) + ImGui.TextUnformatted(GroupFullInfo.GroupAliasOrGID + " Administrative Panel"); + + ImGui.Separator(); + var perm = GroupFullInfo.GroupPermissions; + + using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); + + if (tabbar) + { + var inviteTab = ImRaii.TabItem("Invites"); + if (inviteTab) + { + bool isInvitesDisabled = perm.IsDisableInvites(); + + if (UiSharedService.NormalizedIconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock, + isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell")) + { + perm.SetDisableInvites(!isInvitesDisabled); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGuiHelpers.ScaledDummy(2f); + + UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password."); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) + { + ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), 1).Result.FirstOrDefault() ?? string.Empty); + } + UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); + ImGui.InputInt("##amountofinvites", ref _multiInvites); + ImGui.SameLine(); + using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100)) + { + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites")) + { + _oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), _multiInvites).Result); + } + } + + if (_oneTimeInvites.Any()) + { + var invites = string.Join(Environment.NewLine, _oneTimeInvites); + ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard")) + { + ImGui.SetClipboardText(invites); + } + } + } + inviteTab.Dispose(); + + var mgmtTab = ImRaii.TabItem("User Management"); + if (mgmtTab) + { + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) + { + _ = _apiController.GroupClear(new(GroupFullInfo.Group)); + } + UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell"); + + ImGuiHelpers.ScaledDummy(2f); + + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result; + } + + if (ImGui.BeginTable("bannedusertable" + GroupFullInfo.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) + { + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); + ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); + + ImGui.TableHeadersRow(); + + foreach (var bannedUser in _bannedUsers.ToList()) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.UID); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.BannedBy); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); + ImGui.TableNextColumn(); + UiSharedService.TextWrapped(bannedUser.Reason); + ImGui.TableNextColumn(); + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Check, "Unban##" + bannedUser.UID)) + { + _ = _apiController.GroupUnbanUser(bannedUser); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + } + + ImGui.EndTable(); + } + } + mgmtTab.Dispose(); + + var permissionTab = ImRaii.TabItem("Permissions"); + if (permissionTab) + { + bool isDisableAnimations = perm.IsDisableAnimations(); + bool isDisableSounds = perm.IsDisableSounds(); + bool isDisableVfx = perm.IsDisableVFX(); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Sound Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableSounds); + ImGui.SameLine(230); + if (UiSharedService.NormalizedIconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, + isDisableSounds ? "Enable sound sync" : "Disable sound sync")) + { + perm.SetDisableSounds(!perm.IsDisableSounds()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Animation Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableAnimations); + ImGui.SameLine(230); + if (UiSharedService.NormalizedIconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, + isDisableAnimations ? "Enable animation sync" : "Disable animation sync")) + { + perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.Text("VFX Sync"); + UiSharedService.BooleanToColoredIcon(!isDisableVfx); + ImGui.SameLine(230); + if (UiSharedService.NormalizedIconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, + isDisableVfx ? "Enable VFX sync" : "Disable VFX sync")) + { + perm.SetDisableVFX(!perm.IsDisableVFX()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + } + permissionTab.Dispose(); + + if (_isOwner) + { + var ownerTab = ImRaii.TabItem("Owner Settings"); + if (ownerTab) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("New Password"); + var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + var buttonSize = UiSharedService.GetNormalizedIconTextButtonSize(FontAwesomeIcon.Passport, "Change Password").X; + var textSize = ImGui.CalcTextSize("New Password").X; + var spacing = ImGui.GetStyle().ItemSpacing.X; + + ImGui.SameLine(); + ImGui.SetNextItemWidth(availableWidth - buttonSize - textSize - spacing * 2); + ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50); + ImGui.SameLine(); + using (ImRaii.Disabled(_newPassword.Length < 10)) + { + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Passport, "Change Password")) + { + _pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(GroupFullInfo.Group, _newPassword)).Result; + _newPassword = string.Empty; + } + } + UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible."); + + if (!_pwChangeSuccess) + { + UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow); + } + + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + IsOpen = false; + _ = _apiController.GroupDelete(new(GroupFullInfo.Group)); + } + UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); + } + ownerTab.Dispose(); + } + } + } + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index 1dffef7..0dff602 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -18,9 +18,7 @@ using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI; -using MareSynchronos.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; -using System.Globalization; using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -127,15 +125,16 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public Dictionary WorldData => _dalamudUtil.WorldData.Value; - public uint WorldId => _dalamudUtil.GetWorldId(); + public uint WorldId => _dalamudUtil.GetHomeWorldId(); public const string TooltipSeparator = "--SEP--"; public static void AttachToolTip(string text) { - if (ImGui.IsItemHovered()) + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) { ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35f); if (text.Contains(TooltipSeparator, StringComparison.Ordinal)) { var splitText = text.Split(TooltipSeparator, StringSplitOptions.RemoveEmptyEntries); @@ -149,13 +148,13 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase { ImGui.TextUnformatted(text); } + ImGui.PopTextWrapPos(); ImGui.EndTooltip(); } } public static void BooleanToColoredIcon(bool value, bool inline = true) { - using var font = ImRaii.PushFont(UiBuilder.IconFont); using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, value); using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value); @@ -163,11 +162,11 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase if (value) { - ImGui.TextUnformatted(FontAwesomeIcon.Check.ToIconString()); + NormalizedIcon(FontAwesomeIcon.Check); } else { - ImGui.TextUnformatted(FontAwesomeIcon.Times.ToIconString()); + NormalizedIcon(FontAwesomeIcon.Times); } } @@ -207,16 +206,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static void ColorText(string text, Vector4 color) { - ImGui.PushStyleColor(ImGuiCol.Text, color); + using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color); ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); } public static void ColorTextWrapped(string text, Vector4 color) { - ImGui.PushStyleColor(ImGuiCol.Text, color); + using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color); TextWrapped(text); - ImGui.PopStyleColor(); } public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; @@ -224,50 +221,41 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static void DrawHelpText(string helpText) { ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SetWindowFontScale(0.8f); - ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); - ImGui.SetWindowFontScale(1.0f); - ImGui.PopFont(); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); - ImGui.TextUnformatted(helpText); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } + NormalizedIcon(FontAwesomeIcon.QuestionCircle, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + AttachToolTip(helpText); } public static void DrawOutlinedFont(string text, Vector4 fontColor, Vector4 outlineColor, int thickness) { var original = ImGui.GetCursorPos(); - ImGui.PushStyleColor(ImGuiCol.Text, outlineColor); - ImGui.SetCursorPos(original with { Y = original.Y - thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X - thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { Y = original.Y + thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X + thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y - thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y + thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y + thickness }); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y - thickness }); - ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); + using (ImRaii.PushColor(ImGuiCol.Text, outlineColor)) + { + ImGui.SetCursorPos(original with { Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + } - ImGui.PushStyleColor(ImGuiCol.Text, fontColor); - ImGui.SetCursorPos(original); - ImGui.TextUnformatted(text); - ImGui.SetCursorPos(original); - ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); + using (ImRaii.PushColor(ImGuiCol.Text, fontColor)) + { + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + } } public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) @@ -293,13 +281,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase drawList.AddText(textPos, fontColor, text); } - public static void DrawWithID(string id, Action drawSubSection) - { - ImGui.PushID(id); - drawSubSection.Invoke(); - ImGui.PopID(); - } - public static void FontText(string text, ImFontPtr font, Vector4? color = null) { using var pushedFont = ImRaii.PushFont(font); @@ -309,22 +290,17 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : - input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; - public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) { - ImGui.PushFont(UiBuilder.IconFont); + using var font = ImRaii.PushFont(UiBuilder.IconFont); var buttonSize = ImGuiHelpers.GetButtonSize(icon.ToIconString()); - ImGui.PopFont(); return buttonSize; } public static Vector2 GetIconSize(FontAwesomeIcon icon) { - ImGui.PushFont(UiBuilder.IconFont); + using var font = ImRaii.PushFont(UiBuilder.IconFont); var iconSize = ImGui.CalcTextSize(icon.ToIconString()); - ImGui.PopFont(); return iconSize; } @@ -349,35 +325,74 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; } - public static bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) + public static Vector2 GetNormalizedIconTextButtonSize(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) { - var wasClicked = false; - - var iconSize = GetIconSize(icon); + var iconData = GetIconData(icon); var textSize = ImGui.CalcTextSize(text); var padding = ImGui.GetStyle().FramePadding; - var spacing = ImGui.GetStyle().ItemSpacing; - var cursor = ImGui.GetCursorPos(); - var drawList = ImGui.GetWindowDrawList(); - var pos = ImGui.GetWindowPos(); - - Vector2 buttonSize; - var buttonSizeY = textSize.Y + padding.Y * 2; + var buttonSizeY = ImGui.GetFrameHeight(); var iconExtraSpacing = isInPopup ? padding.X * 2 : 0; - var iconXoffset = iconSize.X <= iconSize.Y ? (iconSize.Y - iconSize.X) / 2f : 0; - var iconScaling = iconSize.X > iconSize.Y ? 1 / (iconSize.X / iconSize.Y) : 1; - if (width == null || width <= 0) { - var buttonSizeX = (iconScaling == 1 ? iconSize.Y : (iconSize.X * iconScaling)) - + textSize.X + padding.X * 2 + spacing.X + (iconXoffset * 2); - buttonSize = new Vector2(buttonSizeX + iconExtraSpacing, buttonSizeY); + var buttonSizeX = iconData.NormalizedIconScale.X + (padding.X * 3) + iconExtraSpacing + textSize.X; + return new Vector2(buttonSizeX, buttonSizeY); } else { - buttonSize = new Vector2(width.Value, buttonSizeY); + return new Vector2(width.Value, buttonSizeY); } + } + + public static Vector2 NormalizedIconButtonSize(FontAwesomeIcon icon) + { + var iconData = GetIconData(icon); + var padding = ImGui.GetStyle().FramePadding; + + return iconData.NormalizedIconScale with { X = iconData.NormalizedIconScale.X + padding.X * 2, Y = iconData.NormalizedIconScale.Y + padding.Y * 2 }; + } + + public static bool NormalizedIconButton(FontAwesomeIcon icon) + { + bool wasClicked = false; + var iconData = GetIconData(icon); + var padding = ImGui.GetStyle().FramePadding; + var cursor = ImGui.GetCursorPos(); + var drawList = ImGui.GetWindowDrawList(); + var pos = ImGui.GetWindowPos(); + var scrollPosY = ImGui.GetScrollY(); + var scrollPosX = ImGui.GetScrollX(); + + var buttonSize = NormalizedIconButtonSize(icon); + + if (ImGui.Button("###" + icon.ToIconString(), buttonSize)) + { + wasClicked = true; + } + + drawList.AddText(UiBuilder.IconFont, ImGui.GetFontSize() * iconData.IconScaling, + new(pos.X - scrollPosX + cursor.X + iconData.OffsetX + padding.X, + pos.Y - scrollPosY + cursor.Y + (buttonSize.Y - (iconData.IconSize.Y * iconData.IconScaling)) / 2f), + ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + + return wasClicked; + } + + public static bool NormalizedIconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) + { + var wasClicked = false; + + var iconData = GetIconData(icon); + var textSize = ImGui.CalcTextSize(text); + var padding = ImGui.GetStyle().FramePadding; + var cursor = ImGui.GetCursorPos(); + var drawList = ImGui.GetWindowDrawList(); + var pos = ImGui.GetWindowPos(); + var scrollPosY = ImGui.GetScrollY(); + var scrollPosX = ImGui.GetScrollX(); + + Vector2 buttonSize = GetNormalizedIconTextButtonSize(icon, text, width, isInPopup); + var iconExtraSpacing = isInPopup ? padding.X * 2 : 0; using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.PopupBg), isInPopup)) { @@ -387,19 +402,70 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } } - drawList.AddText(UiBuilder.IconFont, ImGui.GetFontSize() * iconScaling, - new(pos.X + cursor.X + iconXoffset + padding.X, - pos.Y + cursor.Y + (buttonSizeY - (iconSize.Y * iconScaling)) / 2f), - ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); - drawList.AddText(UiBuilder.DefaultFont, ImGui.GetFontSize(), - new(pos.X + cursor.X + (padding.X) + spacing.X + (iconSize.X * iconScaling) + (iconXoffset * 2) + iconExtraSpacing, - pos.Y + cursor.Y + ((buttonSizeY - textSize.Y) / 2f)), + new(pos.X - scrollPosX + cursor.X + iconData.NormalizedIconScale.X + (padding.X * 2) + iconExtraSpacing, + pos.Y - scrollPosY + cursor.Y + ((buttonSize.Y - textSize.Y) / 2f)), ImGui.GetColorU32(ImGuiCol.Text), text); + drawList.AddText(UiBuilder.IconFont, ImGui.GetFontSize() * iconData.IconScaling, + new(pos.X - scrollPosX + cursor.X + iconData.OffsetX + padding.X, + pos.Y - scrollPosY + cursor.Y + (buttonSize.Y - (iconData.IconSize.Y * iconData.IconScaling)) / 2f), + ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + return wasClicked; } + public static void NormalizedIcon(FontAwesomeIcon icon, uint color) + { + var cursorPos = ImGui.GetCursorPos(); + var iconData = GetIconData(icon); + var drawList = ImGui.GetWindowDrawList(); + var windowPos = ImGui.GetWindowPos(); + var scrollPosX = ImGui.GetScrollX(); + var scrollPosY = ImGui.GetScrollY(); + var frameHeight = ImGui.GetFrameHeight(); + + var frameOffsetY = ((frameHeight - iconData.IconSize.Y * iconData.IconScaling) / 2f); + + drawList.AddText(UiBuilder.IconFont, UiBuilder.IconFont.FontSize * iconData.IconScaling, + new(windowPos.X - scrollPosX + cursorPos.X + iconData.OffsetX, + windowPos.Y - scrollPosY + cursorPos.Y + frameOffsetY), + color, icon.ToIconString()); + + ImGui.Dummy(new(iconData.NormalizedIconScale.X, ImGui.GetFrameHeight())); + } + + public static void NormalizedIcon(FontAwesomeIcon icon, Vector4? color = null) + { + NormalizedIcon(icon, color == null ? ImGui.GetColorU32(ImGuiCol.Text) : ImGui.GetColorU32(color.Value)); + } + + private static IconScaleData CalcIconScaleData(FontAwesomeIcon icon) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + var iconscaling = (iconSize.X < iconSize.Y ? (iconSize.Y - iconSize.X) / 2f : 0f, iconSize.X > iconSize.Y ? 1f / (iconSize.X / iconSize.Y) : 1f); + var normalized = iconscaling.Item2 == 1f ? + new Vector2(iconSize.Y, iconSize.Y) + : new((iconSize.X * iconscaling.Item2) + (iconscaling.Item1 * 2), (iconSize.X * iconscaling.Item2) + (iconscaling.Item1 * 2)); + return new(iconSize, normalized, iconscaling.Item1, iconscaling.Item2); + } + + public static IconScaleData GetIconData(FontAwesomeIcon icon) + { + if (_iconData.TryGetValue(ImGuiHelpers.GlobalScale, out var iconCache)) + { + if (iconCache.TryGetValue(icon, out var iconData)) return iconData; + return iconCache[icon] = CalcIconScaleData(icon); + } + + _iconData.Add(ImGuiHelpers.GlobalScale, new()); + return _iconData[ImGuiHelpers.GlobalScale][icon] = CalcIconScaleData(icon); + } + + public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling); + private static Dictionary> _iconData = new(); + public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) { try @@ -422,23 +488,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase } } - public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) - { - var cursorPos = ImGui.GetCursorPos(); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); - ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); - ColorTextWrapped(text, outlineColor); - - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - ColorTextWrapped(text, textcolor); - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - ColorTextWrapped(text, textcolor); - } - public static void SetScaledWindowSize(float width, bool centerWindow = true) { var newLineHeight = ImGui.GetCursorPosY(); @@ -510,9 +559,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public void BigText(string text) { - if (UidFontBuilt) ImGui.PushFont(UidFont); + using var font = ImRaii.PushFont(UidFont, UidFontBuilt); ImGui.TextUnformatted(text); - if (UidFontBuilt) ImGui.PopFont(); } public void DrawCacheDirectorySetting() @@ -771,7 +819,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.SameLine(); var text = "Connect"; if (_serverSelectionIndex == _serverConfigurationManager.CurrentServerIndex) text = "Reconnect"; - if (IconTextButton(FontAwesomeIcon.Link, text)) + if (NormalizedIconTextButton(FontAwesomeIcon.Link, text)) { _serverConfigurationManager.SelectServer(_serverSelectionIndex); _ = _apiController.CreateConnections(); @@ -784,7 +832,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.InputText("Custom Service URI", ref _customServerUri, 255); ImGui.SetNextItemWidth(250); ImGui.InputText("Custom Service Name", ref _customServerName, 255); - if (UiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add Custom Service") + if (UiSharedService.NormalizedIconTextButton(FontAwesomeIcon.Plus, "Add Custom Service") && !string.IsNullOrEmpty(_customServerUri) && !string.IsNullOrEmpty(_customServerName)) { @@ -827,24 +875,6 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase Strings.ToS = new Strings.ToSStrings(); } - public void PrintServerState() - { - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); - ImGui.SameLine(); - ImGui.TextUnformatted("("); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); - ImGui.SameLine(); - ImGui.TextUnformatted("Users Online"); - ImGui.SameLine(); - ImGui.TextUnformatted(")"); - } - } - public void RecalculateFileCacheSize() { _cacheScanner.InvokeScan(forced: true); @@ -876,7 +906,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.SetWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); } +#pragma warning disable MA0009 // Add regex evaluation timeout [GeneratedRegex(@"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript)] +#pragma warning restore MA0009 // Add regex evaluation timeout private static partial Regex PathRegex(); private void BuildFont() diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs index 4b66828..aeff6b4 100644 --- a/MareSynchronos/Utils/VariousExtensions.cs +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -59,6 +59,7 @@ public static class VariousExtensions cachedPlayer, objectKind, hasNewButNotOldFileReplacements, hasOldButNotNewFileReplacements, hasNewButNotOldGlamourerData, hasOldButNotNewGlamourerData, PlayerChanges.ModFiles, PlayerChanges.Glamourer); charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles); charaDataToUpdate[objectKind].Add(PlayerChanges.Glamourer); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); } else { @@ -69,6 +70,43 @@ public static class VariousExtensions { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles); charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles); + if (forceApplyMods || objectKind != ObjectKind.Player) + { + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + else + { + var existingFace = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var existingHair = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var existingTail = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var newFace = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var newHair = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + + logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase, + existingFace.Count, newFace.Count, existingHair.Count, newHair.Count, existingTail.Count, newTail.Count, existingTransients.Count, newTransients.Count); + + var differentFace = !existingFace.SequenceEqual(newFace, PlayerData.Data.FileReplacementDataComparer.Instance); + var differentHair = !existingHair.SequenceEqual(newHair, PlayerData.Data.FileReplacementDataComparer.Instance); + var differentTail = !existingTail.SequenceEqual(newTail, PlayerData.Data.FileReplacementDataComparer.Instance); + var differenTransients = !existingTransients.SequenceEqual(newTransients, PlayerData.Data.FileReplacementDataComparer.Instance); + if (differentFace || differentHair || differentTail || differenTransients) + { + logger.LogDebug("[BASE-{appbase}] Different Subparts: Face: {face}, Hair: {hair}, Tail: {tail}, Transients: {transients} => {change}", applicationBase, + differentFace, differentHair, differentTail, differenTransients, PlayerChanges.ForcedRedraw); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + } } } @@ -100,6 +138,7 @@ public static class VariousExtensions { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); } bool heelsOffsetDifferent = !string.Equals(oldData.HeelsData, newData.HeelsData, StringComparison.Ordinal); @@ -109,7 +148,9 @@ public static class VariousExtensions charaDataToUpdate[objectKind].Add(PlayerChanges.Heels); } - bool palettePlusDataDifferent = !string.Equals(oldData.PalettePlusData, newData.PalettePlusData, StringComparison.Ordinal); + bool palettePlusDataDifferent = !string.Equals(oldData.PalettePlusData, newData.PalettePlusData, StringComparison.Ordinal) + || (charaDataToUpdate.TryGetValue(objectKind, out var playerChanges) && playerChanges.Contains(PlayerChanges.Glamourer) + && (!string.IsNullOrEmpty(oldData.PalettePlusData) || !string.IsNullOrEmpty(newData.PalettePlusData))); if (palettePlusDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.PalettePlusData))) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff palette data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Palette); diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs index 87290be..936a190 100644 --- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -19,6 +19,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private readonly FileCompactor _fileCompactor; private readonly FileCacheManager _fileDbManager; private readonly FileTransferOrchestrator _orchestrator; + private readonly List _activeDownloadStreams; public FileDownloadManager(ILogger logger, MareMediator mediator, FileTransferOrchestrator orchestrator, @@ -28,6 +29,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase _orchestrator = orchestrator; _fileDbManager = fileCacheManager; _fileCompactor = fileCompactor; + _activeDownloadStreams = []; + + Mediator.Subscribe(this, (msg) => + { + if (!_activeDownloadStreams.Any()) return; + var newLimit = _orchestrator.DownloadLimitPerSlot(); + Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit); + foreach (var stream in _activeDownloadStreams) + { + stream.BandwidthLimit = newLimit; + } + }); } public List CurrentDownloads { get; private set; } = []; @@ -71,6 +84,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { CancelDownload(); + foreach (var stream in _activeDownloadStreams) + { + try + { + stream.Dispose(); + } + catch { } + } base.Dispose(disposing); } @@ -133,6 +154,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } + ThrottledStream? stream = null; try { var fileStream = File.Create(tempPath); @@ -142,7 +164,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase var buffer = new byte[bufferSize]; var bytesRead = 0; - var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); + var limit = _orchestrator.DownloadLimitPerSlot(); + Logger.LogTrace("Starting Download of {id} with a speed limit of {limit}", requestId, limit); + stream = new ThrottledStream(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit); + _activeDownloadStreams.Add(stream); while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) { ct.ThrowIfCancellationRequested(); @@ -171,6 +196,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } throw; } + finally + { + if (stream != null) + { + _activeDownloadStreams.Remove(stream); + await stream.DisposeAsync().ConfigureAwait(false); + } + } } private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) diff --git a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs index 99aa36a..f1764c6 100644 --- a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs +++ b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs @@ -19,6 +19,7 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase private readonly TokenProvider _tokenProvider; private int _availableDownloadSlots; private SemaphoreSlim _downloadSemaphore; + private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount; public FileTransferOrchestrator(ILogger logger, MareConfigService mareConfig, MareMediator mediator, TokenProvider tokenProvider) : base(logger, mediator) @@ -72,6 +73,7 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase public void ReleaseDownloadSlot() { _downloadSemaphore.Release(); + Mediator.Publish(new DownloadLimitChangedMessage()); } public async Task SendRequestAsync(HttpMethod method, Uri uri, @@ -110,12 +112,28 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase } await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false); + Mediator.Publish(new DownloadLimitChangedMessage()); + } + + public long DownloadLimitPerSlot() + { + var limit = _mareConfig.Current.DownloadSpeedLimitInBytes; + if (limit <= 0) return 0; + limit = _mareConfig.Current.DownloadSpeedType switch + { + MareConfiguration.Models.DownloadSpeeds.Bps => limit, + MareConfiguration.Models.DownloadSpeeds.KBps => limit * 1024, + MareConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024, + _ => limit, + }; + var dividedLimit = limit / (CurrentlyUsedDownloadSlots == 0 ? 1 : CurrentlyUsedDownloadSlots); + return dividedLimit == 0 ? 1 : dividedLimit; } private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _tokenProvider.GetOrUpdateToken(ct!.Value).ConfigureAwait(false)); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenProvider.GetToken()); if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent) { diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs index 15e1935..171c11f 100644 --- a/MareSynchronos/WebAPI/Files/FileUploadManager.cs +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -198,7 +198,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count); var filesToUpload = await FilesSend([.. unverifiedUploadHashes], visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false); - foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) + foreach (var file in filesToUpload.Where(f => !f.IsForbidden).DistinctBy(f => f.Hash)) { try { diff --git a/MareSynchronos/WebAPI/Files/ThrottledStream.cs b/MareSynchronos/WebAPI/Files/ThrottledStream.cs new file mode 100644 index 0000000..0e50a68 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/ThrottledStream.cs @@ -0,0 +1,217 @@ +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.WebAPI.Files +{ + /// + /// Class for streaming data with throttling support. + /// Borrowed from https://github.com/bezzad/Downloader + /// + internal class ThrottledStream : Stream + { + public static long Infinite => long.MaxValue; + private readonly Stream _baseStream; + private long _bandwidthLimit; + private Bandwidth _bandwidth; + private CancellationTokenSource _bandwidthChangeTokenSource = new CancellationTokenSource(); + + /// + /// Initializes a new instance of the class. + /// + /// The base stream. + /// The maximum bytes per second that can be transferred through the base stream. + /// Thrown when is a null reference. + /// Thrown when is a negative value. + public ThrottledStream(Stream baseStream, long bandwidthLimit) + { + if (bandwidthLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(bandwidthLimit), + bandwidthLimit, "The maximum number of bytes per second can't be negative."); + } + + _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + BandwidthLimit = bandwidthLimit; + } + + /// + /// Bandwidth Limit (in B/s) + /// + /// The maximum bytes per second. + public long BandwidthLimit + { + get => _bandwidthLimit; + set + { + if (_bandwidthLimit == value) return; + _bandwidthLimit = value <= 0 ? Infinite : value; + _bandwidth ??= new Bandwidth(); + _bandwidth.BandwidthLimit = _bandwidthLimit; + _bandwidthChangeTokenSource.Cancel(); + _bandwidthChangeTokenSource.Dispose(); + _bandwidthChangeTokenSource = new(); + } + } + + /// + public override bool CanRead => _baseStream.CanRead; + + /// + public override bool CanSeek => _baseStream.CanSeek; + + /// + public override bool CanWrite => _baseStream.CanWrite; + + /// + public override long Length => _baseStream.Length; + + /// + public override long Position + { + get => _baseStream.Position; + set => _baseStream.Position = value; + } + + /// + public override void Flush() + { + _baseStream.Flush(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + return _baseStream.Seek(offset, origin); + } + + /// + public override void SetLength(long value) + { + _baseStream.SetLength(value); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + Throttle(count).Wait(); + return _baseStream.Read(buffer, offset, count); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + await Throttle(count, cancellationToken).ConfigureAwait(false); + return await _baseStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + Throttle(count).Wait(); + _baseStream.Write(buffer, offset, count); + } + + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Throttle(count, cancellationToken).ConfigureAwait(false); + await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + public override void Close() + { + _baseStream.Close(); + base.Close(); + } + + private async Task Throttle(int transmissionVolume, CancellationToken token = default) + { + // Make sure the buffer isn't empty. + if (BandwidthLimit > 0 && transmissionVolume > 0) + { + // Calculate the time to sleep. + _bandwidth.CalculateSpeed(transmissionVolume); + await Sleep(_bandwidth.PopSpeedRetrieveTime(), token).ConfigureAwait(false); + } + } + + private async Task Sleep(int time, CancellationToken token = default) + { + try + { + if (time > 0) + { + var bandWidthtoken = _bandwidthChangeTokenSource.Token; + var linked = CancellationTokenSource.CreateLinkedTokenSource(token, bandWidthtoken).Token; + await Task.Delay(time, linked).ConfigureAwait(false); + } + } + catch (TaskCanceledException) + { + // ignore + } + } + + /// + public override string ToString() + { + return _baseStream.ToString(); + } + + private class Bandwidth + { + private long _count; + private int _lastSecondCheckpoint; + private long _lastTransferredBytesCount; + private int _speedRetrieveTime; + public double Speed { get; private set; } + public double AverageSpeed { get; private set; } + public long BandwidthLimit { get; set; } + + public Bandwidth() + { + BandwidthLimit = long.MaxValue; + Reset(); + } + + public void CalculateSpeed(long receivedBytesCount) + { + int elapsedTime = Environment.TickCount - _lastSecondCheckpoint + 1; + receivedBytesCount = Interlocked.Add(ref _lastTransferredBytesCount, receivedBytesCount); + double momentSpeed = receivedBytesCount * 1000 / elapsedTime; // B/s + + if (1000 < elapsedTime) + { + Speed = momentSpeed; + AverageSpeed = ((AverageSpeed * _count) + Speed) / (_count + 1); + _count++; + SecondCheckpoint(); + } + + if (momentSpeed >= BandwidthLimit) + { + var expectedTime = receivedBytesCount * 1000 / BandwidthLimit; + Interlocked.Add(ref _speedRetrieveTime, (int)expectedTime - elapsedTime); + } + } + + public int PopSpeedRetrieveTime() + { + return Interlocked.Exchange(ref _speedRetrieveTime, 0); + } + + public void Reset() + { + SecondCheckpoint(); + _count = 0; + Speed = 0; + AverageSpeed = 0; + } + + private void SecondCheckpoint() + { + Interlocked.Exchange(ref _lastSecondCheckpoint, Environment.TickCount); + Interlocked.Exchange(ref _lastTransferredBytesCount, 0); + } + } + } +} diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs index acf826d..15ea2f3 100644 --- a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -103,6 +103,7 @@ public partial class ApiController sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); } Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString()); + await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false); } } \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs index 3ccbb04..233652a 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -2,6 +2,7 @@ using MareSynchronos.API.Data; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.User; using MareSynchronos.API.SignalR; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; @@ -33,6 +34,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM private bool _initialized; private HubConnection? _mareHub; private ServerState _serverState; + private CensusUpdateMessage? _lastCensus; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator, @@ -48,9 +50,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); Mediator.Subscribe(this, (msg) => MareHubOnClosed(msg.Exception)); - Mediator.Subscribe(this, (msg) => _ = Task.Run(MareHubOnReconnected)); + Mediator.Subscribe(this, async (msg) => await MareHubOnReconnected().ConfigureAwait(false)); Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); Mediator.Subscribe(this, (msg) => _ = CyclePause(msg.UserData)); + Mediator.Subscribe(this, (msg) => _lastCensus = msg); ServerState = ServerState.Offline; @@ -185,7 +188,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + $"Please keep your Mare Synchronos client up-to-date.", - Dalamud.Interface.Internal.Notifications.NotificationType.Error)); + Dalamud.Interface.Internal.Notifications.NotificationType.Warning)); } await LoadIninitialPairs().ConfigureAwait(false); @@ -210,6 +213,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM Logger.LogInformation("Failed to establish connection, retrying"); await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); } + catch (InvalidOperationException ex) + { + Logger.LogWarning(ex, "InvalidOperationException on connection"); + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + return; + } catch (Exception ex) { Logger.LogWarning(ex, "Exception on Connection"); @@ -369,13 +378,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM try { InitializeApiHooks(); - await LoadIninitialPairs().ConfigureAwait(false); _connectionDto = await GetConnectionDto().ConfigureAwait(false); if (_connectionDto.ServerVersion != IMareHub.ApiVersion) { await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); return; } + await LoadIninitialPairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false); ServerState = ServerState.Connected; } diff --git a/MareSynchronos/WebAPI/SignalR/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs index e9c6562..52913cd 100644 --- a/MareSynchronos/WebAPI/SignalR/HubFactory.cs +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -1,7 +1,4 @@ -using Dalamud.Plugin.Services; -using MareSynchronos.API.SignalR; -using MareSynchronos.Interop; -using MareSynchronos.MareConfiguration; +using MareSynchronos.API.SignalR; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI.SignalR.Utils; @@ -16,21 +13,19 @@ namespace MareSynchronos.WebAPI.SignalR; public class HubFactory : MediatorSubscriberBase { - private readonly MareConfigService _configService; - private readonly IPluginLog _pluginLog; + private readonly ILoggerProvider _loggingProvider; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly TokenProvider _tokenProvider; private HubConnection? _instance; private bool _isDisposed = false; public HubFactory(ILogger logger, MareMediator mediator, - ServerConfigurationManager serverConfigurationManager, MareConfigService configService, - TokenProvider tokenProvider, IPluginLog pluginLog) : base(logger, mediator) + ServerConfigurationManager serverConfigurationManager, + TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator) { _serverConfigurationManager = serverConfigurationManager; - _configService = configService; _tokenProvider = tokenProvider; - _pluginLog = pluginLog; + _loggingProvider = pluginLog; } public async Task DisposeHubAsync() @@ -92,7 +87,7 @@ public class HubFactory : MediatorSubscriberBase .WithAutomaticReconnect(new ForeverRetryPolicy(Mediator)) .ConfigureLogging(a => { - a.ClearProviders().AddProvider(new DalamudLoggingProvider(_configService, _pluginLog)); + a.ClearProviders().AddProvider(_loggingProvider); a.SetMinimumLevel(LogLevel.Information); }) .Build(); diff --git a/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs index fa6c2f5..78ea0cb 100644 --- a/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs +++ b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs @@ -1,3 +1,9 @@ namespace MareSynchronos.WebAPI.SignalR; -public record JwtIdentifier(string ApiUrl, string SecretKey); \ No newline at end of file +public record JwtIdentifier(string ApiUrl, string CharaHash, string SecretKey) +{ + public override string ToString() + { + return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", HasSecretKey: " + !string.IsNullOrEmpty(SecretKey) + "}"; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs index de4d685..a2bf267 100644 --- a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -1,5 +1,6 @@ using MareSynchronos.API.Routes; using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Utils; using Microsoft.Extensions.Logging; @@ -10,7 +11,7 @@ using System.Reflection; namespace MareSynchronos.WebAPI.SignalR; -public sealed class TokenProvider : IDisposable +public sealed class TokenProvider : IDisposable, IMediatorSubscriber { private readonly DalamudUtilService _dalamudUtil; private readonly HttpClient _httpClient; @@ -18,24 +19,38 @@ public sealed class TokenProvider : IDisposable private readonly ServerConfigurationManager _serverManager; private readonly ConcurrentDictionary _tokenCache = new(); - public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil) + public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil, MareMediator mareMediator) { _logger = logger; _serverManager = serverManager; _dalamudUtil = dalamudUtil; _httpClient = new(); var ver = Assembly.GetExecutingAssembly().GetName().Version; + Mediator = mareMediator; + Mediator.Subscribe(this, (_) => + { + _lastJwtIdentifier = null; + _tokenCache.Clear(); + }); + Mediator.Subscribe(this, (_) => + { + _lastJwtIdentifier = null; + _tokenCache.Clear(); + }); _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); } - private JwtIdentifier CurrentIdentifier => new(_serverManager.CurrentApiUrl, _serverManager.GetSecretKey()!); + public MareMediator Mediator { get; } + + private JwtIdentifier? _lastJwtIdentifier; public void Dispose() { + Mediator.UnsubscribeAll(this); _httpClient.Dispose(); } - public async Task GetNewToken(CancellationToken token) + public async Task GetNewToken(JwtIdentifier identifier, CancellationToken token) { Uri tokenUri; string response = string.Empty; @@ -58,16 +73,19 @@ public sealed class TokenProvider : IDisposable response = await result.Content.ReadAsStringAsync().ConfigureAwait(false); result.EnsureSuccessStatusCode(); - _tokenCache[CurrentIdentifier] = response; + _tokenCache[identifier] = response; } catch (HttpRequestException ex) { - _tokenCache.TryRemove(CurrentIdentifier, out _); + _tokenCache.TryRemove(identifier, out _); _logger.LogError(ex, "GetNewToken: Failure to get token"); if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { + Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting to Mare manually.", + Dalamud.Interface.Internal.Notifications.NotificationType.Error)); + Mediator.Publish(new DisconnectedMessage()); throw new MareAuthFailureException(response); } @@ -78,12 +96,54 @@ public sealed class TokenProvider : IDisposable return response; } + private JwtIdentifier? GetIdentifier() + { + JwtIdentifier jwtIdentifier; + try + { + jwtIdentifier = new(_serverManager.CurrentApiUrl, + _dalamudUtil.GetPlayerNameHashedAsync().GetAwaiter().GetResult(), + _serverManager.GetSecretKey()!); + _lastJwtIdentifier = jwtIdentifier; + } + catch (Exception ex) + { + if (_lastJwtIdentifier == null) + { + _logger.LogError("GetOrUpdate: No last identifier found, aborting"); + return null; + } + + _logger.LogWarning(ex, "GetOrUpdate: Could not get JwtIdentifier for some reason or another, reusing last identifier {identifier}", _lastJwtIdentifier); + jwtIdentifier = _lastJwtIdentifier; + } + + _logger.LogDebug("GetOrUpdate: Using identifier {identifier}", jwtIdentifier); + return jwtIdentifier; + } + + public string? GetToken() + { + JwtIdentifier? jwtIdentifier = GetIdentifier(); + if (jwtIdentifier == null) return null; + + if (_tokenCache.TryGetValue(jwtIdentifier, out var token)) + { + return token; + } + + throw new InvalidOperationException("No token present"); + } + public async Task GetOrUpdateToken(CancellationToken ct) { - if (_tokenCache.TryGetValue(CurrentIdentifier, out var token)) + JwtIdentifier? jwtIdentifier = GetIdentifier(); + if (jwtIdentifier == null) return null; + + if (_tokenCache.TryGetValue(jwtIdentifier, out var token)) return token; _logger.LogTrace("GetOrUpdate: Getting new token"); - return await GetNewToken(ct).ConfigureAwait(false); + return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); } } \ No newline at end of file