Db: Fix various things around local persistence/net interactions

This commit is contained in:
Liza 2023-02-22 17:21:48 +01:00
parent 802e0c4cde
commit d5dc55a0c4
18 changed files with 235 additions and 124 deletions

View File

@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Configuration;
using Pal.Client.Extensions;
using Pal.Client.Floors;
@ -25,6 +26,7 @@ namespace Pal.Client.DependencyInjection
internal sealed class FrameworkService : IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<FrameworkService> _logger;
private readonly Framework _framework;
private readonly ConfigurationManager _configurationManager;
private readonly IPalacePalConfiguration _configuration;
@ -42,6 +44,7 @@ namespace Pal.Client.DependencyInjection
public FrameworkService(
IServiceProvider serviceProvider,
ILogger<FrameworkService> logger,
Framework framework,
ConfigurationManager configurationManager,
IPalacePalConfiguration configuration,
@ -54,6 +57,7 @@ namespace Pal.Client.DependencyInjection
RemoteApi remoteApi)
{
_serviceProvider = serviceProvider;
_logger = logger;
_framework = framework;
_configurationManager = configurationManager;
_configuration = configuration;
@ -92,8 +96,11 @@ namespace Pal.Client.DependencyInjection
if (_territoryState.LastTerritory != _clientState.TerritoryType)
{
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (oldTerritory != null)
oldTerritory.SyncState = ESyncState.NotAttempted;
_territoryState.LastTerritory = _clientState.TerritoryType;
_territoryState.TerritorySyncState = ESyncState.NotAttempted;
NextUpdateObjects.Clear();
_floorService.ChangeTerritory(_territoryState.LastTerritory);
@ -106,11 +113,12 @@ namespace Pal.Client.DependencyInjection
if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
return;
if (_configuration.Mode == EMode.Online &&
_territoryState.TerritorySyncState == ESyncState.NotAttempted)
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!;
if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted)
{
_territoryState.TerritorySyncState = ESyncState.Started;
Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory));
memoryTerritory.SyncState = ESyncState.Started;
Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory));
}
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
@ -120,7 +128,6 @@ namespace Pal.Client.DependencyInjection
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
GetRelevantGameObjects();
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
@ -188,7 +195,7 @@ namespace Pal.Client.DependencyInjection
private void UploadLocations()
{
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null)
if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete)
return;
List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
@ -296,10 +303,11 @@ namespace Pal.Client.DependencyInjection
#region Up-/Download
private async Task DownloadMarkersForTerritory(ushort territoryId)
private async Task DownloadLocationsForTerritory(ushort territoryId)
{
try
{
_logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
LateEventQueue.Enqueue(new QueuedSyncResponse
{
@ -319,6 +327,8 @@ namespace Pal.Client.DependencyInjection
{
try
{
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
locationsToUpload.Count, (ETerritoryType)territoryId);
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
LateEventQueue.Enqueue(new QueuedSyncResponse
{
@ -339,6 +349,8 @@ namespace Pal.Client.DependencyInjection
{
try
{
_logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server",
locationsToUpdate.Count, (ETerritoryType)territoryId);
var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
LateEventQueue.Enqueue(new QueuedSyncResponse
{

View File

@ -17,7 +17,6 @@ namespace Pal.Client.DependencyInjection
}
public ushort LastTerritory { get; set; }
public ESyncState TerritorySyncState { get; set; }
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;

View File

@ -20,5 +20,10 @@ namespace Pal.Client.Floors
{
return !Equals(a, b);
}
public override string ToString()
{
return $"EphemeralLocation(Position={Position}, Type={Type})";
}
}
}

View File

@ -111,7 +111,7 @@ namespace Pal.Client.Floors
}
if (markAsSeen.Count > 0)
new MarkAsSeen(_serviceScopeFactory, territory, markAsSeen).Start();
new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start();
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();

View File

@ -22,8 +22,8 @@ namespace Pal.Client.Floors
{
Unknown,
Hoard,
Trap,
Hoard,
SilverCoffer,
}
@ -52,5 +52,15 @@ namespace Pal.Client.Floors
_ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null)
};
}
public static ObjectType ToObjectType(this MemoryLocation.EType type)
{
return type switch
{
MemoryLocation.EType.Trap => ObjectType.Trap,
MemoryLocation.EType.Hoard => ObjectType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Pal.Client.Configuration;
using Pal.Client.Scheduled;
using Pal.Common;
namespace Pal.Client.Floors
@ -18,7 +19,8 @@ namespace Pal.Client.Floors
public ETerritoryType TerritoryType { get; }
public bool IsReady { get; set; }
public bool IsLoading { get; set; }
public bool IsLoading { get; set; } // probably merge this with IsReady as enum
public ESyncState SyncState { get; set; } = ESyncState.NotAttempted;
public ConcurrentBag<PersistentLocation> Locations { get; } = new();
public object LockObj { get; } = new();

View File

@ -44,5 +44,10 @@ namespace Pal.Client.Floors
{
return !Equals(a, b);
}
public override string ToString()
{
return $"PersistentLocation(Position={Position}, Type={Type})";
}
}
}

View File

@ -1,10 +1,13 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal abstract class DbTask
internal abstract class DbTask<T>
where T : DbTask<T>
{
private readonly IServiceScopeFactory _serviceScopeFactory;
@ -18,12 +21,13 @@ namespace Pal.Client.Floors.Tasks
Task.Run(() =>
{
using var scope = _serviceScopeFactory.CreateScope();
ILogger<T> logger = scope.ServiceProvider.GetRequiredService<ILogger<T>>();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
Run(dbContext);
Run(dbContext, logger);
});
}
protected abstract void Run(PalClientContext dbContext);
protected abstract void Run(PalClientContext dbContext, ILogger<T> logger);
}
}

View File

@ -4,11 +4,12 @@ using System.Linq;
using System.Numerics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class LoadTerritory : DbTask
internal sealed class LoadTerritory : DbTask<LoadTerritory>
{
private readonly MemoryTerritory _territory;
@ -18,19 +19,27 @@ namespace Pal.Client.Floors.Tasks
_territory = territory;
}
protected override void Run(PalClientContext dbContext)
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger)
{
lock (_territory.LockObj)
{
if (_territory.IsReady)
{
logger.LogInformation("Territory {Territory} is already loaded", _territory.TerritoryType);
return;
}
logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType);
List<ClientLocation> locations = dbContext.Locations
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
.Include(o => o.ImportedBy)
.Include(o => o.RemoteEncounters)
.AsSplitQuery()
.ToList();
_territory.Initialize(locations.Select(ToMemoryLocation));
logger.LogInformation("Loaded {Count} locations for territory {Territory}", locations.Count,
_territory.TerritoryType);
}
}

View File

@ -2,16 +2,17 @@
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class MarkAsSeen : DbTask
internal sealed class MarkLocalSeen : DbTask<MarkLocalSeen>
{
private readonly MemoryTerritory _territory;
private readonly IReadOnlyList<PersistentLocation> _locations;
public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
IReadOnlyList<PersistentLocation> locations)
: base(serviceScopeFactory)
{
@ -19,10 +20,12 @@ namespace Pal.Client.Floors.Tasks
_locations = locations;
}
protected override void Run(PalClientContext dbContext)
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
{
lock (_territory.LockObj)
{
logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count,
_territory.TerritoryType);
dbContext.Locations
.Where(loc => _locations.Any(l => l.LocalId == loc.LocalId))
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));

View File

@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks
{
internal sealed class MarkRemoteSeen : DbTask
internal sealed class MarkRemoteSeen : DbTask<MarkRemoteSeen>
{
private readonly MemoryTerritory _territory;
private readonly IReadOnlyList<PersistentLocation> _locations;
@ -22,16 +24,26 @@ namespace Pal.Client.Floors.Tasks
_accountId = accountId;
}
protected override void Run(PalClientContext dbContext)
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
{
lock (_territory.LockObj)
{
List<ClientLocation> locationsToUpdate = dbContext.Locations
.Where(loc => _locations.Any(l =>
l.LocalId == loc.LocalId && loc.RemoteEncounters.All(r => r.AccountId != _accountId)))
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
_locations.Count, _accountId, _territory.TerritoryType);
List<int> locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast<int>().ToList();
List<ClientLocation> locationsToUpdate =
dbContext.Locations
.Include(x => x.RemoteEncounters)
.Where(x => locationIds.Contains(x.LocalId))
.ToList()
.Where(x => x.RemoteEncounters.All(encounter => encounter.AccountId != _accountId))
.ToList();
foreach (var clientLocation in locationsToUpdate)
{
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
}
dbContext.SaveChanges();
}
}

View File

@ -2,12 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Pal.Client.Database;
using Pal.Common;
namespace Pal.Client.Floors.Tasks
{
internal sealed class SaveNewLocations : DbTask
internal sealed class SaveNewLocations : DbTask<SaveNewLocations>
{
private readonly MemoryTerritory _territory;
private readonly List<PersistentLocation> _newLocations;
@ -20,16 +21,22 @@ namespace Pal.Client.Floors.Tasks
_newLocations = newLocations;
}
protected override void Run(PalClientContext dbContext)
protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger)
{
Run(_territory, dbContext, _newLocations);
Run(_territory, dbContext, logger, _newLocations);
}
public static void Run(MemoryTerritory territory, PalClientContext dbContext,
public static void Run<T>(
MemoryTerritory territory,
PalClientContext dbContext,
ILogger<T> logger,
List<PersistentLocation> locations)
{
lock (territory.LockObj)
{
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
territory.TerritoryType);
Dictionary<PersistentLocation, ClientLocation> mapping =
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
dbContext.Locations.AddRange(mapping.Values);

View File

@ -11,13 +11,15 @@ using System.Threading.Tasks;
using Pal.Client.Extensions;
using Pal.Client.Properties;
using Pal.Client.Configuration;
using Pal.Client.DependencyInjection;
namespace Pal.Client.Net
{
internal partial class RemoteApi
{
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true)
private readonly SemaphoreSlim connectLock = new(1, 1);
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
ILoggerFactory? loggerFactory = null, bool retry = true)
{
using IDisposable? logScope = _logger.BeginScope("TryConnect");
@ -27,7 +29,8 @@ namespace Pal.Client.Net
return (false, Localization.ConnectionError_NotOnline);
}
if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
if (_channel == null ||
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
{
Dispose();
@ -48,12 +51,20 @@ namespace Pal.Client.Net
cancellationToken.ThrowIfCancellationRequested();
_logger.LogTrace("Acquiring connect lock");
await connectLock.WaitAsync(cancellationToken);
_logger.LogTrace("Obtained connect lock");
try
{
var accountClient = new AccountService.AccountServiceClient(_channel);
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
if (configuredAccount == null)
{
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
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 (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
@ -72,7 +83,9 @@ namespace Pal.Client.Net
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
return (false,
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
}
}
@ -87,11 +100,17 @@ namespace Pal.Client.Net
if (!_loginInfo.IsValid)
{
_logger.LogInformation("Logging in with account id {AccountId}", configuredAccount.AccountId.ToPartialId());
LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = configuredAccount.AccountId.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
_logger.LogInformation("Logging in with account id {AccountId}",
configuredAccount.AccountId.ToPartialId());
LoginReply loginReply = await accountClient.LoginAsync(
new LoginRequest { AccountId = configuredAccount.AccountId.ToString() },
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
cancellationToken: cancellationToken);
if (loginReply.Success)
{
_logger.LogInformation("Login successful with account id: {AccountId}", configuredAccount.AccountId.ToPartialId());
_logger.LogInformation("Login successful with account id: {AccountId}",
configuredAccount.AccountId.ToPartialId());
_loginInfo = new LoginInfo(loginReply.AuthToken);
bool save = configuredAccount.EncryptIfNeeded();
@ -122,24 +141,33 @@ namespace Pal.Client.Net
else
return (false, Localization.ConnectionError_InvalidAccountId);
}
if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade)
{
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error));
}
}
if (!_loginInfo.IsValid)
{
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired);
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn,
_loginInfo.IsExpired);
return (false, Localization.ConnectionError_LoginReturnedNoToken);
}
cancellationToken.ThrowIfCancellationRequested();
return (true, string.Empty);
}
finally
{
_logger.LogTrace("Releasing connectLock");
connectLock.Release();
}
}
private async Task<bool> Connect(CancellationToken cancellationToken)
{
@ -159,7 +187,8 @@ namespace Pal.Client.Net
_logger.LogInformation("Connection established, trying to verify auth token");
var accountClient = new AccountService.AccountServiceClient(_channel);
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(),
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
_logger.LogInformation("Verification returned no errors.");
return Localization.ConnectionSuccessful;
@ -182,7 +211,10 @@ namespace Pal.Client.Net
public bool IsLoggedIn { get; }
public string? AuthToken { get; }
public JwtClaims? Claims { get; }
private DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
private DateTimeOffset ExpiresAt =>
Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
public bool IsValid => IsLoggedIn && !IsExpired;

View File

@ -36,7 +36,7 @@ namespace Pal.Client.Net
};
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
{
Type = (ObjectType)m.Type,
Type = m.Type.ToObjectType(),
X = m.Position.X,
Y = m.Position.Y,
Z = m.Position.Z

View File

@ -11,7 +11,7 @@ namespace Pal.Client.Net
internal sealed partial class RemoteApi : IDisposable
{
#if DEBUG
public const string RemoteUrl = "http://localhost:5145";
public const string RemoteUrl = "http://localhost:5415";
#else
//public const string RemoteUrl = "https://pal.liza.sh";
#endif

View File

@ -36,7 +36,7 @@ namespace Pal.Client
private readonly IServiceScope _rootScope;
private readonly DependencyInjectionLoader _loader;
private Action? _loginAction = null;
private Action? _loginAction;
public Plugin(
DalamudPluginInterface pluginInterface,

View File

@ -9,6 +9,7 @@ using Pal.Client.Extensions;
using Pal.Client.Floors;
using Pal.Client.Floors.Tasks;
using Pal.Client.Net;
using Pal.Common;
namespace Pal.Client.Scheduled
{
@ -47,12 +48,21 @@ namespace Pal.Client.Scheduled
{
recreateLayout = true;
_logger.LogDebug(
"Sync response for territory {Territory} of type {Type}, success = {Success}, response objects = {Count}",
(ETerritoryType)queued.TerritoryType, queued.Type, queued.Success, queued.Locations.Count);
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
if (memoryTerritory == null)
{
_logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready",
(ETerritoryType)queued.TerritoryType);
return;
}
try
{
var remoteMarkers = queued.Locations;
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
if (memoryTerritory != null && _configuration.Mode == EMode.Online && queued.Success &&
remoteMarkers.Count > 0)
if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0)
{
switch (queued.Type)
{
@ -117,16 +127,17 @@ namespace Pal.Client.Scheduled
if (queued.Type == SyncType.Download)
{
if (queued.Success)
_territoryState.TerritorySyncState = ESyncState.Complete;
memoryTerritory.SyncState = ESyncState.Complete;
else
_territoryState.TerritorySyncState = ESyncState.Failed;
memoryTerritory.SyncState = ESyncState.Failed;
}
}
catch (Exception e)
{
_logger.LogError(e, "Sync failed for territory {Territory}", (ETerritoryType)queued.TerritoryType);
_debugState.SetFromException(e);
if (queued.Type == SyncType.Download)
_territoryState.TerritorySyncState = ESyncState.Failed;
memoryTerritory.SyncState = ESyncState.Failed;
}
}
}

View File

@ -375,12 +375,12 @@ namespace Pal.Client.Windows
{
if (_territoryState.IsInDeepDungeon())
{
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
ImGui.Text($"You are in a deep dungeon, territory type {_territoryState.LastTerritory}.");
ImGui.Text($"Sync State = {_territoryState.TerritorySyncState}");
ImGui.Text($"Sync State = {memoryTerritory?.SyncState.ToString() ?? "Unknown"}");
ImGui.Text($"{_debugState.DebugMessage}");
ImGui.Indent();
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory != null)
{
if (_trapConfig.Show)