Save seen locations to approximate marker frequency

This commit is contained in:
Liza 2022-10-31 17:34:47 +01:00
parent 90088ca846
commit 3c639dcccb
6 changed files with 227 additions and 43 deletions

View File

@ -13,7 +13,7 @@ namespace Pal.Client
internal class LocalState internal class LocalState
{ {
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { IncludeFields = true }; 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 uint TerritoryType { get; set; }
public ConcurrentBag<Marker> Markers { get; set; } = new(); public ConcurrentBag<Marker> Markers { get; set; } = new();

View File

@ -1,6 +1,7 @@
using ECommons.SplatoonAPI; using ECommons.SplatoonAPI;
using Palace; using Palace;
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -10,18 +11,44 @@ namespace Pal.Client
{ {
public EType Type { get; set; } = EType.Unknown; public EType Type { get; set; } = EType.Unknown;
public Vector3 Position { get; set; } public Vector3 Position { get; set; }
/// <summary>
/// Whether we have encountered the trap/coffer at this location in-game.
/// </summary>
public bool Seen { get; set; } = false; public bool Seen { get; set; } = false;
/// <summary>
/// Network id for the server you're currently connected to.
/// </summary>
[JsonIgnore] [JsonIgnore]
public bool RemoteSeen { get; set; } = false; public Guid? NetworkId { get; set; }
/// <summary>
/// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
/// </summary>
[JsonIgnore]
public bool UploadRequested { get; set; }
/// <summary>
/// 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).
/// </summary>
public List<Guid> RemoteSeenOn { get; set; } = new List<Guid>();
/// <summary>
/// Whether this marker was requested to be seen, to avoid duplicate requests.
/// </summary>
[JsonIgnore]
public bool RemoteSeenRequested { get; set; } = false;
[JsonIgnore] [JsonIgnore]
public Element? SplatoonElement { get; set; } public Element? SplatoonElement { get; set; }
public Marker(EType type, Vector3 position) public Marker(EType type, Vector3 position, Guid? networkId = null)
{ {
Type = type; Type = type;
Position = position; Position = position;
NetworkId = networkId;
} }
public override int GetHashCode() public override int GetHashCode()

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>9.0</LangVersion> <LangVersion>9.0</LangVersion>
<Version>1.11.0.0</Version> <Version>1.12.0.0</Version>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -13,6 +13,7 @@ using Grpc.Core;
using ImGuiNET; using ImGuiNET;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Pal.Client.Windows; using Pal.Client.Windows;
using Palace;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -31,7 +32,7 @@ namespace Pal.Client
private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard"; private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard";
private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers"; private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers";
private readonly ConcurrentQueue<(ushort territoryId, bool success, IList<Marker> markers)> _remoteDownloads = new(); private readonly ConcurrentQueue<Sync> _pendingSyncResponses = new();
private readonly static Dictionary<Marker.EType, MarkerConfig> _markerConfig = new Dictionary<Marker.EType, MarkerConfig> private readonly static Dictionary<Marker.EType, MarkerConfig> _markerConfig = new Dictionary<Marker.EType, MarkerConfig>
{ {
{ Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } }, { Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } },
@ -256,9 +257,9 @@ namespace Pal.Client
Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory)); Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory));
} }
if (_remoteDownloads.Count > 0) if (_pendingSyncResponses.Count > 0)
{ {
HandleRemoteDownloads(); HandleSyncResponses();
recreateLayout = true; recreateLayout = true;
saveMarkers = true; saveMarkers = true;
} }
@ -281,6 +282,8 @@ namespace Pal.Client
var config = Service.Configuration; var config = Service.Configuration;
var currentFloorMarkers = currentFloor.Markers; var currentFloorMarkers = currentFloor.Markers;
bool updateSeenMarkers = false;
var accountId = Service.RemoteApi.AccountId;
foreach (var visibleMarker in visibleMarkers) foreach (var visibleMarker in visibleMarkers)
{ {
Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker); Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker);
@ -291,6 +294,12 @@ namespace Pal.Client
knownMarker.Seen = true; knownMarker.Seen = true;
saveMarkers = 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; 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) if (saveMarkers)
{ {
currentFloor.Save(); currentFloor.Save();
if (TerritorySyncState == SyncState.Complete) if (TerritorySyncState == SyncState.Complete)
{ {
var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && !x.RemoteSeen).ToList(); var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList();
Task.Run(async () => await Service.RemoteApi.UploadMarker(LastTerritory, markersToUpload)); foreach (var marker in markersToUpload)
marker.UploadRequested = true;
Task.Run(async () => await UploadMarkersForTerritory(LastTerritory, markersToUpload));
} }
} }
@ -442,7 +461,13 @@ namespace Pal.Client
try try
{ {
var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId); 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) catch (Exception e)
{ {
@ -450,6 +475,45 @@ namespace Pal.Client
} }
} }
private async Task UploadMarkersForTerritory(ushort territoryId, List<Marker> 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<Marker> 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() private async Task FetchFloorStatistics()
{ {
if (Service.Configuration.Mode != Configuration.EMode.Online) if (Service.Configuration.Mode != Configuration.EMode.Online)
@ -482,23 +546,46 @@ 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; try
if (Service.Configuration.Mode == Configuration.EMode.Online && success && FloorMarkers.TryGetValue(territoryId, out var currentFloor) && downloadedMarkers.Count > 0)
{ {
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); switch (sync.Type)
if (seenMarker != null)
{ {
seenMarker.RemoteSeen = true; 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; 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;
} }
} }
@ -506,12 +593,22 @@ namespace Pal.Client
if (LastTerritory != territoryId) if (LastTerritory != territoryId)
continue; continue;
if (success) if (sync.Type == SyncType.Download)
{
if (sync.Success)
TerritorySyncState = SyncState.Complete; TerritorySyncState = SyncState.Complete;
else else
TerritorySyncState = SyncState.Failed; TerritorySyncState = SyncState.Failed;
} }
} }
catch (Exception e)
{
DebugMessage = $"{DateTime.Now}\n{e}";
if (sync.Type == SyncType.Download)
TerritorySyncState = SyncState.Failed;
}
}
}
private IList<Marker> GetRelevantGameObjects() private IList<Marker> GetRelevantGameObjects()
{ {
@ -588,14 +685,30 @@ namespace Pal.Client
return Service.DataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; return Service.DataManager.GetExcelSheet<LogMessage>()?.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<Marker> Markers { get; set; } = new();
}
public enum SyncState public enum SyncState
{ {
NotAttempted, NotAttempted,
NotNeeded,
Started, Started,
Complete, Complete,
Failed, Failed,
} }
public enum SyncType
{
Upload,
Download,
MarkSeen,
}
public enum PomanderState public enum PomanderState
{ {
Inactive, Inactive,

View File

@ -26,6 +26,18 @@ namespace Pal.Client
private GrpcChannel? _channel; private GrpcChannel? _channel;
private LoginReply? _lastLoginReply; 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<bool> Connect(CancellationToken cancellationToken, bool retry = true) private async Task<bool> Connect(CancellationToken cancellationToken, bool retry = true)
{ {
if (Service.Configuration.Mode != Configuration.EMode.Online) if (Service.Configuration.Mode != Configuration.EMode.Online)
@ -47,30 +59,27 @@ namespace Pal.Client
} }
var accountClient = new AccountService.AccountServiceClient(_channel); 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); var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
if (createAccountReply.Success) if (createAccountReply.Success)
{ {
accountId = Guid.Parse(createAccountReply.AccountId); AccountId = Guid.Parse(createAccountReply.AccountId);
Service.Configuration.AccountIds[remoteUrl] = accountId.Value;
Service.Configuration.Save(); Service.Configuration.Save();
} }
} }
if (accountId == null) if (AccountId == null)
return false; return false;
if (_lastLoginReply == null || string.IsNullOrEmpty(_lastLoginReply.AuthToken) || _lastLoginReply.ExpiresAt.ToDateTime().ToLocalTime() < DateTime.Now) 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.Success)
{ {
if (_lastLoginReply.Error == LoginError.InvalidAccountId) if (_lastLoginReply.Error == LoginError.InvalidAccountId)
{ {
accountId = null; AccountId = null;
Service.Configuration.AccountIds.Remove(remoteUrl);
Service.Configuration.Save(); Service.Configuration.Save();
if (retry) if (retry)
return await Connect(cancellationToken, retry: false); return await Connect(cancellationToken, retry: false);
@ -100,16 +109,16 @@ namespace Pal.Client
var palaceClient = new PalaceService.PalaceServiceClient(_channel); var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); 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<bool> UploadMarker(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default) public async Task<(bool, List<Marker>)> UploadMarker(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default)
{ {
if (markers.Count == 0) if (markers.Count == 0)
return true; return (true, new());
if (!await Connect(cancellationToken)) if (!await Connect(cancellationToken))
return false; return (false, new());
var palaceClient = new PalaceService.PalaceServiceClient(_channel); var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var uploadRequest = new UploadFloorsRequest var uploadRequest = new UploadFloorsRequest
@ -124,9 +133,30 @@ namespace Pal.Client
Z = m.Position.Z Z = m.Position.Z
})); }));
var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); 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<bool> MarkAsSeen(ushort territoryType, IList<Marker> 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<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default) public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default)
{ {
if (!await Connect(cancellationToken)) if (!await Connect(cancellationToken))

View File

@ -5,6 +5,7 @@ package palace;
service PalaceService { service PalaceService {
rpc DownloadFloors(DownloadFloorsRequest) returns (DownloadFloorsReply); rpc DownloadFloors(DownloadFloorsRequest) returns (DownloadFloorsReply);
rpc UploadFloors(UploadFloorsRequest) returns (UploadFloorsReply); rpc UploadFloors(UploadFloorsRequest) returns (UploadFloorsReply);
rpc MarkObjectsSeen(MarkObjectsSeenRequest) returns (MarkObjectsSeenReply);
rpc FetchStatistics(StatisticsRequest) returns (StatisticsReply); rpc FetchStatistics(StatisticsRequest) returns (StatisticsReply);
} }
@ -24,6 +25,7 @@ message UploadFloorsRequest {
message UploadFloorsReply { message UploadFloorsReply {
bool success = 1; bool success = 1;
repeated PalaceObject objects = 2;
} }
message StatisticsRequest { message StatisticsRequest {
@ -45,6 +47,18 @@ message PalaceObject {
float x = 2; float x = 2;
float y = 3; float y = 3;
float z = 4; 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 { enum ObjectType {