From a0957715a54e27439517778b2616e49900a0c5db Mon Sep 17 00:00:00 2001 From: SirConstance Date: Sat, 13 Sep 2025 20:08:24 +0200 Subject: [PATCH] Update 0.1.6 - Fix UI settings & Delay Detection --- MareSynchronos/MareSynchronos.csproj | 2 +- .../AutoDetect/NearbyDiscoveryService.cs | 80 ++++++++++++++++++- MareSynchronos/Services/Mediator/Messages.cs | 2 + MareSynchronos/UI/SettingsUi.cs | 38 +++++++-- .../WebAPI/AutoDetect/DiscoveryApiClient.cs | 53 ++++++++++++ 5 files changed, 168 insertions(+), 7 deletions(-) diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index b9b60d8..4617480 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ UmbraSync UmbraSync - 0.1.6.2 + 0.1.6.3 diff --git a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs index 9fc3a22..24d0446 100644 --- a/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs +++ b/MareSynchronos/Services/AutoDetect/NearbyDiscoveryService.cs @@ -32,6 +32,8 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber private bool _notifiedEnabled; private bool _disableSent; private bool _lastAutoDetectState; + private DateTime _lastHeartbeat = DateTime.MinValue; + private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75); public NearbyDiscoveryService(ILogger logger, MareMediator mediator, MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService, @@ -53,10 +55,75 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber _loopCts = new CancellationTokenSource(); _mediator.Subscribe(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); }); _mediator.Subscribe(this, _ => { _isConnected = false; _lastPublishedSignature = null; }); + _mediator.Subscribe(this, OnAllowPairRequestsToggled); _ = Task.Run(() => Loop(_loopCts.Token)); _lastAutoDetectState = _config.Current.EnableAutoDetectDiscovery; return Task.CompletedTask; } + private async void OnAllowPairRequestsToggled(AllowPairRequestsToggled msg) + { + try + { + if (!_config.Current.EnableAutoDetectDiscovery) return; + // Force a publish now so the server immediately reflects the new allow/deny state + _lastPublishedSignature = null; // ensure next loop won't skip + await PublishSelfOnceAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "OnAllowPairRequestsToggled failed"); + } + } + + private async Task PublishSelfOnceAsync(CancellationToken ct) + { + try + { + if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected) return; + + 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 not { Length: > 0 }) return; + + 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)) return; + + var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256(); + var ok = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false); + _logger.LogInformation("Nearby publish (manual/immediate): {result}", ok ? "success" : "failed"); + if (ok) + { + _lastPublishedSignature = selfHash; + _lastHeartbeat = DateTime.UtcNow; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Immediate publish failed"); + } + } public Task StopAsync(CancellationToken cancellationToken) { @@ -259,6 +326,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber _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) _lastHeartbeat = DateTime.UtcNow; if (ok) { if (!_notifiedEnabled) @@ -272,7 +340,17 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber } else { - _logger.LogDebug("Nearby publish skipped (no changes)"); + // No changes; perform heartbeat publish if interval elapsed + if (DateTime.UtcNow - _lastHeartbeat >= HeartbeatInterval) + { + var okHb = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false); + _logger.LogDebug("Nearby heartbeat publish: {result}", okHb ? "success" : "failed"); + if (okHb) _lastHeartbeat = DateTime.UtcNow; + } + else + { + _logger.LogDebug("Nearby publish skipped (no changes)"); + } } } // else: no self character available; skip publish silently diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index 7dbbaaa..b6302c1 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -110,6 +110,8 @@ public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) 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 NearbyDetectionToggled(bool Enabled) : MessageBase; +public record AllowPairRequestsToggled(bool Enabled) : MessageBase; public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName); #pragma warning restore S2094 diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index e363b80..77168c5 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -218,14 +218,42 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.EnableAutoDetectDiscovery = enableDiscovery; _configService.Save(); + + // notify services of toggle + Mediator.Publish(new NearbyDetectionToggled(enableDiscovery)); + + // if Nearby is turned OFF, force Allow Pair Requests OFF as well + if (!enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) + { + _configService.Current.AllowAutoDetectPairRequests = false; + _configService.Save(); + Mediator.Publish(new AllowPairRequestsToggled(false)); + } } - bool allowRequests = _configService.Current.AllowAutoDetectPairRequests; - if (ImGui.Checkbox("Allow pair requests", ref allowRequests)) + + // Allow Pair Requests is disabled when Nearby is OFF + using (ImRaii.Disabled(!enableDiscovery)) { - _configService.Current.AllowAutoDetectPairRequests = allowRequests; - _configService.Save(); + bool allowRequests = _configService.Current.AllowAutoDetectPairRequests; + if (ImGui.Checkbox("Allow pair requests", ref allowRequests)) + { + _configService.Current.AllowAutoDetectPairRequests = allowRequests; + _configService.Save(); + + // notify services of toggle + Mediator.Publish(new AllowPairRequestsToggled(allowRequests)); + + // user-facing info toast + Mediator.Publish(new NotificationMessage( + "Nearby Detection", + allowRequests ? "Pair requests enabled: others can invite you." : "Pair requests disabled: others cannot invite you.", + NotificationType.Info, + default)); + } } - if (enableDiscovery) + + // Radius only available when both Nearby and Allow Pair Requests are ON + if (enableDiscovery && _configService.Current.AllowAutoDetectPairRequests) { ImGui.Indent(); int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters; diff --git a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs index 0731a94..e149f79 100644 --- a/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs +++ b/MareSynchronos/WebAPI/AutoDetect/DiscoveryApiClient.cs @@ -39,6 +39,21 @@ public class DiscoveryApiClient }); 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) + { + // Retry once with a fresh token + var token2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(token2)) return []; + using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); + req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2); + var body2 = JsonSerializer.Serialize(new + { + hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), + salt = _configProvider.SaltB64 + }); + req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); + resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); + } resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); var result = JsonSerializer.Deserialize>(json, JsonOpt) ?? []; @@ -62,6 +77,16 @@ public class DiscoveryApiClient var body = JsonSerializer.Serialize(new { token, 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) + { + var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + 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 }); + req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); + resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); + } if (!resp.IsSuccessStatusCode) { string txt = string.Empty; @@ -96,6 +121,16 @@ public class DiscoveryApiClient var body = JsonSerializer.Serialize(bodyObj); 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) + { + var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + 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(bodyObj); + req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); + resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); + } return resp.IsSuccessStatusCode; } catch (Exception ex) @@ -117,6 +152,16 @@ public class DiscoveryApiClient var body = JsonSerializer.Serialize(bodyObj); 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) + { + var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + 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(bodyObj); + req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); + resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); + } return resp.IsSuccessStatusCode; } catch (Exception ex) @@ -135,6 +180,14 @@ public class DiscoveryApiClient req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt); // no body required var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); + if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); + if (string.IsNullOrEmpty(jwt2)) return; + using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); + req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); + resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false); + } if (!resp.IsSuccessStatusCode) { string txt = string.Empty;