diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj
index b0ff00a..b9b60d8 100644
--- a/MareSynchronos/MareSynchronos.csproj
+++ b/MareSynchronos/MareSynchronos.csproj
@@ -3,7 +3,7 @@
UmbraSync
UmbraSync
- 0.1.4.1
+ 0.1.6.2
diff --git a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
index 0ee9e31..527c08c 100644
--- a/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
+++ b/MareSynchronos/Services/AutoDetect/AutoDetectRequestService.cs
@@ -61,4 +61,23 @@ public class AutoDetectRequestService
}
return ok;
}
+
+ public async Task SendAcceptNotifyAsync(string targetUid, CancellationToken ct = default)
+ {
+ var endpoint = _configProvider.AcceptEndpoint;
+ if (string.IsNullOrEmpty(endpoint))
+ {
+ _logger.LogDebug("No accept endpoint configured");
+ return false;
+ }
+ string? displayName = null;
+ try
+ {
+ var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
+ displayName = me?.Name.TextValue;
+ }
+ catch { }
+ _logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
+ return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
+ }
}
diff --git a/MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs b/MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
index 248aefc..ffd921d 100644
--- a/MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
+++ b/MareSynchronos/Services/AutoDetect/DiscoveryConfigProvider.cs
@@ -29,6 +29,7 @@ public class DiscoveryConfigProvider
public bool HasConfig => _config != null;
public bool NearbyEnabled => _config?.NearbyDiscovery?.Enabled ?? false;
public byte[]? Salt => _config?.NearbyDiscovery?.SaltBytes;
+ public string? SaltB64 => _config?.NearbyDiscovery?.SaltB64;
public DateTimeOffset? SaltExpiresAt => _config?.NearbyDiscovery?.SaltExpiresAt;
public int RefreshSec => _config?.NearbyDiscovery?.RefreshSec ?? 300;
public int MinQueryIntervalMs => _config?.NearbyDiscovery?.Policies?.MinQueryIntervalMs ?? 2000;
@@ -36,6 +37,7 @@ public class DiscoveryConfigProvider
public string? PublishEndpoint => _config?.NearbyDiscovery?.Endpoints?.Publish;
public string? QueryEndpoint => _config?.NearbyDiscovery?.Endpoints?.Query;
public string? RequestEndpoint => _config?.NearbyDiscovery?.Endpoints?.Request;
+ public string? AcceptEndpoint => _config?.NearbyDiscovery?.Endpoints?.Accept;
public bool TryLoadFromStapled()
{
@@ -50,7 +52,7 @@ public class DiscoveryConfigProvider
root.NearbyDiscovery?.Hydrate();
_config = root;
_lastLoad = DateTimeOffset.UtcNow;
- _logger.LogInformation("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
+ _logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
return true;
}
catch (Exception ex)
@@ -158,6 +160,7 @@ public class DiscoveryConfigProvider
[JsonPropertyName("publish")] public string? Publish { get; set; }
[JsonPropertyName("query")] public string? Query { get; set; }
[JsonPropertyName("request")] public string? Request { get; set; }
+ [JsonPropertyName("accept")] public string? Accept { get; set; }
}
private sealed class Policies
diff --git a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
index 06a644b..9fc3a22 100644
--- a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
+++ b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs
@@ -28,6 +28,10 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
private bool _loggedConfigReady;
private string? _lastSnapshotSig;
private volatile bool _isConnected;
+ private bool _notifiedDisabled;
+ private bool _notifiedEnabled;
+ private bool _disableSent;
+ private bool _lastAutoDetectState;
public NearbyDiscoveryService(ILogger logger, MareMediator mediator,
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
@@ -50,6 +54,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
_mediator.Subscribe(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
_mediator.Subscribe(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
_ = Task.Run(() => Loop(_loopCts.Token));
+ _lastAutoDetectState = _config.Current.EnableAutoDetectDiscovery;
return Task.CompletedTask;
}
@@ -62,20 +67,112 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
private async Task Loop(CancellationToken ct)
{
- // best effort config load
_configProvider.TryLoadFromStapled();
while (!ct.IsCancellationRequested)
{
try
{
+ bool currentState = _config.Current.EnableAutoDetectDiscovery;
+ if (currentState != _lastAutoDetectState)
+ {
+ _lastAutoDetectState = currentState;
+ if (currentState)
+ {
+ // Force immediate publish on toggle ON
+ try
+ {
+ // Ensure well-known is present
+ if (!_configProvider.HasConfig || _configProvider.IsExpired())
+ {
+ if (!_configProvider.TryLoadFromStapled())
+ {
+ await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
+ }
+ }
+
+ var ep = _configProvider.PublishEndpoint;
+ var saltBytes = _configProvider.Salt;
+ if (!string.IsNullOrEmpty(ep) && saltBytes is { Length: > 0 })
+ {
+ var saltHex = Convert.ToHexString(saltBytes);
+ string? displayName = null;
+ ushort meWorld = 0;
+ try
+ {
+ var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
+ if (me != null)
+ {
+ displayName = me.Name.TextValue;
+ if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
+ meWorld = (ushort)mePc.HomeWorld.RowId;
+ }
+ }
+ catch { }
+
+ if (!string.IsNullOrEmpty(displayName))
+ {
+ var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
+ _lastPublishedSignature = null; // ensure future loop doesn't skip
+ var okNow = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
+ _logger.LogInformation("Nearby immediate publish on toggle ON: {result}", okNow ? "success" : "failed");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Nearby immediate publish on toggle ON failed");
+ }
+
+ if (!_notifiedEnabled)
+ {
+ _mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
+ _notifiedEnabled = true;
+ _notifiedDisabled = false;
+ _disableSent = false;
+ }
+ }
+ else
+ {
+ var ep = _configProvider.PublishEndpoint;
+ if (!string.IsNullOrEmpty(ep) && !_disableSent)
+ {
+ var disableUrl = ep.Replace("/publish", "/disable");
+ try { await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false); _disableSent = true; } catch { }
+ }
+ if (!_notifiedDisabled)
+ {
+ _mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
+ _notifiedDisabled = true;
+ _notifiedEnabled = false;
+ }
+ }
+ }
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected)
{
+ if (!_config.Current.EnableAutoDetectDiscovery && !string.IsNullOrEmpty(_configProvider.PublishEndpoint))
+ {
+ var disableUrl = _configProvider.PublishEndpoint.Replace("/publish", "/disable");
+ try
+ {
+ if (!_disableSent)
+ {
+ await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false);
+ _disableSent = true;
+ }
+ }
+ catch { }
+
+ if (!_notifiedDisabled)
+ {
+ _mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
+ _notifiedDisabled = true;
+ _notifiedEnabled = false;
+ }
+ }
await Task.Delay(1000, ct).ConfigureAwait(false);
continue;
}
-
- // Ensure we have a valid Nearby config: try stapled, then HTTP fallback
if (!_configProvider.HasConfig || _configProvider.IsExpired())
{
if (!_configProvider.TryLoadFromStapled())
@@ -114,7 +211,6 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
hashes.Add(h);
}
- // Debug snapshot once per change
try
{
var snapSig = string.Join(',', hashes.OrderBy(s => s, StringComparer.Ordinal)).GetHash256();
@@ -134,7 +230,6 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
}
catch { }
- // Publish OUR presence (own hash) if endpoint is available (deduplicated)
if (!string.IsNullOrEmpty(_configProvider.PublishEndpoint))
{
string? displayName = null;
@@ -161,9 +256,19 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
{
_lastPublishedSignature = sig;
var shortSelf = selfHash!.Length > 8 ? selfHash[..8] : selfHash;
- _logger.LogInformation("Nearby publish: self presence updated (hash={hash})", shortSelf);
- var ok = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct).ConfigureAwait(false);
+ _logger.LogDebug("Nearby publish: self presence updated (hash={hash})", shortSelf);
+ var ok = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
_logger.LogInformation("Nearby publish result: {result}", ok ? "success" : "failed");
+ if (ok)
+ {
+ if (!_notifiedEnabled)
+ {
+ _mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
+ _notifiedEnabled = true;
+ _notifiedDisabled = false;
+ _disableSent = false; // allow future /disable when turning off again
+ }
+ }
}
else
{
@@ -193,7 +298,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
if (hashToIndex.TryGetValue(m.Hash, out var idx))
{
var e = entries[idx];
- entries[idx] = new NearbyEntry(e.Name, e.WorldId, e.Distance, true, m.Token);
+ entries[idx] = new NearbyEntry(e.Name, e.WorldId, e.Distance, true, m.Token, m.DisplayName, m.Uid);
}
}
}
@@ -204,13 +309,18 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
if (matchCount != _lastMatchCount)
{
_lastMatchCount = matchCount;
- _logger.LogInformation("Nearby: {count} Umbra users nearby", matchCount);
+ _logger.LogDebug("Nearby: {count} Umbra users nearby", matchCount);
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Nearby query failed; falling back to local list");
+ if (ex.Message.Contains("DISCOVERY_SALT_EXPIRED", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("Nearby: salt expired, refetching well-known");
+ try { await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false); } catch { }
+ }
}
}
else
@@ -218,12 +328,13 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
if (!_loggedLocalOnly)
{
_loggedLocalOnly = true;
- _logger.LogInformation("Nearby: well-known not available or disabled; running in local-only mode");
+ _logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
}
}
_mediator.Publish(new DiscoveryListUpdated(entries));
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
+ if (entries.Count == 0) delayMs = Math.Max(delayMs, 5000);
await Task.Delay(delayMs, ct).ConfigureAwait(false);
}
catch (OperationCanceledException) { }
@@ -259,7 +370,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
if (obj is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter pc)
worldId = (ushort)pc.HomeWorld.RowId;
- list.Add(new NearbyEntry(name, worldId, dist, false, null));
+ list.Add(new NearbyEntry(name, worldId, dist, false, null, null, null));
}
}
catch
diff --git a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
index ddc3855..478ecbe 100644
--- a/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
+++ b/MareSynchronos/Services/AutoDetect/NearbyPendingService.cs
@@ -11,14 +11,17 @@ public sealed class NearbyPendingService : IMediatorSubscriber
private readonly ILogger _logger;
private readonly MareMediator _mediator;
private readonly ApiController _api;
+ private readonly AutoDetectRequestService _requestService;
private readonly ConcurrentDictionary _pending = new(StringComparer.Ordinal);
private static readonly Regex ReqRegex = new(@"^Nearby Request: (.+) \[(?[A-Z0-9]+)\]$", RegexOptions.Compiled);
+ private static readonly Regex AcceptRegex = new(@"^Nearby Accept: (.+) \[(?[A-Z0-9]+)\]$", RegexOptions.Compiled);
- public NearbyPendingService(ILogger logger, MareMediator mediator, ApiController api)
+ public NearbyPendingService(ILogger logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService)
{
_logger = logger;
_mediator = mediator;
_api = api;
+ _requestService = requestService;
_mediator.Subscribe(this, OnNotification);
}
@@ -30,6 +33,19 @@ public sealed class NearbyPendingService : IMediatorSubscriber
{
// Watch info messages for Nearby request pattern
if (msg.Type != MareSynchronos.MareConfiguration.Models.NotificationType.Info) return;
+ var ma = AcceptRegex.Match(msg.Message);
+ if (ma.Success)
+ {
+ var uidA = ma.Groups["uid"].Value;
+ if (!string.IsNullOrEmpty(uidA))
+ {
+ _ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
+ _pending.TryRemove(uidA, out _);
+ _logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
+ }
+ return;
+ }
+
var m = ReqRegex.Match(msg.Message);
if (!m.Success) return;
var uid = m.Groups["uid"].Value;
@@ -59,6 +75,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
{
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
_pending.TryRemove(uid, out _);
+ _ = _requestService.SendAcceptNotifyAsync(uid);
return true;
}
catch (Exception ex)
diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs
index 078e845..7dbbaaa 100644
--- a/MareSynchronos/Services/Mediator/Messages.cs
+++ b/MareSynchronos/Services/Mediator/Messages.cs
@@ -108,9 +108,9 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
-public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMatch, string? Token);
+public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMatch, string? Token, string? DisplayName, string? Uid);
public record DiscoveryListUpdated(List Entries) : MessageBase;
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
#pragma warning restore S2094
-#pragma warning restore MA0048
\ No newline at end of file
+#pragma warning restore MA0048
diff --git a/MareSynchronos/UI/AutoDetectUi.cs b/MareSynchronos/UI/AutoDetectUi.cs
index 1d4d95b..57246c7 100644
--- a/MareSynchronos/UI/AutoDetectUi.cs
+++ b/MareSynchronos/UI/AutoDetectUi.cs
@@ -6,9 +6,12 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services;
+using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Numerics;
+using System.Globalization;
+using System.Text;
namespace MareSynchronos.UI;
@@ -18,18 +21,20 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
private readonly DalamudUtilService _dalamud;
private readonly IObjectTable _objectTable;
private readonly Services.AutoDetect.AutoDetectRequestService _requestService;
+ private readonly PairManager _pairManager;
private List _entries = new();
public AutoDetectUi(ILogger logger, MareMediator mediator,
MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable,
- Services.AutoDetect.AutoDetectRequestService requestService,
+ Services.AutoDetect.AutoDetectRequestService requestService, PairManager pairManager,
PerformanceCollectorService performanceCollectorService)
- : base(logger, mediator, "Umbra Nearby", performanceCollectorService)
+ : base(logger, mediator, "AutoDetect", performanceCollectorService)
{
_configService = configService;
_dalamud = dalamudUtilService;
_objectTable = objectTable;
_requestService = requestService;
+ _pairManager = pairManager;
Flags |= ImGuiWindowFlags.NoScrollbar;
SizeConstraints = new WindowSizeConstraints()
@@ -78,7 +83,7 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
ImGui.TableSetupColumn("Action");
ImGui.TableHeadersRow();
- var data = _entries.Count > 0 ? _entries : BuildLocalSnapshot(maxDist);
+ var data = _entries.Count > 0 ? _entries.Where(e => e.IsMatch).ToList() : new List();
foreach (var e in data)
{
ImGui.TableNextColumn();
@@ -88,20 +93,25 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
ImGui.TableNextColumn();
ImGui.TextUnformatted(float.IsNaN(e.Distance) ? "-" : $"{e.Distance:0.0} m");
ImGui.TableNextColumn();
- ImGui.TextUnformatted(e.IsMatch ? "On Umbra" : "Unknown");
+ bool alreadyPaired = IsAlreadyPairedByUidOrAlias(e);
+ string status = alreadyPaired ? "Paired" : (string.IsNullOrEmpty(e.Token) ? "Requests disabled" : "On Umbra");
+ ImGui.TextUnformatted(status);
ImGui.TableNextColumn();
- bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
- using (ImRaii.Disabled(!allowRequests || !e.IsMatch || string.IsNullOrEmpty(e.Token)))
+ using (ImRaii.Disabled(alreadyPaired || string.IsNullOrEmpty(e.Token)))
{
- if (ImGui.Button($"Send request##{e.Name}"))
+ if (alreadyPaired)
+ {
+ ImGui.Button($"Already sync##{e.Name}");
+ }
+ else if (string.IsNullOrEmpty(e.Token))
+ {
+ ImGui.Button($"Requests disabled##{e.Name}");
+ }
+ else if (ImGui.Button($"Send request##{e.Name}"))
{
_ = _requestService.SendRequestAsync(e.Token!);
}
}
- if (!allowRequests)
- {
- UiSharedService.AttachToolTip("Enable 'Allow pair requests' in Settings to send a request.");
- }
}
ImGui.EndTable();
@@ -140,8 +150,52 @@ public class AutoDetectUi : WindowMediatorSubscriberBase
string name = obj.Name.ToString();
ushort worldId = 0;
if (obj is IPlayerCharacter pc) worldId = (ushort)pc.HomeWorld.RowId;
- list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null));
+ list.Add(new Services.Mediator.NearbyEntry(name, worldId, dist, false, null, null, null));
}
return list;
}
+
+ private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
+ {
+ try
+ {
+ // 1) Match by UID when available (authoritative)
+ if (!string.IsNullOrEmpty(e.Uid))
+ {
+ foreach (var p in _pairManager.DirectPairs)
+ {
+ if (string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal))
+ return true;
+ }
+ }
+
+ // 2) Fallback on alias/name (legacy compatibility)
+ var key = NormalizeKey(e.DisplayName ?? e.Name);
+ if (string.IsNullOrEmpty(key)) return false;
+ foreach (var p in _pairManager.DirectPairs)
+ {
+ if (NormalizeKey(p.UserData.AliasOrUID) == key) return true;
+ if (!string.IsNullOrEmpty(p.UserData.Alias) && NormalizeKey(p.UserData.Alias!) == key) return true;
+ }
+ }
+ catch
+ {
+ // ignore
+ }
+ return false;
+ }
+
+ private static string NormalizeKey(string? input)
+ {
+ if (string.IsNullOrWhiteSpace(input)) return string.Empty;
+ var formD = input.Normalize(NormalizationForm.FormD);
+ var sb = new StringBuilder(formD.Length);
+ foreach (var ch in formD)
+ {
+ var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
+ if (cat != UnicodeCategory.NonSpacingMark)
+ sb.Append(char.ToLowerInvariant(ch));
+ }
+ return sb.ToString();
+ }
}
diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs
index 69f724c..84b7005 100644
--- a/MareSynchronos/UI/CompactUI.cs
+++ b/MareSynchronos/UI/CompactUI.cs
@@ -384,11 +384,8 @@ public class CompactUi : WindowMediatorSubscriberBase
_uiSharedService.IconText(icon);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
ImGui.SameLine();
- var nearbyCount = _nearbyEntries?.Count ?? 0;
var onUmbra = _nearbyEntries?.Count(e => e.IsMatch) ?? 0;
- ImGui.TextUnformatted(onUmbra > 0
- ? $"Nearby ({nearbyCount} — {onUmbra} on Umbra)"
- : $"Nearby ({nearbyCount} Players)");
+ ImGui.TextUnformatted($"Nearby ({onUmbra})");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
// Header action button to open Nearby window
@@ -404,7 +401,7 @@ public class CompactUi : WindowMediatorSubscriberBase
if (_nearbyOpen)
{
ImGui.Indent();
- if (nearbyCount == 0)
+ if (onUmbra == 0)
{
UiSharedService.ColorTextWrapped("No nearby players detected.", ImGuiColors.DalamudGrey3);
}
diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs
index 11d3787..e363b80 100644
--- a/MareSynchronos/UI/SettingsUi.cs
+++ b/MareSynchronos/UI/SettingsUi.cs
@@ -235,13 +235,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
_configService.Save();
}
- int muteMin = _configService.Current.AutoDetectMuteMinutes;
- ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
- if (ImGui.SliderInt("Default mute duration (minutes)", ref muteMin, 1, 120))
- {
- _configService.Current.AutoDetectMuteMinutes = muteMin;
- _configService.Save();
- }
ImGui.Unindent();
}
diff --git a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
index dd24560..0731a94 100644
--- a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
+++ b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.SignalR;
+using MareSynchronos.Services.AutoDetect;
namespace MareSynchronos.WebAPI.AutoDetect;
@@ -10,13 +11,16 @@ public class DiscoveryApiClient
{
private readonly ILogger _logger;
private readonly TokenProvider _tokenProvider;
+ private readonly DiscoveryConfigProvider _configProvider;
private readonly HttpClient _httpClient = new();
private static readonly JsonSerializerOptions JsonOpt = new() { PropertyNameCaseInsensitive = true };
+ // private readonly ISaltProvider _saltProvider; // For future use if needed
- public DiscoveryApiClient(ILogger logger, TokenProvider tokenProvider)
+ public DiscoveryApiClient(ILogger logger, TokenProvider tokenProvider, DiscoveryConfigProvider configProvider)
{
_logger = logger;
_tokenProvider = tokenProvider;
+ _configProvider = configProvider;
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
@@ -28,7 +32,11 @@ public class DiscoveryApiClient
if (string.IsNullOrEmpty(token)) return [];
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
- var body = JsonSerializer.Serialize(new { hashes = hashes.Distinct(StringComparer.Ordinal).ToArray() });
+ var body = JsonSerializer.Serialize(new
+ {
+ hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(),
+ salt = _configProvider.SaltB64
+ });
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
@@ -70,7 +78,7 @@ public class DiscoveryApiClient
}
}
- public async Task PublishAsync(string endpoint, IEnumerable hashes, string? displayName, CancellationToken ct)
+ public async Task PublishAsync(string endpoint, IEnumerable hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
{
try
{
@@ -78,7 +86,13 @@ public class DiscoveryApiClient
if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
- var bodyObj = new { hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), displayName };
+ var bodyObj = new
+ {
+ hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(),
+ displayName,
+ salt = _configProvider.SaltB64,
+ allowRequests
+ };
var body = JsonSerializer.Serialize(bodyObj);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
@@ -90,11 +104,55 @@ public class DiscoveryApiClient
return false;
}
}
+
+ public async Task SendAcceptAsync(string endpoint, string targetUid, string? displayName, CancellationToken ct)
+ {
+ try
+ {
+ 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 bodyObj = new { targetUid, displayName };
+ var body = JsonSerializer.Serialize(bodyObj);
+ req.Content = new StringContent(body, Encoding.UTF8, "application/json");
+ var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
+ return resp.IsSuccessStatusCode;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Discovery accept notify failed");
+ return false;
+ }
+ }
+ public async Task DisableAsync(string endpoint, CancellationToken ct)
+ {
+ try
+ {
+ var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
+ if (string.IsNullOrEmpty(jwt)) return;
+ using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
+ req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
+ // no body required
+ var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
+ if (!resp.IsSuccessStatusCode)
+ {
+ string txt = string.Empty;
+ try { txt = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); } catch { }
+ _logger.LogWarning("Discovery disable failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Discovery disable failed");
+ }
+ }
}
public sealed class ServerMatch
{
public string Hash { get; set; } = string.Empty;
- public string Token { get; set; } = string.Empty;
+ public string? Token { get; set; }
+ public string? Uid { get; set; }
public string? DisplayName { get; set; }
}