Fix bubble party + Download queue + Allow pause user in syncshell + add visual feature + clean log info

This commit is contained in:
2025-10-19 01:29:57 +02:00
parent 1f6e86ec2d
commit 89fa1a999f
20 changed files with 590 additions and 72 deletions

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<AssemblyName>UmbraSync</AssemblyName>
<RootNamespace>UmbraSync</RootNamespace>
<Version>0.1.9.4</Version>
<Version>0.1.9.5</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -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<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
}
}
}

View File

@@ -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<string, PendingRequestInfo> _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<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService)
public AutoDetectRequestService(ILogger<AutoDetectRequestService> 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<bool> SendRequestAsync(string token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
public async Task<bool> 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<bool> 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;

View File

@@ -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<NotificationMessage>(this, OnNotification);
_mediator.Subscribe<ManualPairInviteMessage>(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 _);

View File

@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
private void ManageWispsNearby(List<PoseEntryExtended> 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))
{

View File

@@ -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<ChatService> _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;

View File

@@ -115,6 +115,7 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
public record DiscoveryListUpdated(List<NearbyEntry> 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;

View File

@@ -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);
}
}

View File

@@ -169,6 +169,14 @@ public sealed class ChangelogUi : WindowMediatorSubscriberBase
{
return new List<ChangelogEntry>
{
new(new Version(0, 1, 9, 5), "0.1.9.5", new List<ChangelogLine>
{
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<ChangelogLine>
{
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."),

View File

@@ -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))

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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<string, bool> _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);

View File

@@ -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();

View File

@@ -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;

View File

@@ -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<bool> SendRequestAsync(string endpoint, string token, string? displayName, CancellationToken ct)
public async Task<bool> 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<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
{
try

View File

@@ -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<string, FileDownloadStatus> _downloadStatus;
private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager;
private readonly MareConfigService _mareConfigService;
private readonly FileTransferOrchestrator _orchestrator;
private readonly List<ThrottledStream> _activeDownloadStreams;
private readonly object _queueLock = new();
private SemaphoreSlim? _downloadQueueSemaphore;
private int _downloadQueueCapacity = -1;
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator,
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService) : base(logger, mediator)
{
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator;
_fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor;
_mareConfigService = mareConfigService;
_activeDownloadStreams = [];
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
@@ -59,6 +65,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> 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<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> 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()));