diff --git a/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs b/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs
index e773b37..b92ac89 100644
--- a/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs
+++ b/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs
@@ -12,8 +12,9 @@ public class CharaDataConfig : IMareConfiguration
public bool NearbyOwnServerOnly { get; set; } = false;
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
public bool NearbyDrawWisps { get; set; } = true;
+ public int NearbyMaxWisps { get; set; } = 20;
public int NearbyDistanceFilter { get; set; } = 100;
public bool NearbyShowOwnData { get; set; } = false;
public bool ShowHelpTexts { get; set; } = true;
public bool NearbyShowAlways { get; set; } = false;
-}
\ No newline at end of file
+}
diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs
index 6fe3d6a..f8218cf 100644
--- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs
+++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs
@@ -38,6 +38,7 @@ public class MareConfig : IMareConfiguration
public bool OpenGposeImportOnGposeStart { get; set; } = false;
public bool OpenPopupOnAdd { get; set; } = true;
public int ParallelDownloads { get; set; } = 10;
+ public bool EnableDownloadQueue { get; set; } = false;
public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj
index bfaf3f1..d71724f 100644
--- a/MareSynchronos/MareSynchronos.csproj
+++ b/MareSynchronos/MareSynchronos.csproj
@@ -3,7 +3,7 @@
UmbraSync
UmbraSync
- 0.1.9.4
+ 0.1.9.5
diff --git a/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs
index f208ee9..003ab5d 100644
--- a/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs
+++ b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs
@@ -1,4 +1,5 @@
using MareSynchronos.FileCache;
+using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging;
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
private readonly FileCacheManager _fileCacheManager;
private readonly FileCompactor _fileCompactor;
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
+ private readonly MareConfigService _mareConfigService;
private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator;
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
- FileCacheManager fileCacheManager, FileCompactor fileCompactor)
+ FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
{
_loggerFactory = loggerFactory;
_mareMediator = mareMediator;
_fileTransferOrchestrator = fileTransferOrchestrator;
_fileCacheManager = fileCacheManager;
_fileCompactor = fileCompactor;
+ _mareConfigService = mareConfigService;
}
public FileDownloadManager Create()
{
- return new FileDownloadManager(_loggerFactory.CreateLogger(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
+ return new FileDownloadManager(_loggerFactory.CreateLogger(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
}
-}
\ No newline at end of file
+}
diff --git a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
index a621362..b83544c 100644
--- a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
+++ b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
@@ -9,6 +9,9 @@ using MareSynchronos.MareConfiguration;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
+using MareSynchronos.WebAPI;
+using MareSynchronos.API.Dto.User;
+using MareSynchronos.API.Data;
namespace MareSynchronos.Services.AutoDetect;
@@ -26,8 +29,9 @@ public class AutoDetectRequestService
private readonly ConcurrentDictionary _pendingRequests = new(StringComparer.Ordinal);
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
+ private readonly ApiController _apiController;
- public AutoDetectRequestService(ILogger logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService)
+ public AutoDetectRequestService(ILogger logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController)
{
_logger = logger;
_configProvider = configProvider;
@@ -35,9 +39,10 @@ public class AutoDetectRequestService
_configService = configService;
_mediator = mediator;
_dalamud = dalamudUtilService;
+ _apiController = apiController;
}
- public async Task SendRequestAsync(string token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
+ public async Task SendRequestAsync(string? token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
{
if (!_configService.Current.AllowAutoDetectPairRequests)
{
@@ -45,6 +50,13 @@ public class AutoDetectRequestService
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
return false;
}
+
+ if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(uid))
+ {
+ _logger.LogDebug("Nearby request blocked: no token or UID provided");
+ return false;
+ }
+
var targetKey = BuildTargetKey(uid, token, targetDisplayName);
if (!string.IsNullOrEmpty(targetKey))
{
@@ -101,8 +113,11 @@ public class AutoDetectRequestService
}
catch { }
+ var requestToken = string.IsNullOrEmpty(token) ? null : token;
+ var requestUid = requestToken == null ? uid : null;
+
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
- var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false);
+ var ok = await _client.SendRequestAsync(endpoint!, requestToken, requestUid, displayName, ct).ConfigureAwait(false);
if (ok)
{
if (!string.IsNullOrEmpty(targetKey))
@@ -180,6 +195,126 @@ public class AutoDetectRequestService
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
}
+ public async Task SendDirectUidRequestAsync(string uid, string? targetDisplayName = null, CancellationToken ct = default)
+ {
+ if (!_configService.Current.AllowAutoDetectPairRequests)
+ {
+ _logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
+ _mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(uid))
+ {
+ _logger.LogDebug("Direct pair request aborted: UID is empty");
+ return false;
+ }
+
+ var targetKey = BuildTargetKey(uid, null, targetDisplayName);
+ if (!string.IsNullOrEmpty(targetKey))
+ {
+ var now = DateTime.UtcNow;
+ lock (_syncRoot)
+ {
+ if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
+ {
+ if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
+ {
+ PublishLockNotification(tracker.LockUntil.Value - now);
+ return false;
+ }
+
+ if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
+ {
+ tracker.LockUntil = null;
+ tracker.Count = 0;
+ if (tracker.Count == 0 && tracker.LockUntil == null)
+ {
+ _refusalTrackers.Remove(targetKey);
+ }
+ }
+ }
+
+ if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
+ {
+ var elapsed = now - lastSent;
+ if (elapsed < RequestCooldown)
+ {
+ PublishCooldownNotification(RequestCooldown - elapsed);
+ return false;
+ }
+
+ if (elapsed >= RequestCooldown)
+ {
+ _activeCooldowns.Remove(targetKey);
+ }
+ }
+ }
+ }
+
+ try
+ {
+ await _apiController.UserAddPair(new UserDto(new UserData(uid))).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(targetKey))
+ {
+ lock (_syncRoot)
+ {
+ _activeCooldowns[targetKey] = DateTime.UtcNow;
+ if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
+ {
+ tracker.Count = 0;
+ tracker.LockUntil = null;
+ if (tracker.Count == 0 && tracker.LockUntil == null)
+ {
+ _refusalTrackers.Remove(targetKey);
+ }
+ }
+ }
+ }
+
+ _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! : uid;
+ _pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, null, label, DateTime.UtcNow);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Direct pair request failed for {uid}", uid);
+ if (!string.IsNullOrEmpty(targetKey))
+ {
+ var now = DateTime.UtcNow;
+ lock (_syncRoot)
+ {
+ _activeCooldowns.Remove(targetKey);
+ if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
+ {
+ tracker = new RefusalTracker();
+ _refusalTrackers[targetKey] = tracker;
+ }
+
+ if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
+ {
+ tracker.LockUntil = null;
+ tracker.Count = 0;
+ }
+
+ tracker.Count++;
+ if (tracker.Count >= 3)
+ {
+ 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));
+ return false;
+ }
+ }
+
private static string? BuildTargetKey(string? uid, string? token, string? displayName)
{
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;
diff --git a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
index 8ff5e8c..28a079f 100644
--- a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
+++ b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
@@ -1,5 +1,7 @@
+using System;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
+using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
@@ -23,6 +25,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_api = api;
_requestService = requestService;
_mediator.Subscribe(this, OnNotification);
+ _mediator.Subscribe(this, OnManualPairInvite);
}
public MareMediator Mediator => _mediator;
@@ -65,6 +68,20 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
}
+ private void OnManualPairInvite(ManualPairInviteMessage msg)
+ {
+ if (!string.Equals(msg.TargetUid, _api.UID, StringComparison.Ordinal))
+ return;
+
+ var display = !string.IsNullOrWhiteSpace(msg.DisplayName)
+ ? msg.DisplayName!
+ : (!string.IsNullOrWhiteSpace(msg.SourceAlias) ? msg.SourceAlias : msg.SourceUid);
+
+ _pending[msg.SourceUid] = display;
+ _logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
+ _mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
+ }
+
public void Remove(string uid)
{
_pending.TryRemove(uid, out _);
diff --git a/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
index 21eb91c..a7ed9fb 100644
--- a/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
+++ b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
private void ManageWispsNearby(List previousPoses)
{
- foreach (var data in _nearbyData.Keys)
+ int maxWisps = _charaDataConfigService.Current.NearbyMaxWisps;
+ if (maxWisps <= 0)
{
- if (_poseVfx.TryGetValue(data, out var _)) continue;
+ ClearAllVfx();
+ return;
+ }
+
+ const int hardLimit = 200;
+ if (maxWisps > hardLimit) maxWisps = hardLimit;
+
+ var orderedAllowedPoses = _nearbyData
+ .OrderBy(kvp => kvp.Value.Distance)
+ .Take(maxWisps)
+ .Select(kvp => kvp.Key)
+ .ToList();
+
+ var allowedPoseSet = orderedAllowedPoses.ToHashSet();
+
+ foreach (var data in orderedAllowedPoses)
+ {
+ if (_poseVfx.TryGetValue(data, out _)) continue;
+
+ Guid? vfxGuid = data.MetaInfo.IsOwnData
+ ? _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f)
+ : _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
- Guid? vfxGuid;
- if (data.MetaInfo.IsOwnData)
- {
- vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
- }
- else
- {
- vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
- }
if (vfxGuid != null)
{
_poseVfx[data] = vfxGuid.Value;
}
}
- foreach (var data in previousPoses.Except(_nearbyData.Keys))
+ foreach (var data in previousPoses.Except(allowedPoseSet))
+ {
+ if (_poseVfx.Remove(data, out var guid))
+ {
+ _vfxSpawnManager.DespawnObject(guid);
+ }
+ }
+
+ foreach (var data in _poseVfx.Keys.Except(allowedPoseSet).ToList())
{
if (_poseVfx.Remove(data, out var guid))
{
diff --git a/MareSynchronos/Services/ChatService.cs b/MareSynchronos/Services/ChatService.cs
index b52d61f..f893903 100644
--- a/MareSynchronos/Services/ChatService.cs
+++ b/MareSynchronos/Services/ChatService.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Text;
using System.Threading;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
@@ -19,6 +21,7 @@ namespace MareSynchronos.Services;
public class ChatService : DisposableMediatorSubscriberBase
{
public const int DefaultColor = 710;
+ private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
public const int CommandMaxNumber = 50;
private readonly ILogger _logger;
@@ -191,6 +194,10 @@ public class ChatService : DisposableMediatorSubscriberBase
var extraChatTags = _mareConfig.Current.ExtraChatTags;
var logKind = ResolveShellLogKind(shellConfig.LogKind);
+ var payload = SeString.Parse(message.ChatMsg.PayloadContent);
+ if (TryHandleManualPairInvite(message, payload))
+ return;
+
var msg = new SeStringBuilder();
if (extraChatTags)
{
@@ -209,7 +216,7 @@ public class ChatService : DisposableMediatorSubscriberBase
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
}
msg.AddText("> ");
- msg.Append(SeString.Parse(message.ChatMsg.PayloadContent));
+ msg.Append(payload);
if (color != 0)
msg.AddUiForegroundOff();
@@ -219,6 +226,52 @@ public class ChatService : DisposableMediatorSubscriberBase
Type = logKind
});
}
+
+ private bool TryHandleManualPairInvite(GroupChatMsgMessage message, SeString payload)
+ {
+ var textValue = payload.TextValue;
+ if (string.IsNullOrEmpty(textValue) || !textValue.StartsWith(ManualPairInvitePrefix, StringComparison.Ordinal))
+ return false;
+
+ var content = textValue[ManualPairInvitePrefix.Length..];
+ if (content.EndsWith("]", StringComparison.Ordinal))
+ {
+ content = content[..^1];
+ }
+
+ var parts = content.Split('|');
+ if (parts.Length < 4)
+ return true;
+
+ var sourceUid = parts[0];
+ var sourceAlias = DecodeInviteField(parts[1]);
+ var targetUid = parts[2];
+ var displayName = DecodeInviteField(parts[3]);
+ var inviteId = parts.Length > 4 ? parts[4] : Guid.NewGuid().ToString("N");
+
+ if (!string.Equals(targetUid, _apiController.UID, StringComparison.Ordinal))
+ return true;
+
+ Mediator.Publish(new ManualPairInviteMessage(sourceUid, sourceAlias, targetUid, string.IsNullOrEmpty(displayName) ? null : displayName, inviteId));
+ _logger.LogDebug("Received manual pair invite from {source} via syncshell", sourceUid);
+
+ return true;
+ }
+
+ private static string DecodeInviteField(string encoded)
+ {
+ if (string.IsNullOrEmpty(encoded)) return string.Empty;
+
+ try
+ {
+ var bytes = Convert.FromBase64String(encoded);
+ return Encoding.UTF8.GetString(bytes);
+ }
+ catch
+ {
+ return encoded;
+ }
+ }
public void PrintChannelExample(string message, string gid = "")
{
int chatType = _mareConfig.Current.ChatLogKind;
diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs
index cb68774..09e01fb 100644
--- a/MareSynchronos/Services/Mediator/Messages.cs
+++ b/MareSynchronos/Services/Mediator/Messages.cs
@@ -115,6 +115,7 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
public record DiscoveryListUpdated(List Entries) : MessageBase;
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
+public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
diff --git a/MareSynchronos/Services/TypingIndicatorStateService.cs b/MareSynchronos/Services/TypingIndicatorStateService.cs
index 87aae1a..9e4ce2c 100644
--- a/MareSynchronos/Services/TypingIndicatorStateService.cs
+++ b/MareSynchronos/Services/TypingIndicatorStateService.cs
@@ -76,12 +76,10 @@ public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposab
_typingUsers.AddOrUpdate(uid,
_ => new TypingEntry(msg.Typing.User, now, now),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
- _logger.LogInformation("Typing state {uid} -> true", uid);
}
else
{
_typingUsers.TryRemove(uid, out _);
- _logger.LogInformation("Typing state {uid} -> false", uid);
}
}
diff --git a/MareSynchronos/UI/ChangelogUi.cs b/MareSynchronos/UI/ChangelogUi.cs
index c123fad..d111f94 100644
--- a/MareSynchronos/UI/ChangelogUi.cs
+++ b/MareSynchronos/UI/ChangelogUi.cs
@@ -169,6 +169,14 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase
{
return new List
{
+ new(new Version(0, 1, 9, 5), "0.1.9.5", new List
+ {
+ new("Fix l'affichage de la bulle dans la liste du groupe."),
+ new("Amélioration de l'ajout des utilisateurs via le bouton +."),
+ new("Possibilité de mettre en pause individuellement des utilisateurs d'une syncshell."),
+ new("Amélioration de la stabilité du plugin en cas de petite connexion / petite configuration."),
+ new("Divers fix de l'interface."),
+ }),
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."),
diff --git a/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs b/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs
index 8486375..cb183e5 100644
--- a/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs
+++ b/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs
@@ -57,6 +57,14 @@ internal partial class CharaDataHubUi
_configService.Save();
}
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
+ int maxWisps = _configService.Current.NearbyMaxWisps;
+ ImGui.SetNextItemWidth(140);
+ if (ImGui.SliderInt("Maximum wisps", ref maxWisps, 0, 200))
+ {
+ _configService.Current.NearbyMaxWisps = maxWisps;
+ _configService.Save();
+ }
+ _uiSharedService.DrawHelpText("Limit how many wisps are active at once. Set to 0 to disable wisps even when enabled above.");
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
ImGui.SetNextItemWidth(100);
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))
diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs
index df97403..c2e59e2 100644
--- a/MareSynchronos/UI/CompactUI.cs
+++ b/MareSynchronos/UI/CompactUI.cs
@@ -88,7 +88,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_characterAnalyzer = characterAnalyzer;
var tagHandler = new TagHandler(_serverManager);
- _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager);
+ _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager, _autoDetectRequestService);
_selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService);
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler);
_pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService);
@@ -433,6 +433,8 @@ public class CompactUi : WindowMediatorSubscriberBase
var compressedValue = UiSharedService.ByteToString(summary.TotalCompressedSize);
Vector4? compressedColor = null;
+ FontAwesomeIcon? compressedIcon = null;
+ Vector4? compressedIconColor = null;
string? compressedTooltip = null;
if (summary.HasUncomputedEntries)
{
@@ -443,19 +445,25 @@ public class CompactUi : WindowMediatorSubscriberBase
{
compressedColor = ImGuiColors.DalamudYellow;
compressedTooltip = "Au-delà de 300 MiB, certains joueurs peuvent ne pas voir toutes vos modifications.";
+ compressedIcon = FontAwesomeIcon.ExclamationTriangle;
+ compressedIconColor = ImGuiColors.DalamudYellow;
}
- DrawSelfAnalysisStatRow("Taille compressée", compressedValue, compressedColor, compressedTooltip);
+ DrawSelfAnalysisStatRow("Taille compressée", compressedValue, compressedColor, compressedTooltip, compressedIcon, compressedIconColor);
DrawSelfAnalysisStatRow("Taille extraite", UiSharedService.ByteToString(summary.TotalOriginalSize));
Vector4? trianglesColor = null;
+ FontAwesomeIcon? trianglesIcon = null;
+ Vector4? trianglesIconColor = null;
string? trianglesTooltip = null;
if (summary.TotalTriangles >= SelfAnalysisTriangleWarningThreshold)
{
trianglesColor = ImGuiColors.DalamudYellow;
trianglesTooltip = "Plus de 150k triangles peuvent entraîner un auto-pause et impacter les performances.";
+ trianglesIcon = FontAwesomeIcon.ExclamationTriangle;
+ trianglesIconColor = ImGuiColors.DalamudYellow;
}
- DrawSelfAnalysisStatRow("Triangles moddés", UiSharedService.TrisToString(summary.TotalTriangles), trianglesColor, trianglesTooltip);
+ DrawSelfAnalysisStatRow("Triangles moddés", UiSharedService.TrisToString(summary.TotalTriangles), trianglesColor, trianglesTooltip, trianglesIcon, trianglesIconColor);
ImGui.EndTable();
}
@@ -490,12 +498,29 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
- private static void DrawSelfAnalysisStatRow(string label, string value, Vector4? valueColor = null, string? tooltip = null)
+ private static void DrawSelfAnalysisStatRow(string label, string value, Vector4? valueColor = null, string? tooltip = null, FontAwesomeIcon? icon = null, Vector4? iconColor = null)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(label);
ImGui.TableNextColumn();
+ if (icon.HasValue)
+ {
+ using (ImRaii.PushFont(UiBuilder.IconFont))
+ {
+ if (iconColor.HasValue)
+ {
+ using var iconColorPush = ImRaii.PushColor(ImGuiCol.Text, iconColor.Value);
+ ImGui.TextUnformatted(icon.Value.ToIconString());
+ }
+ else
+ {
+ ImGui.TextUnformatted(icon.Value.ToIconString());
+ }
+ }
+ ImGui.SameLine(0f, 4f);
+ }
+
if (valueColor.HasValue)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, valueColor.Value);
@@ -687,9 +712,9 @@ public class CompactUi : WindowMediatorSubscriberBase
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();
- var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
- var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + 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, _serverManager)).ToList();
+ var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
+ var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
_pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers);
diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs
index 488aafb..3e42d59 100644
--- a/MareSynchronos/UI/Components/DrawGroupPair.cs
+++ b/MareSynchronos/UI/Components/DrawGroupPair.cs
@@ -1,15 +1,21 @@
-using System.Numerics;
+using System;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
+using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group;
-using MareSynchronos.API.Dto.User;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services;
+using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator;
+using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI;
@@ -21,16 +27,22 @@ public class DrawGroupPair : DrawPairBase
private readonly GroupPairFullInfoDto _fullInfoDto;
private readonly GroupFullInfoDto _group;
private readonly CharaDataManager _charaDataManager;
+ private readonly AutoDetectRequestService _autoDetectRequestService;
+ private readonly ServerConfigurationManager _serverConfigurationManager;
+ private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
public DrawGroupPair(string id, Pair entry, ApiController apiController,
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto,
- UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager)
+ UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager,
+ AutoDetectRequestService autoDetectRequestService, ServerConfigurationManager serverConfigurationManager)
: base(id, entry, apiController, handler, uiSharedService)
{
_group = group;
_fullInfoDto = fullInfoDto;
_mediator = mareMediator;
_charaDataManager = charaDataManager;
+ _autoDetectRequestService = autoDetectRequestService;
+ _serverConfigurationManager = serverConfigurationManager;
}
protected override void DrawLeftSide(float textPosY, float originalY)
@@ -153,8 +165,9 @@ public class DrawGroupPair : DrawPairBase
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled);
- bool showPlus = _pair.UserPair == null;
+ bool showPlus = _pair.UserPair == null && _pair.IsOnline;
bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
+ bool showPause = true;
var spacing = ImGui.GetStyle().ItemSpacing.X;
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
@@ -162,12 +175,15 @@ public class DrawGroupPair : DrawPairBase
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
var infoIconWidth = UiSharedService.GetIconSize(permIcon).X;
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
+ var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
+ var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing
- (showShared ? (runningIconWidth + spacing) : 0)
- (showInfo ? (infoIconWidth + spacing) : 0)
- (showPlus ? (plusButtonWidth + spacing) : 0)
+ - (showPause ? (pauseButtonWidth + spacing) : 0)
- (showBars ? (barButtonWidth + spacing) : 0);
ImGui.SameLine(pos);
@@ -280,9 +296,28 @@ public class DrawGroupPair : DrawPairBase
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{
- _ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID)));
+ var targetUid = _pair.UserData.UID;
+ if (!string.IsNullOrEmpty(targetUid))
+ {
+ _ = SendGroupPairInviteAsync(targetUid, entryUID);
+ }
}
- UiSharedService.AttachToolTip("Pair with " + entryUID + " individually");
+ UiSharedService.AttachToolTip(AppendSeenInfo("Send pairing invite to " + entryUID));
+ ImGui.SameLine();
+ }
+
+ if (showPause)
+ {
+ ImGui.SetCursorPosY(originalY);
+
+ if (_uiSharedService.IconButton(pauseIcon))
+ {
+ var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
+ _fullInfoDto.GroupUserPermissions = newPermissions;
+ _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_group.Group, _pair.UserData, newPermissions));
+ }
+
+ UiSharedService.AttachToolTip(AppendSeenInfo((_fullInfoDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " syncing with " + entryUID));
ImGui.SameLine();
}
@@ -383,4 +418,74 @@ public class DrawGroupPair : DrawPairBase
return pos - spacing;
}
+
+ private string AppendSeenInfo(string tooltip)
+ {
+ if (_pair.IsVisible) return tooltip;
+
+ var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
+ if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
+
+ return tooltip + " (Vu sous : " + lastSeen + ")";
+ }
+
+ private async Task SendGroupPairInviteAsync(string targetUid, string displayName)
+ {
+ try
+ {
+ var ok = await _autoDetectRequestService.SendDirectUidRequestAsync(targetUid, displayName).ConfigureAwait(false);
+ if (!ok) return;
+
+ await SendManualInviteSignalAsync(targetUid, displayName).ConfigureAwait(false);
+ }
+ catch
+ {
+ // errors are logged within the request service; ignore here
+ }
+ }
+
+ private async Task SendManualInviteSignalAsync(string targetUid, string displayName)
+ {
+ if (string.IsNullOrEmpty(_apiController.UID)) return;
+
+ var senderAliasRaw = string.IsNullOrEmpty(_apiController.DisplayName) ? _apiController.UID : _apiController.DisplayName;
+ var senderAlias = EncodeInviteField(senderAliasRaw);
+ var targetDisplay = EncodeInviteField(displayName);
+ var inviteId = Guid.NewGuid().ToString("N");
+ var payloadText = new StringBuilder()
+ .Append(ManualPairInvitePrefix)
+ .Append(_apiController.UID)
+ .Append('|')
+ .Append(senderAlias)
+ .Append('|')
+ .Append(targetUid)
+ .Append('|')
+ .Append(targetDisplay)
+ .Append('|')
+ .Append(inviteId)
+ .Append(']')
+ .ToString();
+
+ var payload = new SeStringBuilder().AddText(payloadText).Build().Encode();
+ var chatMessage = new ChatMessage
+ {
+ SenderName = senderAlias,
+ PayloadContent = payload
+ };
+
+ try
+ {
+ await _apiController.GroupChatSendMsg(new GroupDto(_group.Group), chatMessage).ConfigureAwait(false);
+ }
+ catch
+ {
+ // ignore - invite remains tracked locally even if group chat signal fails
+ }
+ }
+
+ private static string EncodeInviteField(string value)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
+ return Convert.ToBase64String(bytes);
+ }
}
diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs
index f6e4b3c..69fbbb8 100644
--- a/MareSynchronos/UI/Components/DrawUserPair.cs
+++ b/MareSynchronos/UI/Components/DrawUserPair.cs
@@ -11,6 +11,7 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI;
using System.Numerics;
+using MareSynchronos.Services.ServerConfiguration;
namespace MareSynchronos.UI.Components;
@@ -20,10 +21,12 @@ public class DrawUserPair : DrawPairBase
protected readonly MareMediator _mediator;
private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly CharaDataManager _charaDataManager;
+ private readonly ServerConfigurationManager _serverConfigurationManager;
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
- UiSharedService uiSharedService, CharaDataManager charaDataManager)
+ UiSharedService uiSharedService, CharaDataManager charaDataManager,
+ ServerConfigurationManager serverConfigurationManager)
: base(id, entry, apiController, displayHandler, uiSharedService)
{
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
@@ -31,6 +34,7 @@ public class DrawUserPair : DrawPairBase
_selectGroupForPairUi = selectGroupForPairUi;
_mediator = mareMediator;
_charaDataManager = charaDataManager;
+ _serverConfigurationManager = serverConfigurationManager;
}
public bool IsOnline => _pair.IsOnline;
@@ -135,9 +139,9 @@ public class DrawUserPair : DrawPairBase
perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
}
- UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused()
+ UiSharedService.AttachToolTip(AppendSeenInfo(!_pair.UserPair!.OwnPermissions.IsPaused()
? "Pause pairing with " + entryUID
- : "Resume pairing with " + entryUID);
+ : "Resume pairing with " + entryUID));
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
@@ -263,7 +267,7 @@ public class DrawUserPair : DrawPairBase
{
_selectGroupForPairUi.Open(entry);
}
- UiSharedService.AttachToolTip("Choose pair groups for " + entryUID);
+ UiSharedService.AttachToolTip(AppendSeenInfo("Choose pair groups for " + entryUID));
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
@@ -302,6 +306,16 @@ public class DrawUserPair : DrawPairBase
{
_ = _apiController.UserRemovePair(new(entry.UserData));
}
- UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID);
+ UiSharedService.AttachToolTip(AppendSeenInfo("Hold CTRL and click to unpair permanently from " + entryUID));
+ }
+
+ private string AppendSeenInfo(string tooltip)
+ {
+ if (_pair.IsVisible) return tooltip;
+
+ var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
+ if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
+
+ return tooltip + " (Vu sous : " + lastSeen + ")";
}
}
diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs
index 07abb5b..5a71c80 100644
--- a/MareSynchronos/UI/Components/GroupPanel.cs
+++ b/MareSynchronos/UI/Components/GroupPanel.cs
@@ -12,6 +12,7 @@ using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services;
+using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components;
@@ -32,6 +33,7 @@ internal sealed class GroupPanel
private readonly MareConfigService _mareConfig;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CharaDataManager _charaDataManager;
+ private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly Dictionary _showGidForEntry = new(StringComparer.Ordinal);
private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiShared;
@@ -74,7 +76,7 @@ internal sealed class GroupPanel
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager,
- CharaDataManager charaDataManager)
+ CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService)
{
_mainUi = mainUi;
_uiShared = uiShared;
@@ -84,6 +86,7 @@ internal sealed class GroupPanel
_mareConfig = mareConfig;
_serverConfigurationManager = serverConfigurationManager;
_charaDataManager = charaDataManager;
+ _autoDetectRequestService = autoDetectRequestService;
}
private ApiController ApiController => _uiShared.ApiController;
@@ -566,7 +569,9 @@ internal sealed class GroupPanel
).Value,
_uidDisplayHandler,
_uiShared,
- _charaDataManager);
+ _charaDataManager,
+ _autoDetectRequestService,
+ _serverConfigurationManager);
if (pair.IsVisible)
visibleUsers.Add(drawPair);
diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs
index 898b3b8..c0015cf 100644
--- a/MareSynchronos/UI/SettingsUi.cs
+++ b/MareSynchronos/UI/SettingsUi.cs
@@ -209,12 +209,24 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("0 = No limit/infinite");
+ bool enableDownloadQueue = _configService.Current.EnableDownloadQueue;
+ if (ImGui.Checkbox("Activer la file de téléchargements", ref enableDownloadQueue))
+ {
+ _configService.Current.EnableDownloadQueue = enableDownloadQueue;
+ _configService.Save();
+ }
+ UiSharedService.AttachToolTip("Lance les téléchargements de personnages de manière séquentielle plutôt que tous en même temps. "
+ + "Quand l'option est activée, seul le nombre configuré de téléchargements fonctionne en parallèle.");
+
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
{
_configService.Current.ParallelDownloads = maxParallelDownloads;
_configService.Save();
}
+ UiSharedService.AttachToolTip(enableDownloadQueue
+ ? "Nombre maximal de téléchargements de personnages autorisés simultanément lorsque la file est activée."
+ : "Nombre maximal de flux de fichiers lancés en parallèle pour chaque téléchargement.");
ImGui.Separator();
_uiShared.BigText("AutoDetect");
@@ -222,7 +234,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
using (ImRaii.Disabled(isAutoDetectSuppressed))
{
- if (ImGui.Checkbox("Enable AutoDetect", ref enableDiscovery))
+ if (ImGui.Checkbox("Activer l'AutoDetect", ref enableDiscovery))
{
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
_configService.Save();
@@ -248,7 +260,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery))
{
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
- if (ImGui.Checkbox("Allow pair requests", ref allowRequests))
+ if (ImGui.Checkbox("Activer les invitations entrantes", ref allowRequests))
{
_configService.Current.AllowAutoDetectPairRequests = allowRequests;
_configService.Save();
@@ -275,7 +287,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Indent();
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
- if (ImGui.SliderInt("Max distance (meters)", ref maxMeters, 5, 100))
+ if (ImGui.SliderInt("Distance max (en mètre)", ref maxMeters, 5, 100))
{
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
_configService.Save();
diff --git a/MareSynchronos/UI/TypingIndicatorOverlay.cs b/MareSynchronos/UI/TypingIndicatorOverlay.cs
index fef7edf..d1c5fe8 100644
--- a/MareSynchronos/UI/TypingIndicatorOverlay.cs
+++ b/MareSynchronos/UI/TypingIndicatorOverlay.cs
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Numerics;
+using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services;
using MareSynchronos.API.Data;
+using MareSynchronos.API.Data.Extensions;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
@@ -115,25 +117,34 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
continue;
var pair = _pairManager.GetPairByUID(uid);
- if (pair == null)
- {
- var alias = entry.User.AliasOrUID;
- if (string.IsNullOrEmpty(alias))
- continue;
+ var targetIndex = -1;
+ var playerName = pair?.PlayerName;
+ var objectId = pair?.PlayerCharacterId ?? uint.MaxValue;
- var aliasIndex = GetPartyIndexForName(alias);
- if (aliasIndex >= 0)
+ if (objectId != 0 && objectId != uint.MaxValue)
+ {
+ targetIndex = GetPartyIndexForObjectId(objectId);
+ if (targetIndex >= 0 && !string.IsNullOrEmpty(playerName))
{
- DrawPartyMemberTyping(drawList, partyAddon, aliasIndex);
+ var member = _partyList[targetIndex];
+ var memberName = member?.Name?.TextValue;
+ if (!string.IsNullOrEmpty(memberName) && !memberName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
+ {
+ var nameIndex = GetPartyIndexForName(playerName);
+ targetIndex = nameIndex;
+ }
}
- continue;
}
- var index = GetPartyIndexForObjectId(pair.PlayerCharacterId);
- if (index < 0)
+ if (targetIndex < 0 && !string.IsNullOrEmpty(playerName))
+ {
+ targetIndex = GetPartyIndexForName(playerName);
+ }
+
+ if (targetIndex < 0)
continue;
- DrawPartyMemberTyping(drawList, partyAddon, index);
+ DrawPartyMemberTyping(drawList, partyAddon, targetIndex);
}
}
@@ -198,10 +209,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
var pair = _pairManager.GetPairByUID(uid);
var objectId = pair?.PlayerCharacterId ?? 0;
- if (pair == null)
- {
- _logger.LogInformation("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
- }
+ var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
+ var pairIdent = pair?.Ident ?? string.Empty;
+ var isPartyMember = IsPartyMember(objectId, pairName);
+ var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
{
@@ -209,20 +220,28 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
continue;
}
- var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
- var pairIdent = pair?.Ident ?? string.Empty;
+ var hasWorldPosition = TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos);
+ var isNearby = hasWorldPosition && IsWithinRelevantDistance(worldPos);
- _logger.LogInformation("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
+ if (!isRelevantMember && !isNearby)
+ continue;
+
+ if (pair == null)
+ {
+ _logger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
+ }
+
+ _logger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
uid, objectId, pairName, pairIdent);
- if (TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos))
+ if (hasWorldPosition)
{
DrawWorldFallbackIcon(drawList, iconWrap, worldPos);
- _logger.LogInformation("TypingIndicator: fallback world draw for {uid} at {pos}", uid, worldPos);
+ _logger.LogTrace("TypingIndicator: fallback world draw for {uid} at {pos}", uid, worldPos);
}
else
{
- _logger.LogInformation("TypingIndicator: could not resolve position for {uid}", uid);
+ _logger.LogTrace("TypingIndicator: could not resolve position for {uid}", uid);
}
}
}
@@ -464,6 +483,48 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
return -1;
}
+ private bool IsPartyMember(uint objectId, string? playerName)
+ {
+ if (objectId != 0 && objectId != uint.MaxValue && GetPartyIndexForObjectId(objectId) >= 0)
+ return true;
+
+ if (!string.IsNullOrEmpty(playerName) && GetPartyIndexForName(playerName) >= 0)
+ return true;
+
+ return false;
+ }
+
+ private bool IsPlayerRelevant(Pair? pair, bool isPartyMember)
+ {
+ if (isPartyMember)
+ return true;
+
+ if (pair?.UserPair != null)
+ {
+ var userPair = pair.UserPair;
+ if (userPair.OtherPermissions.IsPaired() || userPair.OwnPermissions.IsPaired())
+ return true;
+ }
+
+ if (pair?.GroupPair != null && pair.GroupPair.Any(g =>
+ !g.Value.GroupUserPermissions.IsPaused() &&
+ !g.Key.GroupUserPermissions.IsPaused()))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsWithinRelevantDistance(Vector3 position)
+ {
+ if (_clientState.LocalPlayer == null)
+ return false;
+
+ var distance = Vector3.Distance(_clientState.LocalPlayer.Position, position);
+ return distance <= 40f;
+ }
+
private static unsafe uint GetEntityId(nint address)
{
if (address == nint.Zero) return 0;
diff --git a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
index 740696f..4f99e67 100644
--- a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
+++ b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
+using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.SignalR;
using MareSynchronos.Services.AutoDetect;
@@ -65,15 +66,21 @@ public class DiscoveryApiClient
}
}
- public async Task SendRequestAsync(string endpoint, string token, string? displayName, CancellationToken ct)
+ public async Task SendRequestAsync(string endpoint, string? token, string? targetUid, string? displayName, CancellationToken ct)
{
try
{
+ if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(targetUid))
+ {
+ _logger.LogWarning("Discovery request aborted: no token or targetUid provided");
+ return false;
+ }
+
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
- var body = JsonSerializer.Serialize(new { token, displayName });
+ var body = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
@@ -82,7 +89,7 @@ public class DiscoveryApiClient
if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
- var body2 = JsonSerializer.Serialize(new { token, displayName });
+ var body2 = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
@@ -102,6 +109,14 @@ public class DiscoveryApiClient
}
}
+ private sealed record RequestPayload(
+ [property: JsonPropertyName("token"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ string? Token,
+ [property: JsonPropertyName("targetUid"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ string? TargetUid,
+ [property: JsonPropertyName("displayName"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ string? DisplayName);
+
public async Task PublishAsync(string endpoint, IEnumerable hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
{
try
diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs
index 3020064..035fae7 100644
--- a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs
+++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs
@@ -4,6 +4,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes;
using MareSynchronos.FileCache;
+using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
@@ -20,17 +21,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly Dictionary _downloadStatus;
private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager;
+ private readonly MareConfigService _mareConfigService;
private readonly FileTransferOrchestrator _orchestrator;
private readonly List _activeDownloadStreams;
+ private readonly object _queueLock = new();
+ private SemaphoreSlim? _downloadQueueSemaphore;
+ private int _downloadQueueCapacity = -1;
public FileDownloadManager(ILogger logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
- FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
+ FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService) : base(logger, mediator)
{
_downloadStatus = new Dictionary(StringComparer.Ordinal);
_orchestrator = orchestrator;
_fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor;
+ _mareConfigService = mareConfigService;
_activeDownloadStreams = [];
Mediator.Subscribe(this, (msg) =>
@@ -59,6 +65,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
public async Task DownloadFiles(GameObjectHandler gameObject, List fileReplacementDto, CancellationToken ct)
{
+ SemaphoreSlim? queueSemaphore = null;
+ if (_mareConfigService.Current.EnableDownloadQueue)
+ {
+ queueSemaphore = GetQueueSemaphore();
+ Logger.LogTrace("Queueing download for {name}. Currently queued: {queued}", gameObject.Name, queueSemaphore.CurrentCount);
+ await queueSemaphore.WaitAsync(ct).ConfigureAwait(false);
+ }
+
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
try
{
@@ -70,6 +84,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
}
finally
{
+ if (queueSemaphore != null)
+ {
+ queueSemaphore.Release();
+ }
+
Mediator.Publish(new DownloadFinishedMessage(gameObject));
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
}
@@ -132,6 +151,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return (string.Join("", hashName), long.Parse(string.Join("", fileLength)));
}
+ private SemaphoreSlim GetQueueSemaphore()
+ {
+ var desiredCapacity = Math.Clamp(_mareConfigService.Current.ParallelDownloads, 1, 10);
+
+ lock (_queueLock)
+ {
+ if (_downloadQueueSemaphore == null || _downloadQueueCapacity != desiredCapacity)
+ {
+ _downloadQueueSemaphore = new SemaphoreSlim(desiredCapacity, desiredCapacity);
+ _downloadQueueCapacity = desiredCapacity;
+ }
+
+ return _downloadQueueSemaphore;
+ }
+ }
+
private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List fileTransfer, string tempPath, IProgress progress, CancellationToken ct)
{
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));