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 NearbyOwnServerOnly { get; set; } = false;
public bool NearbyIgnoreHousingLimitations { get; set; } = false; public bool NearbyIgnoreHousingLimitations { get; set; } = false;
public bool NearbyDrawWisps { get; set; } = true; public bool NearbyDrawWisps { get; set; } = true;
public int NearbyMaxWisps { get; set; } = 20;
public int NearbyDistanceFilter { get; set; } = 100; public int NearbyDistanceFilter { get; set; } = 100;
public bool NearbyShowOwnData { get; set; } = false; public bool NearbyShowOwnData { get; set; } = false;
public bool ShowHelpTexts { get; set; } = true; public bool ShowHelpTexts { get; set; } = true;
public bool NearbyShowAlways { get; set; } = false; public bool NearbyShowAlways { get; set; } = false;
} }

View File

@@ -38,6 +38,7 @@ public class MareConfig : IMareConfiguration
public bool OpenGposeImportOnGposeStart { get; set; } = false; public bool OpenGposeImportOnGposeStart { get; set; } = false;
public bool OpenPopupOnAdd { get; set; } = true; public bool OpenPopupOnAdd { get; set; } = true;
public int ParallelDownloads { get; set; } = 10; public int ParallelDownloads { get; set; } = 10;
public bool EnableDownloadQueue { get; set; } = false;
public int DownloadSpeedLimitInBytes { get; set; } = 0; public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false; [Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;

View File

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

View File

@@ -1,4 +1,5 @@
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly MareConfigService _mareConfigService;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator; private readonly MareMediator _mareMediator;
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator, public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
FileCacheManager fileCacheManager, FileCompactor fileCompactor) FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_mareMediator = mareMediator; _mareMediator = mareMediator;
_fileTransferOrchestrator = fileTransferOrchestrator; _fileTransferOrchestrator = fileTransferOrchestrator;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_mareConfigService = mareConfigService;
} }
public FileDownloadManager Create() 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;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
using MareSynchronos.WebAPI;
using MareSynchronos.API.Dto.User;
using MareSynchronos.API.Data;
namespace MareSynchronos.Services.AutoDetect; namespace MareSynchronos.Services.AutoDetect;
@@ -26,8 +29,9 @@ public class AutoDetectRequestService
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5); private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15); private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
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; _logger = logger;
_configProvider = configProvider; _configProvider = configProvider;
@@ -35,9 +39,10 @@ public class AutoDetectRequestService
_configService = configService; _configService = configService;
_mediator = mediator; _mediator = mediator;
_dalamud = dalamudUtilService; _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) 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)); _mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
return false; 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); var targetKey = BuildTargetKey(uid, token, targetDisplayName);
if (!string.IsNullOrEmpty(targetKey)) if (!string.IsNullOrEmpty(targetKey))
{ {
@@ -101,8 +113,11 @@ public class AutoDetectRequestService
} }
catch { } catch { }
var requestToken = string.IsNullOrEmpty(token) ? null : token;
var requestUid = requestToken == null ? uid : null;
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint); _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 (ok)
{ {
if (!string.IsNullOrEmpty(targetKey)) if (!string.IsNullOrEmpty(targetKey))
@@ -180,6 +195,126 @@ public class AutoDetectRequestService
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false); 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) private static string? BuildTargetKey(string? uid, string? token, string? displayName)
{ {
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid; if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -23,6 +25,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_api = api; _api = api;
_requestService = requestService; _requestService = requestService;
_mediator.Subscribe<NotificationMessage>(this, OnNotification); _mediator.Subscribe<NotificationMessage>(this, OnNotification);
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
} }
public MareMediator Mediator => _mediator; public MareMediator Mediator => _mediator;
@@ -65,6 +68,20 @@ public sealed class NearbyPendingService : IMediatorSubscriber
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name); _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) public void Remove(string uid)
{ {
_pending.TryRemove(uid, out _); _pending.TryRemove(uid, out _);

View File

@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses) 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) if (vfxGuid != null)
{ {
_poseVfx[data] = vfxGuid.Value; _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)) if (_poseVfx.Remove(data, out var guid))
{ {

View File

@@ -1,3 +1,5 @@
using System;
using System.Text;
using System.Threading; using System.Threading;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
@@ -19,6 +21,7 @@ namespace MareSynchronos.Services;
public class ChatService : DisposableMediatorSubscriberBase public class ChatService : DisposableMediatorSubscriberBase
{ {
public const int DefaultColor = 710; public const int DefaultColor = 710;
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
public const int CommandMaxNumber = 50; public const int CommandMaxNumber = 50;
private readonly ILogger<ChatService> _logger; private readonly ILogger<ChatService> _logger;
@@ -191,6 +194,10 @@ public class ChatService : DisposableMediatorSubscriberBase
var extraChatTags = _mareConfig.Current.ExtraChatTags; var extraChatTags = _mareConfig.Current.ExtraChatTags;
var logKind = ResolveShellLogKind(shellConfig.LogKind); var logKind = ResolveShellLogKind(shellConfig.LogKind);
var payload = SeString.Parse(message.ChatMsg.PayloadContent);
if (TryHandleManualPairInvite(message, payload))
return;
var msg = new SeStringBuilder(); var msg = new SeStringBuilder();
if (extraChatTags) if (extraChatTags)
{ {
@@ -209,7 +216,7 @@ public class ChatService : DisposableMediatorSubscriberBase
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId)); msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
} }
msg.AddText("> "); msg.AddText("> ");
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); msg.Append(payload);
if (color != 0) if (color != 0)
msg.AddUiForegroundOff(); msg.AddUiForegroundOff();
@@ -219,6 +226,52 @@ public class ChatService : DisposableMediatorSubscriberBase
Type = logKind 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 = "") public void PrintChannelExample(string message, string gid = "")
{ {
int chatType = _mareConfig.Current.ChatLogKind; 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 DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
public record NearbyDetectionToggled(bool Enabled) : MessageBase; public record NearbyDetectionToggled(bool Enabled) : MessageBase;
public record AllowPairRequestsToggled(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 ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase; public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : 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, _typingUsers.AddOrUpdate(uid,
_ => new TypingEntry(msg.Typing.User, now, now), _ => new TypingEntry(msg.Typing.User, now, now),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now)); (_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
_logger.LogInformation("Typing state {uid} -> true", uid);
} }
else else
{ {
_typingUsers.TryRemove(uid, out _); _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> 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(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
{ {
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."), new("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(); _configService.Save();
} }
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world."); _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; int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
ImGui.SetNextItemWidth(100); ImGui.SetNextItemWidth(100);
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000)) if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))

View File

@@ -88,7 +88,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_characterAnalyzer = characterAnalyzer; _characterAnalyzer = characterAnalyzer;
var tagHandler = new TagHandler(_serverManager); 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); _selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService);
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler); _selectPairsForGroupUi = new(tagHandler, uidDisplayHandler);
_pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService); _pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService);
@@ -433,6 +433,8 @@ public class CompactUi : WindowMediatorSubscriberBase
var compressedValue = UiSharedService.ByteToString(summary.TotalCompressedSize); var compressedValue = UiSharedService.ByteToString(summary.TotalCompressedSize);
Vector4? compressedColor = null; Vector4? compressedColor = null;
FontAwesomeIcon? compressedIcon = null;
Vector4? compressedIconColor = null;
string? compressedTooltip = null; string? compressedTooltip = null;
if (summary.HasUncomputedEntries) if (summary.HasUncomputedEntries)
{ {
@@ -443,19 +445,25 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
compressedColor = ImGuiColors.DalamudYellow; compressedColor = ImGuiColors.DalamudYellow;
compressedTooltip = "Au-delà de 300 MiB, certains joueurs peuvent ne pas voir toutes vos modifications."; 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)); DrawSelfAnalysisStatRow("Taille extraite", UiSharedService.ByteToString(summary.TotalOriginalSize));
Vector4? trianglesColor = null; Vector4? trianglesColor = null;
FontAwesomeIcon? trianglesIcon = null;
Vector4? trianglesIconColor = null;
string? trianglesTooltip = null; string? trianglesTooltip = null;
if (summary.TotalTriangles >= SelfAnalysisTriangleWarningThreshold) if (summary.TotalTriangles >= SelfAnalysisTriangleWarningThreshold)
{ {
trianglesColor = ImGuiColors.DalamudYellow; trianglesColor = ImGuiColors.DalamudYellow;
trianglesTooltip = "Plus de 150k triangles peuvent entraîner un auto-pause et impacter les performances."; 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(); 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.TableNextRow();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(label); ImGui.TextUnformatted(label);
ImGui.TableNextColumn(); 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) if (valueColor.HasValue)
{ {
using var color = ImRaii.PushColor(ImGuiCol.Text, valueColor.Value); using var color = ImRaii.PushColor(ImGuiCol.Text, valueColor.Value);
@@ -687,9 +712,9 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGuiHelpers.ScaledDummy(4); 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 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)).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)).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); _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.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -21,16 +27,22 @@ public class DrawGroupPair : DrawPairBase
private readonly GroupPairFullInfoDto _fullInfoDto; private readonly GroupPairFullInfoDto _fullInfoDto;
private readonly GroupFullInfoDto _group; private readonly GroupFullInfoDto _group;
private readonly CharaDataManager _charaDataManager; 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, public DrawGroupPair(string id, Pair entry, ApiController apiController,
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto, 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) : base(id, entry, apiController, handler, uiSharedService)
{ {
_group = group; _group = group;
_fullInfoDto = fullInfoDto; _fullInfoDto = fullInfoDto;
_mediator = mareMediator; _mediator = mareMediator;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_autoDetectRequestService = autoDetectRequestService;
_serverConfigurationManager = serverConfigurationManager;
} }
protected override void DrawLeftSide(float textPosY, float originalY) 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 showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled); 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 showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
bool showPause = true;
var spacing = ImGui.GetStyle().ItemSpacing.X; var spacing = ImGui.GetStyle().ItemSpacing.X;
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
@@ -162,12 +175,15 @@ public class DrawGroupPair : DrawPairBase
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X; var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
var infoIconWidth = UiSharedService.GetIconSize(permIcon).X; var infoIconWidth = UiSharedService.GetIconSize(permIcon).X;
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).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 barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing
- (showShared ? (runningIconWidth + spacing) : 0) - (showShared ? (runningIconWidth + spacing) : 0)
- (showInfo ? (infoIconWidth + spacing) : 0) - (showInfo ? (infoIconWidth + spacing) : 0)
- (showPlus ? (plusButtonWidth + spacing) : 0) - (showPlus ? (plusButtonWidth + spacing) : 0)
- (showPause ? (pauseButtonWidth + spacing) : 0)
- (showBars ? (barButtonWidth + spacing) : 0); - (showBars ? (barButtonWidth + spacing) : 0);
ImGui.SameLine(pos); ImGui.SameLine(pos);
@@ -280,9 +296,28 @@ public class DrawGroupPair : DrawPairBase
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) 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(); ImGui.SameLine();
} }
@@ -383,4 +418,74 @@ public class DrawGroupPair : DrawPairBase
return pos - spacing; 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.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System.Numerics; using System.Numerics;
using MareSynchronos.Services.ServerConfiguration;
namespace MareSynchronos.UI.Components; namespace MareSynchronos.UI.Components;
@@ -20,10 +21,12 @@ public class DrawUserPair : DrawPairBase
protected readonly MareMediator _mediator; protected readonly MareMediator _mediator;
private readonly SelectGroupForPairUi _selectGroupForPairUi; private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController, public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi, MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
UiSharedService uiSharedService, CharaDataManager charaDataManager) UiSharedService uiSharedService, CharaDataManager charaDataManager,
ServerConfigurationManager serverConfigurationManager)
: base(id, entry, apiController, displayHandler, uiSharedService) : base(id, entry, apiController, displayHandler, uiSharedService)
{ {
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry)); if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
@@ -31,6 +34,7 @@ public class DrawUserPair : DrawPairBase
_selectGroupForPairUi = selectGroupForPairUi; _selectGroupForPairUi = selectGroupForPairUi;
_mediator = mareMediator; _mediator = mareMediator;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_serverConfigurationManager = serverConfigurationManager;
} }
public bool IsOnline => _pair.IsOnline; public bool IsOnline => _pair.IsOnline;
@@ -135,9 +139,9 @@ public class DrawUserPair : DrawPairBase
perm.SetPaused(!perm.IsPaused()); perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm)); _ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
} }
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused() UiSharedService.AttachToolTip(AppendSeenInfo(!_pair.UserPair!.OwnPermissions.IsPaused()
? "Pause pairing with " + entryUID ? "Pause pairing with " + entryUID
: "Resume pairing with " + entryUID); : "Resume pairing with " + entryUID));
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
@@ -263,7 +267,7 @@ public class DrawUserPair : DrawPairBase
{ {
_selectGroupForPairUi.Open(entry); _selectGroupForPairUi.Open(entry);
} }
UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); UiSharedService.AttachToolTip(AppendSeenInfo("Choose pair groups for " + entryUID));
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds(); var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync"; string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
@@ -302,6 +306,16 @@ public class DrawUserPair : DrawPairBase
{ {
_ = _apiController.UserRemovePair(new(entry.UserData)); _ = _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.MareConfiguration;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
@@ -32,6 +33,7 @@ internal sealed class GroupPanel
private readonly MareConfigService _mareConfig; private readonly MareConfigService _mareConfig;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal); private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
private readonly UidDisplayHandler _uidDisplayHandler; private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
@@ -74,7 +76,7 @@ internal sealed class GroupPanel
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce, public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager, UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager,
CharaDataManager charaDataManager) CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService)
{ {
_mainUi = mainUi; _mainUi = mainUi;
_uiShared = uiShared; _uiShared = uiShared;
@@ -84,6 +86,7 @@ internal sealed class GroupPanel
_mareConfig = mareConfig; _mareConfig = mareConfig;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_autoDetectRequestService = autoDetectRequestService;
} }
private ApiController ApiController => _uiShared.ApiController; private ApiController ApiController => _uiShared.ApiController;
@@ -566,7 +569,9 @@ internal sealed class GroupPanel
).Value, ).Value,
_uidDisplayHandler, _uidDisplayHandler,
_uiShared, _uiShared,
_charaDataManager); _charaDataManager,
_autoDetectRequestService,
_serverConfigurationManager);
if (pair.IsVisible) if (pair.IsVisible)
visibleUsers.Add(drawPair); visibleUsers.Add(drawPair);

View File

@@ -209,12 +209,24 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("0 = No limit/infinite"); 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); ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10)) if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
{ {
_configService.Current.ParallelDownloads = maxParallelDownloads; _configService.Current.ParallelDownloads = maxParallelDownloads;
_configService.Save(); _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(); ImGui.Separator();
_uiShared.BigText("AutoDetect"); _uiShared.BigText("AutoDetect");
@@ -222,7 +234,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery; bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
using (ImRaii.Disabled(isAutoDetectSuppressed)) using (ImRaii.Disabled(isAutoDetectSuppressed))
{ {
if (ImGui.Checkbox("Enable AutoDetect", ref enableDiscovery)) if (ImGui.Checkbox("Activer l'AutoDetect", ref enableDiscovery))
{ {
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery; _configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
_configService.Save(); _configService.Save();
@@ -248,7 +260,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery)) using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery))
{ {
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests; bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
if (ImGui.Checkbox("Allow pair requests", ref allowRequests)) if (ImGui.Checkbox("Activer les invitations entrantes", ref allowRequests))
{ {
_configService.Current.AllowAutoDetectPairRequests = allowRequests; _configService.Current.AllowAutoDetectPairRequests = allowRequests;
_configService.Save(); _configService.Save();
@@ -275,7 +287,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Indent(); ImGui.Indent();
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters; int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); 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.Current.AutoDetectMaxDistanceMeters = maxMeters;
_configService.Save(); _configService.Save();

View File

@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Linq;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
@@ -115,25 +117,34 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
continue; continue;
var pair = _pairManager.GetPairByUID(uid); var pair = _pairManager.GetPairByUID(uid);
if (pair == null) var targetIndex = -1;
{ var playerName = pair?.PlayerName;
var alias = entry.User.AliasOrUID; var objectId = pair?.PlayerCharacterId ?? uint.MaxValue;
if (string.IsNullOrEmpty(alias))
continue;
var aliasIndex = GetPartyIndexForName(alias); if (objectId != 0 && objectId != uint.MaxValue)
if (aliasIndex >= 0) {
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 (targetIndex < 0 && !string.IsNullOrEmpty(playerName))
if (index < 0) {
targetIndex = GetPartyIndexForName(playerName);
}
if (targetIndex < 0)
continue; continue;
DrawPartyMemberTyping(drawList, partyAddon, index); DrawPartyMemberTyping(drawList, partyAddon, targetIndex);
} }
} }
@@ -198,10 +209,10 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
var pair = _pairManager.GetPairByUID(uid); var pair = _pairManager.GetPairByUID(uid);
var objectId = pair?.PlayerCharacterId ?? 0; var objectId = pair?.PlayerCharacterId ?? 0;
if (pair == null) var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
{ var pairIdent = pair?.Ident ?? string.Empty;
_logger.LogInformation("TypingIndicator: no pair found for {uid}, attempting fallback", uid); var isPartyMember = IsPartyMember(objectId, pairName);
} var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId)) if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
{ {
@@ -209,20 +220,28 @@ public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
continue; continue;
} }
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty; var hasWorldPosition = TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos);
var pairIdent = pair?.Ident ?? string.Empty; 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); uid, objectId, pairName, pairIdent);
if (TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos)) if (hasWorldPosition)
{ {
DrawWorldFallbackIcon(drawList, iconWrap, worldPos); 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 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; 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) private static unsafe uint GetEntityId(nint address)
{ {
if (address == nint.Zero) return 0; if (address == nint.Zero) return 0;

View File

@@ -1,6 +1,7 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.SignalR; using MareSynchronos.WebAPI.SignalR;
using MareSynchronos.Services.AutoDetect; 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 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); var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return false; if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt); 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"); req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
@@ -82,7 +89,7 @@ public class DiscoveryApiClient
if (string.IsNullOrEmpty(jwt2)) return false; if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); 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"); req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); 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) public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
{ {
try try

View File

@@ -4,6 +4,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files; using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes; using MareSynchronos.API.Routes;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils; using MareSynchronos.Utils;
@@ -20,17 +21,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus; private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly MareConfigService _mareConfigService;
private readonly FileTransferOrchestrator _orchestrator; private readonly FileTransferOrchestrator _orchestrator;
private readonly List<ThrottledStream> _activeDownloadStreams; private readonly List<ThrottledStream> _activeDownloadStreams;
private readonly object _queueLock = new();
private SemaphoreSlim? _downloadQueueSemaphore;
private int _downloadQueueCapacity = -1;
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator, public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator, 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); _downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator; _orchestrator = orchestrator;
_fileDbManager = fileCacheManager; _fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_mareConfigService = mareConfigService;
_activeDownloadStreams = []; _activeDownloadStreams = [];
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) => 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) 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))); Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
try try
{ {
@@ -70,6 +84,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
finally finally
{ {
if (queueSemaphore != null)
{
queueSemaphore.Release();
}
Mediator.Publish(new DownloadFinishedMessage(gameObject)); Mediator.Publish(new DownloadFinishedMessage(gameObject));
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles))); Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
} }
@@ -132,6 +151,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return (string.Join("", hashName), long.Parse(string.Join("", fileLength))); 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) 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())); Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));