Db: Fix various things around local persistence/net interactions
This commit is contained in:
parent
802e0c4cde
commit
d5dc55a0c4
@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Objects.Types;
|
|||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Configuration;
|
using Pal.Client.Configuration;
|
||||||
using Pal.Client.Extensions;
|
using Pal.Client.Extensions;
|
||||||
using Pal.Client.Floors;
|
using Pal.Client.Floors;
|
||||||
@ -25,6 +26,7 @@ namespace Pal.Client.DependencyInjection
|
|||||||
internal sealed class FrameworkService : IDisposable
|
internal sealed class FrameworkService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<FrameworkService> _logger;
|
||||||
private readonly Framework _framework;
|
private readonly Framework _framework;
|
||||||
private readonly ConfigurationManager _configurationManager;
|
private readonly ConfigurationManager _configurationManager;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
@ -42,6 +44,7 @@ namespace Pal.Client.DependencyInjection
|
|||||||
|
|
||||||
public FrameworkService(
|
public FrameworkService(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<FrameworkService> logger,
|
||||||
Framework framework,
|
Framework framework,
|
||||||
ConfigurationManager configurationManager,
|
ConfigurationManager configurationManager,
|
||||||
IPalacePalConfiguration configuration,
|
IPalacePalConfiguration configuration,
|
||||||
@ -54,6 +57,7 @@ namespace Pal.Client.DependencyInjection
|
|||||||
RemoteApi remoteApi)
|
RemoteApi remoteApi)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
@ -92,8 +96,11 @@ namespace Pal.Client.DependencyInjection
|
|||||||
|
|
||||||
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
||||||
{
|
{
|
||||||
|
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
||||||
|
if (oldTerritory != null)
|
||||||
|
oldTerritory.SyncState = ESyncState.NotAttempted;
|
||||||
|
|
||||||
_territoryState.LastTerritory = _clientState.TerritoryType;
|
_territoryState.LastTerritory = _clientState.TerritoryType;
|
||||||
_territoryState.TerritorySyncState = ESyncState.NotAttempted;
|
|
||||||
NextUpdateObjects.Clear();
|
NextUpdateObjects.Clear();
|
||||||
|
|
||||||
_floorService.ChangeTerritory(_territoryState.LastTerritory);
|
_floorService.ChangeTerritory(_territoryState.LastTerritory);
|
||||||
@ -106,11 +113,12 @@ namespace Pal.Client.DependencyInjection
|
|||||||
if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
|
if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_configuration.Mode == EMode.Online &&
|
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
|
||||||
_territoryState.TerritorySyncState == ESyncState.NotAttempted)
|
MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!;
|
||||||
|
if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted)
|
||||||
{
|
{
|
||||||
_territoryState.TerritorySyncState = ESyncState.Started;
|
memoryTerritory.SyncState = ESyncState.Started;
|
||||||
Task.Run(async () => await DownloadMarkersForTerritory(_territoryState.LastTerritory));
|
Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory));
|
||||||
}
|
}
|
||||||
|
|
||||||
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||||
@ -120,7 +128,6 @@ namespace Pal.Client.DependencyInjection
|
|||||||
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
|
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
|
||||||
GetRelevantGameObjects();
|
GetRelevantGameObjects();
|
||||||
|
|
||||||
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
|
|
||||||
HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
|
HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
|
||||||
|
|
||||||
if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
|
if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
|
||||||
@ -188,7 +195,7 @@ namespace Pal.Client.DependencyInjection
|
|||||||
private void UploadLocations()
|
private void UploadLocations()
|
||||||
{
|
{
|
||||||
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
||||||
if (memoryTerritory == null)
|
if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
|
List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
|
||||||
@ -296,10 +303,11 @@ namespace Pal.Client.DependencyInjection
|
|||||||
|
|
||||||
#region Up-/Download
|
#region Up-/Download
|
||||||
|
|
||||||
private async Task DownloadMarkersForTerritory(ushort territoryId)
|
private async Task DownloadLocationsForTerritory(ushort territoryId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
|
||||||
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
|
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
|
||||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||||
{
|
{
|
||||||
@ -319,6 +327,8 @@ namespace Pal.Client.DependencyInjection
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
|
||||||
|
locationsToUpload.Count, (ETerritoryType)territoryId);
|
||||||
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
|
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
|
||||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||||
{
|
{
|
||||||
@ -339,6 +349,8 @@ namespace Pal.Client.DependencyInjection
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server",
|
||||||
|
locationsToUpdate.Count, (ETerritoryType)territoryId);
|
||||||
var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
|
var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
|
||||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,6 @@ namespace Pal.Client.DependencyInjection
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ushort LastTerritory { get; set; }
|
public ushort LastTerritory { get; set; }
|
||||||
public ESyncState TerritorySyncState { get; set; }
|
|
||||||
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
|
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
|
||||||
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
|
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
|
||||||
|
|
||||||
|
@ -20,5 +20,10 @@ namespace Pal.Client.Floors
|
|||||||
{
|
{
|
||||||
return !Equals(a, b);
|
return !Equals(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"EphemeralLocation(Position={Position}, Type={Type})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ namespace Pal.Client.Floors
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (markAsSeen.Count > 0)
|
if (markAsSeen.Count > 0)
|
||||||
new MarkAsSeen(_serviceScopeFactory, territory, markAsSeen).Start();
|
new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start();
|
||||||
|
|
||||||
if (newLocations.Count > 0)
|
if (newLocations.Count > 0)
|
||||||
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
|
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
|
||||||
|
@ -22,8 +22,8 @@ namespace Pal.Client.Floors
|
|||||||
{
|
{
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
||||||
Hoard,
|
|
||||||
Trap,
|
Trap,
|
||||||
|
Hoard,
|
||||||
|
|
||||||
SilverCoffer,
|
SilverCoffer,
|
||||||
}
|
}
|
||||||
@ -52,5 +52,15 @@ namespace Pal.Client.Floors
|
|||||||
_ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null)
|
_ => 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Pal.Client.Configuration;
|
using Pal.Client.Configuration;
|
||||||
|
using Pal.Client.Scheduled;
|
||||||
using Pal.Common;
|
using Pal.Common;
|
||||||
|
|
||||||
namespace Pal.Client.Floors
|
namespace Pal.Client.Floors
|
||||||
@ -18,7 +19,8 @@ namespace Pal.Client.Floors
|
|||||||
|
|
||||||
public ETerritoryType TerritoryType { get; }
|
public ETerritoryType TerritoryType { get; }
|
||||||
public bool IsReady { get; set; }
|
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 ConcurrentBag<PersistentLocation> Locations { get; } = new();
|
||||||
public object LockObj { get; } = new();
|
public object LockObj { get; } = new();
|
||||||
|
@ -44,5 +44,10 @@ namespace Pal.Client.Floors
|
|||||||
{
|
{
|
||||||
return !Equals(a, b);
|
return !Equals(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"PersistentLocation(Position={Position}, Type={Type})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using System.Threading.Tasks;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Database;
|
using Pal.Client.Database;
|
||||||
|
|
||||||
namespace Pal.Client.Floors.Tasks
|
namespace Pal.Client.Floors.Tasks
|
||||||
{
|
{
|
||||||
internal abstract class DbTask
|
internal abstract class DbTask<T>
|
||||||
|
where T : DbTask<T>
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
|
||||||
@ -18,12 +21,13 @@ namespace Pal.Client.Floors.Tasks
|
|||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
ILogger<T> logger = scope.ServiceProvider.GetRequiredService<ILogger<T>>();
|
||||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,12 @@ using System.Linq;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Database;
|
using Pal.Client.Database;
|
||||||
|
|
||||||
namespace Pal.Client.Floors.Tasks
|
namespace Pal.Client.Floors.Tasks
|
||||||
{
|
{
|
||||||
internal sealed class LoadTerritory : DbTask
|
internal sealed class LoadTerritory : DbTask<LoadTerritory>
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
private readonly MemoryTerritory _territory;
|
||||||
|
|
||||||
@ -18,19 +19,27 @@ namespace Pal.Client.Floors.Tasks
|
|||||||
_territory = territory;
|
_territory = territory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext)
|
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger)
|
||||||
{
|
{
|
||||||
lock (_territory.LockObj)
|
lock (_territory.LockObj)
|
||||||
{
|
{
|
||||||
if (_territory.IsReady)
|
if (_territory.IsReady)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Territory {Territory} is already loaded", _territory.TerritoryType);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType);
|
||||||
List<ClientLocation> locations = dbContext.Locations
|
List<ClientLocation> locations = dbContext.Locations
|
||||||
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
|
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
|
||||||
.Include(o => o.ImportedBy)
|
.Include(o => o.ImportedBy)
|
||||||
.Include(o => o.RemoteEncounters)
|
.Include(o => o.RemoteEncounters)
|
||||||
|
.AsSplitQuery()
|
||||||
.ToList();
|
.ToList();
|
||||||
_territory.Initialize(locations.Select(ToMemoryLocation));
|
_territory.Initialize(locations.Select(ToMemoryLocation));
|
||||||
|
|
||||||
|
logger.LogInformation("Loaded {Count} locations for territory {Territory}", locations.Count,
|
||||||
|
_territory.TerritoryType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,16 +2,17 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Database;
|
using Pal.Client.Database;
|
||||||
|
|
||||||
namespace Pal.Client.Floors.Tasks
|
namespace Pal.Client.Floors.Tasks
|
||||||
{
|
{
|
||||||
internal sealed class MarkAsSeen : DbTask
|
internal sealed class MarkLocalSeen : DbTask<MarkLocalSeen>
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
private readonly MemoryTerritory _territory;
|
||||||
private readonly IReadOnlyList<PersistentLocation> _locations;
|
private readonly IReadOnlyList<PersistentLocation> _locations;
|
||||||
|
|
||||||
public MarkAsSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
||||||
IReadOnlyList<PersistentLocation> locations)
|
IReadOnlyList<PersistentLocation> locations)
|
||||||
: base(serviceScopeFactory)
|
: base(serviceScopeFactory)
|
||||||
{
|
{
|
||||||
@ -19,10 +20,12 @@ namespace Pal.Client.Floors.Tasks
|
|||||||
_locations = locations;
|
_locations = locations;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext)
|
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
|
||||||
{
|
{
|
||||||
lock (_territory.LockObj)
|
lock (_territory.LockObj)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count,
|
||||||
|
_territory.TerritoryType);
|
||||||
dbContext.Locations
|
dbContext.Locations
|
||||||
.Where(loc => _locations.Any(l => l.LocalId == loc.LocalId))
|
.Where(loc => _locations.Any(l => l.LocalId == loc.LocalId))
|
||||||
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));
|
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));
|
@ -1,11 +1,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Database;
|
using Pal.Client.Database;
|
||||||
|
|
||||||
namespace Pal.Client.Floors.Tasks
|
namespace Pal.Client.Floors.Tasks
|
||||||
{
|
{
|
||||||
internal sealed class MarkRemoteSeen : DbTask
|
internal sealed class MarkRemoteSeen : DbTask<MarkRemoteSeen>
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
private readonly MemoryTerritory _territory;
|
||||||
private readonly IReadOnlyList<PersistentLocation> _locations;
|
private readonly IReadOnlyList<PersistentLocation> _locations;
|
||||||
@ -22,16 +24,26 @@ namespace Pal.Client.Floors.Tasks
|
|||||||
_accountId = accountId;
|
_accountId = accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext)
|
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
|
||||||
{
|
{
|
||||||
lock (_territory.LockObj)
|
lock (_territory.LockObj)
|
||||||
{
|
{
|
||||||
List<ClientLocation> locationsToUpdate = dbContext.Locations
|
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
|
||||||
.Where(loc => _locations.Any(l =>
|
_locations.Count, _accountId, _territory.TerritoryType);
|
||||||
l.LocalId == loc.LocalId && loc.RemoteEncounters.All(r => r.AccountId != _accountId)))
|
|
||||||
.ToList();
|
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)
|
foreach (var clientLocation in locationsToUpdate)
|
||||||
|
{
|
||||||
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
|
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
|
||||||
|
}
|
||||||
|
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Pal.Client.Database;
|
using Pal.Client.Database;
|
||||||
using Pal.Common;
|
using Pal.Common;
|
||||||
|
|
||||||
namespace Pal.Client.Floors.Tasks
|
namespace Pal.Client.Floors.Tasks
|
||||||
{
|
{
|
||||||
internal sealed class SaveNewLocations : DbTask
|
internal sealed class SaveNewLocations : DbTask<SaveNewLocations>
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
private readonly MemoryTerritory _territory;
|
||||||
private readonly List<PersistentLocation> _newLocations;
|
private readonly List<PersistentLocation> _newLocations;
|
||||||
@ -20,16 +21,22 @@ namespace Pal.Client.Floors.Tasks
|
|||||||
_newLocations = newLocations;
|
_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)
|
List<PersistentLocation> locations)
|
||||||
{
|
{
|
||||||
lock (territory.LockObj)
|
lock (territory.LockObj)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
|
||||||
|
territory.TerritoryType);
|
||||||
|
|
||||||
Dictionary<PersistentLocation, ClientLocation> mapping =
|
Dictionary<PersistentLocation, ClientLocation> mapping =
|
||||||
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
|
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
|
||||||
dbContext.Locations.AddRange(mapping.Values);
|
dbContext.Locations.AddRange(mapping.Values);
|
||||||
|
@ -11,13 +11,15 @@ using System.Threading.Tasks;
|
|||||||
using Pal.Client.Extensions;
|
using Pal.Client.Extensions;
|
||||||
using Pal.Client.Properties;
|
using Pal.Client.Properties;
|
||||||
using Pal.Client.Configuration;
|
using Pal.Client.Configuration;
|
||||||
using Pal.Client.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Pal.Client.Net
|
namespace Pal.Client.Net
|
||||||
{
|
{
|
||||||
internal partial class RemoteApi
|
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");
|
using IDisposable? logScope = _logger.BeginScope("TryConnect");
|
||||||
|
|
||||||
@ -27,7 +29,8 @@ namespace Pal.Client.Net
|
|||||||
return (false, Localization.ConnectionError_NotOnline);
|
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();
|
Dispose();
|
||||||
|
|
||||||
@ -48,97 +51,122 @@ namespace Pal.Client.Net
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
_logger.LogTrace("Acquiring connect lock");
|
||||||
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
await connectLock.WaitAsync(cancellationToken);
|
||||||
if (configuredAccount == null)
|
_logger.LogTrace("Obtained connect lock");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
|
var accountClient = new AccountService.AccountServiceClient(_channel);
|
||||||
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
|
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
||||||
if (createAccountReply.Success)
|
if (configuredAccount == null)
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
|
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
|
||||||
throw new InvalidOperationException("invalid account id returned");
|
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(),
|
||||||
|
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
||||||
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId);
|
cancellationToken: cancellationToken);
|
||||||
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
|
if (createAccountReply.Success)
|
||||||
|
|
||||||
_configurationManager.Save(_configuration);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
|
|
||||||
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
|
|
||||||
{
|
{
|
||||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
|
||||||
_warnedAboutUpgrade = true;
|
throw new InvalidOperationException("invalid account id returned");
|
||||||
}
|
|
||||||
return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId);
|
||||||
|
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
|
||||||
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
||||||
if (configuredAccount == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("No account to login with");
|
|
||||||
return (false, Localization.ConnectionError_CreateAccountReturnedNoId);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
if (loginReply.Success)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Login successful with account id: {AccountId}", configuredAccount.AccountId.ToPartialId());
|
|
||||||
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
|
||||||
|
|
||||||
bool save = configuredAccount.EncryptIfNeeded();
|
|
||||||
|
|
||||||
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
|
|
||||||
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
|
|
||||||
{
|
|
||||||
configuredAccount.CachedRoles = newRoles;
|
|
||||||
save = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (save)
|
|
||||||
_configurationManager.Save(_configuration);
|
_configurationManager.Save(_configuration);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
_logger.LogError("Login failed with error {Error}", loginReply.Error);
|
|
||||||
_loginInfo = new LoginInfo(null);
|
|
||||||
if (loginReply.Error == LoginError.InvalidAccountId)
|
|
||||||
{
|
{
|
||||||
_configuration.RemoveAccount(RemoteUrl);
|
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
|
||||||
_configurationManager.Save(_configuration);
|
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
|
||||||
if (retry)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Attempting connection retry without account id");
|
_chat.Error(Localization.ConnectionError_OldVersion);
|
||||||
return await TryConnect(cancellationToken, retry: false);
|
_warnedAboutUpgrade = true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
return (false, Localization.ConnectionError_InvalidAccountId);
|
return (false,
|
||||||
|
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
|
||||||
}
|
}
|
||||||
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)
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||||
|
if (configuredAccount == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No account to login with");
|
||||||
|
return (false, Localization.ConnectionError_CreateAccountReturnedNoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (loginReply.Success)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Login successful with account id: {AccountId}",
|
||||||
|
configuredAccount.AccountId.ToPartialId());
|
||||||
|
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
||||||
|
|
||||||
|
bool save = configuredAccount.EncryptIfNeeded();
|
||||||
|
|
||||||
|
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
|
||||||
|
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
|
||||||
|
{
|
||||||
|
configuredAccount.CachedRoles = newRoles;
|
||||||
|
save = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (save)
|
||||||
|
_configurationManager.Save(_configuration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("Login failed with error {Error}", loginReply.Error);
|
||||||
|
_loginInfo = new LoginInfo(null);
|
||||||
|
if (loginReply.Error == LoginError.InvalidAccountId)
|
||||||
|
{
|
||||||
|
_configuration.RemoveAccount(RemoteUrl);
|
||||||
|
_configurationManager.Save(_configuration);
|
||||||
|
if (retry)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Attempting connection retry without account id");
|
||||||
|
return await TryConnect(cancellationToken, retry: false);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
return (false, Localization.ConnectionError_LoginReturnedNoToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return (true, string.Empty);
|
||||||
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, _loginInfo.IsExpired);
|
_logger.LogTrace("Releasing connectLock");
|
||||||
return (false, Localization.ConnectionError_LoginReturnedNoToken);
|
connectLock.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
return (true, string.Empty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> Connect(CancellationToken cancellationToken)
|
private async Task<bool> Connect(CancellationToken cancellationToken)
|
||||||
@ -159,7 +187,8 @@ namespace Pal.Client.Net
|
|||||||
|
|
||||||
_logger.LogInformation("Connection established, trying to verify auth token");
|
_logger.LogInformation("Connection established, trying to verify auth token");
|
||||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
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.");
|
_logger.LogInformation("Verification returned no errors.");
|
||||||
return Localization.ConnectionSuccessful;
|
return Localization.ConnectionSuccessful;
|
||||||
@ -182,7 +211,10 @@ namespace Pal.Client.Net
|
|||||||
public bool IsLoggedIn { get; }
|
public bool IsLoggedIn { get; }
|
||||||
public string? AuthToken { get; }
|
public string? AuthToken { get; }
|
||||||
public JwtClaims? Claims { 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 IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
public bool IsValid => IsLoggedIn && !IsExpired;
|
public bool IsValid => IsLoggedIn && !IsExpired;
|
||||||
|
@ -36,7 +36,7 @@ namespace Pal.Client.Net
|
|||||||
};
|
};
|
||||||
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
|
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
|
||||||
{
|
{
|
||||||
Type = (ObjectType)m.Type,
|
Type = m.Type.ToObjectType(),
|
||||||
X = m.Position.X,
|
X = m.Position.X,
|
||||||
Y = m.Position.Y,
|
Y = m.Position.Y,
|
||||||
Z = m.Position.Z
|
Z = m.Position.Z
|
||||||
|
@ -11,7 +11,7 @@ namespace Pal.Client.Net
|
|||||||
internal sealed partial class RemoteApi : IDisposable
|
internal sealed partial class RemoteApi : IDisposable
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public const string RemoteUrl = "http://localhost:5145";
|
public const string RemoteUrl = "http://localhost:5415";
|
||||||
#else
|
#else
|
||||||
//public const string RemoteUrl = "https://pal.liza.sh";
|
//public const string RemoteUrl = "https://pal.liza.sh";
|
||||||
#endif
|
#endif
|
||||||
|
@ -36,7 +36,7 @@ namespace Pal.Client
|
|||||||
private readonly IServiceScope _rootScope;
|
private readonly IServiceScope _rootScope;
|
||||||
private readonly DependencyInjectionLoader _loader;
|
private readonly DependencyInjectionLoader _loader;
|
||||||
|
|
||||||
private Action? _loginAction = null;
|
private Action? _loginAction;
|
||||||
|
|
||||||
public Plugin(
|
public Plugin(
|
||||||
DalamudPluginInterface pluginInterface,
|
DalamudPluginInterface pluginInterface,
|
||||||
|
@ -9,6 +9,7 @@ using Pal.Client.Extensions;
|
|||||||
using Pal.Client.Floors;
|
using Pal.Client.Floors;
|
||||||
using Pal.Client.Floors.Tasks;
|
using Pal.Client.Floors.Tasks;
|
||||||
using Pal.Client.Net;
|
using Pal.Client.Net;
|
||||||
|
using Pal.Common;
|
||||||
|
|
||||||
namespace Pal.Client.Scheduled
|
namespace Pal.Client.Scheduled
|
||||||
{
|
{
|
||||||
@ -47,12 +48,21 @@ namespace Pal.Client.Scheduled
|
|||||||
{
|
{
|
||||||
recreateLayout = true;
|
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
|
try
|
||||||
{
|
{
|
||||||
var remoteMarkers = queued.Locations;
|
var remoteMarkers = queued.Locations;
|
||||||
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
|
if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0)
|
||||||
if (memoryTerritory != null && _configuration.Mode == EMode.Online && queued.Success &&
|
|
||||||
remoteMarkers.Count > 0)
|
|
||||||
{
|
{
|
||||||
switch (queued.Type)
|
switch (queued.Type)
|
||||||
{
|
{
|
||||||
@ -117,16 +127,17 @@ namespace Pal.Client.Scheduled
|
|||||||
if (queued.Type == SyncType.Download)
|
if (queued.Type == SyncType.Download)
|
||||||
{
|
{
|
||||||
if (queued.Success)
|
if (queued.Success)
|
||||||
_territoryState.TerritorySyncState = ESyncState.Complete;
|
memoryTerritory.SyncState = ESyncState.Complete;
|
||||||
else
|
else
|
||||||
_territoryState.TerritorySyncState = ESyncState.Failed;
|
memoryTerritory.SyncState = ESyncState.Failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(e, "Sync failed for territory {Territory}", (ETerritoryType)queued.TerritoryType);
|
||||||
_debugState.SetFromException(e);
|
_debugState.SetFromException(e);
|
||||||
if (queued.Type == SyncType.Download)
|
if (queued.Type == SyncType.Download)
|
||||||
_territoryState.TerritorySyncState = ESyncState.Failed;
|
memoryTerritory.SyncState = ESyncState.Failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,12 +375,12 @@ namespace Pal.Client.Windows
|
|||||||
{
|
{
|
||||||
if (_territoryState.IsInDeepDungeon())
|
if (_territoryState.IsInDeepDungeon())
|
||||||
{
|
{
|
||||||
|
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
||||||
ImGui.Text($"You are in a deep dungeon, territory type {_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.Text($"{_debugState.DebugMessage}");
|
||||||
|
|
||||||
ImGui.Indent();
|
ImGui.Indent();
|
||||||
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
|
||||||
if (memoryTerritory != null)
|
if (memoryTerritory != null)
|
||||||
{
|
{
|
||||||
if (_trapConfig.Show)
|
if (_trapConfig.Show)
|
||||||
|
Loading…
Reference in New Issue
Block a user