From d225a3844a7687974114fb04241433584f99c288 Mon Sep 17 00:00:00 2001 From: Keda Date: Sun, 12 Oct 2025 12:42:31 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20UI=20+=20Am=C3=A9lioration=20AutoDetect?= =?UTF-8?q?=20&=20Self=20Analyse=20+=20Update=20Penumbra=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configurations/MareConfig.cs | 4 +- MareSynchronos/Plugin.cs | 4 + .../AutoDetect/AutoDetectRequestService.cs | 41 ++++ .../AutoDetectSuppressionService.cs | 209 ++++++++++++++++++ .../AutoDetect/NearbyPendingService.cs | 3 + MareSynchronos/Services/CharacterAnalyzer.cs | 179 ++++++++++++++- .../Services/ChatTwoCompatibilityService.cs | 68 ++++++ .../Services/CommandManagerService.cs | 20 +- .../Services/NotificationService.cs | 8 +- MareSynchronos/UI/AutoDetectUi.cs | 202 +++++++++++++++-- MareSynchronos/UI/ChangelogUi.cs | 10 + MareSynchronos/UI/CompactUI.cs | 56 ++--- MareSynchronos/UI/SettingsUi.cs | 52 +++-- Penumbra.Api | 2 +- 14 files changed, 763 insertions(+), 95 deletions(-) create mode 100644 MareSynchronos/Services/AutoDetect/AutoDetectSuppressionService.cs create mode 100644 MareSynchronos/Services/ChatTwoCompatibilityService.cs diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index d999d60..6fe3d6a 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -66,8 +66,8 @@ public class MareConfig : IMareConfiguration public bool DefaultDisableVfx { get; set; } = false; public Dictionary PairSyncOverrides { get; set; } = new(StringComparer.Ordinal); public Dictionary GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal); - public bool EnableAutoDetectDiscovery { get; set; } = false; - public bool AllowAutoDetectPairRequests { get; set; } = false; + public bool EnableAutoDetectDiscovery { get; set; } = true; + public bool AllowAutoDetectPairRequests { get; set; } = true; public int AutoDetectMaxDistanceMeters { get; set; } = 40; public int AutoDetectMuteMinutes { get; set; } = 5; public int TimeSpanBetweenScansInSeconds { get; set; } = 30; diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index e0da18b..fbfc915 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -101,6 +101,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); @@ -149,6 +150,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); @@ -220,6 +222,8 @@ public sealed class Plugin : IDalamudPlugin collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); }) .Build(); diff --git a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs index ffe2e0f..a621362 100644 --- a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs +++ b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Threading; using Microsoft.Extensions.Logging; using MareSynchronos.WebAPI.AutoDetect; using MareSynchronos.MareConfiguration; @@ -20,6 +23,7 @@ public class AutoDetectRequestService private readonly object _syncRoot = new(); private readonly Dictionary _activeCooldowns = new(StringComparer.Ordinal); private readonly Dictionary _refusalTrackers = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _pendingRequests = new(StringComparer.Ordinal); private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5); private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15); @@ -118,6 +122,11 @@ public class AutoDetectRequestService } } _mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info)); + var pendingKey = EnsureTargetKey(targetKey); + var label = !string.IsNullOrWhiteSpace(targetDisplayName) + ? targetDisplayName! + : (!string.IsNullOrEmpty(uid) ? uid : (!string.IsNullOrEmpty(token) ? token : pendingKey)); + _pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, token, label, DateTime.UtcNow); } else { @@ -145,6 +154,7 @@ public class AutoDetectRequestService tracker.LockUntil = now.Add(RefusalLockDuration); } } + _pendingRequests.TryRemove(targetKey, out _); } _mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning)); } @@ -207,4 +217,35 @@ public class AutoDetectRequestService public int Count; public DateTime? LockUntil; } + + public IReadOnlyCollection GetPendingRequestsSnapshot() + { + return _pendingRequests.Values.OrderByDescending(v => v.SentAt).ToList(); + } + + public void RemovePendingRequestByUid(string uid) + { + if (string.IsNullOrEmpty(uid)) return; + foreach (var kvp in _pendingRequests) + { + if (string.Equals(kvp.Value.Uid, uid, StringComparison.Ordinal)) + { + _pendingRequests.TryRemove(kvp.Key, out _); + break; + } + } + } + + public void RemovePendingRequestByKey(string key) + { + if (string.IsNullOrEmpty(key)) return; + _pendingRequests.TryRemove(key, out _); + } + + private static string EnsureTargetKey(string? targetKey) + { + return !string.IsNullOrEmpty(targetKey) ? targetKey! : "target:" + Guid.NewGuid().ToString("N"); + } + + public sealed record PendingRequestInfo(string Key, string? Uid, string? Token, string TargetDisplayName, DateTime SentAt); } diff --git a/MareSynchronos/Services/AutoDetect/AutoDetectSuppressionService.cs b/MareSynchronos/Services/AutoDetect/AutoDetectSuppressionService.cs new file mode 100644 index 0000000..3d98e54 --- /dev/null +++ b/MareSynchronos/Services/AutoDetect/AutoDetectSuppressionService.cs @@ -0,0 +1,209 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Plugin.Services; +using Lumina.Excel.Sheets; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.AutoDetect; + +public sealed class AutoDetectSuppressionService : IHostedService, IMediatorSubscriber +{ + private static readonly string[] ContentTypeKeywords = + [ + "dungeon", + "donjon", + "raid", + "trial", + "défi", + "front", + "frontline", + "pvp", + "jcj", + "conflict", + "conflit" + ]; + + private readonly ILogger _logger; + private readonly MareConfigService _configService; + private readonly IClientState _clientState; + private readonly IDataManager _dataManager; + private readonly MareMediator _mediator; + private readonly DalamudUtilService _dalamudUtilService; + + private bool _isSuppressed; + private bool _hasSavedState; + private bool _savedDiscoveryEnabled; + private bool _savedAllowRequests; + private bool _suppressionWarningShown; + public bool IsSuppressed => _isSuppressed; + + public AutoDetectSuppressionService(ILogger logger, + MareConfigService configService, IClientState clientState, + IDataManager dataManager, DalamudUtilService dalamudUtilService, MareMediator mediator) + { + _logger = logger; + _configService = configService; + _clientState = clientState; + _dataManager = dataManager; + _dalamudUtilService = dalamudUtilService; + _mediator = mediator; + } + + public MareMediator Mediator => _mediator; + + public Task StartAsync(CancellationToken cancellationToken) + { + _mediator.Subscribe(this, _ => UpdateSuppressionState()); + _mediator.Subscribe(this, _ => UpdateSuppressionState()); + _mediator.Subscribe(this, _ => ClearSuppression()); + UpdateSuppressionState(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _mediator.UnsubscribeAll(this); + return Task.CompletedTask; + } + + private void UpdateSuppressionState() + { + _ = _dalamudUtilService.RunOnFrameworkThread(() => + { + try + { + if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null) + { + ClearSuppression(); + return; + } + + uint territoryId = _clientState.TerritoryType; + bool shouldSuppress = ShouldSuppressForTerritory(territoryId); + ApplySuppression(shouldSuppress, territoryId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update AutoDetect suppression state"); + } + }); + } + + private void ApplySuppression(bool shouldSuppress, uint territoryId) + { + if (shouldSuppress) + { + if (!_isSuppressed) + { + _savedDiscoveryEnabled = _configService.Current.EnableAutoDetectDiscovery; + _savedAllowRequests = _configService.Current.AllowAutoDetectPairRequests; + _hasSavedState = true; + _isSuppressed = true; + } + + bool changed = false; + if (_configService.Current.EnableAutoDetectDiscovery) + { + _configService.Current.EnableAutoDetectDiscovery = false; + changed = true; + } + if (_configService.Current.AllowAutoDetectPairRequests) + { + _configService.Current.AllowAutoDetectPairRequests = false; + changed = true; + } + + if (changed) + { + _logger.LogInformation("AutoDetect temporarily disabled in instanced content (territory {territoryId}).", territoryId); + if (!_suppressionWarningShown) + { + _suppressionWarningShown = true; + const string warningText = "Zone instanciée détectée : les fonctions AutoDetect/Nearby sont coupées pour économiser de la bande passante."; + _mediator.Publish(new DualNotificationMessage("AutoDetect désactivé", + warningText, + NotificationType.Warning, TimeSpan.FromSeconds(5))); + } + } + + return; + } + else + { + if (!_isSuppressed) return; + + bool changed = false; + bool wasSuppressed = _suppressionWarningShown; + if (_hasSavedState) + { + if (_configService.Current.EnableAutoDetectDiscovery != _savedDiscoveryEnabled) + { + _configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled; + changed = true; + } + if (_configService.Current.AllowAutoDetectPairRequests != _savedAllowRequests) + { + _configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests; + changed = true; + } + } + + _isSuppressed = false; + _hasSavedState = false; + _suppressionWarningShown = false; + + if (changed || wasSuppressed) + { + _logger.LogInformation("AutoDetect restored after leaving instanced content (territory {territoryId}).", territoryId); + const string restoredText = "Vous avez quitté la zone instanciée : AutoDetect/Nearby fonctionnent de nouveau."; + _mediator.Publish(new DualNotificationMessage("AutoDetect réactivé", + restoredText, + NotificationType.Info, TimeSpan.FromSeconds(5))); + } + } + } + + private void ClearSuppression() + { + if (!_isSuppressed) return; + _isSuppressed = false; + if (_hasSavedState) + { + _configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled; + _configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests; + } + _hasSavedState = false; + _suppressionWarningShown = false; + } + + private bool ShouldSuppressForTerritory(uint territoryId) + { + if (territoryId == 0) return false; + + var cfcSheet = _dataManager.GetExcelSheet(); + if (cfcSheet == null) return false; + + var cfc = cfcSheet.FirstOrDefault(c => c.TerritoryType.RowId == territoryId); + if (cfc.RowId == 0) return false; + + if (MatchesSuppressionKeyword(cfc.Name.ToString())) return true; + + var contentType = cfc.ContentType.Value; + if (contentType.RowId == 0) return false; + + return MatchesSuppressionKeyword(contentType.Name.ToString()); + } + + private static bool MatchesSuppressionKeyword(string? text) + { + if (string.IsNullOrWhiteSpace(text)) return false; + return ContentTypeKeywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs index 478ecbe..8ff5e8c 100644 --- a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs +++ b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs @@ -41,6 +41,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber { _ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA))); _pending.TryRemove(uidA, out _); + _requestService.RemovePendingRequestByUid(uidA); _logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA); } return; @@ -67,6 +68,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber public void Remove(string uid) { _pending.TryRemove(uid, out _); + _requestService.RemovePendingRequestByUid(uid); } public async Task AcceptAsync(string uid) @@ -75,6 +77,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber { await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false); _pending.TryRemove(uid, out _); + _requestService.RemovePendingRequestByUid(uid); _ = _requestService.SendAcceptNotifyAsync(uid); return true; } diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index 26429d4..922da46 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -1,7 +1,9 @@ -using Lumina.Data.Files; +using System; +using Lumina.Data.Files; using MareSynchronos.API.Data; using MareSynchronos.API.Data.Enum; using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration.Models; using MareSynchronos.Services.Mediator; using MareSynchronos.UI; using MareSynchronos.Utils; @@ -16,6 +18,16 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase private CancellationTokenSource? _analysisCts; private CancellationTokenSource _baseAnalysisCts = new(); private string _lastDataHash = string.Empty; + private CharacterAnalysisSummary _previousSummary = CharacterAnalysisSummary.Empty; + private DateTime _lastAutoAnalysis = DateTime.MinValue; + private string _lastAutoAnalysisHash = string.Empty; + private const int AutoAnalysisFileDeltaThreshold = 25; + private const long AutoAnalysisSizeDeltaThreshold = 50L * 1024 * 1024; + private static readonly TimeSpan AutoAnalysisCooldown = TimeSpan.FromMinutes(2); + private const long NotificationSizeThreshold = 300L * 1024 * 1024; + private const long NotificationTriangleThreshold = 150_000; + private bool _sizeWarningShown; + private bool _triangleWarningShown; public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) : base(logger, mediator) @@ -33,6 +45,7 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase public int CurrentFile { get; internal set; } public bool IsAnalysisRunning => _analysisCts != null; public int TotalFiles { get; internal set; } + public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty; internal Dictionary> LastAnalysis { get; } = []; public void CancelAnalyze() @@ -80,6 +93,8 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase } } + RefreshSummary(false, _lastDataHash); + Mediator.Publish(new CharacterDataAnalyzedMessage()); _analysisCts.CancelDispose(); @@ -142,9 +157,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase LastAnalysis[obj.Key] = data; } + _lastDataHash = charaData.DataHash.Value; + RefreshSummary(true, _lastDataHash); + Mediator.Publish(new CharacterDataAnalyzedMessage()); - _lastDataHash = charaData.DataHash.Value; } private void PrintAnalysis() @@ -193,6 +210,162 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); } + private void RefreshSummary(bool evaluateAutoAnalysis, string dataHash) + { + var summary = CalculateSummary(); + CurrentSummary = summary; + + if (evaluateAutoAnalysis) + { + EvaluateAutoAnalysis(summary, dataHash); + } + else + { + _previousSummary = summary; + + if (!summary.HasUncomputedEntries && string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal)) + { + _lastAutoAnalysisHash = string.Empty; + } + } + + EvaluateThresholdNotifications(summary); + } + + private CharacterAnalysisSummary CalculateSummary() + { + if (LastAnalysis.Count == 0) + { + return CharacterAnalysisSummary.Empty; + } + + long original = 0; + long compressed = 0; + long triangles = 0; + int files = 0; + bool hasUncomputed = false; + + foreach (var obj in LastAnalysis.Values) + { + foreach (var entry in obj.Values) + { + files++; + original += entry.OriginalSize; + compressed += entry.CompressedSize; + triangles += entry.Triangles; + hasUncomputed |= !entry.IsComputed; + } + } + + return new CharacterAnalysisSummary(files, original, compressed, triangles, hasUncomputed); + } + + private void EvaluateAutoAnalysis(CharacterAnalysisSummary newSummary, string dataHash) + { + var previous = _previousSummary; + _previousSummary = newSummary; + + if (newSummary.TotalFiles == 0) + { + return; + } + + if (string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal)) + { + return; + } + + if (IsAnalysisRunning) + { + return; + } + + var now = DateTime.UtcNow; + if (now - _lastAutoAnalysis < AutoAnalysisCooldown) + { + return; + } + + bool firstSummary = previous.TotalFiles == 0; + bool filesIncreased = newSummary.TotalFiles - previous.TotalFiles >= AutoAnalysisFileDeltaThreshold; + bool sizeIncreased = newSummary.TotalCompressedSize - previous.TotalCompressedSize >= AutoAnalysisSizeDeltaThreshold; + bool needsCompute = newSummary.HasUncomputedEntries; + + if (!firstSummary && !filesIncreased && !sizeIncreased && !needsCompute) + { + return; + } + + _lastAutoAnalysis = now; + _lastAutoAnalysisHash = dataHash; + _ = ComputeAnalysis(print: false); + } + + private void EvaluateThresholdNotifications(CharacterAnalysisSummary summary) + { + if (summary.IsEmpty || summary.HasUncomputedEntries) + { + ResetThresholdFlagsIfNeeded(summary); + return; + } + + bool sizeExceeded = summary.TotalCompressedSize >= NotificationSizeThreshold; + bool trianglesExceeded = summary.TotalTriangles >= NotificationTriangleThreshold; + List exceededReasons = new(); + + if (sizeExceeded && !_sizeWarningShown) + { + exceededReasons.Add($"un poids partagé de {UiSharedService.ByteToString(summary.TotalCompressedSize)} (≥ 300 MiB)"); + _sizeWarningShown = true; + } + else if (!sizeExceeded && _sizeWarningShown) + { + _sizeWarningShown = false; + } + + if (trianglesExceeded && !_triangleWarningShown) + { + exceededReasons.Add($"un total de {UiSharedService.TrisToString(summary.TotalTriangles)} triangles (≥ 150k)"); + _triangleWarningShown = true; + } + else if (!trianglesExceeded && _triangleWarningShown) + { + _triangleWarningShown = false; + } + + if (exceededReasons.Count == 0) return; + + string combined = string.Join(" et ", exceededReasons); + string message = $"Attention : votre self-analysis indique {combined}. Des joueurs risquent de ne pas vous voir et UmbraSync peut activer un auto-pause. Pensez à réduire textures ou modèles lourds."; + Mediator.Publish(new DualNotificationMessage("Self Analysis", message, NotificationType.Warning)); + } + + private void ResetThresholdFlagsIfNeeded(CharacterAnalysisSummary summary) + { + if (summary.IsEmpty) + { + _sizeWarningShown = false; + _triangleWarningShown = false; + return; + } + + if (summary.TotalCompressedSize < NotificationSizeThreshold) + { + _sizeWarningShown = false; + } + + if (summary.TotalTriangles < NotificationTriangleThreshold) + { + _triangleWarningShown = false; + } + } + + public readonly record struct CharacterAnalysisSummary(int TotalFiles, long TotalOriginalSize, long TotalCompressedSize, long TotalTriangles, bool HasUncomputedEntries) + { + public static CharacterAnalysisSummary Empty => new(); + public bool IsEmpty => TotalFiles == 0 && TotalOriginalSize == 0 && TotalCompressedSize == 0 && TotalTriangles == 0; + } + internal sealed record FileDataEntry(string Hash, string FileType, List GamePaths, List FilePaths, long OriginalSize, long CompressedSize, long Triangles) { public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; @@ -239,4 +412,4 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase } }); } -} \ No newline at end of file +} diff --git a/MareSynchronos/Services/ChatTwoCompatibilityService.cs b/MareSynchronos/Services/ChatTwoCompatibilityService.cs new file mode 100644 index 0000000..2198448 --- /dev/null +++ b/MareSynchronos/Services/ChatTwoCompatibilityService.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Plugin; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class ChatTwoCompatibilityService : MediatorSubscriberBase, IHostedService +{ + private const string ChatTwoInternalName = "ChatTwo"; + private readonly IDalamudPluginInterface _pluginInterface; + private bool _warningShown; + + public ChatTwoCompatibilityService(ILogger logger, IDalamudPluginInterface pluginInterface, MareMediator mediator) + : base(logger, mediator) + { + _pluginInterface = pluginInterface; + + Mediator.SubscribeKeyed(this, ChatTwoInternalName, OnChatTwoStateChanged); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + try + { + var initialState = PluginWatcherService.GetInitialPluginState(_pluginInterface, ChatTwoInternalName); + if (initialState?.IsLoaded == true) + { + ShowWarning(); + } + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to inspect ChatTwo initial state"); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Mediator.UnsubscribeAll(this); + return Task.CompletedTask; + } + + private void OnChatTwoStateChanged(PluginChangeMessage message) + { + if (message.IsLoaded) + { + ShowWarning(); + } + } + + private void ShowWarning() + { + if (_warningShown) return; + _warningShown = true; + + const string warningTitle = "ChatTwo détecté"; + const string warningBody = "Actuellement, le plugin ChatTwo n'est pas compatible avec la bulle d'écriture d'UmbraSync. Désactivez ChatTwo si vous souhaitez conserver l'indicateur de saisie."; + + Mediator.Publish(new NotificationMessage(warningTitle, warningBody, NotificationType.Warning, TimeSpan.FromSeconds(10))); + } +} diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs index 38289c7..3279ceb 100644 --- a/MareSynchronos/Services/CommandManagerService.cs +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -9,12 +9,14 @@ using MareSynchronos.UI; using MareSynchronos.WebAPI; using System.Globalization; using System.Text; +using System.Numerics; namespace MareSynchronos.Services; public sealed class CommandManagerService : IDisposable { private const string _commandName = "/usync"; + private const string _autoDetectCommand = "/autodetect"; private const string _ssCommandPrefix = "/ums"; private readonly ApiController _apiController; @@ -43,6 +45,11 @@ public sealed class CommandManagerService : IDisposable HelpMessage = "Opens the UmbraSync UI" }); + _commandManager.AddHandler(_autoDetectCommand, new CommandInfo(OnAutoDetectCommand) + { + HelpMessage = "Opens the AutoDetect window" + }); + // Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) { @@ -56,12 +63,21 @@ public sealed class CommandManagerService : IDisposable public void Dispose() { _commandManager.RemoveHandler(_commandName); - + _commandManager.RemoveHandler(_autoDetectCommand); for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) _commandManager.RemoveHandler($"{_ssCommandPrefix}{i}"); } + private void OnAutoDetectCommand(string command, string args) + { + UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f); + UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f); + UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor; + _mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); + } + + private void OnCommand(string command, string args) { var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); @@ -145,4 +161,4 @@ public sealed class CommandManagerService : IDisposable _chatService.SendChatShell(shellNumber, chatBytes); } } -} \ No newline at end of file +} diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs index 0f0e545..a06036b 100644 --- a/MareSynchronos/Services/NotificationService.cs +++ b/MareSynchronos/Services/NotificationService.cs @@ -85,7 +85,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ bool appendInstruction; bool forceChat = ShouldForceChat(msg, out appendInstruction); - var effectiveMessage = forceChat && appendInstruction ? AppendUsyncInstruction(msg.Message) : msg.Message; + var effectiveMessage = forceChat && appendInstruction ? AppendAutoDetectInstruction(msg.Message) : msg.Message; var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg; switch (adjustedMsg.Type) @@ -155,13 +155,13 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return false; } - private static string AppendUsyncInstruction(string? message) + private static string AppendAutoDetectInstruction(string? message) { - const string suffix = " | Ouvrez /usync pour voir l'invitation."; + const string suffix = " | Ouvrez /autodetect pour gérer l'invitation."; if (string.IsNullOrWhiteSpace(message)) return suffix.TrimStart(' ', '|'); - if (message.Contains("/usync", StringComparison.OrdinalIgnoreCase)) + if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase)) return message; return message.TrimEnd() + suffix; diff --git a/MareSynchronos/UI/AutoDetectUi.cs b/MareSynchronos/UI/AutoDetectUi.cs index 81ff943..24c07e4 100644 --- a/MareSynchronos/UI/AutoDetectUi.cs +++ b/MareSynchronos/UI/AutoDetectUi.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using Dalamud.Game.ClientState.Objects.SubKinds; @@ -5,13 +9,15 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using MareSynchronos.MareConfiguration; -using MareSynchronos.Services; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using System.Numerics; using System.Globalization; using System.Text; +using MareSynchronos.Services; +using MareSynchronos.Services.AutoDetect; +using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; namespace MareSynchronos.UI; @@ -20,13 +26,15 @@ public class AutoDetectUi : WindowMediatorSubscriberBase private readonly MareConfigService _configService; private readonly DalamudUtilService _dalamud; private readonly IObjectTable _objectTable; - private readonly Services.AutoDetect.AutoDetectRequestService _requestService; + private readonly AutoDetectRequestService _requestService; + private readonly NearbyPendingService _pendingService; private readonly PairManager _pairManager; private List _entries = new(); + private readonly HashSet _acceptInFlight = new(StringComparer.Ordinal); public AutoDetectUi(ILogger logger, MareMediator mediator, MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable, - Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager, + AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "AutoDetect", performanceCollectorService) { @@ -34,6 +42,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase _dalamud = dalamudUtilService; _objectTable = objectTable; _requestService = requestService; + _pendingService = pendingService; _pairManager = pairManager; Flags |= ImGuiWindowFlags.NoScrollbar; @@ -53,9 +62,141 @@ public class AutoDetectUi : WindowMediatorSubscriberBase { using var idScope = ImRaii.PushId("autodetect-ui"); + var incomingInvites = _pendingService.Pending.ToList(); + var outgoingInvites = _requestService.GetPendingRequestsSnapshot(); + + Vector4 accent = UiSharedService.AccentColor; + if (accent.W <= 0f) accent = ImGuiColors.ParsedPurple; + Vector4 inactiveTab = new(accent.X * 0.45f, accent.Y * 0.45f, accent.Z * 0.45f, Math.Clamp(accent.W + 0.15f, 0f, 1f)); + Vector4 hoverTab = UiSharedService.AccentHoverColor; + + using var tabs = ImRaii.TabBar("AutoDetectTabs"); + if (!tabs.Success) return; + + var incomingCount = incomingInvites.Count; + DrawStyledTab($"Invitations ({incomingCount})", accent, inactiveTab, hoverTab, () => + { + DrawInvitationsTab(incomingInvites, outgoingInvites); + }); + + DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab); + + using (ImRaii.Disabled(true)) + { + DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, () => + { + UiSharedService.ColorTextWrapped("Disponible prochainement.", ImGuiColors.DalamudGrey3); + }, true); + } + } + + private static void DrawStyledTab(string label, Vector4 accent, Vector4 inactive, Vector4 hover, Action draw, bool disabled = false) + { + var tabColor = disabled ? ImGuiColors.DalamudGrey3 : inactive; + var tabHover = disabled ? ImGuiColors.DalamudGrey3 : hover; + var tabActive = disabled ? ImGuiColors.DalamudGrey2 : accent; + using var baseColor = ImRaii.PushColor(ImGuiCol.Tab, tabColor); + using var hoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, tabHover); + using var activeColor = ImRaii.PushColor(ImGuiCol.TabActive, tabActive); + using var activeText = ImRaii.PushColor(ImGuiCol.Text, disabled ? ImGuiColors.DalamudGrey2 : Vector4.One, false); + using var tab = ImRaii.TabItem(label); + if (tab.Success) + { + draw(); + } + } + + private void DrawInvitationsTab(List> incomingInvites, IReadOnlyCollection outgoingInvites) + { + if (incomingInvites.Count == 0 && outgoingInvites.Count == 0) + { + UiSharedService.ColorTextWrapped("Aucune invitation en attente. Cette page regroupera les demandes reçues et celles que vous avez envoyées.", ImGuiColors.DalamudGrey3); + return; + } + + if (incomingInvites.Count == 0) + { + UiSharedService.ColorTextWrapped("Vous n'avez aucune invitation de pair en attente pour le moment.", ImGuiColors.DalamudGrey3); + } + + ImGuiHelpers.ScaledDummy(4); + float leftWidth = Math.Max(220f * ImGuiHelpers.GlobalScale, ImGui.CalcTextSize("Invitations reçues (00)").X + ImGui.GetStyle().FramePadding.X * 4f); + var avail = ImGui.GetContentRegionAvail(); + + ImGui.BeginChild("incoming-requests", new Vector2(leftWidth, avail.Y), true); + ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations reçues ({incomingInvites.Count})"); + ImGui.Separator(); + if (incomingInvites.Count == 0) + { + ImGui.TextDisabled("Aucune invitation reçue."); + } + else + { + foreach (var (uid, name) in incomingInvites.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase)) + { + using var id = ImRaii.PushId(uid); + bool processing = _acceptInFlight.Contains(uid); + ImGui.TextUnformatted(name); + ImGui.TextDisabled(uid); + if (processing) + { + ImGui.TextDisabled("Traitement en cours..."); + } + else + { + if (ImGui.Button("Accepter")) + { + TriggerAccept(uid); + } + ImGui.SameLine(); + if (ImGui.Button("Refuser")) + { + _pendingService.Remove(uid); + } + } + ImGui.Separator(); + } + } + ImGui.EndChild(); + + ImGui.SameLine(); + + ImGui.BeginChild("outgoing-requests", new Vector2(0, avail.Y), true); + ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations envoyées ({outgoingInvites.Count})"); + ImGui.Separator(); + if (outgoingInvites.Count == 0) + { + ImGui.TextDisabled("Aucune invitation envoyée en attente."); + ImGui.EndChild(); + return; + } + + foreach (var info in outgoingInvites.OrderByDescending(i => i.SentAt)) + { + using var id = ImRaii.PushId(info.Key); + ImGui.TextUnformatted(info.TargetDisplayName); + if (!string.IsNullOrEmpty(info.Uid)) + { + ImGui.TextDisabled(info.Uid); + } + + ImGui.TextDisabled($"Envoyée il y a {FormatDuration(DateTime.UtcNow - info.SentAt)}"); + if (ImGui.Button("Retirer")) + { + _requestService.RemovePendingRequestByKey(info.Key); + } + UiSharedService.AttachToolTip("Retire uniquement cette entrée locale de suivi."); + ImGui.Separator(); + } + + ImGui.EndChild(); + } + + private void DrawNearbyTab() + { if (!_configService.Current.EnableAutoDetectDiscovery) { - UiSharedService.ColorTextWrapped("Nearby detection is disabled. Enable it in Settings to start detecting nearby Umbra users.", ImGuiColors.DalamudYellow); + UiSharedService.ColorTextWrapped("AutoDetect est désactivé. Activez-le dans les paramètres pour détecter les utilisateurs Umbra à proximité.", ImGuiColors.DalamudYellow); ImGuiHelpers.ScaledDummy(6); } @@ -134,26 +275,6 @@ public class AutoDetectUi : WindowMediatorSubscriberBase _entries = msg.Entries; } - private List BuildLocalSnapshot(int maxDist) - { - var list = new List(); - var local = _dalamud.GetPlayerCharacter(); - var localPos = local?.Position ?? Vector3.Zero; - for (int i = 0; i < 200; i += 2) - { - var obj = _objectTable[i]; - if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - if (local != null && obj.Address == local.Address) continue; - float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position); - if (!float.IsNaN(dist) && dist > maxDist) continue; - string name = obj.Name.ToString(); - ushort worldId = 0; - if (obj is IPlayerCharacter pc) worldId = (ushort)pc.HomeWorld.RowId; - list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null, null, null)); - } - return list; - } - private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e) { try @@ -194,4 +315,37 @@ public class AutoDetectUi : WindowMediatorSubscriberBase } return sb.ToString(); } + + private void TriggerAccept(string uid) + { + if (!_acceptInFlight.Add(uid)) return; + + Task.Run(async () => + { + try + { + bool ok = await _pendingService.AcceptAsync(uid).ConfigureAwait(false); + if (!ok) + { + Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5))); + } + } + finally + { + _acceptInFlight.Remove(uid); + } + }); + } + + private static string FormatDuration(TimeSpan span) + { + if (span.TotalMinutes >= 1) + { + var minutes = Math.Max(1, (int)Math.Round(span.TotalMinutes)); + return minutes == 1 ? "1 minute" : $"{minutes} minutes"; + } + + var seconds = Math.Max(1, (int)Math.Round(span.TotalSeconds)); + return seconds == 1 ? "1 seconde" : $"{seconds} secondes"; + } } diff --git a/MareSynchronos/UI/ChangelogUi.cs b/MareSynchronos/UI/ChangelogUi.cs index 3b679f0..c123fad 100644 --- a/MareSynchronos/UI/ChangelogUi.cs +++ b/MareSynchronos/UI/ChangelogUi.cs @@ -169,6 +169,16 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase { return new List { + new(new Version(0, 1, 9, 4), "0.1.9.4", new List + { + new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."), + new("Désactivation de l'AutoDetect en zone instanciée."), + new("Réécriture interface AutoDetect pour acceuillir les invitations en attente et préparer les synchsells publiques."), + new("Amélioration de la compréhension des activations / désactivations des préférences de synchronisation par défaut."), + new("Mise en avant du Self Analyse avec une alerte lorsqu'un seuil de donnée a été atteint."), + new("Ajout de l'alerte de la non-compatibilité du plugin Chat2."), + new("Divers fix de l'interface."), + }), new(new Version(0, 1, 9, 3), "0.1.9.3", new List { new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."), diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 92b4b2a..fea0c89 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -62,7 +62,6 @@ public class CompactUi : WindowMediatorSubscriberBase private bool _showSyncShells; private bool _wasOpen; private bool _nearbyOpen = true; - private bool _pendingOpen = true; private List _nearbyEntries = new(); public CompactUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, @@ -297,6 +296,10 @@ public class CompactUi : WindowMediatorSubscriberBase bool animsDisabled = _configService.Current.DefaultDisableAnimations; bool vfxDisabled = _configService.Current.DefaultDisableVfx; bool showNearby = _configService.Current.EnableAutoDetectDiscovery; + int pendingInvites = _nearbyPending.Pending.Count; + + const string nearbyLabel = "AutoDetect"; + var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp; var animIcon = animsDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running; @@ -306,7 +309,7 @@ public class CompactUi : WindowMediatorSubscriberBase float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel); float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel); float vfxWidth = _uiSharedService.GetIconTextButtonSize(vfxIcon, vfxLabel); - float nearbyWidth = showNearby ? _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, "Nearby") : 0f; + float nearbyWidth = showNearby ? _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel) : 0f; int buttonCount = 3 + (showNearby ? 1 : 0); float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1); float available = ImGui.GetContentRegionAvail().X; @@ -346,11 +349,15 @@ public class CompactUi : WindowMediatorSubscriberBase if (showNearby) { ImGui.SameLine(0, spacing); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Nearby", nearbyWidth)) + var autodetectLabel = pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, autodetectLabel, nearbyWidth)) { Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi))); } - UiSharedService.AttachToolTip("Ouvrir la détection de proximité"); + string tooltip = pendingInvites > 0 + ? string.Format("Vous avez {0} invitation{1} reçue. Ouvrez l\'interface AutoDetect pour y répondre.", pendingInvites, pendingInvites > 1 ? "s" : string.Empty) + : "Ouvrir les outils AutoDetect (invitations et proximité).\n\nLes demandes reçues sont listées dans l\'onglet 'Invitations'."; + UiSharedService.AttachToolTip(tooltip); } } ImGui.Separator(); @@ -524,48 +531,11 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false); - try - { - // intentionally left blank; pending requests handled in collapsible section below - } - catch { } - var pendingCount = _nearbyPending?.Pending.Count ?? 0; if (pendingCount > 0) { - using (ImRaii.PushId("group-Pending")) - { - var icon = _pendingOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; - _uiSharedService.IconText(icon); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; - ImGui.SameLine(); - ImGui.TextUnformatted($"En attente ({pendingCount})"); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _pendingOpen = !_pendingOpen; - - if (_pendingOpen) - { - ImGui.Indent(); - foreach (var kv in _nearbyPending!.Pending) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"{kv.Value} [{kv.Key}]"); - ImGui.SameLine(); - if (_uiSharedService.IconButton(FontAwesomeIcon.Check)) - { - _ = _nearbyPending.AcceptAsync(kv.Key); - } - UiSharedService.AttachToolTip("Accept and add as pair"); - ImGui.SameLine(); - if (_uiSharedService.IconButton(FontAwesomeIcon.Times)) - { - _nearbyPending.Remove(kv.Key); - } - UiSharedService.AttachToolTip("Dismiss request"); - } - ImGui.Unindent(); - ImGui.Separator(); - } - } + UiSharedService.ColorTextWrapped("Invitation AutoDetect en attente. Ouvrez l\'interface AutoDetect pour gérer vos demandes.", ImGuiColors.DalamudYellow); + ImGuiHelpers.ScaledDummy(4); } var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList(); diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 08ebf05..898b3b8 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -14,6 +14,7 @@ using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services; +using MareSynchronos.Services.AutoDetect; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.WebAPI; @@ -44,6 +45,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly PairManager _pairManager; private readonly ChatService _chatService; private readonly GuiHookService _guiHookService; + private readonly AutoDetectSuppressionService _autoDetectSuppressionService; private readonly PerformanceCollectorService _performanceCollector; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceService _playerPerformanceService; @@ -77,7 +79,8 @@ public class SettingsUi : WindowMediatorSubscriberBase FileCacheManager fileCacheManager, FileCompactor fileCompactor, ApiController apiController, IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, - DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mediator, "Umbra Settings", performanceCollector) + DalamudUtilService dalamudUtilService, AccountRegistrationService registerService, + AutoDetectSuppressionService autoDetectSuppressionService) : base(logger, mediator, "Umbra Settings", performanceCollector) { _configService = configService; _pairManager = pairManager; @@ -96,6 +99,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _cacheMonitor = cacheMonitor; _dalamudUtilService = dalamudUtilService; _registerService = registerService; + _autoDetectSuppressionService = autoDetectSuppressionService; _fileCompactor = fileCompactor; _uiShared = uiShared; AllowClickthrough = false; @@ -214,26 +218,34 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); _uiShared.BigText("AutoDetect"); + bool isAutoDetectSuppressed = _autoDetectSuppressionService?.IsSuppressed ?? false; bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery; - if (ImGui.Checkbox("Enable Nearby detection (beta)", ref enableDiscovery)) + using (ImRaii.Disabled(isAutoDetectSuppressed)) { - _configService.Current.EnableAutoDetectDiscovery = enableDiscovery; - _configService.Save(); - - // notify services of toggle - Mediator.Publish(new NearbyDetectionToggled(enableDiscovery)); - - // if Nearby is turned OFF, force Allow Pair Requests OFF as well - if (!enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) + if (ImGui.Checkbox("Enable AutoDetect", ref enableDiscovery)) { - _configService.Current.AllowAutoDetectPairRequests = false; + _configService.Current.EnableAutoDetectDiscovery = enableDiscovery; _configService.Save(); - Mediator.Publish(new AllowPairRequestsToggled(false)); + + // notify services of toggle + Mediator.Publish(new NearbyDetectionToggled(enableDiscovery)); + + // if Nearby is turned OFF, force Allow Pair Requests OFF as well + if (!enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) + { + _configService.Current.AllowAutoDetectPairRequests = false; + _configService.Save(); + Mediator.Publish(new AllowPairRequestsToggled(false)); + } + } + if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée."); } } // Allow Pair Requests is disabled when Nearby is OFF - using (ImRaii.Disabled(!enableDiscovery)) + using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery)) { bool allowRequests = _configService.Current.AllowAutoDetectPairRequests; if (ImGui.Checkbox("Allow pair requests", ref allowRequests)) @@ -246,15 +258,19 @@ public class SettingsUi : WindowMediatorSubscriberBase // user-facing info toast Mediator.Publish(new NotificationMessage( - "Nearby Detection", - allowRequests ? "Pair requests enabled: others can invite you." : "Pair requests disabled: others cannot invite you.", + "AutoDetect", + allowRequests ? "Invitations entrantes autorisées : les autres peuvent vous inviter." : "Invitations entrantes désactivées : les autres ne peuvent pas vous inviter.", NotificationType.Info, default)); } + if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée."); + } } // Radius only available when both Nearby and Allow Pair Requests are ON - if (enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) + if (!isAutoDetectSuppressed && enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) { ImGui.Indent(); int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters; @@ -266,6 +282,10 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Unindent(); } + else if (isAutoDetectSuppressed) + { + UiSharedService.ColorTextWrapped("AutoDetect est verrouillé tant que vous restez dans une zone instanciée.", ImGuiColors.DalamudYellow); + } ImGui.Separator(); _uiShared.BigText("Transfer UI"); diff --git a/Penumbra.Api b/Penumbra.Api index 648b6fc..97fe622 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 648b6fc2ce600a95ab2b2ced27e1639af2b04502 +Subproject commit 97fe622e4ec0a5469a26aba8a8c3933fa8ef7fd6