Fix UI + Amélioration AutoDetect & Self Analyse + Update Penumbra API

This commit is contained in:
2025-10-12 12:42:31 +02:00
parent d4a46910f9
commit d225a3844a
14 changed files with 763 additions and 95 deletions

View File

@@ -66,8 +66,8 @@ public class MareConfig : IMareConfiguration
public bool DefaultDisableVfx { get; set; } = false; public bool DefaultDisableVfx { get; set; } = false;
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal); public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal); public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public bool EnableAutoDetectDiscovery { get; set; } = false; public bool EnableAutoDetectDiscovery { get; set; } = true;
public bool AllowAutoDetectPairRequests { get; set; } = false; public bool AllowAutoDetectPairRequests { get; set; } = true;
public int AutoDetectMaxDistanceMeters { get; set; } = 40; public int AutoDetectMaxDistanceMeters { get; set; } = 40;
public int AutoDetectMuteMinutes { get; set; } = 5; public int AutoDetectMuteMinutes { get; set; } = 5;
public int TimeSpanBetweenScansInSeconds { get; set; } = 30; public int TimeSpanBetweenScansInSeconds { get; set; } = 30;

View File

@@ -101,6 +101,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>(); collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
collection.AddSingleton<MarePlugin>(); collection.AddSingleton<MarePlugin>();
collection.AddSingleton<MareProfileManager>(); collection.AddSingleton<MareProfileManager>();
collection.AddSingleton<GameObjectHandlerFactory>(); collection.AddSingleton<GameObjectHandlerFactory>();
@@ -149,6 +150,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<TemporarySyncshellNotificationService>(); collection.AddSingleton<TemporarySyncshellNotificationService>();
collection.AddSingleton<PartyListTypingService>(); collection.AddSingleton<PartyListTypingService>();
collection.AddSingleton<TypingIndicatorStateService>(); collection.AddSingleton<TypingIndicatorStateService>();
collection.AddSingleton<ChatTwoCompatibilityService>();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(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<MarePlugin>()); collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>()); collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>()); collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
}) })
.Build(); .Build();

View File

@@ -1,5 +1,8 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.AutoDetect; using MareSynchronos.WebAPI.AutoDetect;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
@@ -20,6 +23,7 @@ public class AutoDetectRequestService
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal); private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal);
private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal); private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5); private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15); 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)); _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 else
{ {
@@ -145,6 +154,7 @@ public class AutoDetectRequestService
tracker.LockUntil = now.Add(RefusalLockDuration); 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)); _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 int Count;
public DateTime? LockUntil; public DateTime? LockUntil;
} }
public IReadOnlyCollection<PendingRequestInfo> 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);
} }

View File

@@ -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<AutoDetectSuppressionService> _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<AutoDetectSuppressionService> 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<ZoneSwitchEndMessage>(this, _ => UpdateSuppressionState());
_mediator.Subscribe<DalamudLoginMessage>(this, _ => UpdateSuppressionState());
_mediator.Subscribe<DalamudLogoutMessage>(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<ContentFinderCondition>();
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));
}
}

View File

@@ -41,6 +41,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
{ {
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA))); _ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
_pending.TryRemove(uidA, out _); _pending.TryRemove(uidA, out _);
_requestService.RemovePendingRequestByUid(uidA);
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA); _logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
} }
return; return;
@@ -67,6 +68,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
public void Remove(string uid) public void Remove(string uid)
{ {
_pending.TryRemove(uid, out _); _pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid);
} }
public async Task<bool> AcceptAsync(string uid) public async Task<bool> 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); await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
_pending.TryRemove(uid, out _); _pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid);
_ = _requestService.SendAcceptNotifyAsync(uid); _ = _requestService.SendAcceptNotifyAsync(uid);
return true; return true;
} }

View File

@@ -1,7 +1,9 @@
using Lumina.Data.Files; using System;
using Lumina.Data.Files;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.UI; using MareSynchronos.UI;
using MareSynchronos.Utils; using MareSynchronos.Utils;
@@ -16,6 +18,16 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
private CancellationTokenSource? _analysisCts; private CancellationTokenSource? _analysisCts;
private CancellationTokenSource _baseAnalysisCts = new(); private CancellationTokenSource _baseAnalysisCts = new();
private string _lastDataHash = string.Empty; 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<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
: base(logger, mediator) : base(logger, mediator)
@@ -33,6 +45,7 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
public int CurrentFile { get; internal set; } public int CurrentFile { get; internal set; }
public bool IsAnalysisRunning => _analysisCts != null; public bool IsAnalysisRunning => _analysisCts != null;
public int TotalFiles { get; internal set; } public int TotalFiles { get; internal set; }
public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty;
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = []; internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
public void CancelAnalyze() public void CancelAnalyze()
@@ -80,6 +93,8 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
} }
} }
RefreshSummary(false, _lastDataHash);
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
@@ -142,9 +157,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
LastAnalysis[obj.Key] = data; LastAnalysis[obj.Key] = data;
} }
_lastDataHash = charaData.DataHash.Value;
RefreshSummary(true, _lastDataHash);
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value;
} }
private void PrintAnalysis() 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."); 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<string> 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<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles) internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
{ {
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;

View File

@@ -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<ChatTwoCompatibilityService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator)
: base(logger, mediator)
{
_pluginInterface = pluginInterface;
Mediator.SubscribeKeyed<PluginChangeMessage>(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)));
}
}

View File

@@ -9,12 +9,14 @@ using MareSynchronos.UI;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Numerics;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
public sealed class CommandManagerService : IDisposable public sealed class CommandManagerService : IDisposable
{ {
private const string _commandName = "/usync"; private const string _commandName = "/usync";
private const string _autoDetectCommand = "/autodetect";
private const string _ssCommandPrefix = "/ums"; private const string _ssCommandPrefix = "/ums";
private readonly ApiController _apiController; private readonly ApiController _apiController;
@@ -43,6 +45,11 @@ public sealed class CommandManagerService : IDisposable
HelpMessage = "Opens the UmbraSync UI" 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 // 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) for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
{ {
@@ -56,12 +63,21 @@ public sealed class CommandManagerService : IDisposable
public void Dispose() public void Dispose()
{ {
_commandManager.RemoveHandler(_commandName); _commandManager.RemoveHandler(_commandName);
_commandManager.RemoveHandler(_autoDetectCommand);
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
_commandManager.RemoveHandler($"{_ssCommandPrefix}{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) private void OnCommand(string command, string args)
{ {
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);

View File

@@ -85,7 +85,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
bool appendInstruction; bool appendInstruction;
bool forceChat = ShouldForceChat(msg, out 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; var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg;
switch (adjustedMsg.Type) switch (adjustedMsg.Type)
@@ -155,13 +155,13 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return false; 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)) if (string.IsNullOrWhiteSpace(message))
return suffix.TrimStart(' ', '|'); return suffix.TrimStart(' ', '|');
if (message.Contains("/usync", StringComparison.OrdinalIgnoreCase)) if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase))
return message; return message;
return message.TrimEnd() + suffix; return message.TrimEnd() + suffix;

View File

@@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
@@ -5,13 +9,15 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.Services;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
@@ -20,13 +26,15 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
private readonly MareConfigService _configService; private readonly MareConfigService _configService;
private readonly DalamudUtilService _dalamud; private readonly DalamudUtilService _dalamud;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly Services.AutoDetect.AutoDetectRequestService _requestService; private readonly AutoDetectRequestService _requestService;
private readonly NearbyPendingService _pendingService;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private List<Services.Mediator.NearbyEntry> _entries = new(); private List<Services.Mediator.NearbyEntry> _entries = new();
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator, public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable, MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable,
Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager, AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "AutoDetect", performanceCollectorService) : base(logger, mediator, "AutoDetect", performanceCollectorService)
{ {
@@ -34,6 +42,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
_dalamud = dalamudUtilService; _dalamud = dalamudUtilService;
_objectTable = objectTable; _objectTable = objectTable;
_requestService = requestService; _requestService = requestService;
_pendingService = pendingService;
_pairManager = pairManager; _pairManager = pairManager;
Flags |= ImGuiWindowFlags.NoScrollbar; Flags |= ImGuiWindowFlags.NoScrollbar;
@@ -53,9 +62,141 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
{ {
using var idScope = ImRaii.PushId("autodetect-ui"); 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<KeyValuePair<string, string>> incomingInvites, IReadOnlyCollection<AutoDetectRequestService.PendingRequestInfo> 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) 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); ImGuiHelpers.ScaledDummy(6);
} }
@@ -134,26 +275,6 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
_entries = msg.Entries; _entries = msg.Entries;
} }
private List<Services.Mediator.NearbyEntry> BuildLocalSnapshot(int maxDist)
{
var list = new List<Services.Mediator.NearbyEntry>();
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) private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
{ {
try try
@@ -194,4 +315,37 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
} }
return sb.ToString(); 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";
}
} }

View File

@@ -169,6 +169,16 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase
{ {
return new List<ChangelogEntry> return new List<ChangelogEntry>
{ {
new(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
{
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<ChangelogLine> new(new Version(0, 1, 9, 3), "0.1.9.3", new List<ChangelogLine>
{ {
new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."), new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."),

View File

@@ -62,7 +62,6 @@ public class CompactUi : WindowMediatorSubscriberBase
private bool _showSyncShells; private bool _showSyncShells;
private bool _wasOpen; private bool _wasOpen;
private bool _nearbyOpen = true; private bool _nearbyOpen = true;
private bool _pendingOpen = true;
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new(); private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, public CompactUi(ILogger<CompactUi> 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 animsDisabled = _configService.Current.DefaultDisableAnimations;
bool vfxDisabled = _configService.Current.DefaultDisableVfx; bool vfxDisabled = _configService.Current.DefaultDisableVfx;
bool showNearby = _configService.Current.EnableAutoDetectDiscovery; bool showNearby = _configService.Current.EnableAutoDetectDiscovery;
int pendingInvites = _nearbyPending.Pending.Count;
const string nearbyLabel = "AutoDetect";
var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp; var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
var animIcon = animsDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running; var animIcon = animsDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
@@ -306,7 +309,7 @@ public class CompactUi : WindowMediatorSubscriberBase
float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel); float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel);
float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel); float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel);
float vfxWidth = _uiSharedService.GetIconTextButtonSize(vfxIcon, vfxLabel); 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); int buttonCount = 3 + (showNearby ? 1 : 0);
float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1); float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1);
float available = ImGui.GetContentRegionAvail().X; float available = ImGui.GetContentRegionAvail().X;
@@ -346,11 +349,15 @@ public class CompactUi : WindowMediatorSubscriberBase
if (showNearby) if (showNearby)
{ {
ImGui.SameLine(0, spacing); 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))); 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(); ImGui.Separator();
@@ -524,48 +531,11 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false); 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; var pendingCount = _nearbyPending?.Pending.Count ?? 0;
if (pendingCount > 0) if (pendingCount > 0)
{ {
using (ImRaii.PushId("group-Pending")) UiSharedService.ColorTextWrapped("Invitation AutoDetect en attente. Ouvrez l\'interface AutoDetect pour gérer vos demandes.", ImGuiColors.DalamudYellow);
{ ImGuiHelpers.ScaledDummy(4);
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();
}
}
} }
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(); 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();

View File

@@ -14,6 +14,7 @@ using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -44,6 +45,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly ChatService _chatService; private readonly ChatService _chatService;
private readonly GuiHookService _guiHookService; private readonly GuiHookService _guiHookService;
private readonly AutoDetectSuppressionService _autoDetectSuppressionService;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService; private readonly PlayerPerformanceService _playerPerformanceService;
@@ -77,7 +79,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
FileCompactor fileCompactor, ApiController apiController, FileCompactor fileCompactor, ApiController apiController,
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, 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; _configService = configService;
_pairManager = pairManager; _pairManager = pairManager;
@@ -96,6 +99,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_cacheMonitor = cacheMonitor; _cacheMonitor = cacheMonitor;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_registerService = registerService; _registerService = registerService;
_autoDetectSuppressionService = autoDetectSuppressionService;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_uiShared = uiShared; _uiShared = uiShared;
AllowClickthrough = false; AllowClickthrough = false;
@@ -214,26 +218,34 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Separator(); ImGui.Separator();
_uiShared.BigText("AutoDetect"); _uiShared.BigText("AutoDetect");
bool isAutoDetectSuppressed = _autoDetectSuppressionService?.IsSuppressed ?? false;
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery; bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
if (ImGui.Checkbox("Enable Nearby detection (beta)", ref enableDiscovery)) using (ImRaii.Disabled(isAutoDetectSuppressed))
{ {
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery; if (ImGui.Checkbox("Enable AutoDetect", ref 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)
{ {
_configService.Current.AllowAutoDetectPairRequests = false; _configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
_configService.Save(); _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 // Allow Pair Requests is disabled when Nearby is OFF
using (ImRaii.Disabled(!enableDiscovery)) using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery))
{ {
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests; bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
if (ImGui.Checkbox("Allow pair requests", ref allowRequests)) if (ImGui.Checkbox("Allow pair requests", ref allowRequests))
@@ -246,15 +258,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
// user-facing info toast // user-facing info toast
Mediator.Publish(new NotificationMessage( Mediator.Publish(new NotificationMessage(
"Nearby Detection", "AutoDetect",
allowRequests ? "Pair requests enabled: others can invite you." : "Pair requests disabled: others cannot invite you.", allowRequests ? "Invitations entrantes autorisées : les autres peuvent vous inviter." : "Invitations entrantes désactivées : les autres ne peuvent pas vous inviter.",
NotificationType.Info, NotificationType.Info,
default)); 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 // 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(); ImGui.Indent();
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters; int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
@@ -266,6 +282,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
ImGui.Unindent(); ImGui.Unindent();
} }
else if (isAutoDetectSuppressed)
{
UiSharedService.ColorTextWrapped("AutoDetect est verrouillé tant que vous restez dans une zone instanciée.", ImGuiColors.DalamudYellow);
}
ImGui.Separator(); ImGui.Separator();
_uiShared.BigText("Transfer UI"); _uiShared.BigText("Transfer UI");