From 3c639dcccb1ef61680a1b0dcbf7dc8144470ff3c Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 31 Oct 2022 17:34:47 +0100 Subject: [PATCH] Save seen locations to approximate marker frequency --- Pal.Client/LocalState.cs | 2 +- Pal.Client/Marker.cs | 31 ++++++- Pal.Client/Pal.Client.csproj | 2 +- Pal.Client/Plugin.cs | 165 +++++++++++++++++++++++++++------ Pal.Client/RemoteApi.cs | 56 ++++++++--- Pal.Common/Protos/palace.proto | 14 +++ 6 files changed, 227 insertions(+), 43 deletions(-) diff --git a/Pal.Client/LocalState.cs b/Pal.Client/LocalState.cs index 3f0c7aa..23ad8b4 100644 --- a/Pal.Client/LocalState.cs +++ b/Pal.Client/LocalState.cs @@ -13,7 +13,7 @@ namespace Pal.Client internal class LocalState { private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { IncludeFields = true }; - private static readonly int _currentVersion = 2; + private static readonly int _currentVersion = 3; public uint TerritoryType { get; set; } public ConcurrentBag Markers { get; set; } = new(); diff --git a/Pal.Client/Marker.cs b/Pal.Client/Marker.cs index 867cb8c..8551a9b 100644 --- a/Pal.Client/Marker.cs +++ b/Pal.Client/Marker.cs @@ -1,6 +1,7 @@ using ECommons.SplatoonAPI; using Palace; using System; +using System.Collections.Generic; using System.Numerics; using System.Text.Json.Serialization; @@ -10,18 +11,44 @@ namespace Pal.Client { public EType Type { get; set; } = EType.Unknown; public Vector3 Position { get; set; } + + /// + /// Whether we have encountered the trap/coffer at this location in-game. + /// public bool Seen { get; set; } = false; + /// + /// Network id for the server you're currently connected to. + /// [JsonIgnore] - public bool RemoteSeen { get; set; } = false; + public Guid? NetworkId { get; set; } + + /// + /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests. + /// + [JsonIgnore] + public bool UploadRequested { get; set; } + + /// + /// Which account ids this marker was seen. This is a list merely to support different remote endpoints + /// (where each server would assign you a different id). + /// + public List RemoteSeenOn { get; set; } = new List(); + + /// + /// Whether this marker was requested to be seen, to avoid duplicate requests. + /// + [JsonIgnore] + public bool RemoteSeenRequested { get; set; } = false; [JsonIgnore] public Element? SplatoonElement { get; set; } - public Marker(EType type, Vector3 position) + public Marker(EType type, Vector3 position, Guid? networkId = null) { Type = type; Position = position; + NetworkId = networkId; } public override int GetHashCode() diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index 8fba640..b0e850d 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -3,7 +3,7 @@ net6.0-windows 9.0 - 1.11.0.0 + 1.12.0.0 enable diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index 10366c1..badd613 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -13,6 +13,7 @@ using Grpc.Core; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Pal.Client.Windows; +using Palace; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -31,7 +32,7 @@ namespace Pal.Client private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard"; private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers"; - private readonly ConcurrentQueue<(ushort territoryId, bool success, IList markers)> _remoteDownloads = new(); + private readonly ConcurrentQueue _pendingSyncResponses = new(); private readonly static Dictionary _markerConfig = new Dictionary { { Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } }, @@ -256,9 +257,9 @@ namespace Pal.Client Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory)); } - if (_remoteDownloads.Count > 0) + if (_pendingSyncResponses.Count > 0) { - HandleRemoteDownloads(); + HandleSyncResponses(); recreateLayout = true; saveMarkers = true; } @@ -281,6 +282,8 @@ namespace Pal.Client var config = Service.Configuration; var currentFloorMarkers = currentFloor.Markers; + bool updateSeenMarkers = false; + var accountId = Service.RemoteApi.AccountId; foreach (var visibleMarker in visibleMarkers) { Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); @@ -291,6 +294,12 @@ namespace Pal.Client knownMarker.Seen = true; saveMarkers = true; } + + // This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, + // markers discovered afterwards are automatically marked seen. + if (accountId != null && knownMarker.NetworkId != null && !knownMarker.RemoteSeenRequested && !knownMarker.RemoteSeenOn.Contains(accountId.Value)) + updateSeenMarkers = true; + continue; } @@ -324,14 +333,24 @@ namespace Pal.Client } } + if (updateSeenMarkers && accountId != null) + { + var markersToUpdate = currentFloorMarkers.Where(x => x.Seen && x.NetworkId != null && !x.RemoteSeenRequested && !x.RemoteSeenOn.Contains(accountId.Value)).ToList(); + foreach (var marker in markersToUpdate) + marker.RemoteSeenRequested = true; + Task.Run(async () => await SyncSeenMarkersForTerritory(LastTerritory, markersToUpdate)); + } + if (saveMarkers) { currentFloor.Save(); if (TerritorySyncState == SyncState.Complete) { - var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && !x.RemoteSeen).ToList(); - Task.Run(async () => await Service.RemoteApi.UploadMarker(LastTerritory, markersToUpload)); + var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList(); + foreach (var marker in markersToUpload) + marker.UploadRequested = true; + Task.Run(async () => await UploadMarkersForTerritory(LastTerritory, markersToUpload)); } } @@ -442,7 +461,13 @@ namespace Pal.Client try { var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId); - _remoteDownloads.Enqueue((territoryId, success, downloadedMarkers)); + _pendingSyncResponses.Enqueue(new Sync + { + Type = SyncType.Download, + TerritoryType = territoryId, + Success = success, + Markers = downloadedMarkers + }); } catch (Exception e) { @@ -450,6 +475,45 @@ namespace Pal.Client } } + private async Task UploadMarkersForTerritory(ushort territoryId, List markersToUpload) + { + try + { + var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload); + _pendingSyncResponses.Enqueue(new Sync + { + Type = SyncType.Upload, + TerritoryType = territoryId, + Success = success, + Markers = uploadedMarkers + }); + } + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + } + } + + private async Task SyncSeenMarkersForTerritory(ushort territoryId, List markersToUpdate) + { + try + { + var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate); + _pendingSyncResponses.Enqueue(new Sync + { + Type = SyncType.MarkSeen, + TerritoryType = territoryId, + Success = success, + Markers = markersToUpdate, + }); + } + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + } + } + + private async Task FetchFloorStatistics() { if (Service.Configuration.Mode != Configuration.EMode.Online) @@ -482,34 +546,67 @@ namespace Pal.Client } } - private void HandleRemoteDownloads() + private void HandleSyncResponses() { - while (_remoteDownloads.TryDequeue(out var download)) + while (_pendingSyncResponses.TryDequeue(out Sync? sync) && sync != null) { - var (territoryId, success, downloadedMarkers) = download; - if (Service.Configuration.Mode == Configuration.EMode.Online && success && FloorMarkers.TryGetValue(territoryId, out var currentFloor) && downloadedMarkers.Count > 0) + try { - foreach (var downloadedMarker in downloadedMarkers) + var territoryId = sync.TerritoryType; + var remoteMarkers = sync.Markers; + if (Service.Configuration.Mode == Configuration.EMode.Online && sync.Success && FloorMarkers.TryGetValue(territoryId, out var currentFloor) && remoteMarkers.Count > 0) { - Marker? seenMarker = currentFloor.Markers.SingleOrDefault(x => x == downloadedMarker); - if (seenMarker != null) + switch (sync.Type) { - seenMarker.RemoteSeen = true; - continue; - } + case SyncType.Download: + case SyncType.Upload: + foreach (var remoteMarker in remoteMarkers) + { + // Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved. + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + { + localMarker.NetworkId = remoteMarker.NetworkId; + continue; + } - currentFloor.Markers.Add(downloadedMarker); + if (sync.Type == SyncType.Download) + currentFloor.Markers.Add(remoteMarker); + } + break; + + case SyncType.MarkSeen: + var accountId = Service.RemoteApi.AccountId; + if (accountId == null) + break; + foreach (var remoteMarker in remoteMarkers) + { + Marker? localMarker = currentFloor.Markers.SingleOrDefault(x => x == remoteMarker); + if (localMarker != null) + localMarker.RemoteSeenOn.Add(accountId.Value); + } + break; + } + } + + // don't modify state for outdated floors + if (LastTerritory != territoryId) + continue; + + if (sync.Type == SyncType.Download) + { + if (sync.Success) + TerritorySyncState = SyncState.Complete; + else + TerritorySyncState = SyncState.Failed; } } - - // don't modify state for outdated floors - if (LastTerritory != territoryId) - continue; - - if (success) - TerritorySyncState = SyncState.Complete; - else - TerritorySyncState = SyncState.Failed; + catch (Exception e) + { + DebugMessage = $"{DateTime.Now}\n{e}"; + if (sync.Type == SyncType.Download) + TerritorySyncState = SyncState.Failed; + } } } @@ -588,14 +685,30 @@ namespace Pal.Client return Service.DataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; } + internal class Sync + { + public SyncType Type { get; set; } + public ushort TerritoryType { get; set; } + public bool Success { get; set; } + public List Markers { get; set; } = new(); + } + public enum SyncState { NotAttempted, + NotNeeded, Started, Complete, Failed, } + public enum SyncType + { + Upload, + Download, + MarkSeen, + } + public enum PomanderState { Inactive, diff --git a/Pal.Client/RemoteApi.cs b/Pal.Client/RemoteApi.cs index 3e48024..356cdcc 100644 --- a/Pal.Client/RemoteApi.cs +++ b/Pal.Client/RemoteApi.cs @@ -26,6 +26,18 @@ namespace Pal.Client private GrpcChannel? _channel; private LoginReply? _lastLoginReply; + public Guid? AccountId + { + get => Service.Configuration.AccountIds[remoteUrl]; + set + { + if (value != null) + Service.Configuration.AccountIds[remoteUrl] = value.Value; + else + Service.Configuration.AccountIds.Remove(remoteUrl); + } + } + private async Task Connect(CancellationToken cancellationToken, bool retry = true) { if (Service.Configuration.Mode != Configuration.EMode.Online) @@ -47,30 +59,27 @@ namespace Pal.Client } var accountClient = new AccountService.AccountServiceClient(_channel); - Guid? accountId = Service.Configuration.AccountIds[remoteUrl]; - if (accountId == null) + if (AccountId == null) { var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); if (createAccountReply.Success) { - accountId = Guid.Parse(createAccountReply.AccountId); - Service.Configuration.AccountIds[remoteUrl] = accountId.Value; + AccountId = Guid.Parse(createAccountReply.AccountId); Service.Configuration.Save(); } } - if (accountId == null) + if (AccountId == null) return false; if (_lastLoginReply == null || string.IsNullOrEmpty(_lastLoginReply.AuthToken) || _lastLoginReply.ExpiresAt.ToDateTime().ToLocalTime() < DateTime.Now) { - _lastLoginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = accountId.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + _lastLoginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = AccountId?.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); if (!_lastLoginReply.Success) { if (_lastLoginReply.Error == LoginError.InvalidAccountId) { - accountId = null; - Service.Configuration.AccountIds.Remove(remoteUrl); + AccountId = null; Service.Configuration.Save(); if (retry) return await Connect(cancellationToken, retry: false); @@ -100,16 +109,16 @@ namespace Pal.Client var palaceClient = new PalaceService.PalaceServiceClient(_channel); var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return (downloadReply.Success, downloadReply.Objects.Select(o => new Marker((Marker.EType)o.Type, new Vector3(o.X, o.Y, o.Z)) { RemoteSeen = true }).ToList()); + return (downloadReply.Success, downloadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); } - public async Task UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + public async Task<(bool, List)> UploadMarker(ushort territoryType, IList markers, CancellationToken cancellationToken = default) { if (markers.Count == 0) - return true; + return (true, new()); if (!await Connect(cancellationToken)) - return false; + return (false, new()); var palaceClient = new PalaceService.PalaceServiceClient(_channel); var uploadRequest = new UploadFloorsRequest @@ -124,9 +133,30 @@ namespace Pal.Client Z = m.Position.Z })); var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return uploadReply.Success; + return (uploadReply.Success, uploadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList()); } + public async Task MarkAsSeen(ushort territoryType, IList markers, CancellationToken cancellationToken = default) + { + Service.Chat.Print($"Marking {markers.Count} as seen"); + if (markers.Count == 0) + return true; + + if (!await Connect(cancellationToken)) + return false; + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; + foreach (var marker in markers) + seenRequest.NetworkIds.Add(marker.NetworkId.ToString()); + + var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken); + return seenReply.Success; + } + + private Marker CreateMarkerFromNetworkObject(PalaceObject obj) => + new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId)); + public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) { if (!await Connect(cancellationToken)) diff --git a/Pal.Common/Protos/palace.proto b/Pal.Common/Protos/palace.proto index a5a5797..39fce83 100644 --- a/Pal.Common/Protos/palace.proto +++ b/Pal.Common/Protos/palace.proto @@ -5,6 +5,7 @@ package palace; service PalaceService { rpc DownloadFloors(DownloadFloorsRequest) returns (DownloadFloorsReply); rpc UploadFloors(UploadFloorsRequest) returns (UploadFloorsReply); + rpc MarkObjectsSeen(MarkObjectsSeenRequest) returns (MarkObjectsSeenReply); rpc FetchStatistics(StatisticsRequest) returns (StatisticsReply); } @@ -24,6 +25,7 @@ message UploadFloorsRequest { message UploadFloorsReply { bool success = 1; + repeated PalaceObject objects = 2; } message StatisticsRequest { @@ -45,6 +47,18 @@ message PalaceObject { float x = 2; float y = 3; float z = 4; + + // Ignored for uploaded markers. + string networkId = 5; +} + +message MarkObjectsSeenRequest { + uint32 territoryType = 1; + repeated string networkIds = 2; +} + +message MarkObjectsSeenReply { + bool success = 1; } enum ObjectType {