diff --git a/.editorconfig b/.editorconfig index 6369c21..dfc497e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,7 +21,7 @@ resharper_indent_text = ZeroIndent csharp_style_expression_bodied_methods = true # namespaces -csharp_style_namespace_declarations = block_scoped +csharp_style_namespace_declarations = file_scoped # braces csharp_prefer_braces = when_multiline diff --git a/Pal.Client/Commands/ISubCommand.cs b/Pal.Client/Commands/ISubCommand.cs index c70cbed..d4e9521 100644 --- a/Pal.Client/Commands/ISubCommand.cs +++ b/Pal.Client/Commands/ISubCommand.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -namespace Pal.Client.Commands +namespace Pal.Client.Commands; + +public interface ISubCommand { - public interface ISubCommand - { - IReadOnlyDictionary> GetHandlers(); - } + IReadOnlyDictionary> GetHandlers(); } diff --git a/Pal.Client/Commands/PalConfigCommand.cs b/Pal.Client/Commands/PalConfigCommand.cs index e2f9432..76e477a 100644 --- a/Pal.Client/Commands/PalConfigCommand.cs +++ b/Pal.Client/Commands/PalConfigCommand.cs @@ -3,38 +3,37 @@ using System.Collections.Generic; using Pal.Client.Configuration; using Pal.Client.Windows; -namespace Pal.Client.Commands +namespace Pal.Client.Commands; + +internal class PalConfigCommand : ISubCommand { - internal class PalConfigCommand : ISubCommand + private readonly IPalacePalConfiguration _configuration; + private readonly AgreementWindow _agreementWindow; + private readonly ConfigWindow _configWindow; + + public PalConfigCommand( + IPalacePalConfiguration configuration, + AgreementWindow agreementWindow, + ConfigWindow configWindow) { - private readonly IPalacePalConfiguration _configuration; - private readonly AgreementWindow _agreementWindow; - private readonly ConfigWindow _configWindow; + _configuration = configuration; + _agreementWindow = agreementWindow; + _configWindow = configWindow; + } - public PalConfigCommand( - IPalacePalConfiguration configuration, - AgreementWindow agreementWindow, - ConfigWindow configWindow) + + public IReadOnlyDictionary> GetHandlers() + => new Dictionary> { - _configuration = configuration; - _agreementWindow = agreementWindow; - _configWindow = configWindow; - } + { "config", _ => Execute() }, + { "", _ => Execute() } + }; - - public IReadOnlyDictionary> GetHandlers() - => new Dictionary> - { - { "config", _ => Execute() }, - { "", _ => Execute() } - }; - - public void Execute() - { - if (_configuration.FirstUse) - _agreementWindow.IsOpen = true; - else - _configWindow.Toggle(); - } + public void Execute() + { + if (_configuration.FirstUse) + _agreementWindow.IsOpen = true; + else + _configWindow.Toggle(); } } diff --git a/Pal.Client/Commands/PalNearCommand.cs b/Pal.Client/Commands/PalNearCommand.cs index 4649bb5..4fe3f0c 100644 --- a/Pal.Client/Commands/PalNearCommand.cs +++ b/Pal.Client/Commands/PalNearCommand.cs @@ -7,57 +7,56 @@ using Pal.Client.Extensions; using Pal.Client.Floors; using Pal.Client.Rendering; -namespace Pal.Client.Commands +namespace Pal.Client.Commands; + +internal sealed class PalNearCommand : ISubCommand { - internal sealed class PalNearCommand : ISubCommand + private readonly Chat _chat; + private readonly ClientState _clientState; + private readonly TerritoryState _territoryState; + private readonly FloorService _floorService; + + public PalNearCommand(Chat chat, ClientState clientState, TerritoryState territoryState, + FloorService floorService) { - private readonly Chat _chat; - private readonly ClientState _clientState; - private readonly TerritoryState _territoryState; - private readonly FloorService _floorService; + _chat = chat; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + } - public PalNearCommand(Chat chat, ClientState clientState, TerritoryState territoryState, - FloorService floorService) + + public IReadOnlyDictionary> GetHandlers() + => new Dictionary> { - _chat = chat; - _clientState = clientState; - _territoryState = territoryState; - _floorService = floorService; - } + { "near", _ => DebugNearest(_ => true) }, + { "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) }, + { "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) }, + }; + private void DebugNearest(Predicate predicate) + { + if (!_territoryState.IsInDeepDungeon()) + return; - public IReadOnlyDictionary> GetHandlers() - => new Dictionary> - { - { "near", _ => DebugNearest(_ => true) }, - { "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) }, - { "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) }, - }; + var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType); + if (state == null) + return; - private void DebugNearest(Predicate predicate) - { - if (!_territoryState.IsInDeepDungeon()) - return; + var playerPosition = _clientState.LocalPlayer?.Position; + if (playerPosition == null) + return; + _chat.Message($"Your position: {playerPosition}"); - var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType); - if (state == null) - return; - - var playerPosition = _clientState.LocalPlayer?.Position; - if (playerPosition == null) - return; - _chat.Message($"Your position: {playerPosition}"); - - var nearbyMarkers = state.Locations - .Where(m => predicate(m)) - .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) - .Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() }) - .OrderBy(m => m.distance) - .Take(5) - .ToList(); - foreach (var nearbyMarker in nearbyMarkers) - _chat.UnformattedMessage( - $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); - } + var nearbyMarkers = state.Locations + .Where(m => predicate(m)) + .Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible) + .Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() }) + .OrderBy(m => m.distance) + .Take(5) + .ToList(); + foreach (var nearbyMarker in nearbyMarkers) + _chat.UnformattedMessage( + $"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); } } diff --git a/Pal.Client/Commands/PalStatsCommand.cs b/Pal.Client/Commands/PalStatsCommand.cs index 366f97f..f45d179 100644 --- a/Pal.Client/Commands/PalStatsCommand.cs +++ b/Pal.Client/Commands/PalStatsCommand.cs @@ -2,24 +2,23 @@ using System.Collections.Generic; using Pal.Client.DependencyInjection; -namespace Pal.Client.Commands +namespace Pal.Client.Commands; + +internal sealed class PalStatsCommand : ISubCommand { - internal sealed class PalStatsCommand : ISubCommand + private readonly StatisticsService _statisticsService; + + public PalStatsCommand(StatisticsService statisticsService) { - private readonly StatisticsService _statisticsService; - - public PalStatsCommand(StatisticsService statisticsService) - { - _statisticsService = statisticsService; - } - - public IReadOnlyDictionary> GetHandlers() - => new Dictionary> - { - { "stats", _ => Execute() }, - }; - - private void Execute() - => _statisticsService.ShowGlobalStatistics(); + _statisticsService = statisticsService; } + + public IReadOnlyDictionary> GetHandlers() + => new Dictionary> + { + { "stats", _ => Execute() }, + }; + + private void Execute() + => _statisticsService.ShowGlobalStatistics(); } diff --git a/Pal.Client/Commands/PalTestConnectionCommand.cs b/Pal.Client/Commands/PalTestConnectionCommand.cs index eb15b14..603d48f 100644 --- a/Pal.Client/Commands/PalTestConnectionCommand.cs +++ b/Pal.Client/Commands/PalTestConnectionCommand.cs @@ -3,28 +3,27 @@ using System.Collections.Generic; using ECommons.Schedulers; using Pal.Client.Windows; -namespace Pal.Client.Commands +namespace Pal.Client.Commands; + +internal sealed class PalTestConnectionCommand : ISubCommand { - internal sealed class PalTestConnectionCommand : ISubCommand + private readonly ConfigWindow _configWindow; + + public PalTestConnectionCommand(ConfigWindow configWindow) { - private readonly ConfigWindow _configWindow; + _configWindow = configWindow; + } - public PalTestConnectionCommand(ConfigWindow configWindow) + public IReadOnlyDictionary> GetHandlers() + => new Dictionary> { - _configWindow = configWindow; - } + { "test-connection", _ => Execute() }, + { "tc", _ => Execute() }, + }; - public IReadOnlyDictionary> GetHandlers() - => new Dictionary> - { - { "test-connection", _ => Execute() }, - { "tc", _ => Execute() }, - }; - - private void Execute() - { - _configWindow.IsOpen = true; - var _ = new TickScheduler(() => _configWindow.TestConnection()); - } + private void Execute() + { + _configWindow.IsOpen = true; + var _ = new TickScheduler(() => _configWindow.TestConnection()); } } diff --git a/Pal.Client/Configuration/AccountConfigurationV7.cs b/Pal.Client/Configuration/AccountConfigurationV7.cs index 97ea445..093fcce 100644 --- a/Pal.Client/Configuration/AccountConfigurationV7.cs +++ b/Pal.Client/Configuration/AccountConfigurationV7.cs @@ -4,146 +4,145 @@ using System.Security.Cryptography; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration; + +public sealed class AccountConfigurationV7 : IAccountConfiguration { - public sealed class AccountConfigurationV7 : IAccountConfiguration + private const int DefaultEntropyLength = 16; + + private static readonly ILogger _logger = + DependencyInjectionContext.LoggerProvider.CreateLogger(); + + [JsonConstructor] + public AccountConfigurationV7() { - private const int DefaultEntropyLength = 16; + } - private static readonly ILogger _logger = - DependencyInjectionContext.LoggerProvider.CreateLogger(); + public AccountConfigurationV7(string server, Guid accountId) + { + Server = server; + (EncryptedId, Entropy, Format) = EncryptAccountId(accountId); + } - [JsonConstructor] - public AccountConfigurationV7() + [Obsolete("for V1 import")] + public AccountConfigurationV7(string server, string accountId) + { + Server = server; + + if (accountId.StartsWith("s:")) { + EncryptedId = accountId.Substring(2); + Entropy = ConfigurationData.FixedV1Entropy; + Format = EFormat.UseProtectedData; + EncryptIfNeeded(); } + else if (Guid.TryParse(accountId, out Guid guid)) + (EncryptedId, Entropy, Format) = EncryptAccountId(guid); + else + throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}"); + } - public AccountConfigurationV7(string server, Guid accountId) + [JsonInclude] + [JsonRequired] + public EFormat Format { get; private set; } = EFormat.Unencrypted; + + /// + /// Depending on , this is either a Guid as string or a base64 encoded byte array. + /// + [JsonPropertyName("Id")] + [JsonInclude] + [JsonRequired] + public string EncryptedId { get; private set; } = null!; + + [JsonInclude] + public byte[]? Entropy { get; private set; } + + [JsonRequired] + public string Server { get; init; } = null!; + + [JsonIgnore] public bool IsUsable => DecryptAccountId() != null; + + [JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read"); + + public List CachedRoles { get; set; } = new(); + + private Guid? DecryptAccountId() + { + if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi) { - Server = server; - (EncryptedId, Entropy, Format) = EncryptAccountId(accountId); - } - - [Obsolete("for V1 import")] - public AccountConfigurationV7(string server, string accountId) - { - Server = server; - - if (accountId.StartsWith("s:")) + try { - EncryptedId = accountId.Substring(2); - Entropy = ConfigurationData.FixedV1Entropy; - Format = EFormat.UseProtectedData; - EncryptIfNeeded(); + byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser); + return new Guid(guidBytes); } - else if (Guid.TryParse(accountId, out Guid guid)) - (EncryptedId, Entropy, Format) = EncryptAccountId(guid); - else - throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}"); - } - - [JsonInclude] - [JsonRequired] - public EFormat Format { get; private set; } = EFormat.Unencrypted; - - /// - /// Depending on , this is either a Guid as string or a base64 encoded byte array. - /// - [JsonPropertyName("Id")] - [JsonInclude] - [JsonRequired] - public string EncryptedId { get; private set; } = null!; - - [JsonInclude] - public byte[]? Entropy { get; private set; } - - [JsonRequired] - public string Server { get; init; } = null!; - - [JsonIgnore] public bool IsUsable => DecryptAccountId() != null; - - [JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read"); - - public List CachedRoles { get; set; } = new(); - - private Guid? DecryptAccountId() - { - if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi) + catch (Exception e) { - try - { - byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser); - return new Guid(guidBytes); - } - catch (Exception e) - { - _logger.LogTrace(e, "Could not load account id {Id}", EncryptedId); - return null; - } - } - else if (Format == EFormat.Unencrypted) - return Guid.Parse(EncryptedId); - else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi) - return Guid.Parse(EncryptedId); - else + _logger.LogTrace(e, "Could not load account id {Id}", EncryptedId); return null; - } - - private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g) - { - if (!ConfigurationData.SupportsDpapi) - return (g.ToString(), null, EFormat.ProtectedDataUnsupported); - else - { - try - { - byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength); - byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); - return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); - } - catch (Exception) - { - return (g.ToString(), null, EFormat.Unencrypted); - } } } + else if (Format == EFormat.Unencrypted) + return Guid.Parse(EncryptedId); + else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi) + return Guid.Parse(EncryptedId); + else + return null; + } - public bool EncryptIfNeeded() + private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g) + { + if (!ConfigurationData.SupportsDpapi) + return (g.ToString(), null, EFormat.ProtectedDataUnsupported); + else { - if (Format == EFormat.Unencrypted) + try { - var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId)); - if (newFormat != EFormat.Unencrypted) - { - EncryptedId = newId; - Entropy = newEntropy; - Format = newFormat; - return true; - } + byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength); + byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser); + return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData); } - else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength }) + catch (Exception) { - Guid? g = DecryptAccountId(); - if (g != null) - { - (EncryptedId, Entropy, Format) = EncryptAccountId(g.Value); - return true; - } + return (g.ToString(), null, EFormat.Unencrypted); } - - return false; - } - - public enum EFormat - { - Unencrypted = 1, - UseProtectedData = 2, - - /// - /// Used for filtering: We don't want to overwrite any entries of this value using DPAPI, ever. - /// This is mostly a wine fallback. - /// - ProtectedDataUnsupported = 3, } } + + public bool EncryptIfNeeded() + { + if (Format == EFormat.Unencrypted) + { + var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId)); + if (newFormat != EFormat.Unencrypted) + { + EncryptedId = newId; + Entropy = newEntropy; + Format = newFormat; + return true; + } + } + else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength }) + { + Guid? g = DecryptAccountId(); + if (g != null) + { + (EncryptedId, Entropy, Format) = EncryptAccountId(g.Value); + return true; + } + } + + return false; + } + + public enum EFormat + { + Unencrypted = 1, + UseProtectedData = 2, + + /// + /// Used for filtering: We don't want to overwrite any entries of this value using DPAPI, ever. + /// This is mostly a wine fallback. + /// + ProtectedDataUnsupported = 3, + } } diff --git a/Pal.Client/Configuration/ConfigurationData.cs b/Pal.Client/Configuration/ConfigurationData.cs index 929ce43..c4a7c7c 100644 --- a/Pal.Client/Configuration/ConfigurationData.cs +++ b/Pal.Client/Configuration/ConfigurationData.cs @@ -3,42 +3,41 @@ using System.Linq; using System.Security.Cryptography; using Microsoft.Extensions.Logging; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration; + +internal static class ConfigurationData { - internal static class ConfigurationData + private static readonly ILogger _logger = + DependencyInjectionContext.LoggerProvider.CreateLogger(typeof(ConfigurationData)); + + [Obsolete("for V1 import")] + internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; + + public const string ConfigFileName = "palace-pal.config.json"; + + private static bool? _supportsDpapi; + public static bool SupportsDpapi { - private static readonly ILogger _logger = - DependencyInjectionContext.LoggerProvider.CreateLogger(typeof(ConfigurationData)); - - [Obsolete("for V1 import")] - internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 }; - - public const string ConfigFileName = "palace-pal.config.json"; - - private static bool? _supportsDpapi; - public static bool SupportsDpapi + get { - get + if (_supportsDpapi == null) { - if (_supportsDpapi == null) + try { - try - { - byte[] input = RandomNumberGenerator.GetBytes(32); - byte[] entropy = RandomNumberGenerator.GetBytes(16); - byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser); - byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser); - _supportsDpapi = input.SequenceEqual(output); - } - catch (Exception) - { - _supportsDpapi = false; - } - - _logger.LogTrace("DPAPI support: {Supported}", _supportsDpapi); + byte[] input = RandomNumberGenerator.GetBytes(32); + byte[] entropy = RandomNumberGenerator.GetBytes(16); + byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser); + byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser); + _supportsDpapi = input.SequenceEqual(output); } - return _supportsDpapi.Value; + catch (Exception) + { + _supportsDpapi = false; + } + + _logger.LogTrace("DPAPI support: {Supported}", _supportsDpapi); } + return _supportsDpapi.Value; } } } diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 4b75aa8..8e6ed9d 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -13,144 +13,143 @@ using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using NJson = Newtonsoft.Json; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration; + +internal sealed class ConfigurationManager { - internal sealed class ConfigurationManager + private readonly ILogger _logger; + private readonly DalamudPluginInterface _pluginInterface; + private readonly IServiceProvider _serviceProvider; + + public event EventHandler? Saved; + + public ConfigurationManager(ILogger logger, DalamudPluginInterface pluginInterface, + IServiceProvider serviceProvider) { - private readonly ILogger _logger; - private readonly DalamudPluginInterface _pluginInterface; - private readonly IServiceProvider _serviceProvider; + _logger = logger; + _pluginInterface = pluginInterface; + _serviceProvider = serviceProvider; + } - public event EventHandler? Saved; + private string ConfigPath => + Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName); - public ConfigurationManager(ILogger logger, DalamudPluginInterface pluginInterface, - IServiceProvider serviceProvider) + public IPalacePalConfiguration Load() + { + if (!File.Exists(ConfigPath)) { - _logger = logger; - _pluginInterface = pluginInterface; - _serviceProvider = serviceProvider; + _logger.LogInformation("No config file exists, creating one"); + Save(new ConfigurationV7(), false); } - private string ConfigPath => - Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName); + return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? + new ConfigurationV7(); + } - public IPalacePalConfiguration Load() - { - if (!File.Exists(ConfigPath)) - { - _logger.LogInformation("No config file exists, creating one"); - Save(new ConfigurationV7(), false); - } + public void Save(IConfigurationInConfigDirectory config, bool queue = true) + { + File.WriteAllText(ConfigPath, + JsonSerializer.Serialize(config, config.GetType(), + new JsonSerializerOptions + { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), + Encoding.UTF8); - return JsonSerializer.Deserialize(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? - new ConfigurationV7(); - } - - public void Save(IConfigurationInConfigDirectory config, bool queue = true) - { - File.WriteAllText(ConfigPath, - JsonSerializer.Serialize(config, config.GetType(), - new JsonSerializerOptions - { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), - Encoding.UTF8); - - if (queue && config is ConfigurationV7 v7) - Saved?.Invoke(this, v7); - } + if (queue && config is ConfigurationV7 v7) + Saved?.Invoke(this, v7); + } #pragma warning disable CS0612 #pragma warning disable CS0618 - public void Migrate() + public void Migrate() + { + if (_pluginInterface.ConfigFile.Exists) { - if (_pluginInterface.ConfigFile.Exists) + _logger.LogInformation("Migrating config file from v1-v6 format"); + + ConfigurationV1 configurationV1 = + NJson.JsonConvert.DeserializeObject( + File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); + configurationV1.Migrate(_pluginInterface, + _serviceProvider.GetRequiredService>()); + configurationV1.Save(_pluginInterface); + + var v7 = MigrateToV7(configurationV1); + Save(v7, queue: false); + + using (var scope = _serviceProvider.CreateScope()) { - _logger.LogInformation("Migrating config file from v1-v6 format"); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Imports.RemoveRange(dbContext.Imports); - ConfigurationV1 configurationV1 = - NJson.JsonConvert.DeserializeObject( - File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1(); - configurationV1.Migrate(_pluginInterface, - _serviceProvider.GetRequiredService>()); - configurationV1.Save(_pluginInterface); - - var v7 = MigrateToV7(configurationV1); - Save(v7, queue: false); - - using (var scope = _serviceProvider.CreateScope()) + foreach (var importHistory in configurationV1.ImportHistory) { - using var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Imports.RemoveRange(dbContext.Imports); - - foreach (var importHistory in configurationV1.ImportHistory) + _logger.LogInformation("Migrating import {Id}", importHistory.Id); + dbContext.Imports.Add(new ImportHistory { - _logger.LogInformation("Migrating import {Id}", importHistory.Id); - dbContext.Imports.Add(new ImportHistory - { - Id = importHistory.Id, - RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"), - ExportedAt = importHistory.ExportedAt, - ImportedAt = importHistory.ImportedAt - }); - } - - dbContext.SaveChanges(); + Id = importHistory.Id, + RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"), + ExportedAt = importHistory.ExportedAt, + ImportedAt = importHistory.ImportedAt + }); } - File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); + dbContext.SaveChanges(); } - } - private ConfigurationV7 MigrateToV7(ConfigurationV1 v1) + File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); + } + } + + private ConfigurationV7 MigrateToV7(ConfigurationV1 v1) + { + ConfigurationV7 v7 = new() { - ConfigurationV7 v7 = new() - { - Version = 7, - FirstUse = v1.FirstUse, - Mode = v1.Mode, - BetaKey = v1.BetaKey, + Version = 7, + FirstUse = v1.FirstUse, + Mode = v1.Mode, + BetaKey = v1.BetaKey, - DeepDungeons = new DeepDungeonConfiguration + DeepDungeons = new DeepDungeonConfiguration + { + Traps = new MarkerConfiguration { - Traps = new MarkerConfiguration - { - Show = v1.ShowTraps, - Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor), - OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander, - Fill = false - }, - HoardCoffers = new MarkerConfiguration - { - Show = v1.ShowHoard, - Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor), - OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander, - Fill = false - }, - SilverCoffers = new MarkerConfiguration - { - Show = v1.ShowSilverCoffers, - Color = ImGui.ColorConvertFloat4ToU32(v1.SilverCofferColor), - OnlyVisibleAfterPomander = false, - Fill = v1.FillSilverCoffers - } + Show = v1.ShowTraps, + Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor), + OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander, + Fill = false + }, + HoardCoffers = new MarkerConfiguration + { + Show = v1.ShowHoard, + Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor), + OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander, + Fill = false + }, + SilverCoffers = new MarkerConfiguration + { + Show = v1.ShowSilverCoffers, + Color = ImGui.ColorConvertFloat4ToU32(v1.SilverCofferColor), + OnlyVisibleAfterPomander = false, + Fill = v1.FillSilverCoffers } - }; - - foreach (var (server, oldAccount) in v1.Accounts) - { - string? accountId = oldAccount.Id; - if (string.IsNullOrEmpty(accountId)) - continue; - - string serverName = server.Replace(".μ.tv", ".liza.sh"); - IAccountConfiguration newAccount = v7.CreateAccount(serverName, accountId); - newAccount.CachedRoles = oldAccount.CachedRoles.ToList(); } + }; - // TODO Migrate ImportHistory + foreach (var (server, oldAccount) in v1.Accounts) + { + string? accountId = oldAccount.Id; + if (string.IsNullOrEmpty(accountId)) + continue; - return v7; + string serverName = server.Replace(".μ.tv", ".liza.sh"); + IAccountConfiguration newAccount = v7.CreateAccount(serverName, accountId); + newAccount.CachedRoles = oldAccount.CachedRoles.ToList(); } + + // TODO Migrate ImportHistory + + return v7; + } #pragma warning restore CS0618 #pragma warning restore CS0612 - } } diff --git a/Pal.Client/Configuration/ConfigurationV7.cs b/Pal.Client/Configuration/ConfigurationV7.cs index f25f2b5..0d98df8 100644 --- a/Pal.Client/Configuration/ConfigurationV7.cs +++ b/Pal.Client/Configuration/ConfigurationV7.cs @@ -2,53 +2,52 @@ using System.Collections.Generic; using System.Linq; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration; + +public sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory { - public sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory + public int Version { get; set; } = 7; + + public bool FirstUse { get; set; } = true; + public EMode Mode { get; set; } + public string BetaKey { get; init; } = ""; + + public DeepDungeonConfiguration DeepDungeons { get; set; } = new(); + public RendererConfiguration Renderer { get; set; } = new(); + public List Accounts { get; set; } = new(); + public BackupConfiguration Backups { get; set; } = new(); + + public IAccountConfiguration CreateAccount(string server, Guid accountId) { - public int Version { get; set; } = 7; + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } - public bool FirstUse { get; set; } = true; - public EMode Mode { get; set; } - public string BetaKey { get; init; } = ""; + [Obsolete("for V1 import")] + internal IAccountConfiguration CreateAccount(string server, string accountId) + { + var account = new AccountConfigurationV7(server, accountId); + Accounts.Add(account); + return account; + } - public DeepDungeonConfiguration DeepDungeons { get; set; } = new(); - public RendererConfiguration Renderer { get; set; } = new(); - public List Accounts { get; set; } = new(); - public BackupConfiguration Backups { get; set; } = new(); + public IAccountConfiguration? FindAccount(string server) + { + return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable); + } - public IAccountConfiguration CreateAccount(string server, Guid accountId) - { - var account = new AccountConfigurationV7(server, accountId); - Accounts.Add(account); - return account; - } + public void RemoveAccount(string server) + { + Accounts.RemoveAll(a => a.Server == server && a.IsUsable); + } - [Obsolete("for V1 import")] - internal IAccountConfiguration CreateAccount(string server, string accountId) - { - var account = new AccountConfigurationV7(server, accountId); - Accounts.Add(account); - return account; - } + public bool HasRoleOnCurrentServer(string server, string role) + { + if (Mode != EMode.Online) + return false; - public IAccountConfiguration? FindAccount(string server) - { - return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable); - } - - public void RemoveAccount(string server) - { - Accounts.RemoveAll(a => a.Server == server && a.IsUsable); - } - - public bool HasRoleOnCurrentServer(string server, string role) - { - if (Mode != EMode.Online) - return false; - - var account = FindAccount(server); - return account == null || account.CachedRoles.Contains(role); - } + var account = FindAccount(server); + return account == null || account.CachedRoles.Contains(role); } } diff --git a/Pal.Client/Configuration/EMode.cs b/Pal.Client/Configuration/EMode.cs index dde18f2..a7f59d2 100644 --- a/Pal.Client/Configuration/EMode.cs +++ b/Pal.Client/Configuration/EMode.cs @@ -1,15 +1,14 @@ -namespace Pal.Client.Configuration -{ - public enum EMode - { - /// - /// Fetches trap locations from remote server. - /// - Online = 1, +namespace Pal.Client.Configuration; - /// - /// Only shows traps found by yourself using a pomander of sight. - /// - Offline = 2, - } +public enum EMode +{ + /// + /// Fetches trap locations from remote server. + /// + Online = 1, + + /// + /// Only shows traps found by yourself using a pomander of sight. + /// + Offline = 2, } diff --git a/Pal.Client/Configuration/ERenderer.cs b/Pal.Client/Configuration/ERenderer.cs index 2ea7568..eed7901 100644 --- a/Pal.Client/Configuration/ERenderer.cs +++ b/Pal.Client/Configuration/ERenderer.cs @@ -1,11 +1,10 @@ -namespace Pal.Client.Configuration -{ - public enum ERenderer - { - /// - Simple = 0, +namespace Pal.Client.Configuration; - /// - Splatoon = 1, - } +public enum ERenderer +{ + /// + Simple = 0, + + /// + Splatoon = 1, } diff --git a/Pal.Client/Configuration/IPalacePalConfiguration.cs b/Pal.Client/Configuration/IPalacePalConfiguration.cs index 19530a8..e06112c 100644 --- a/Pal.Client/Configuration/IPalacePalConfiguration.cs +++ b/Pal.Client/Configuration/IPalacePalConfiguration.cs @@ -4,108 +4,107 @@ using System.Numerics; using ImGuiNET; using Newtonsoft.Json; -namespace Pal.Client.Configuration +namespace Pal.Client.Configuration; + +public interface IVersioned { - public interface IVersioned - { - int Version { get; set; } - } - public interface IConfigurationInConfigDirectory : IVersioned - { - } - - public interface IPalacePalConfiguration : IConfigurationInConfigDirectory - { - bool FirstUse { get; set; } - EMode Mode { get; set; } - string BetaKey { get; } - bool HasBetaFeature(string feature) => BetaKey.Contains(feature); - - DeepDungeonConfiguration DeepDungeons { get; set; } - RendererConfiguration Renderer { get; set; } - BackupConfiguration Backups { get; set; } - - IAccountConfiguration CreateAccount(string server, Guid accountId); - IAccountConfiguration? FindAccount(string server); - void RemoveAccount(string server); - - bool HasRoleOnCurrentServer(string server, string role); - } - - public class DeepDungeonConfiguration - { - public MarkerConfiguration Traps { get; set; } = new() - { - Show = true, - Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)), - OnlyVisibleAfterPomander = true, - Fill = false - }; - - public MarkerConfiguration HoardCoffers { get; set; } = new() - { - Show = true, - Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)), - OnlyVisibleAfterPomander = true, - Fill = false - }; - - public MarkerConfiguration SilverCoffers { get; set; } = new() - { - Show = false, - Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)), - OnlyVisibleAfterPomander = false, - Fill = true - }; - - public MarkerConfiguration GoldCoffers { get; set; } = new() - { - Show = false, - Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)), - OnlyVisibleAfterPomander = false, - Fill = true - }; - } - - public class MarkerConfiguration - { - [JsonRequired] - public bool Show { get; set; } - - [JsonRequired] - public uint Color { get; set; } - - public bool OnlyVisibleAfterPomander { get; set; } - public bool Fill { get; set; } - } - - public class RendererConfiguration - { - public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon; - } - - public interface IAccountConfiguration - { - bool IsUsable { get; } - string Server { get; } - Guid AccountId { get; } - - /// - /// This is taken from the JWT, and is only refreshed on a successful login. - /// - /// If you simply reload the plugin without any server interaction, this doesn't change. - /// - /// This has no impact on what roles the JWT actually contains, but is just to make it - /// easier to draw a consistent UI. The server will still reject unauthorized calls. - /// - List CachedRoles { get; set; } - - bool EncryptIfNeeded(); - } - - public class BackupConfiguration - { - public int MinimumBackupsToKeep { get; set; } = 3; - public int DaysToDeleteAfter { get; set; } = 21; - } + int Version { get; set; } +} +public interface IConfigurationInConfigDirectory : IVersioned +{ +} + +public interface IPalacePalConfiguration : IConfigurationInConfigDirectory +{ + bool FirstUse { get; set; } + EMode Mode { get; set; } + string BetaKey { get; } + bool HasBetaFeature(string feature) => BetaKey.Contains(feature); + + DeepDungeonConfiguration DeepDungeons { get; set; } + RendererConfiguration Renderer { get; set; } + BackupConfiguration Backups { get; set; } + + IAccountConfiguration CreateAccount(string server, Guid accountId); + IAccountConfiguration? FindAccount(string server); + void RemoveAccount(string server); + + bool HasRoleOnCurrentServer(string server, string role); +} + +public class DeepDungeonConfiguration +{ + public MarkerConfiguration Traps { get; set; } = new() + { + Show = true, + Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)), + OnlyVisibleAfterPomander = true, + Fill = false + }; + + public MarkerConfiguration HoardCoffers { get; set; } = new() + { + Show = true, + Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)), + OnlyVisibleAfterPomander = true, + Fill = false + }; + + public MarkerConfiguration SilverCoffers { get; set; } = new() + { + Show = false, + Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)), + OnlyVisibleAfterPomander = false, + Fill = true + }; + + public MarkerConfiguration GoldCoffers { get; set; } = new() + { + Show = false, + Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)), + OnlyVisibleAfterPomander = false, + Fill = true + }; +} + +public class MarkerConfiguration +{ + [JsonRequired] + public bool Show { get; set; } + + [JsonRequired] + public uint Color { get; set; } + + public bool OnlyVisibleAfterPomander { get; set; } + public bool Fill { get; set; } +} + +public class RendererConfiguration +{ + public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon; +} + +public interface IAccountConfiguration +{ + bool IsUsable { get; } + string Server { get; } + Guid AccountId { get; } + + /// + /// This is taken from the JWT, and is only refreshed on a successful login. + /// + /// If you simply reload the plugin without any server interaction, this doesn't change. + /// + /// This has no impact on what roles the JWT actually contains, but is just to make it + /// easier to draw a consistent UI. The server will still reject unauthorized calls. + /// + List CachedRoles { get; set; } + + bool EncryptIfNeeded(); +} + +public class BackupConfiguration +{ + public int MinimumBackupsToKeep { get; set; } = 3; + public int DaysToDeleteAfter { get; set; } = 21; } diff --git a/Pal.Client/Configuration/Legacy/ConfigurationV1.cs b/Pal.Client/Configuration/Legacy/ConfigurationV1.cs index 6b94807..f26f439 100644 --- a/Pal.Client/Configuration/Legacy/ConfigurationV1.cs +++ b/Pal.Client/Configuration/Legacy/ConfigurationV1.cs @@ -8,160 +8,159 @@ using Dalamud.Plugin; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Pal.Client.Configuration.Legacy +namespace Pal.Client.Configuration.Legacy; + +[Obsolete] +public sealed class ConfigurationV1 { + public int Version { get; set; } = 6; + + #region Saved configuration values + public bool FirstUse { get; set; } = true; + public EMode Mode { get; set; } = EMode.Offline; + public ERenderer Renderer { get; set; } = ERenderer.Splatoon; + [Obsolete] - public sealed class ConfigurationV1 + public string? DebugAccountId { private get; set; } + + [Obsolete] + public string? AccountId { private get; set; } + + [Obsolete] + public Dictionary AccountIds { private get; set; } = new(); + public Dictionary Accounts { get; set; } = new(); + + public List ImportHistory { get; set; } = new(); + + public bool ShowTraps { get; set; } = true; + public Vector4 TrapColor { get; set; } = new(1, 0, 0, 0.4f); + public bool OnlyVisibleTrapsAfterPomander { get; set; } = true; + + public bool ShowHoard { get; set; } = true; + public Vector4 HoardColor { get; set; } = new(0, 1, 1, 0.4f); + public bool OnlyVisibleHoardAfterPomander { get; set; } = true; + + public bool ShowSilverCoffers { get; set; } + public Vector4 SilverCofferColor { get; set; } = new(1, 1, 1, 0.4f); + public bool FillSilverCoffers { get; set; } = true; + + /// + /// Needs to be manually set. + /// + public string BetaKey { get; set; } = ""; + #endregion + + public void Migrate(DalamudPluginInterface pluginInterface, ILogger logger) { - public int Version { get; set; } = 6; - - #region Saved configuration values - public bool FirstUse { get; set; } = true; - public EMode Mode { get; set; } = EMode.Offline; - public ERenderer Renderer { get; set; } = ERenderer.Splatoon; - - [Obsolete] - public string? DebugAccountId { private get; set; } - - [Obsolete] - public string? AccountId { private get; set; } - - [Obsolete] - public Dictionary AccountIds { private get; set; } = new(); - public Dictionary Accounts { get; set; } = new(); - - public List ImportHistory { get; set; } = new(); - - public bool ShowTraps { get; set; } = true; - public Vector4 TrapColor { get; set; } = new(1, 0, 0, 0.4f); - public bool OnlyVisibleTrapsAfterPomander { get; set; } = true; - - public bool ShowHoard { get; set; } = true; - public Vector4 HoardColor { get; set; } = new(0, 1, 1, 0.4f); - public bool OnlyVisibleHoardAfterPomander { get; set; } = true; - - public bool ShowSilverCoffers { get; set; } - public Vector4 SilverCofferColor { get; set; } = new(1, 1, 1, 0.4f); - public bool FillSilverCoffers { get; set; } = true; - - /// - /// Needs to be manually set. - /// - public string BetaKey { get; set; } = ""; - #endregion - - public void Migrate(DalamudPluginInterface pluginInterface, ILogger logger) + if (Version == 1) { - if (Version == 1) + logger.LogInformation("Updating config to version 2"); + + if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId)) + AccountIds["http://localhost:5145"] = debugAccountId; + + if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId)) + AccountIds["https://pal.μ.tv"] = accountId; + + Version = 2; + Save(pluginInterface); + } + + if (Version == 2) + { + logger.LogInformation("Updating config to version 3"); + + Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo { - logger.LogInformation("Updating config to version 2"); + Id = x.Value.ToString() // encryption happens in V7 migration at latest + }); + Version = 3; + Save(pluginInterface); + } - if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId)) - AccountIds["http://localhost:5145"] = debugAccountId; + if (Version == 3) + { + Version = 4; + Save(pluginInterface); + } - if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId)) - AccountIds["https://pal.μ.tv"] = accountId; - - Version = 2; - Save(pluginInterface); - } - - if (Version == 2) + if (Version == 4) + { + // 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up. + // Not a problem for online players, but offline players might be fucked. + //bool changedAnyFile = false; + JsonFloorState.ForEach(s => { - logger.LogInformation("Updating config to version 3"); + foreach (var marker in s.Markers) + marker.SinceVersion = "0.0"; - Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo + var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation()); + if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc)) { - Id = x.Value.ToString() // encryption happens in V7 migration at latest - }); - Version = 3; - Save(pluginInterface); - } + s.Backup(suffix: "bak"); - if (Version == 3) - { - Version = 4; - Save(pluginInterface); - } + s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported)); + s.Save(); - if (Version == 4) - { - // 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up. - // Not a problem for online players, but offline players might be fucked. - //bool changedAnyFile = false; - JsonFloorState.ForEach(s => - { - foreach (var marker in s.Markers) - marker.SinceVersion = "0.0"; - - var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation()); - if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc)) - { - s.Backup(suffix: "bak"); - - s.Markers = new ConcurrentBag(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported)); - s.Save(); - - //changedAnyFile = true; - } - else - { - // just add version information, nothing else - s.Save(); - } - }); - - /* - // Only notify offline users - we can just re-download the backup markers from the server seamlessly. - if (Mode == EMode.Offline && changedAnyFile) - { - _ = new TickScheduler(delegate - { - Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up."); - Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before."); - Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023."); - }, 2500); + //changedAnyFile = true; } - */ + else + { + // just add version information, nothing else + s.Save(); + } + }); - Version = 5; - Save(pluginInterface); - } - - if (Version == 5) + /* + // Only notify offline users - we can just re-download the backup markers from the server seamlessly. + if (Mode == EMode.Offline && changedAnyFile) { - JsonFloorState.UpdateAll(); - - Version = 6; - Save(pluginInterface); + _ = new TickScheduler(delegate + { + Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up."); + Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before."); + Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023."); + }, 2500); } + */ + + Version = 5; + Save(pluginInterface); } - public void Save(DalamudPluginInterface pluginInterface) + if (Version == 5) { - File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings - { - TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, - TypeNameHandling = TypeNameHandling.Objects - })); - } + JsonFloorState.UpdateAll(); - public sealed class AccountInfo - { - public string? Id { get; set; } - public List CachedRoles { get; set; } = new(); - } - - public sealed class ImportHistoryEntry - { - public Guid Id { get; set; } - public string? RemoteUrl { get; set; } - public DateTime ExportedAt { get; set; } - - /// - /// Set when the file is imported locally. - /// - public DateTime ImportedAt { get; set; } + Version = 6; + Save(pluginInterface); } } + + public void Save(DalamudPluginInterface pluginInterface) + { + File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings + { + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + TypeNameHandling = TypeNameHandling.Objects + })); + } + + public sealed class AccountInfo + { + public string? Id { get; set; } + public List CachedRoles { get; set; } = new(); + } + + public sealed class ImportHistoryEntry + { + public Guid Id { get; set; } + public string? RemoteUrl { get; set; } + public DateTime ExportedAt { get; set; } + + /// + /// Set when the file is imported locally. + /// + public DateTime ImportedAt { get; set; } + } } diff --git a/Pal.Client/Configuration/Legacy/JsonFloorState.cs b/Pal.Client/Configuration/Legacy/JsonFloorState.cs index 90d8d7d..c4afbac 100644 --- a/Pal.Client/Configuration/Legacy/JsonFloorState.cs +++ b/Pal.Client/Configuration/Legacy/JsonFloorState.cs @@ -7,156 +7,155 @@ using System.Text.Json; using Pal.Client.Extensions; using Pal.Common; -namespace Pal.Client.Configuration.Legacy +namespace Pal.Client.Configuration.Legacy; + +/// +/// Legacy JSON file for marker locations. +/// +[Obsolete] +public sealed class JsonFloorState { - /// - /// Legacy JSON file for marker locations. - /// - [Obsolete] - public sealed class JsonFloorState + private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; + private const int CurrentVersion = 4; + + private static string _pluginConfigDirectory = null!; + + internal static void SetContextProperties(string pluginConfigDirectory) { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; - private const int CurrentVersion = 4; + _pluginConfigDirectory = pluginConfigDirectory; + } - private static string _pluginConfigDirectory = null!; + public ushort TerritoryType { get; set; } + public ConcurrentBag Markers { get; set; } = new(); - internal static void SetContextProperties(string pluginConfigDirectory) + public JsonFloorState(ushort territoryType) + { + TerritoryType = territoryType; + } + + private void ApplyFilters() + { + Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); + } + + public static JsonFloorState? Load(ushort territoryType) + { + string path = GetSaveLocation(territoryType); + if (!File.Exists(path)) + return null; + + string content = File.ReadAllText(path); + if (content.Length == 0) + return null; + + JsonFloorState localState; + int version = 1; + if (content[0] == '[') { - _pluginConfigDirectory = pluginConfigDirectory; + // v1 only had a list of markers, not a JSON object as root + localState = new JsonFloorState(territoryType) + { + Markers = new ConcurrentBag(JsonSerializer.Deserialize>(content, JsonSerializerOptions) ?? new()), + }; } - - public ushort TerritoryType { get; set; } - public ConcurrentBag Markers { get; set; } = new(); - - public JsonFloorState(ushort territoryType) + else { - TerritoryType = territoryType; - } - - private void ApplyFilters() - { - Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); - } - - public static JsonFloorState? Load(ushort territoryType) - { - string path = GetSaveLocation(territoryType); - if (!File.Exists(path)) + var save = JsonSerializer.Deserialize(content, JsonSerializerOptions); + if (save == null) return null; - string content = File.ReadAllText(path); - if (content.Length == 0) - return null; - - JsonFloorState localState; - int version = 1; - if (content[0] == '[') + localState = new JsonFloorState(territoryType) { - // v1 only had a list of markers, not a JSON object as root - localState = new JsonFloorState(territoryType) - { - Markers = new ConcurrentBag(JsonSerializer.Deserialize>(content, JsonSerializerOptions) ?? new()), - }; - } - else - { - var save = JsonSerializer.Deserialize(content, JsonSerializerOptions); - if (save == null) - return null; - - localState = new JsonFloorState(territoryType) - { - Markers = new ConcurrentBag(save.Markers.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)), - }; - version = save.Version; - } - - localState.ApplyFilters(); - - if (version <= 3) - { - foreach (var marker in localState.Markers) - marker.RemoteSeenOn = marker.RemoteSeenOn.Select(x => x.ToPartialId()).ToList(); - } - - if (version < CurrentVersion) - localState.Save(); - - return localState; + Markers = new ConcurrentBag(save.Markers.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)), + }; + version = save.Version; } - public void Save() - { - string path = GetSaveLocation(TerritoryType); + localState.ApplyFilters(); - ApplyFilters(); + if (version <= 3) + { + foreach (var marker in localState.Markers) + marker.RemoteSeenOn = marker.RemoteSeenOn.Select(x => x.ToPartialId()).ToList(); + } + + if (version < CurrentVersion) + localState.Save(); + + return localState; + } + + public void Save() + { + string path = GetSaveLocation(TerritoryType); + + ApplyFilters(); + SaveImpl(path); + } + + public void Backup(string suffix) + { + string path = $"{GetSaveLocation(TerritoryType)}.{suffix}"; + if (!File.Exists(path)) + { SaveImpl(path); } + } - public void Backup(string suffix) + private void SaveImpl(string path) + { + foreach (var marker in Markers) { - string path = $"{GetSaveLocation(TerritoryType)}.{suffix}"; - if (!File.Exists(path)) + if (string.IsNullOrEmpty(marker.SinceVersion)) + marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2); + } + + if (Markers.Count == 0) + File.Delete(path); + else + { + File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile { - SaveImpl(path); - } - } - - private void SaveImpl(string path) - { - foreach (var marker in Markers) - { - if (string.IsNullOrEmpty(marker.SinceVersion)) - marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2); - } - - if (Markers.Count == 0) - File.Delete(path); - else - { - File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile - { - Version = CurrentVersion, - Markers = new HashSet(Markers) - }, JsonSerializerOptions)); - } - } - - public string GetSaveLocation() => GetSaveLocation(TerritoryType); - - private static string GetSaveLocation(uint territoryType) => Path.Join(_pluginConfigDirectory, $"{territoryType}.json"); - - public static void ForEach(Action action) - { - foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) - { - // we never had markers for eureka orthos, so don't bother - if (territory > ETerritoryType.HeavenOnHigh_91_100) - break; - - JsonFloorState? localState = Load((ushort)territory); - if (localState != null) - action(localState); - } - } - - public static void UpdateAll() - { - ForEach(s => s.Save()); - } - - public void UndoImport(List importIds) - { - // When saving a floor state, any markers not seen, not remote seen, and not having an import id are removed; - // so it is possible to remove "wrong" markers by not having them be in the current import. - foreach (var marker in Markers) - marker.Imports.RemoveAll(importIds.Contains); - } - - public sealed class SaveFile - { - public int Version { get; set; } - public HashSet Markers { get; set; } = new(); + Version = CurrentVersion, + Markers = new HashSet(Markers) + }, JsonSerializerOptions)); } } + + public string GetSaveLocation() => GetSaveLocation(TerritoryType); + + private static string GetSaveLocation(uint territoryType) => Path.Join(_pluginConfigDirectory, $"{territoryType}.json"); + + public static void ForEach(Action action) + { + foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) + { + // we never had markers for eureka orthos, so don't bother + if (territory > ETerritoryType.HeavenOnHigh_91_100) + break; + + JsonFloorState? localState = Load((ushort)territory); + if (localState != null) + action(localState); + } + } + + public static void UpdateAll() + { + ForEach(s => s.Save()); + } + + public void UndoImport(List importIds) + { + // When saving a floor state, any markers not seen, not remote seen, and not having an import id are removed; + // so it is possible to remove "wrong" markers by not having them be in the current import. + foreach (var marker in Markers) + marker.Imports.RemoveAll(importIds.Contains); + } + + public sealed class SaveFile + { + public int Version { get; set; } + public HashSet Markers { get; set; } = new(); + } } diff --git a/Pal.Client/Configuration/Legacy/JsonMarker.cs b/Pal.Client/Configuration/Legacy/JsonMarker.cs index 06b4607..7de748f 100644 --- a/Pal.Client/Configuration/Legacy/JsonMarker.cs +++ b/Pal.Client/Configuration/Legacy/JsonMarker.cs @@ -2,25 +2,24 @@ using System.Collections.Generic; using System.Numerics; -namespace Pal.Client.Configuration.Legacy -{ - [Obsolete] - public class JsonMarker - { - public EType Type { get; set; } = EType.Unknown; - public Vector3 Position { get; set; } - public bool Seen { get; set; } - public List RemoteSeenOn { get; set; } = new(); - public List Imports { get; set; } = new(); - public bool WasImported { get; set; } - public string? SinceVersion { get; set; } +namespace Pal.Client.Configuration.Legacy; - public enum EType - { - Unknown = 0, - Trap = 1, - Hoard = 2, - Debug = 3, - } +[Obsolete] +public class JsonMarker +{ + public EType Type { get; set; } = EType.Unknown; + public Vector3 Position { get; set; } + public bool Seen { get; set; } + public List RemoteSeenOn { get; set; } = new(); + public List Imports { get; set; } = new(); + public bool WasImported { get; set; } + public string? SinceVersion { get; set; } + + public enum EType + { + Unknown = 0, + Trap = 1, + Hoard = 2, + Debug = 3, } } diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs index c9b9a6a..5e45f7c 100644 --- a/Pal.Client/Configuration/Legacy/JsonMigration.cs +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -12,136 +12,135 @@ using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Common; -namespace Pal.Client.Configuration.Legacy -{ - /// - /// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist. - /// - internal sealed class JsonMigration - { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly DalamudPluginInterface _pluginInterface; +namespace Pal.Client.Configuration.Legacy; - public JsonMigration(ILogger logger, IServiceScopeFactory serviceScopeFactory, - DalamudPluginInterface pluginInterface) - { - _logger = logger; - _serviceScopeFactory = serviceScopeFactory; - _pluginInterface = pluginInterface; - } +/// +/// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist. +/// +internal sealed class JsonMigration +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly DalamudPluginInterface _pluginInterface; + + public JsonMigration(ILogger logger, IServiceScopeFactory serviceScopeFactory, + DalamudPluginInterface pluginInterface) + { + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; + _pluginInterface = pluginInterface; + } #pragma warning disable CS0612 - public async Task MigrateAsync(CancellationToken cancellationToken) + public async Task MigrateAsync(CancellationToken cancellationToken) + { + List floorsToMigrate = new(); + JsonFloorState.ForEach(floorsToMigrate.Add); + + if (floorsToMigrate.Count == 0) { - List floorsToMigrate = new(); - JsonFloorState.ForEach(floorsToMigrate.Add); + _logger.LogInformation("Found no floors to migrate"); + return; + } - if (floorsToMigrate.Count == 0) - { - _logger.LogInformation("Found no floors to migrate"); - return; - } + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + await using var scope = _serviceScopeFactory.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); - await using var scope = _serviceScopeFactory.CreateAsyncScope(); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); + var fileStream = new FileStream( + Path.Join(_pluginInterface.GetPluginConfigDirectory(), + $"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"), + FileMode.CreateNew); + using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) + { + IReadOnlyDictionary imports = + await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken); - var fileStream = new FileStream( - Path.Join(_pluginInterface.GetPluginConfigDirectory(), - $"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"), - FileMode.CreateNew); - using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) - { - IReadOnlyDictionary imports = - await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken); - - foreach (var floorToMigrate in floorsToMigrate) - { - backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(), - Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize); - await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken); - } - - await dbContext.SaveChangesAsync(cancellationToken); - } - - _logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count); foreach (var floorToMigrate in floorsToMigrate) - File.Delete(floorToMigrate.GetSaveLocation()); - } - - /// Whether to archive this file once complete - private async Task MigrateFloor( - PalClientContext dbContext, - JsonFloorState floorToMigrate, - IReadOnlyDictionary imports, - CancellationToken cancellationToken) - { - using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}"); - if (floorToMigrate.Markers.Count == 0) { - _logger.LogInformation("Skipping migration, floor has no markers"); + backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(), + Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize); + await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken); } - if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType, - cancellationToken)) - { - _logger.LogInformation("Skipping migration, floor already has locations in the database"); - return; - } - - _logger.LogInformation("Starting migration of {Count} locations", floorToMigrate.Markers.Count); - List clientLocations = floorToMigrate.Markers - .Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard) - .Select(o => - { - var clientLocation = new ClientLocation - { - TerritoryType = floorToMigrate.TerritoryType, - Type = MapJsonType(o.Type), - X = o.Position.X, - Y = o.Position.Y, - Z = o.Position.Z, - Seen = o.Seen, - - // the SelectMany is misleading here, each import has either 0 or 1 associated db entry with that id - ImportedBy = o.Imports - .Select(importId => - imports.TryGetValue(importId, out ImportHistory? import) ? import : null) - .Where(import => import != null) - .Cast() - .Distinct() - .ToList(), - - // if we have a location not encountered locally, which also wasn't imported, - // it very likely is a download (but we have no information to track this). - Source = o.Seen ? ClientLocation.ESource.SeenLocally : - o.Imports.Count > 0 ? ClientLocation.ESource.Import : ClientLocation.ESource.Download, - SinceVersion = o.SinceVersion ?? "0.0", - }; - - clientLocation.RemoteEncounters = o.RemoteSeenOn - .Select(accountId => new RemoteEncounter(clientLocation, accountId)) - .ToList(); - - return clientLocation; - }).ToList(); - await dbContext.Locations.AddRangeAsync(clientLocations, cancellationToken); - - _logger.LogInformation("Migrated {Count} locations", clientLocations.Count); + await dbContext.SaveChangesAsync(cancellationToken); } - private ClientLocation.EType MapJsonType(JsonMarker.EType type) - { - return type switch - { - JsonMarker.EType.Trap => ClientLocation.EType.Trap, - JsonMarker.EType.Hoard => ClientLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - } -#pragma warning restore CS0612 + _logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count); + foreach (var floorToMigrate in floorsToMigrate) + File.Delete(floorToMigrate.GetSaveLocation()); } + + /// Whether to archive this file once complete + private async Task MigrateFloor( + PalClientContext dbContext, + JsonFloorState floorToMigrate, + IReadOnlyDictionary imports, + CancellationToken cancellationToken) + { + using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}"); + if (floorToMigrate.Markers.Count == 0) + { + _logger.LogInformation("Skipping migration, floor has no markers"); + } + + if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType, + cancellationToken)) + { + _logger.LogInformation("Skipping migration, floor already has locations in the database"); + return; + } + + _logger.LogInformation("Starting migration of {Count} locations", floorToMigrate.Markers.Count); + List clientLocations = floorToMigrate.Markers + .Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard) + .Select(o => + { + var clientLocation = new ClientLocation + { + TerritoryType = floorToMigrate.TerritoryType, + Type = MapJsonType(o.Type), + X = o.Position.X, + Y = o.Position.Y, + Z = o.Position.Z, + Seen = o.Seen, + + // the SelectMany is misleading here, each import has either 0 or 1 associated db entry with that id + ImportedBy = o.Imports + .Select(importId => + imports.TryGetValue(importId, out ImportHistory? import) ? import : null) + .Where(import => import != null) + .Cast() + .Distinct() + .ToList(), + + // if we have a location not encountered locally, which also wasn't imported, + // it very likely is a download (but we have no information to track this). + Source = o.Seen ? ClientLocation.ESource.SeenLocally : + o.Imports.Count > 0 ? ClientLocation.ESource.Import : ClientLocation.ESource.Download, + SinceVersion = o.SinceVersion ?? "0.0", + }; + + clientLocation.RemoteEncounters = o.RemoteSeenOn + .Select(accountId => new RemoteEncounter(clientLocation, accountId)) + .ToList(); + + return clientLocation; + }).ToList(); + await dbContext.Locations.AddRangeAsync(clientLocations, cancellationToken); + + _logger.LogInformation("Migrated {Count} locations", clientLocations.Count); + } + + private ClientLocation.EType MapJsonType(JsonMarker.EType type) + { + return type switch + { + JsonMarker.EType.Trap => ClientLocation.EType.Trap, + JsonMarker.EType.Hoard => ClientLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } +#pragma warning restore CS0612 } diff --git a/Pal.Client/Database/Cleanup.cs b/Pal.Client/Database/Cleanup.cs index 40db00f..06886f6 100644 --- a/Pal.Client/Database/Cleanup.cs +++ b/Pal.Client/Database/Cleanup.cs @@ -6,62 +6,61 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Common; -namespace Pal.Client.Database +namespace Pal.Client.Database; + +internal sealed class Cleanup { - internal sealed class Cleanup + private readonly ILogger _logger; + private readonly IPalacePalConfiguration _configuration; + + public Cleanup(ILogger logger, IPalacePalConfiguration configuration) { - private readonly ILogger _logger; - private readonly IPalacePalConfiguration _configuration; + _logger = logger; + _configuration = configuration; + } - public Cleanup(ILogger logger, IPalacePalConfiguration configuration) - { - _logger = logger; - _configuration = configuration; - } + public void Purge(PalClientContext dbContext) + { + var toDelete = dbContext.Locations + .Include(o => o.ImportedBy) + .Include(o => o.RemoteEncounters) + .AsSplitQuery() + .Where(DefaultPredicate()) + .Where(AnyRemoteEncounter()) + .ToList(); + _logger.LogInformation("Cleaning up {Count} outdated locations", toDelete.Count); + dbContext.Locations.RemoveRange(toDelete); + } - public void Purge(PalClientContext dbContext) - { - var toDelete = dbContext.Locations - .Include(o => o.ImportedBy) - .Include(o => o.RemoteEncounters) - .AsSplitQuery() - .Where(DefaultPredicate()) - .Where(AnyRemoteEncounter()) - .ToList(); - _logger.LogInformation("Cleaning up {Count} outdated locations", toDelete.Count); - dbContext.Locations.RemoveRange(toDelete); - } + public void Purge(PalClientContext dbContext, ETerritoryType territoryType) + { + var toDelete = dbContext.Locations + .Include(o => o.ImportedBy) + .Include(o => o.RemoteEncounters) + .AsSplitQuery() + .Where(o => o.TerritoryType == (ushort)territoryType) + .Where(DefaultPredicate()) + .Where(AnyRemoteEncounter()) + .ToList(); + _logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count, + territoryType); + dbContext.Locations.RemoveRange(toDelete); + } - public void Purge(PalClientContext dbContext, ETerritoryType territoryType) - { - var toDelete = dbContext.Locations - .Include(o => o.ImportedBy) - .Include(o => o.RemoteEncounters) - .AsSplitQuery() - .Where(o => o.TerritoryType == (ushort)territoryType) - .Where(DefaultPredicate()) - .Where(AnyRemoteEncounter()) - .ToList(); - _logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count, - territoryType); - dbContext.Locations.RemoveRange(toDelete); - } + private Expression> DefaultPredicate() + { + return o => !o.Seen && + o.ImportedBy.Count == 0 && + o.Source != ClientLocation.ESource.SeenLocally && + o.Source != ClientLocation.ESource.ExplodedLocally; + } - private Expression> DefaultPredicate() - { - return o => !o.Seen && - o.ImportedBy.Count == 0 && - o.Source != ClientLocation.ESource.SeenLocally && - o.Source != ClientLocation.ESource.ExplodedLocally; - } - - private Expression> AnyRemoteEncounter() - { - if (_configuration.Mode == EMode.Offline) - return o => true; - else - // keep downloaded markers - return o => o.Source != ClientLocation.ESource.Download; - } + private Expression> AnyRemoteEncounter() + { + if (_configuration.Mode == EMode.Offline) + return o => true; + else + // keep downloaded markers + return o => o.Source != ClientLocation.ESource.Download; } } diff --git a/Pal.Client/Database/ClientLocation.cs b/Pal.Client/Database/ClientLocation.cs index ab748f5..8c8a999 100644 --- a/Pal.Client/Database/ClientLocation.cs +++ b/Pal.Client/Database/ClientLocation.cs @@ -1,59 +1,58 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Pal.Client.Database +namespace Pal.Client.Database; + +internal sealed class ClientLocation { - internal sealed class ClientLocation + [Key] public int LocalId { get; set; } + public ushort TerritoryType { get; set; } + public EType Type { get; set; } + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + /// + /// Whether we have encountered the trap/coffer at this location in-game. + /// + public bool Seen { 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 RemoteEncounters { get; set; } = new(); + + /// + /// To keep track of which markers were imported through a downloaded file, we save the associated import-id. + /// + /// Importing another file for the same remote server will remove the old import-id, and add the new import-id here. + /// + public List ImportedBy { get; set; } = new(); + + /// + /// Determines where this location is originally from. + /// + public ESource Source { get; set; } + + + /// + /// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially. + /// + public string SinceVersion { get; set; } = "0.0"; + + public enum EType { - [Key] public int LocalId { get; set; } - public ushort TerritoryType { get; set; } - public EType Type { get; set; } - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } + Trap = 1, + Hoard = 2, + } - /// - /// Whether we have encountered the trap/coffer at this location in-game. - /// - public bool Seen { 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 RemoteEncounters { get; set; } = new(); - - /// - /// To keep track of which markers were imported through a downloaded file, we save the associated import-id. - /// - /// Importing another file for the same remote server will remove the old import-id, and add the new import-id here. - /// - public List ImportedBy { get; set; } = new(); - - /// - /// Determines where this location is originally from. - /// - public ESource Source { get; set; } - - - /// - /// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially. - /// - public string SinceVersion { get; set; } = "0.0"; - - public enum EType - { - Trap = 1, - Hoard = 2, - } - - public enum ESource - { - Unknown = 0, - SeenLocally = 1, - ExplodedLocally = 2, - Import = 3, - Download = 4, - } + public enum ESource + { + Unknown = 0, + SeenLocally = 1, + ExplodedLocally = 2, + Import = 3, + Download = 4, } } diff --git a/Pal.Client/Database/ImportHistory.cs b/Pal.Client/Database/ImportHistory.cs index 535b502..215feec 100644 --- a/Pal.Client/Database/ImportHistory.cs +++ b/Pal.Client/Database/ImportHistory.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -namespace Pal.Client.Database -{ - internal sealed class ImportHistory - { - public Guid Id { get; set; } - public string? RemoteUrl { get; set; } - public DateTime ExportedAt { get; set; } - public DateTime ImportedAt { get; set; } +namespace Pal.Client.Database; - public List ImportedLocations { get; set; } = new(); - } +internal sealed class ImportHistory +{ + public Guid Id { get; set; } + public string? RemoteUrl { get; set; } + public DateTime ExportedAt { get; set; } + public DateTime ImportedAt { get; set; } + + public List ImportedLocations { get; set; } = new(); } diff --git a/Pal.Client/Database/PalClientContext.cs b/Pal.Client/Database/PalClientContext.cs index 8cbd6a6..1d8fea3 100644 --- a/Pal.Client/Database/PalClientContext.cs +++ b/Pal.Client/Database/PalClientContext.cs @@ -1,24 +1,23 @@ using Microsoft.EntityFrameworkCore; -namespace Pal.Client.Database +namespace Pal.Client.Database; + +internal class PalClientContext : DbContext { - internal class PalClientContext : DbContext + public DbSet Locations { get; set; } = null!; + public DbSet Imports { get; set; } = null!; + public DbSet RemoteEncounters { get; set; } = null!; + + public PalClientContext(DbContextOptions options) + : base(options) { - public DbSet Locations { get; set; } = null!; - public DbSet Imports { get; set; } = null!; - public DbSet RemoteEncounters { get; set; } = null!; + } - public PalClientContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .HasMany(o => o.ImportedBy) - .WithMany(o => o.ImportedLocations) - .UsingEntity(o => o.ToTable("LocationImports")); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(o => o.ImportedBy) + .WithMany(o => o.ImportedLocations) + .UsingEntity(o => o.ToTable("LocationImports")); } } diff --git a/Pal.Client/Database/RemoteEncounter.cs b/Pal.Client/Database/RemoteEncounter.cs index 0a0f0a1..fce3a39 100644 --- a/Pal.Client/Database/RemoteEncounter.cs +++ b/Pal.Client/Database/RemoteEncounter.cs @@ -2,40 +2,39 @@ using Pal.Client.Extensions; using Pal.Client.Net; -namespace Pal.Client.Database +namespace Pal.Client.Database; + +/// +/// To avoid sending too many requests to the server, we cache which locations have been seen +/// locally. These never expire, and locations which have been seen with a specific account +/// are never sent to the server again. +/// +/// To be marked as seen, it needs to be essentially processed by . +/// +internal sealed class RemoteEncounter { + [Key] + public int Id { get; private set; } + + public int ClientLocationId { get; private set; } + public ClientLocation ClientLocation { get; private set; } = null!; + /// - /// To avoid sending too many requests to the server, we cache which locations have been seen - /// locally. These never expire, and locations which have been seen with a specific account - /// are never sent to the server again. - /// - /// To be marked as seen, it needs to be essentially processed by . + /// Partial account id. This is partially unique - however problems would (in theory) + /// only occur once you have two account-ids where the first 13 characters are equal. /// - internal sealed class RemoteEncounter + [MaxLength(13)] + public string AccountId { get; private set; } + + private RemoteEncounter(int clientLocationId, string accountId) { - [Key] - public int Id { get; private set; } + ClientLocationId = clientLocationId; + AccountId = accountId; + } - public int ClientLocationId { get; private set; } - public ClientLocation ClientLocation { get; private set; } = null!; - - /// - /// Partial account id. This is partially unique - however problems would (in theory) - /// only occur once you have two account-ids where the first 13 characters are equal. - /// - [MaxLength(13)] - public string AccountId { get; private set; } - - private RemoteEncounter(int clientLocationId, string accountId) - { - ClientLocationId = clientLocationId; - AccountId = accountId; - } - - public RemoteEncounter(ClientLocation clientLocation, string accountId) - { - ClientLocation = clientLocation; - AccountId = accountId.ToPartialId(); - } + public RemoteEncounter(ClientLocation clientLocation, string accountId) + { + ClientLocation = clientLocation; + AccountId = accountId.ToPartialId(); } } diff --git a/Pal.Client/DependencyContextInitializer.cs b/Pal.Client/DependencyContextInitializer.cs index ccf8b4f..b5e52d0 100644 --- a/Pal.Client/DependencyContextInitializer.cs +++ b/Pal.Client/DependencyContextInitializer.cs @@ -19,178 +19,177 @@ using Pal.Client.DependencyInjection; using Pal.Client.Floors; using Pal.Client.Windows; -namespace Pal.Client +namespace Pal.Client; + +/// +/// Takes care of async plugin init - this is mostly everything that requires either the config or the database to +/// be available. +/// +internal sealed class DependencyContextInitializer { - /// - /// Takes care of async plugin init - this is mostly everything that requires either the config or the database to - /// be available. - /// - internal sealed class DependencyContextInitializer + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public DependencyContextInitializer(ILogger logger, + IServiceProvider serviceProvider) { - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; + _logger = logger; + _serviceProvider = serviceProvider; + } - public DependencyContextInitializer(ILogger logger, - IServiceProvider serviceProvider) - { - _logger = logger; - _serviceProvider = serviceProvider; - } + public async Task InitializeAsync(CancellationToken cancellationToken) + { + using IDisposable? logScope = _logger.BeginScope("AsyncInit"); - public async Task InitializeAsync(CancellationToken cancellationToken) - { - using IDisposable? logScope = _logger.BeginScope("AsyncInit"); + _logger.LogInformation("Starting async init"); - _logger.LogInformation("Starting async init"); + await CreateBackup(); + cancellationToken.ThrowIfCancellationRequested(); - await CreateBackup(); - cancellationToken.ThrowIfCancellationRequested(); + await RunMigrations(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - await RunMigrations(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - // v1 migration: config migration for import history, json migration for markers - _serviceProvider.GetRequiredService().Migrate(); - await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + await RunCleanup(); + cancellationToken.ThrowIfCancellationRequested(); - await RunCleanup(); - cancellationToken.ThrowIfCancellationRequested(); + await RemoveOldBackups(); + cancellationToken.ThrowIfCancellationRequested(); - await RemoveOldBackups(); - cancellationToken.ThrowIfCancellationRequested(); + // windows that have logic to open on startup + _serviceProvider.GetRequiredService(); - // windows that have logic to open on startup - _serviceProvider.GetRequiredService(); + // initialize components that are mostly self-contained/self-registered + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); - // initialize components that are mostly self-contained/self-registered - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + // eager load any commands to find errors now, not when running them + _serviceProvider.GetRequiredService>(); - // eager load any commands to find errors now, not when running them - _serviceProvider.GetRequiredService>(); + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + if (_serviceProvider.GetRequiredService().HasBetaFeature(ObjectTableDebug.FeatureName)) + _serviceProvider.GetRequiredService(); - if (_serviceProvider.GetRequiredService().HasBetaFeature(ObjectTableDebug.FeatureName)) - _serviceProvider.GetRequiredService(); + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Async init complete"); + } - _logger.LogInformation("Async init complete"); - } + private async Task RemoveOldBackups() + { + await using var scope = _serviceProvider.CreateAsyncScope(); + var pluginInterface = scope.ServiceProvider.GetRequiredService(); + var configuration = scope.ServiceProvider.GetRequiredService(); - private async Task RemoveOldBackups() - { - await using var scope = _serviceProvider.CreateAsyncScope(); - var pluginInterface = scope.ServiceProvider.GetRequiredService(); - var configuration = scope.ServiceProvider.GetRequiredService(); - - var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3", - new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = false, - MatchCasing = MatchCasing.CaseSensitive, - AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System, - ReturnSpecialDirectories = false, - }); - if (paths.Length == 0) - return; - - Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled); - List<(DateTime Date, string Path)> backupFiles = new(); - foreach (string path in paths) + var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3", + new EnumerationOptions { - var match = backupRegex.Match(Path.GetFileName(path)); - if (!match.Success) - continue; + IgnoreInaccessible = true, + RecurseSubdirectories = false, + MatchCasing = MatchCasing.CaseSensitive, + AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System, + ReturnSpecialDirectories = false, + }); + if (paths.Length == 0) + return; - if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal, out DateTime backupDate)) - { - backupFiles.Add((backupDate, path)); - } - } + Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled); + List<(DateTime Date, string Path)> backupFiles = new(); + foreach (string path in paths) + { + var match = backupRegex.Match(Path.GetFileName(path)); + if (!match.Success) + continue; - var toDelete = backupFiles.OrderByDescending(x => x.Date) - .Skip(configuration.Backups.MinimumBackupsToKeep) - .Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter) - .Select(x => x.Path); - foreach (var path in toDelete) + if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal, out DateTime backupDate)) { - try - { - File.Delete(path); - _logger.LogInformation("Deleted old backup file '{Path}'", path); - } - catch (Exception e) - { - _logger.LogWarning(e, "Could not delete backup file '{Path}'", path); - } + backupFiles.Add((backupDate, path)); } } - private async Task CreateBackup() + var toDelete = backupFiles.OrderByDescending(x => x.Date) + .Skip(configuration.Backups.MinimumBackupsToKeep) + .Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter) + .Select(x => x.Path); + foreach (var path in toDelete) { - await using var scope = _serviceProvider.CreateAsyncScope(); - - var pluginInterface = scope.ServiceProvider.GetRequiredService(); - string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(), - $"backup-{DateTime.Now.ToUniversalTime():yyyy-MM-dd}.data.sqlite3"); - string sourcePath = Path.Join(pluginInterface.GetPluginConfigDirectory(), - DependencyInjectionContext.DatabaseFileName); - if (File.Exists(sourcePath) && !File.Exists(backupPath)) + try { - try - { - if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal")) - { - _logger.LogInformation("Creating database backup '{Path}' (open db)", backupPath); - await using var db = scope.ServiceProvider.GetRequiredService(); - await using SqliteConnection source = new(db.Database.GetConnectionString()); - await source.OpenAsync(); - await using SqliteConnection backup = new($"Data Source={backupPath}"); - source.BackupDatabase(backup); - SqliteConnection.ClearPool(backup); - } - else - { - _logger.LogInformation("Creating database backup '{Path}' (file copy)", backupPath); - File.Copy(sourcePath, backupPath); - } - } - catch (Exception e) - { - _logger.LogError(e, "Could not create backup"); - } + File.Delete(path); + _logger.LogInformation("Deleted old backup file '{Path}'", path); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not delete backup file '{Path}'", path); } - else - _logger.LogInformation("Database backup in '{Path}' already exists", backupPath); - } - - private async Task RunMigrations(CancellationToken cancellationToken) - { - await using var scope = _serviceProvider.CreateAsyncScope(); - - _logger.LogInformation("Loading database & running migrations"); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); - - // takes 2-3 seconds with initializing connections, loading driver etc. - await dbContext.Database.MigrateAsync(cancellationToken); - _logger.LogInformation("Completed database migrations"); - } - - private async Task RunCleanup() - { - await using var scope = _serviceProvider.CreateAsyncScope(); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); - var cleanup = scope.ServiceProvider.GetRequiredService(); - - cleanup.Purge(dbContext); - - await dbContext.SaveChangesAsync(); } } + + private async Task CreateBackup() + { + await using var scope = _serviceProvider.CreateAsyncScope(); + + var pluginInterface = scope.ServiceProvider.GetRequiredService(); + string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(), + $"backup-{DateTime.Now.ToUniversalTime():yyyy-MM-dd}.data.sqlite3"); + string sourcePath = Path.Join(pluginInterface.GetPluginConfigDirectory(), + DependencyInjectionContext.DatabaseFileName); + if (File.Exists(sourcePath) && !File.Exists(backupPath)) + { + try + { + if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal")) + { + _logger.LogInformation("Creating database backup '{Path}' (open db)", backupPath); + await using var db = scope.ServiceProvider.GetRequiredService(); + await using SqliteConnection source = new(db.Database.GetConnectionString()); + await source.OpenAsync(); + await using SqliteConnection backup = new($"Data Source={backupPath}"); + source.BackupDatabase(backup); + SqliteConnection.ClearPool(backup); + } + else + { + _logger.LogInformation("Creating database backup '{Path}' (file copy)", backupPath); + File.Copy(sourcePath, backupPath); + } + } + catch (Exception e) + { + _logger.LogError(e, "Could not create backup"); + } + } + else + _logger.LogInformation("Database backup in '{Path}' already exists", backupPath); + } + + private async Task RunMigrations(CancellationToken cancellationToken) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + + _logger.LogInformation("Loading database & running migrations"); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + + // takes 2-3 seconds with initializing connections, loading driver etc. + await dbContext.Database.MigrateAsync(cancellationToken); + _logger.LogInformation("Completed database migrations"); + } + + private async Task RunCleanup() + { + await using var scope = _serviceProvider.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + var cleanup = scope.ServiceProvider.GetRequiredService(); + + cleanup.Purge(dbContext); + + await dbContext.SaveChangesAsync(); + } } diff --git a/Pal.Client/DependencyInjection/Chat.cs b/Pal.Client/DependencyInjection/Chat.cs index 05a91e1..d6fdb3d 100644 --- a/Pal.Client/DependencyInjection/Chat.cs +++ b/Pal.Client/DependencyInjection/Chat.cs @@ -3,36 +3,35 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Pal.Client.Properties; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed class Chat { - internal sealed class Chat + private readonly ChatGui _chatGui; + + public Chat(ChatGui chatGui) { - private readonly ChatGui _chatGui; - - public Chat(ChatGui chatGui) - { - _chatGui = chatGui; - } - - public void Error(string e) - { - _chatGui.PrintChat(new XivChatEntry - { - Message = new SeStringBuilder() - .AddUiForeground($"[{Localization.Palace_Pal}] ", 16) - .AddText(e).Build(), - Type = XivChatType.Urgent - }); - } - - public void Message(string message) - { - _chatGui.Print(new SeStringBuilder() - .AddUiForeground($"[{Localization.Palace_Pal}] ", 57) - .AddText(message).Build()); - } - - public void UnformattedMessage(string message) - => _chatGui.Print(message); + _chatGui = chatGui; } + + public void Error(string e) + { + _chatGui.PrintChat(new XivChatEntry + { + Message = new SeStringBuilder() + .AddUiForeground($"[{Localization.Palace_Pal}] ", 16) + .AddText(e).Build(), + Type = XivChatType.Urgent + }); + } + + public void Message(string message) + { + _chatGui.Print(new SeStringBuilder() + .AddUiForeground($"[{Localization.Palace_Pal}] ", 57) + .AddText(message).Build()); + } + + public void UnformattedMessage(string message) + => _chatGui.Print(message); } diff --git a/Pal.Client/DependencyInjection/ChatService.cs b/Pal.Client/DependencyInjection/ChatService.cs index 97ffaed..2623aca 100644 --- a/Pal.Client/DependencyInjection/ChatService.cs +++ b/Pal.Client/DependencyInjection/ChatService.cs @@ -8,103 +8,102 @@ using Lumina.Excel.GeneratedSheets; using Pal.Client.Configuration; using Pal.Client.Floors; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed class ChatService : IDisposable { - internal sealed class ChatService : IDisposable + private readonly ChatGui _chatGui; + private readonly TerritoryState _territoryState; + private readonly IPalacePalConfiguration _configuration; + private readonly DataManager _dataManager; + private readonly LocalizedChatMessages _localizedChatMessages; + + public ChatService(ChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration, + DataManager dataManager) { - private readonly ChatGui _chatGui; - private readonly TerritoryState _territoryState; - private readonly IPalacePalConfiguration _configuration; - private readonly DataManager _dataManager; - private readonly LocalizedChatMessages _localizedChatMessages; + _chatGui = chatGui; + _territoryState = territoryState; + _configuration = configuration; + _dataManager = dataManager; - public ChatService(ChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration, - DataManager dataManager) + _localizedChatMessages = LoadLanguageStrings(); + + _chatGui.ChatMessage += OnChatMessage; + } + + public void Dispose() + => _chatGui.ChatMessage -= OnChatMessage; + + private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, + ref bool isHandled) + { + if (_configuration.FirstUse) + return; + + if (type != (XivChatType)2105) + return; + + string message = seMessage.ToString(); + if (_localizedChatMessages.FloorChanged.IsMatch(message)) { - _chatGui = chatGui; - _territoryState = territoryState; - _configuration = configuration; - _dataManager = dataManager; + _territoryState.PomanderOfSight = PomanderState.Inactive; - _localizedChatMessages = LoadLanguageStrings(); - - _chatGui.ChatMessage += OnChatMessage; + if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) + _territoryState.PomanderOfIntuition = PomanderState.Inactive; } - - public void Dispose() - => _chatGui.ChatMessage -= OnChatMessage; - - private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, - ref bool isHandled) + else if (message.EndsWith(_localizedChatMessages.MapRevealed)) { - if (_configuration.FirstUse) - return; - - if (type != (XivChatType)2105) - return; - - string message = seMessage.ToString(); - if (_localizedChatMessages.FloorChanged.IsMatch(message)) - { - _territoryState.PomanderOfSight = PomanderState.Inactive; - - if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) - _territoryState.PomanderOfIntuition = PomanderState.Inactive; - } - else if (message.EndsWith(_localizedChatMessages.MapRevealed)) - { - _territoryState.PomanderOfSight = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved)) - { - _territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed; - } - else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) || - message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor)) - { - // There is no functional difference between these - if you don't open the marked coffer, - // going to higher floors will keep the pomander active. - _territoryState.PomanderOfIntuition = PomanderState.Active; - } - else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened)) - { - _territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; - } + _territoryState.PomanderOfSight = PomanderState.Active; } - - private LocalizedChatMessages LoadLanguageStrings() + else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved)) { - return new LocalizedChatMessages - { - MapRevealed = GetLocalizedString(7256), - AllTrapsRemoved = GetLocalizedString(7255), - HoardOnCurrentFloor = GetLocalizedString(7272), - HoardNotOnCurrentFloor = GetLocalizedString(7273), - HoardCofferOpened = GetLocalizedString(7274), - FloorChanged = - new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") + - "$"), - }; + _territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed; } - - private string GetLocalizedString(uint id) + else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) || + message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor)) { - return _dataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; + // There is no functional difference between these - if you don't open the marked coffer, + // going to higher floors will keep the pomander active. + _territoryState.PomanderOfIntuition = PomanderState.Active; } - - private sealed class LocalizedChatMessages + else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened)) { - public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!"; - public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!"; - public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you..."; - - public string HoardNotOnCurrentFloor { get; init; } = - "???"; // "You do not sense the call of the Accursed Hoard on this floor..."; - - public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!"; - - public Regex FloorChanged { get; init; } = - new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$"); + _territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; } } + + private LocalizedChatMessages LoadLanguageStrings() + { + return new LocalizedChatMessages + { + MapRevealed = GetLocalizedString(7256), + AllTrapsRemoved = GetLocalizedString(7255), + HoardOnCurrentFloor = GetLocalizedString(7272), + HoardNotOnCurrentFloor = GetLocalizedString(7273), + HoardCofferOpened = GetLocalizedString(7274), + FloorChanged = + new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") + + "$"), + }; + } + + private string GetLocalizedString(uint id) + { + return _dataManager.GetExcelSheet()?.GetRow(id)?.Text?.ToString() ?? "Unknown"; + } + + private sealed class LocalizedChatMessages + { + public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!"; + public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!"; + public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you..."; + + public string HoardNotOnCurrentFloor { get; init; } = + "???"; // "You do not sense the call of the Accursed Hoard on this floor..."; + + public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!"; + + public Regex FloorChanged { get; init; } = + new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$"); + } } diff --git a/Pal.Client/DependencyInjection/DebugState.cs b/Pal.Client/DependencyInjection/DebugState.cs index 1fe7624..9833ab2 100644 --- a/Pal.Client/DependencyInjection/DebugState.cs +++ b/Pal.Client/DependencyInjection/DebugState.cs @@ -1,15 +1,14 @@ using System; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed class DebugState { - internal sealed class DebugState - { - public string? DebugMessage { get; set; } + public string? DebugMessage { get; set; } - public void SetFromException(Exception e) - => DebugMessage = $"{DateTime.Now}\n{e}"; + public void SetFromException(Exception e) + => DebugMessage = $"{DateTime.Now}\n{e}"; - public void Reset() - => DebugMessage = null; - } + public void Reset() + => DebugMessage = null; } diff --git a/Pal.Client/DependencyInjection/GameHooks.cs b/Pal.Client/DependencyInjection/GameHooks.cs index 7e7d868..7762c6e 100644 --- a/Pal.Client/DependencyInjection/GameHooks.cs +++ b/Pal.Client/DependencyInjection/GameHooks.cs @@ -8,99 +8,98 @@ using Dalamud.Utility.Signatures; using Microsoft.Extensions.Logging; using Pal.Client.Floors; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed unsafe class GameHooks : IDisposable { - internal sealed unsafe class GameHooks : IDisposable - { - private readonly ILogger _logger; - private readonly ObjectTable _objectTable; - private readonly TerritoryState _territoryState; - private readonly FrameworkService _frameworkService; + private readonly ILogger _logger; + private readonly ObjectTable _objectTable; + private readonly TerritoryState _territoryState; + private readonly FrameworkService _frameworkService; #pragma warning disable CS0649 - private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7); + private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7); - [Signature("40 53 55 56 57 48 81 EC ?? ?? ?? ?? 0F 29 B4 24 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 AC 24 ?? ?? ?? ?? 0F 28 F3 49 8B F8", DetourName = nameof(ActorVfxCreate))] - private Hook ActorVfxCreateHook { get; init; } = null!; + [Signature("40 53 55 56 57 48 81 EC ?? ?? ?? ?? 0F 29 B4 24 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 AC 24 ?? ?? ?? ?? 0F 28 F3 49 8B F8", DetourName = nameof(ActorVfxCreate))] + private Hook ActorVfxCreateHook { get; init; } = null!; #pragma warning restore CS0649 - public GameHooks(ILogger logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) + public GameHooks(ILogger logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService) + { + _logger = logger; + _objectTable = objectTable; + _territoryState = territoryState; + _frameworkService = frameworkService; + + _logger.LogDebug("Initializing game hooks"); + SignatureHelper.Initialise(this); + ActorVfxCreateHook.Enable(); + + _logger.LogDebug("Game hooks initialized"); + } + + /// + /// Even with a pomander of sight, the BattleChara's position for the trap remains at {0, 0, 0} until it is activated. + /// Upon exploding, the trap's position is moved to the exact location that the pomander of sight would have revealed. + /// + /// That exact position appears to be used for VFX playing when you walk into it - even if you barely walk into the + /// outer ring of an otter/luring/impeding/landmine trap, the VFX plays at the exact center and not at your character's + /// location. + /// + /// Especially at higher floors, you're more likely to walk into an undiscovered trap compared to e.g. 51-60, + /// and you probably don't want to/can't use sight on every floor - yet the trap location is still useful information. + /// + /// Some (but not all) chests also count as BattleChara named 'Trap', however the effect upon opening isn't played via + /// ActorVfxCreate even if they explode (but probably as a Vfx with static location, doesn't matter for here). + /// + /// Landmines and luring traps also don't play a VFX attached to their BattleChara. + /// + /// otter: vfx/common/eff/dk05th_stdn0t.avfx
+ /// toading: vfx/common/eff/dk05th_stdn0t.avfx
+ /// enfeebling: vfx/common/eff/dk05th_stdn0t.avfx
+ /// landmine: none
+ /// luring: none
+ /// impeding: vfx/common/eff/dk05ht_ipws0t.avfx (one of silence/pacification)
+ /// impeding: vfx/common/eff/dk05ht_slet0t.avfx (the other of silence/pacification)
+ /// + /// It is of course annoying that, when testing, almost all traps are landmines. + /// There's also vfx/common/eff/dk01gd_inv0h.avfx for e.g. impeding when you're invulnerable, but not sure if that + /// has other trigger conditions. + ///
+ public nint ActorVfxCreate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7) + { + try { - _logger = logger; - _objectTable = objectTable; - _territoryState = territoryState; - _frameworkService = frameworkService; - - _logger.LogDebug("Initializing game hooks"); - SignatureHelper.Initialise(this); - ActorVfxCreateHook.Enable(); - - _logger.LogDebug("Game hooks initialized"); - } - - /// - /// Even with a pomander of sight, the BattleChara's position for the trap remains at {0, 0, 0} until it is activated. - /// Upon exploding, the trap's position is moved to the exact location that the pomander of sight would have revealed. - /// - /// That exact position appears to be used for VFX playing when you walk into it - even if you barely walk into the - /// outer ring of an otter/luring/impeding/landmine trap, the VFX plays at the exact center and not at your character's - /// location. - /// - /// Especially at higher floors, you're more likely to walk into an undiscovered trap compared to e.g. 51-60, - /// and you probably don't want to/can't use sight on every floor - yet the trap location is still useful information. - /// - /// Some (but not all) chests also count as BattleChara named 'Trap', however the effect upon opening isn't played via - /// ActorVfxCreate even if they explode (but probably as a Vfx with static location, doesn't matter for here). - /// - /// Landmines and luring traps also don't play a VFX attached to their BattleChara. - /// - /// otter: vfx/common/eff/dk05th_stdn0t.avfx
- /// toading: vfx/common/eff/dk05th_stdn0t.avfx
- /// enfeebling: vfx/common/eff/dk05th_stdn0t.avfx
- /// landmine: none
- /// luring: none
- /// impeding: vfx/common/eff/dk05ht_ipws0t.avfx (one of silence/pacification)
- /// impeding: vfx/common/eff/dk05ht_slet0t.avfx (the other of silence/pacification)
- /// - /// It is of course annoying that, when testing, almost all traps are landmines. - /// There's also vfx/common/eff/dk01gd_inv0h.avfx for e.g. impeding when you're invulnerable, but not sure if that - /// has other trigger conditions. - ///
- public nint ActorVfxCreate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7) - { - try + if (_territoryState.IsInDeepDungeon()) { - if (_territoryState.IsInDeepDungeon()) + var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); + var obj = _objectTable.CreateObjectReference(a2); + + /* + if (Service.Configuration.BetaKey == "VFX") + _chat.PalPrint($"{vfxPath} on {obj}"); + */ + + if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395)) { - var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); - var obj = _objectTable.CreateObjectReference(a2); - - /* - if (Service.Configuration.BetaKey == "VFX") - _chat.PalPrint($"{vfxPath} on {obj}"); - */ - - if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395)) + if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") { - if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") - { - _logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position); - _frameworkService.NextUpdateObjects.Enqueue(obj.Address); - } + _logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position); + _frameworkService.NextUpdateObjects.Enqueue(obj.Address); } } } - catch (Exception e) - { - _logger.LogError(e, "VFX Create Hook failed"); - } - return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7); } - - public void Dispose() + catch (Exception e) { - _logger.LogDebug("Disposing game hooks"); - ActorVfxCreateHook.Dispose(); + _logger.LogError(e, "VFX Create Hook failed"); } + return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7); + } + + public void Dispose() + { + _logger.LogDebug("Disposing game hooks"); + ActorVfxCreateHook.Dispose(); } } diff --git a/Pal.Client/DependencyInjection/ImportService.cs b/Pal.Client/DependencyInjection/ImportService.cs index b67e1fd..c7d02e3 100644 --- a/Pal.Client/DependencyInjection/ImportService.cs +++ b/Pal.Client/DependencyInjection/ImportService.cs @@ -12,155 +12,154 @@ using Pal.Client.Floors; using Pal.Client.Floors.Tasks; using Pal.Common; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed class ImportService { - internal sealed class ImportService + private readonly IServiceProvider _serviceProvider; + private readonly FloorService _floorService; + private readonly Cleanup _cleanup; + + public ImportService( + IServiceProvider serviceProvider, + FloorService floorService, + Cleanup cleanup) { - private readonly IServiceProvider _serviceProvider; - private readonly FloorService _floorService; - private readonly Cleanup _cleanup; + _serviceProvider = serviceProvider; + _floorService = floorService; + _cleanup = cleanup; + } - public ImportService( - IServiceProvider serviceProvider, - FloorService floorService, - Cleanup cleanup) + public async Task FindLast(CancellationToken token = default) + { + await using var scope = _serviceProvider.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + + return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id) + .FirstOrDefaultAsync(cancellationToken: token); + } + + public (int traps, int hoard) Import(ExportRoot import) + { + try { - _serviceProvider = serviceProvider; - _floorService = floorService; - _cleanup = cleanup; - } + _floorService.SetToImportState(); - public async Task FindLast(CancellationToken token = default) - { - await using var scope = _serviceProvider.CreateAsyncScope(); - await using var dbContext = scope.ServiceProvider.GetRequiredService(); + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); - return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id) - .FirstOrDefaultAsync(cancellationToken: token); - } + dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); + dbContext.SaveChanges(); - public (int traps, int hoard) Import(ExportRoot import) - { - try + ImportHistory importHistory = new ImportHistory { - _floorService.SetToImportState(); + Id = Guid.Parse(import.ExportId), + RemoteUrl = import.ServerUrl, + ExportedAt = import.CreatedAt.ToDateTime(), + ImportedAt = DateTime.UtcNow, + }; + dbContext.Imports.Add(importHistory); - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); + int traps = 0; + int hoard = 0; + foreach (var floor in import.Floors) + { + ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType; - dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); - dbContext.SaveChanges(); - - ImportHistory importHistory = new ImportHistory + List existingLocations = dbContext.Locations + .Where(loc => loc.TerritoryType == floor.TerritoryType) + .ToList() + .Select(LoadTerritory.ToMemoryLocation) + .ToList(); + foreach (var exportLocation in floor.Objects) { - Id = Guid.Parse(import.ExportId), - RemoteUrl = import.ServerUrl, - ExportedAt = import.CreatedAt.ToDateTime(), - ImportedAt = DateTime.UtcNow, - }; - dbContext.Imports.Add(importHistory); - - int traps = 0; - int hoard = 0; - foreach (var floor in import.Floors) - { - ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType; - - List existingLocations = dbContext.Locations - .Where(loc => loc.TerritoryType == floor.TerritoryType) - .ToList() - .Select(LoadTerritory.ToMemoryLocation) - .ToList(); - foreach (var exportLocation in floor.Objects) + PersistentLocation persistentLocation = new PersistentLocation { - PersistentLocation persistentLocation = new PersistentLocation - { - Type = ToMemoryType(exportLocation.Type), - Position = new Vector3(exportLocation.X, exportLocation.Y, exportLocation.Z), - Source = ClientLocation.ESource.Unknown, - }; + Type = ToMemoryType(exportLocation.Type), + Position = new Vector3(exportLocation.X, exportLocation.Y, exportLocation.Z), + Source = ClientLocation.ESource.Unknown, + }; - var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation); - if (existingLocation != null) - { - var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId); - clientLoc?.ImportedBy.Add(importHistory); + var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation); + if (existingLocation != null) + { + var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId); + clientLoc?.ImportedBy.Add(importHistory); - continue; - } - - ClientLocation clientLocation = new ClientLocation - { - TerritoryType = (ushort)territoryType, - Type = ToClientLocationType(exportLocation.Type), - X = exportLocation.X, - Y = exportLocation.Y, - Z = exportLocation.Z, - Seen = false, - Source = ClientLocation.ESource.Import, - ImportedBy = new List { importHistory }, - SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), - }; - dbContext.Locations.Add(clientLocation); - - if (exportLocation.Type == ExportObjectType.Trap) - traps++; - else if (exportLocation.Type == ExportObjectType.Hoard) - hoard++; + continue; } + + ClientLocation clientLocation = new ClientLocation + { + TerritoryType = (ushort)territoryType, + Type = ToClientLocationType(exportLocation.Type), + X = exportLocation.X, + Y = exportLocation.Y, + Z = exportLocation.Z, + Seen = false, + Source = ClientLocation.ESource.Import, + ImportedBy = new List { importHistory }, + SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), + }; + dbContext.Locations.Add(clientLocation); + + if (exportLocation.Type == ExportObjectType.Trap) + traps++; + else if (exportLocation.Type == ExportObjectType.Hoard) + hoard++; } - - dbContext.SaveChanges(); - - _cleanup.Purge(dbContext); - dbContext.SaveChanges(); - - return (traps, hoard); - } - finally - { - _floorService.ResetAll(); } + + dbContext.SaveChanges(); + + _cleanup.Purge(dbContext); + dbContext.SaveChanges(); + + return (traps, hoard); } - - private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType) + finally { - return exportLocationType switch - { - ExportObjectType.Trap => MemoryLocation.EType.Trap, - ExportObjectType.Hoard => MemoryLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null) - }; + _floorService.ResetAll(); } + } - private ClientLocation.EType ToClientLocationType(ExportObjectType exportLocationType) + private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType) + { + return exportLocationType switch { - return exportLocationType switch - { - ExportObjectType.Trap => ClientLocation.EType.Trap, - ExportObjectType.Hoard => ClientLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null) - }; + ExportObjectType.Trap => MemoryLocation.EType.Trap, + ExportObjectType.Hoard => MemoryLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null) + }; + } + + private ClientLocation.EType ToClientLocationType(ExportObjectType exportLocationType) + { + return exportLocationType switch + { + ExportObjectType.Trap => ClientLocation.EType.Trap, + ExportObjectType.Hoard => ClientLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null) + }; + } + + public void RemoveById(Guid id) + { + try + { + _floorService.SetToImportState(); + using var scope = _serviceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); + dbContext.SaveChanges(); + + _cleanup.Purge(dbContext); + dbContext.SaveChanges(); } - - public void RemoveById(Guid id) + finally { - try - { - _floorService.SetToImportState(); - using var scope = _serviceProvider.CreateScope(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id)); - dbContext.SaveChanges(); - - _cleanup.Purge(dbContext); - dbContext.SaveChanges(); - } - finally - { - _floorService.ResetAll(); - } + _floorService.ResetAll(); } } } diff --git a/Pal.Client/DependencyInjection/RepoVerification.cs b/Pal.Client/DependencyInjection/RepoVerification.cs index 349bf46..26cdd69 100644 --- a/Pal.Client/DependencyInjection/RepoVerification.cs +++ b/Pal.Client/DependencyInjection/RepoVerification.cs @@ -6,25 +6,24 @@ using Microsoft.Extensions.Logging; using Pal.Client.Extensions; using Pal.Client.Properties; -namespace Pal.Client.DependencyInjection -{ - internal sealed class RepoVerification - { - public RepoVerification(ILogger logger, DalamudPluginInterface pluginInterface, Chat chat) - { - logger.LogInformation("Install source: {Repo}", pluginInterface.SourceRepository); - if (!pluginInterface.IsDev - && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") - && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) - { - chat.Error(string.Format(Localization.Error_WrongRepository, - "https://github.com/carvelli/Dalamud-Plugins")); - throw new RepoVerificationFailedException(); - } - } +namespace Pal.Client.DependencyInjection; - internal sealed class RepoVerificationFailedException : Exception +internal sealed class RepoVerification +{ + public RepoVerification(ILogger logger, DalamudPluginInterface pluginInterface, Chat chat) + { + logger.LogInformation("Install source: {Repo}", pluginInterface.SourceRepository); + if (!pluginInterface.IsDev + && !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/") + && !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/")) { + chat.Error(string.Format(Localization.Error_WrongRepository, + "https://github.com/carvelli/Dalamud-Plugins")); + throw new RepoVerificationFailedException(); } } + + internal sealed class RepoVerificationFailedException : Exception + { + } } diff --git a/Pal.Client/DependencyInjection/StatisticsService.cs b/Pal.Client/DependencyInjection/StatisticsService.cs index 5b461f6..14dc8f3 100644 --- a/Pal.Client/DependencyInjection/StatisticsService.cs +++ b/Pal.Client/DependencyInjection/StatisticsService.cs @@ -9,67 +9,66 @@ using Pal.Client.Net; using Pal.Client.Properties; using Pal.Client.Windows; -namespace Pal.Client.DependencyInjection +namespace Pal.Client.DependencyInjection; + +internal sealed class StatisticsService { - internal sealed class StatisticsService + private readonly IPalacePalConfiguration _configuration; + private readonly ILogger _logger; + private readonly RemoteApi _remoteApi; + private readonly StatisticsWindow _statisticsWindow; + private readonly Chat _chat; + + public StatisticsService( + IPalacePalConfiguration configuration, + ILogger logger, + RemoteApi remoteApi, + StatisticsWindow statisticsWindow, + Chat chat) { - private readonly IPalacePalConfiguration _configuration; - private readonly ILogger _logger; - private readonly RemoteApi _remoteApi; - private readonly StatisticsWindow _statisticsWindow; - private readonly Chat _chat; + _configuration = configuration; + _logger = logger; + _remoteApi = remoteApi; + _statisticsWindow = statisticsWindow; + _chat = chat; + } - public StatisticsService( - IPalacePalConfiguration configuration, - ILogger logger, - RemoteApi remoteApi, - StatisticsWindow statisticsWindow, - Chat chat) - { - _configuration = configuration; - _logger = logger; - _remoteApi = remoteApi; - _statisticsWindow = statisticsWindow; - _chat = chat; - } + public void ShowGlobalStatistics() + { + Task.Run(async () => await FetchFloorStatistics()); + } - public void ShowGlobalStatistics() + private async Task FetchFloorStatistics() + { + try { - Task.Run(async () => await FetchFloorStatistics()); - } - - private async Task FetchFloorStatistics() - { - try + if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view")) { - if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view")) - { - _chat.Error(Localization.Command_pal_stats_CurrentFloor); - return; - } - - var (success, floorStatistics) = await _remoteApi.FetchStatistics(); - if (success) - { - _statisticsWindow.SetFloorData(floorStatistics); - _statisticsWindow.IsOpen = true; - } - else - { - _chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics); - } - } - catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) - { - _logger.LogWarning(e, "Access denied while fetching floor statistics"); _chat.Error(Localization.Command_pal_stats_CurrentFloor); + return; } - catch (Exception e) + + var (success, floorStatistics) = await _remoteApi.FetchStatistics(); + if (success) { - _logger.LogError(e, "Could not fetch floor statistics"); - _chat.Error(string.Format(Localization.Error_CommandFailed, - $"{e.GetType()} - {e.Message}")); + _statisticsWindow.SetFloorData(floorStatistics); + _statisticsWindow.IsOpen = true; } + else + { + _chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics); + } + } + catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied) + { + _logger.LogWarning(e, "Access denied while fetching floor statistics"); + _chat.Error(Localization.Command_pal_stats_CurrentFloor); + } + catch (Exception e) + { + _logger.LogError(e, "Could not fetch floor statistics"); + _chat.Error(string.Format(Localization.Error_CommandFailed, + $"{e.GetType()} - {e.Message}")); } } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index f2779db..5ab26e5 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -25,166 +25,165 @@ using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Client.Windows; -namespace Pal.Client +namespace Pal.Client; + +/// +/// DI-aware Plugin. +/// +internal sealed class DependencyInjectionContext : IDisposable { + public const string DatabaseFileName = "palace-pal.data.sqlite3"; + public static DalamudLoggerProvider LoggerProvider { get; } = new(typeof(Plugin).Assembly); + /// - /// DI-aware Plugin. + /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. /// - internal sealed class DependencyInjectionContext : IDisposable + private ILogger _logger = LoggerProvider.CreateLogger(); + + private readonly string _sqliteConnectionString; + private readonly ServiceCollection _serviceCollection = new(); + private ServiceProvider? _serviceProvider; + + public DependencyInjectionContext( + DalamudPluginInterface pluginInterface, + ClientState clientState, + GameGui gameGui, + ChatGui chatGui, + ObjectTable objectTable, + Framework framework, + Condition condition, + CommandManager commandManager, + DataManager dataManager, + Plugin plugin) { - public const string DatabaseFileName = "palace-pal.data.sqlite3"; - public static DalamudLoggerProvider LoggerProvider { get; } = new(typeof(Plugin).Assembly); + _logger.LogInformation("Building dalamud service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); - /// - /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. - /// - private ILogger _logger = LoggerProvider.CreateLogger(); - - private readonly string _sqliteConnectionString; - private readonly ServiceCollection _serviceCollection = new(); - private ServiceProvider? _serviceProvider; - - public DependencyInjectionContext( - DalamudPluginInterface pluginInterface, - ClientState clientState, - GameGui gameGui, - ChatGui chatGui, - ObjectTable objectTable, - Framework framework, - Condition condition, - CommandManager commandManager, - DataManager dataManager, - Plugin plugin) - { - _logger.LogInformation("Building dalamud service container for {Assembly}", - typeof(DependencyInjectionContext).Assembly.FullName); - - // set up legacy services + // set up legacy services #pragma warning disable CS0612 - JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory()); + JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory()); #pragma warning restore CS0612 - // set up logging - _serviceCollection.AddLogging(builder => - builder.AddFilter("Pal", LogLevel.Trace) - .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) - .AddFilter("Grpc", LogLevel.Debug) - .ClearProviders() - .AddDalamudLogger(plugin)); + // set up logging + _serviceCollection.AddLogging(builder => + builder.AddFilter("Pal", LogLevel.Trace) + .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) + .AddFilter("Grpc", LogLevel.Debug) + .ClearProviders() + .AddDalamudLogger(plugin)); - // dalamud - _serviceCollection.AddSingleton(plugin); - _serviceCollection.AddSingleton(pluginInterface); - _serviceCollection.AddSingleton(clientState); - _serviceCollection.AddSingleton(gameGui); - _serviceCollection.AddSingleton(chatGui); - _serviceCollection.AddSingleton(); - _serviceCollection.AddSingleton(objectTable); - _serviceCollection.AddSingleton(framework); - _serviceCollection.AddSingleton(condition); - _serviceCollection.AddSingleton(commandManager); - _serviceCollection.AddSingleton(dataManager); - _serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); + // dalamud + _serviceCollection.AddSingleton(plugin); + _serviceCollection.AddSingleton(pluginInterface); + _serviceCollection.AddSingleton(clientState); + _serviceCollection.AddSingleton(gameGui); + _serviceCollection.AddSingleton(chatGui); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(objectTable); + _serviceCollection.AddSingleton(framework); + _serviceCollection.AddSingleton(condition); + _serviceCollection.AddSingleton(commandManager); + _serviceCollection.AddSingleton(dataManager); + _serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); - _sqliteConnectionString = - $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}"; - } + _sqliteConnectionString = + $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}"; + } - public IServiceProvider BuildServiceContainer() + public IServiceProvider BuildServiceContainer() + { + _logger.LogInformation("Building async service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); + + // EF core + _serviceCollection.AddDbContext(o => o + .UseSqlite(_sqliteConnectionString) + .UseModel(Database.Compiled.PalClientContextModel.Instance)); + _serviceCollection.AddTransient(); + _serviceCollection.AddScoped(); + + // plugin-specific + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(sp => + sp.GetRequiredService().Load()); + _serviceCollection.AddTransient(); + + // commands + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + + // territory & marker related services + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + + // windows & related services + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + + // rendering + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + + // queue handling + _serviceCollection.AddTransient, QueuedImport.Handler>(); + _serviceCollection + .AddTransient, QueuedUndoImport.Handler>(); + _serviceCollection + .AddTransient, QueuedConfigUpdate.Handler>(); + _serviceCollection + .AddTransient, QueuedSyncResponse.Handler>(); + + // build + _serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions { - _logger.LogInformation("Building async service container for {Assembly}", - typeof(DependencyInjectionContext).Assembly.FullName); - - // EF core - _serviceCollection.AddDbContext(o => o - .UseSqlite(_sqliteConnectionString) - .UseModel(Database.Compiled.PalClientContextModel.Instance)); - _serviceCollection.AddTransient(); - _serviceCollection.AddScoped(); - - // plugin-specific - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(sp => - sp.GetRequiredService().Load()); - _serviceCollection.AddTransient(); - - // commands - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - - // territory & marker related services - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - - // windows & related services - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - - // rendering - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - _serviceCollection.AddScoped(); - - // queue handling - _serviceCollection.AddTransient, QueuedImport.Handler>(); - _serviceCollection - .AddTransient, QueuedUndoImport.Handler>(); - _serviceCollection - .AddTransient, QueuedConfigUpdate.Handler>(); - _serviceCollection - .AddTransient, QueuedSyncResponse.Handler>(); - - // build - _serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions - { - ValidateOnBuild = true, - ValidateScopes = true, - }); + ValidateOnBuild = true, + ValidateScopes = true, + }); #if RELEASE - // You're welcome to remove this code in your fork, but please make sure that: - // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and - // - you host your own server instance - // - // This is mainly to avoid this plugin being included in 'mega-repos' that, for whatever reason, decide - // that collecting all plugins is a good idea (and break half in the process). - _serviceProvider.GetService(); + // You're welcome to remove this code in your fork, but please make sure that: + // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and + // - you host your own server instance + // + // This is mainly to avoid this plugin being included in 'mega-repos' that, for whatever reason, decide + // that collecting all plugins is a good idea (and break half in the process). + _serviceProvider.GetService(); #endif - // This is not ideal as far as loading the plugin goes, because there's no way to check for errors and - // tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized, - // even if it's not. - // - // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for - // config stuff. - _logger = _serviceProvider.GetRequiredService>(); - _logger.LogInformation("Service container built"); + // This is not ideal as far as loading the plugin goes, because there's no way to check for errors and + // tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized, + // even if it's not. + // + // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for + // config stuff. + _logger = _serviceProvider.GetRequiredService>(); + _logger.LogInformation("Service container built"); - return _serviceProvider; - } + return _serviceProvider; + } - public void Dispose() - { - _logger.LogInformation("Disposing DI Context"); - _serviceProvider?.Dispose(); + public void Dispose() + { + _logger.LogInformation("Disposing DI Context"); + _serviceProvider?.Dispose(); - // ensure we're not keeping the file open longer than the plugin is loaded - using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) - SqliteConnection.ClearPool(sqliteConnection); - } + // ensure we're not keeping the file open longer than the plugin is loaded + using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) + SqliteConnection.ClearPool(sqliteConnection); } } diff --git a/Pal.Client/Extensions/GuidExtensions.cs b/Pal.Client/Extensions/GuidExtensions.cs index b80a6bb..b60dabf 100644 --- a/Pal.Client/Extensions/GuidExtensions.cs +++ b/Pal.Client/Extensions/GuidExtensions.cs @@ -1,13 +1,12 @@ using System; -namespace Pal.Client.Extensions -{ - public static class GuidExtensions - { - public static string ToPartialId(this Guid g, int length = 13) - => g.ToString().ToPartialId(); +namespace Pal.Client.Extensions; - public static string ToPartialId(this string s, int length = 13) - => s.PadRight(length + 1).Substring(0, length); - } +public static class GuidExtensions +{ + public static string ToPartialId(this Guid g, int length = 13) + => g.ToString().ToPartialId(); + + public static string ToPartialId(this string s, int length = 13) + => s.PadRight(length + 1).Substring(0, length); } diff --git a/Pal.Client/Extensions/PalImGui.cs b/Pal.Client/Extensions/PalImGui.cs index e6215c8..421b359 100644 --- a/Pal.Client/Extensions/PalImGui.cs +++ b/Pal.Client/Extensions/PalImGui.cs @@ -3,34 +3,33 @@ using System.Runtime.InteropServices; using System.Text; using ImGuiNET; -namespace Pal.Client.Extensions +namespace Pal.Client.Extensions; + +internal static class PalImGui { - internal static class PalImGui + /// + /// None of the default BeginTabItem methods allow using flags without making the tab have a close button for some reason. + /// + internal static unsafe bool BeginTabItemWithFlags(string label, ImGuiTabItemFlags flags) { - /// - /// None of the default BeginTabItem methods allow using flags without making the tab have a close button for some reason. - /// - internal static unsafe bool BeginTabItemWithFlags(string label, ImGuiTabItemFlags flags) - { - int labelLength = Encoding.UTF8.GetByteCount(label); - byte* labelPtr = stackalloc byte[labelLength + 1]; - byte[] labelBytes = Encoding.UTF8.GetBytes(label); + int labelLength = Encoding.UTF8.GetByteCount(label); + byte* labelPtr = stackalloc byte[labelLength + 1]; + byte[] labelBytes = Encoding.UTF8.GetBytes(label); - Marshal.Copy(labelBytes, 0, (IntPtr)labelPtr, labelLength); - labelPtr[labelLength] = 0; + Marshal.Copy(labelBytes, 0, (IntPtr)labelPtr, labelLength); + labelPtr[labelLength] = 0; - return ImGuiNative.igBeginTabItem(labelPtr, null, flags) != 0; - } + return ImGuiNative.igBeginTabItem(labelPtr, null, flags) != 0; + } - public static void RadioButtonWrapped(string label, ref int choice, int value) - { - ImGui.BeginGroup(); - ImGui.RadioButton($"##radio{value}", value == choice); - ImGui.SameLine(); - ImGui.TextWrapped(label); - ImGui.EndGroup(); - if (ImGui.IsItemClicked()) - choice = value; - } + public static void RadioButtonWrapped(string label, ref int choice, int value) + { + ImGui.BeginGroup(); + ImGui.RadioButton($"##radio{value}", value == choice); + ImGui.SameLine(); + ImGui.TextWrapped(label); + ImGui.EndGroup(); + if (ImGui.IsItemClicked()) + choice = value; } } diff --git a/Pal.Client/Floors/EphemeralLocation.cs b/Pal.Client/Floors/EphemeralLocation.cs index c4d8f20..d8fc36e 100644 --- a/Pal.Client/Floors/EphemeralLocation.cs +++ b/Pal.Client/Floors/EphemeralLocation.cs @@ -1,29 +1,28 @@ using System; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +/// +/// This is a currently-visible marker. +/// +internal sealed class EphemeralLocation : MemoryLocation { - /// - /// This is a currently-visible marker. - /// - internal sealed class EphemeralLocation : MemoryLocation + public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj); + + public override int GetHashCode() => base.GetHashCode(); + + public static bool operator ==(EphemeralLocation? a, object? b) { - public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj); + return Equals(a, b); + } - public override int GetHashCode() => base.GetHashCode(); + public static bool operator !=(EphemeralLocation? a, object? b) + { + return !Equals(a, b); + } - public static bool operator ==(EphemeralLocation? a, object? b) - { - return Equals(a, b); - } - - public static bool operator !=(EphemeralLocation? a, object? b) - { - return !Equals(a, b); - } - - public override string ToString() - { - return $"EphemeralLocation(Position={Position}, Type={Type})"; - } + public override string ToString() + { + return $"EphemeralLocation(Position={Position}, Type={Type})"; } } diff --git a/Pal.Client/Floors/FloorService.cs b/Pal.Client/Floors/FloorService.cs index bfb2b58..0d8a0df 100644 --- a/Pal.Client/Floors/FloorService.cs +++ b/Pal.Client/Floors/FloorService.cs @@ -10,154 +10,153 @@ using Pal.Client.Floors.Tasks; using Pal.Client.Net; using Pal.Common; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +internal sealed class FloorService { - internal sealed class FloorService + private readonly IPalacePalConfiguration _configuration; + private readonly Cleanup _cleanup; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IReadOnlyDictionary _territories; + + private ConcurrentBag _ephemeralLocations = new(); + + public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup, + IServiceScopeFactory serviceScopeFactory) { - private readonly IPalacePalConfiguration _configuration; - private readonly Cleanup _cleanup; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly IReadOnlyDictionary _territories; + _configuration = configuration; + _cleanup = cleanup; + _serviceScopeFactory = serviceScopeFactory; + _territories = Enum.GetValues().ToDictionary(o => o, o => new MemoryTerritory(o)); + } - private ConcurrentBag _ephemeralLocations = new(); + public IReadOnlyCollection EphemeralLocations => _ephemeralLocations; + public bool IsImportRunning { get; private set; } - public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup, - IServiceScopeFactory serviceScopeFactory) + public void ChangeTerritory(ushort territoryType) + { + _ephemeralLocations = new ConcurrentBag(); + + if (typeof(ETerritoryType).IsEnumDefined(territoryType)) + ChangeTerritory((ETerritoryType)territoryType); + } + + private void ChangeTerritory(ETerritoryType newTerritory) + { + var territory = _territories[newTerritory]; + if (territory.ReadyState == MemoryTerritory.EReadyState.NotLoaded) { - _configuration = configuration; - _cleanup = cleanup; - _serviceScopeFactory = serviceScopeFactory; - _territories = Enum.GetValues().ToDictionary(o => o, o => new MemoryTerritory(o)); + territory.ReadyState = MemoryTerritory.EReadyState.Loading; + new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start(); } + } - public IReadOnlyCollection EphemeralLocations => _ephemeralLocations; - public bool IsImportRunning { get; private set; } + public MemoryTerritory? GetTerritoryIfReady(ushort territoryType) + { + if (typeof(ETerritoryType).IsEnumDefined(territoryType)) + return GetTerritoryIfReady((ETerritoryType)territoryType); - public void ChangeTerritory(ushort territoryType) - { - _ephemeralLocations = new ConcurrentBag(); - - if (typeof(ETerritoryType).IsEnumDefined(territoryType)) - ChangeTerritory((ETerritoryType)territoryType); - } - - private void ChangeTerritory(ETerritoryType newTerritory) - { - var territory = _territories[newTerritory]; - if (territory.ReadyState == MemoryTerritory.EReadyState.NotLoaded) - { - territory.ReadyState = MemoryTerritory.EReadyState.Loading; - new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start(); - } - } - - public MemoryTerritory? GetTerritoryIfReady(ushort territoryType) - { - if (typeof(ETerritoryType).IsEnumDefined(territoryType)) - return GetTerritoryIfReady((ETerritoryType)territoryType); + return null; + } + public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType) + { + var territory = _territories[territoryType]; + if (territory.ReadyState != MemoryTerritory.EReadyState.Ready) return null; - } - public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType) + return territory; + } + + public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null; + + public bool MergePersistentLocations( + ETerritoryType territoryType, + IReadOnlyList visibleLocations, + bool recreateLayout, + out List locationsToSync) + { + MemoryTerritory? territory = GetTerritoryIfReady(territoryType); + locationsToSync = new(); + if (territory == null) + return false; + + var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + var persistentLocations = territory.Locations.ToList(); + + List markAsSeen = new(); + List newLocations = new(); + foreach (var visibleLocation in visibleLocations) { - var territory = _territories[territoryType]; - if (territory.ReadyState != MemoryTerritory.EReadyState.Ready) - return null; - - return territory; - } - - public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null; - - public bool MergePersistentLocations( - ETerritoryType territoryType, - IReadOnlyList visibleLocations, - bool recreateLayout, - out List locationsToSync) - { - MemoryTerritory? territory = GetTerritoryIfReady(territoryType); - locationsToSync = new(); - if (territory == null) - return false; - - var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - var persistentLocations = territory.Locations.ToList(); - - List markAsSeen = new(); - List newLocations = new(); - foreach (var visibleLocation in visibleLocations) + PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation); + if (existingLocation != null) { - PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation); - if (existingLocation != null) + if (existingLocation is { Seen: false, LocalId: { } }) { - if (existingLocation is { Seen: false, LocalId: { } }) - { - existingLocation.Seen = true; - markAsSeen.Add(existingLocation); - } - - // 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 (partialAccountId != null && - existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } && - !existingLocation.RemoteSeenOn.Contains(partialAccountId)) - { - existingLocation.RemoteSeenRequested = true; - locationsToSync.Add(existingLocation); - } - - continue; + existingLocation.Seen = true; + markAsSeen.Add(existingLocation); } - territory.Locations.Add(visibleLocation); - newLocations.Add(visibleLocation); - recreateLayout = 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 (partialAccountId != null && + existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } && + !existingLocation.RemoteSeenOn.Contains(partialAccountId)) + { + existingLocation.RemoteSeenRequested = true; + locationsToSync.Add(existingLocation); + } + + continue; } - if (markAsSeen.Count > 0) - new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start(); - - if (newLocations.Count > 0) - new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start(); - - return recreateLayout; + territory.Locations.Add(visibleLocation); + newLocations.Add(visibleLocation); + recreateLayout = true; } - /// Whether the locations have changed - public bool MergeEphemeralLocations(IReadOnlyList visibleLocations, bool recreate) + if (markAsSeen.Count > 0) + new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start(); + + if (newLocations.Count > 0) + new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start(); + + return recreateLayout; + } + + /// Whether the locations have changed + public bool MergeEphemeralLocations(IReadOnlyList visibleLocations, bool recreate) + { + recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc)); + recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc)); + + if (!recreate) + return false; + + _ephemeralLocations.Clear(); + foreach (var visibleLocation in visibleLocations) + _ephemeralLocations.Add(visibleLocation); + + return true; + } + + public void ResetAll() + { + IsImportRunning = false; + foreach (var memoryTerritory in _territories.Values) { - recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc)); - recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc)); - - if (!recreate) - return false; - - _ephemeralLocations.Clear(); - foreach (var visibleLocation in visibleLocations) - _ephemeralLocations.Add(visibleLocation); - - return true; + lock (memoryTerritory.LockObj) + memoryTerritory.Reset(); } + } - public void ResetAll() + public void SetToImportState() + { + IsImportRunning = true; + foreach (var memoryTerritory in _territories.Values) { - IsImportRunning = false; - foreach (var memoryTerritory in _territories.Values) - { - lock (memoryTerritory.LockObj) - memoryTerritory.Reset(); - } - } - - public void SetToImportState() - { - IsImportRunning = true; - foreach (var memoryTerritory in _territories.Values) - { - lock (memoryTerritory.LockObj) - memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing; - } + lock (memoryTerritory.LockObj) + memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing; } } } diff --git a/Pal.Client/Floors/FrameworkService.cs b/Pal.Client/Floors/FrameworkService.cs index a89a54d..19f9fcd 100644 --- a/Pal.Client/Floors/FrameworkService.cs +++ b/Pal.Client/Floors/FrameworkService.cs @@ -18,451 +18,450 @@ using Pal.Client.Rendering; using Pal.Client.Scheduled; using Pal.Common; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +internal sealed class FrameworkService : IDisposable { - internal sealed class FrameworkService : IDisposable + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly Framework _framework; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly ClientState _clientState; + private readonly TerritoryState _territoryState; + private readonly FloorService _floorService; + private readonly DebugState _debugState; + private readonly RenderAdapter _renderAdapter; + private readonly ObjectTable _objectTable; + private readonly RemoteApi _remoteApi; + + internal Queue EarlyEventQueue { get; } = new(); + internal Queue LateEventQueue { get; } = new(); + internal ConcurrentQueue NextUpdateObjects { get; } = new(); + + public FrameworkService( + IServiceProvider serviceProvider, + ILogger logger, + Framework framework, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + ClientState clientState, + TerritoryState territoryState, + FloorService floorService, + DebugState debugState, + RenderAdapter renderAdapter, + ObjectTable objectTable, + RemoteApi remoteApi) { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly Framework _framework; - private readonly ConfigurationManager _configurationManager; - private readonly IPalacePalConfiguration _configuration; - private readonly ClientState _clientState; - private readonly TerritoryState _territoryState; - private readonly FloorService _floorService; - private readonly DebugState _debugState; - private readonly RenderAdapter _renderAdapter; - private readonly ObjectTable _objectTable; - private readonly RemoteApi _remoteApi; + _serviceProvider = serviceProvider; + _logger = logger; + _framework = framework; + _configurationManager = configurationManager; + _configuration = configuration; + _clientState = clientState; + _territoryState = territoryState; + _floorService = floorService; + _debugState = debugState; + _renderAdapter = renderAdapter; + _objectTable = objectTable; + _remoteApi = remoteApi; - internal Queue EarlyEventQueue { get; } = new(); - internal Queue LateEventQueue { get; } = new(); - internal ConcurrentQueue NextUpdateObjects { get; } = new(); + _framework.Update += OnUpdate; + _configurationManager.Saved += OnSaved; + } - public FrameworkService( - IServiceProvider serviceProvider, - ILogger logger, - Framework framework, - ConfigurationManager configurationManager, - IPalacePalConfiguration configuration, - ClientState clientState, - TerritoryState territoryState, - FloorService floorService, - DebugState debugState, - RenderAdapter renderAdapter, - ObjectTable objectTable, - RemoteApi remoteApi) + public void Dispose() + { + _framework.Update -= OnUpdate; + _configurationManager.Saved -= OnSaved; + } + + private void OnSaved(object? sender, IPalacePalConfiguration? config) + => EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); + + private void OnUpdate(Framework framework) + { + if (_configuration.FirstUse) + return; + + try { - _serviceProvider = serviceProvider; - _logger = logger; - _framework = framework; - _configurationManager = configurationManager; - _configuration = configuration; - _clientState = clientState; - _territoryState = territoryState; - _floorService = floorService; - _debugState = debugState; - _renderAdapter = renderAdapter; - _objectTable = objectTable; - _remoteApi = remoteApi; + bool recreateLayout = false; - _framework.Update += OnUpdate; - _configurationManager.Saved += OnSaved; - } + while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + HandleQueued(queued, ref recreateLayout); - public void Dispose() - { - _framework.Update -= OnUpdate; - _configurationManager.Saved -= OnSaved; - } + if (_territoryState.LastTerritory != _clientState.TerritoryType) + { + MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (oldTerritory != null) + oldTerritory.SyncState = ESyncState.NotAttempted; - private void OnSaved(object? sender, IPalacePalConfiguration? config) - => EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); + _territoryState.LastTerritory = _clientState.TerritoryType; + NextUpdateObjects.Clear(); - private void OnUpdate(Framework framework) - { - if (_configuration.FirstUse) + _floorService.ChangeTerritory(_territoryState.LastTerritory); + _territoryState.PomanderOfSight = PomanderState.Inactive; + _territoryState.PomanderOfIntuition = PomanderState.Inactive; + recreateLayout = true; + _debugState.Reset(); + } + + if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory)) return; + if (_renderAdapter.RequireRedraw) + { + recreateLayout = true; + _renderAdapter.RequireRedraw = false; + } + + ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; + MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!; + if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted) + { + memoryTerritory.SyncState = ESyncState.Started; + Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory)); + } + + while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) + HandleQueued(queued, ref recreateLayout); + + (IReadOnlyList visiblePersistentMarkers, + IReadOnlyList visibleEphemeralMarkers) = + GetRelevantGameObjects(); + + HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout); + + if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout)) + RecreateEphemeralLayout(); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } + + #region Render Markers + + private void HandlePersistentLocations(ETerritoryType territoryType, + IReadOnlyList visiblePersistentMarkers, + bool recreateLayout) + { + bool recreatePersistentLocations = _floorService.MergePersistentLocations( + territoryType, + visiblePersistentMarkers, + recreateLayout, + out List locationsToSync); + recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers); + if (locationsToSync.Count > 0) + { + Task.Run(async () => + await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync)); + } + + UploadLocations(); + + if (recreatePersistentLocations) + RecreatePersistentLayout(visiblePersistentMarkers); + } + + private bool CheckLocationsForPomanders(IReadOnlyList visibleLocations) + { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory is { Locations.Count: > 0 } && + (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || + _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) + { try { - bool recreateLayout = false; - - while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout); - - if (_territoryState.LastTerritory != _clientState.TerritoryType) + foreach (var location in memoryTerritory.Locations) { - MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); - if (oldTerritory != null) - oldTerritory.SyncState = ESyncState.NotAttempted; + uint desiredColor = DetermineColor(location, visibleLocations); + if (location.RenderElement == null || !location.RenderElement.IsValid) + return true; - _territoryState.LastTerritory = _clientState.TerritoryType; - NextUpdateObjects.Clear(); - - _floorService.ChangeTerritory(_territoryState.LastTerritory); - _territoryState.PomanderOfSight = PomanderState.Inactive; - _territoryState.PomanderOfIntuition = PomanderState.Inactive; - recreateLayout = true; - _debugState.Reset(); + if (location.RenderElement.Color != desiredColor) + location.RenderElement.Color = desiredColor; } - - if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory)) - return; - - if (_renderAdapter.RequireRedraw) - { - recreateLayout = true; - _renderAdapter.RequireRedraw = false; - } - - ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory; - MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!; - if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted) - { - memoryTerritory.SyncState = ESyncState.Started; - Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory)); - } - - while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) - HandleQueued(queued, ref recreateLayout); - - (IReadOnlyList visiblePersistentMarkers, - IReadOnlyList visibleEphemeralMarkers) = - GetRelevantGameObjects(); - - HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout); - - if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout)) - RecreateEphemeralLayout(); } catch (Exception e) { _debugState.SetFromException(e); + return true; } } - #region Render Markers + return false; + } - private void HandlePersistentLocations(ETerritoryType territoryType, - IReadOnlyList visiblePersistentMarkers, - bool recreateLayout) + private void UploadLocations() + { + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete) + return; + + List locationsToUpload = memoryTerritory.Locations + .Where(loc => loc.NetworkId == null && loc.UploadRequested == false) + .ToList(); + if (locationsToUpload.Count > 0) { - bool recreatePersistentLocations = _floorService.MergePersistentLocations( - territoryType, - visiblePersistentMarkers, - recreateLayout, - out List locationsToSync); - recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers); - if (locationsToSync.Count > 0) - { - Task.Run(async () => - await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync)); - } + foreach (var location in locationsToUpload) + location.UploadRequested = true; - UploadLocations(); - - if (recreatePersistentLocations) - RecreatePersistentLayout(visiblePersistentMarkers); + Task.Run(async () => + await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload)); } + } - private bool CheckLocationsForPomanders(IReadOnlyList visibleLocations) + private void RecreatePersistentLayout(IReadOnlyList visibleMarkers) + { + _renderAdapter.ResetLayer(ELayer.TrapHoard); + + MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); + if (memoryTerritory == null) + return; + + List elements = new(); + foreach (var location in memoryTerritory.Locations) { - MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); - if (memoryTerritory is { Locations.Count: > 0 } && - (_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || - _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)) + if (location.Type == MemoryLocation.EType.Trap) { - try - { - foreach (var location in memoryTerritory.Locations) - { - uint desiredColor = DetermineColor(location, visibleLocations); - if (location.RenderElement == null || !location.RenderElement.IsValid) - return true; - - if (location.RenderElement.Color != desiredColor) - location.RenderElement.Color = desiredColor; - } - } - catch (Exception e) - { - _debugState.SetFromException(e); - return true; - } + CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), + _configuration.DeepDungeons.Traps); } - - return false; - } - - private void UploadLocations() - { - MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); - if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete) - return; - - List locationsToUpload = memoryTerritory.Locations - .Where(loc => loc.NetworkId == null && loc.UploadRequested == false) - .ToList(); - if (locationsToUpload.Count > 0) + else if (location.Type == MemoryLocation.EType.Hoard) { - foreach (var location in locationsToUpload) - location.UploadRequested = true; - - Task.Run(async () => - await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload)); + CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), + _configuration.DeepDungeons.HoardCoffers); } } - private void RecreatePersistentLayout(IReadOnlyList visibleMarkers) + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.TrapHoard, elements); + } + + private void RecreateEphemeralLayout() + { + _renderAdapter.ResetLayer(ELayer.RegularCoffers); + + List elements = new(); + foreach (var location in _floorService.EphemeralLocations) { - _renderAdapter.ResetLayer(ELayer.TrapHoard); - - MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); - if (memoryTerritory == null) - return; - - List elements = new(); - foreach (var location in memoryTerritory.Locations) + if (location.Type == MemoryLocation.EType.SilverCoffer && + _configuration.DeepDungeons.SilverCoffers.Show) { - if (location.Type == MemoryLocation.EType.Trap) - { - CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), - _configuration.DeepDungeons.Traps); - } - else if (location.Type == MemoryLocation.EType.Hoard) - { - CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers), - _configuration.DeepDungeons.HoardCoffers); - } + CreateRenderElement(location, elements, DetermineColor(location), + _configuration.DeepDungeons.SilverCoffers); } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.TrapHoard, elements); - } - - private void RecreateEphemeralLayout() - { - _renderAdapter.ResetLayer(ELayer.RegularCoffers); - - List elements = new(); - foreach (var location in _floorService.EphemeralLocations) + else if (location.Type == MemoryLocation.EType.GoldCoffer && + _configuration.DeepDungeons.GoldCoffers.Show) { - if (location.Type == MemoryLocation.EType.SilverCoffer && - _configuration.DeepDungeons.SilverCoffers.Show) - { - CreateRenderElement(location, elements, DetermineColor(location), - _configuration.DeepDungeons.SilverCoffers); - } - else if (location.Type == MemoryLocation.EType.GoldCoffer && - _configuration.DeepDungeons.GoldCoffers.Show) - { - CreateRenderElement(location, elements, DetermineColor(location), - _configuration.DeepDungeons.GoldCoffers); - } - } - - if (elements.Count == 0) - return; - - _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); - } - - private uint DetermineColor(PersistentLocation location, IReadOnlyList visibleLocations) - { - switch (location.Type) - { - case MemoryLocation.EType.Trap - when _territoryState.PomanderOfSight == PomanderState.Inactive || - !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || - visibleLocations.Any(x => x == location): - return _configuration.DeepDungeons.Traps.Color; - case MemoryLocation.EType.Hoard - when _territoryState.PomanderOfIntuition == PomanderState.Inactive || - !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || - visibleLocations.Any(x => x == location): - return _configuration.DeepDungeons.HoardCoffers.Color; - default: - return RenderData.ColorInvisible; + CreateRenderElement(location, elements, DetermineColor(location), + _configuration.DeepDungeons.GoldCoffers); } } - private uint DetermineColor(EphemeralLocation location) + if (elements.Count == 0) + return; + + _renderAdapter.SetLayer(ELayer.RegularCoffers, elements); + } + + private uint DetermineColor(PersistentLocation location, IReadOnlyList visibleLocations) + { + switch (location.Type) { - return location.Type switch - { - MemoryLocation.EType.SilverCoffer => _configuration.DeepDungeons.SilverCoffers.Color, - MemoryLocation.EType.GoldCoffer => _configuration.DeepDungeons.GoldCoffers.Color, - _ => RenderData.ColorInvisible - }; + case MemoryLocation.EType.Trap + when _territoryState.PomanderOfSight == PomanderState.Inactive || + !_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander || + visibleLocations.Any(x => x == location): + return _configuration.DeepDungeons.Traps.Color; + case MemoryLocation.EType.Hoard + when _territoryState.PomanderOfIntuition == PomanderState.Inactive || + !_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander || + visibleLocations.Any(x => x == location): + return _configuration.DeepDungeons.HoardCoffers.Color; + default: + return RenderData.ColorInvisible; } + } - private void CreateRenderElement(MemoryLocation location, List elements, uint color, - MarkerConfiguration config) + private uint DetermineColor(EphemeralLocation location) + { + return location.Type switch { - if (!config.Show) - return; + MemoryLocation.EType.SilverCoffer => _configuration.DeepDungeons.SilverCoffers.Color, + MemoryLocation.EType.GoldCoffer => _configuration.DeepDungeons.GoldCoffers.Color, + _ => RenderData.ColorInvisible + }; + } - var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill); - location.RenderElement = element; - elements.Add(element); + private void CreateRenderElement(MemoryLocation location, List elements, uint color, + MarkerConfiguration config) + { + if (!config.Show) + return; + + var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill); + location.RenderElement = element; + elements.Add(element); + } + + #endregion + + #region Up-/Download + + 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 + { + Type = SyncType.Download, + TerritoryType = territoryId, + Success = success, + Locations = downloadedMarkers + }); } - - #endregion - - #region Up-/Download - - private async Task DownloadLocationsForTerritory(ushort territoryId) + catch (Exception e) { - try - { - _logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId); - var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId); - LateEventQueue.Enqueue(new QueuedSyncResponse - { - Type = SyncType.Download, - TerritoryType = territoryId, - Success = success, - Locations = downloadedMarkers - }); - } - catch (Exception e) - { - _debugState.SetFromException(e); - } + _debugState.SetFromException(e); } + } - private async Task UploadLocationsForTerritory(ushort territoryId, List locationsToUpload) + private async Task UploadLocationsForTerritory(ushort territoryId, List locationsToUpload) + { + try { - 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 { - _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 - { - Type = SyncType.Upload, - TerritoryType = territoryId, - Success = success, - Locations = uploadedLocations - }); - } - catch (Exception e) - { - _debugState.SetFromException(e); - } + Type = SyncType.Upload, + TerritoryType = territoryId, + Success = success, + Locations = uploadedLocations + }); } - - private async Task SyncSeenMarkersForTerritory(ushort territoryId, - IReadOnlyList locationsToUpdate) + catch (Exception e) { - 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 - { - Type = SyncType.MarkSeen, - TerritoryType = territoryId, - Success = success, - Locations = locationsToUpdate, - }); - } - catch (Exception e) - { - _debugState.SetFromException(e); - } + _debugState.SetFromException(e); } + } - #endregion - - private (IReadOnlyList, IReadOnlyList) GetRelevantGameObjects() + private async Task SyncSeenMarkersForTerritory(ushort territoryId, + IReadOnlyList locationsToUpdate) + { + try { - List persistentLocations = new(); - List ephemeralLocations = new(); - for (int i = 246; i < _objectTable.Length; i++) + _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 { - GameObject? obj = _objectTable[i]; - if (obj == null) - continue; + Type = SyncType.MarkSeen, + TerritoryType = territoryId, + Success = success, + Locations = locationsToUpdate, + }); + } + catch (Exception e) + { + _debugState.SetFromException(e); + } + } - switch ((uint)Marshal.ReadInt32(obj.Address + 128)) - { - case 2007182: - case 2007183: - case 2007184: - case 2007185: - case 2007186: - case 2009504: - case 2013284: - persistentLocations.Add(new PersistentLocation - { - Type = MemoryLocation.EType.Trap, - Position = obj.Position, - Seen = true, - Source = ClientLocation.ESource.SeenLocally, - }); - break; + #endregion - case 2007542: - case 2007543: - persistentLocations.Add(new PersistentLocation - { - Type = MemoryLocation.EType.Hoard, - Position = obj.Position, - Seen = true, - Source = ClientLocation.ESource.SeenLocally, - }); - break; + private (IReadOnlyList, IReadOnlyList) GetRelevantGameObjects() + { + List persistentLocations = new(); + List ephemeralLocations = new(); + for (int i = 246; i < _objectTable.Length; i++) + { + GameObject? obj = _objectTable[i]; + if (obj == null) + continue; - case 2007357: - ephemeralLocations.Add(new EphemeralLocation - { - Type = MemoryLocation.EType.SilverCoffer, - Position = obj.Position, - Seen = true, - }); - break; - - case 2007358: - ephemeralLocations.Add(new EphemeralLocation - { - Type = MemoryLocation.EType.GoldCoffer, - Position = obj.Position, - Seen = true - }); - break; - } - } - - while (NextUpdateObjects.TryDequeue(out nint address)) + switch ((uint)Marshal.ReadInt32(obj.Address + 128)) { - var obj = _objectTable.FirstOrDefault(x => x.Address == address); - if (obj != null && obj.Position.Length() > 0.1) - { + case 2007182: + case 2007183: + case 2007184: + case 2007185: + case 2007186: + case 2009504: + case 2013284: persistentLocations.Add(new PersistentLocation { Type = MemoryLocation.EType.Trap, Position = obj.Position, Seen = true, - Source = ClientLocation.ESource.ExplodedLocally, - + Source = ClientLocation.ESource.SeenLocally, }); - } + break; + + case 2007542: + case 2007543: + persistentLocations.Add(new PersistentLocation + { + Type = MemoryLocation.EType.Hoard, + Position = obj.Position, + Seen = true, + Source = ClientLocation.ESource.SeenLocally, + }); + break; + + case 2007357: + ephemeralLocations.Add(new EphemeralLocation + { + Type = MemoryLocation.EType.SilverCoffer, + Position = obj.Position, + Seen = true, + }); + break; + + case 2007358: + ephemeralLocations.Add(new EphemeralLocation + { + Type = MemoryLocation.EType.GoldCoffer, + Position = obj.Position, + Seen = true + }); + break; } - - return (persistentLocations, ephemeralLocations); } - private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout) + while (NextUpdateObjects.TryDequeue(out nint address)) { - Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType()); - var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType); + var obj = _objectTable.FirstOrDefault(x => x.Address == address); + if (obj != null && obj.Position.Length() > 0.1) + { + persistentLocations.Add(new PersistentLocation + { + Type = MemoryLocation.EType.Trap, + Position = obj.Position, + Seen = true, + Source = ClientLocation.ESource.ExplodedLocally, - handler.RunIfCompatible(queued, ref recreateLayout); + }); + } } + + return (persistentLocations, ephemeralLocations); + } + + private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout) + { + Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType()); + var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType); + + handler.RunIfCompatible(queued, ref recreateLayout); } } diff --git a/Pal.Client/Floors/MemoryLocation.cs b/Pal.Client/Floors/MemoryLocation.cs index d197cce..b48a768 100644 --- a/Pal.Client/Floors/MemoryLocation.cs +++ b/Pal.Client/Floors/MemoryLocation.cs @@ -5,63 +5,62 @@ using Pal.Client.Rendering; using Pal.Common; using Palace; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +/// +/// Base class for and . +/// +internal abstract class MemoryLocation { - /// - /// Base class for and . - /// - internal abstract class MemoryLocation + public required EType Type { get; init; } + public required Vector3 Position { get; init; } + public bool Seen { get; set; } + + public IRenderElement? RenderElement { get; set; } + + public enum EType { - public required EType Type { get; init; } - public required Vector3 Position { get; init; } - public bool Seen { get; set; } + Unknown, - public IRenderElement? RenderElement { get; set; } + Trap, + Hoard, - public enum EType - { - Unknown, - - Trap, - Hoard, - - SilverCoffer, - GoldCoffer, - } - - public override bool Equals(object? obj) - { - return obj is MemoryLocation otherLocation && - Type == otherLocation.Type && - PalaceMath.IsNearlySamePosition(Position, otherLocation.Position); - } - - public override int GetHashCode() - { - return HashCode.Combine(Type, PalaceMath.GetHashCode(Position)); - } + SilverCoffer, + GoldCoffer, } - internal static class ETypeExtensions + public override bool Equals(object? obj) { - public static MemoryLocation.EType ToMemoryType(this ObjectType objectType) - { - return objectType switch - { - ObjectType.Trap => MemoryLocation.EType.Trap, - ObjectType.Hoard => MemoryLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null) - }; - } + return obj is MemoryLocation otherLocation && + Type == otherLocation.Type && + PalaceMath.IsNearlySamePosition(Position, otherLocation.Position); + } - 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) - }; - } + public override int GetHashCode() + { + return HashCode.Combine(Type, PalaceMath.GetHashCode(Position)); + } +} + +internal static class ETypeExtensions +{ + public static MemoryLocation.EType ToMemoryType(this ObjectType objectType) + { + return objectType switch + { + ObjectType.Trap => MemoryLocation.EType.Trap, + ObjectType.Hoard => MemoryLocation.EType.Hoard, + _ => 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) + }; } } diff --git a/Pal.Client/Floors/MemoryTerritory.cs b/Pal.Client/Floors/MemoryTerritory.cs index e440bc8..30fc356 100644 --- a/Pal.Client/Floors/MemoryTerritory.cs +++ b/Pal.Client/Floors/MemoryTerritory.cs @@ -5,59 +5,58 @@ using Pal.Client.Configuration; using Pal.Client.Scheduled; using Pal.Common; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +/// +/// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60. +/// +internal sealed class MemoryTerritory { - /// - /// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60. - /// - internal sealed class MemoryTerritory + public MemoryTerritory(ETerritoryType territoryType) { - public MemoryTerritory(ETerritoryType territoryType) - { - TerritoryType = territoryType; - } + TerritoryType = territoryType; + } - public ETerritoryType TerritoryType { get; } - public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded; - public ESyncState SyncState { get; set; } = ESyncState.NotAttempted; + public ETerritoryType TerritoryType { get; } + public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded; + public ESyncState SyncState { get; set; } = ESyncState.NotAttempted; - public ConcurrentBag Locations { get; } = new(); - public object LockObj { get; } = new(); + public ConcurrentBag Locations { get; } = new(); + public object LockObj { get; } = new(); - public void Initialize(IEnumerable locations) - { - Locations.Clear(); - foreach (var location in locations) - Locations.Add(location); + public void Initialize(IEnumerable locations) + { + Locations.Clear(); + foreach (var location in locations) + Locations.Add(location); - ReadyState = EReadyState.Ready; - } + ReadyState = EReadyState.Ready; + } - public void Reset() - { - Locations.Clear(); - SyncState = ESyncState.NotAttempted; - ReadyState = EReadyState.NotLoaded; - } + public void Reset() + { + Locations.Clear(); + SyncState = ESyncState.NotAttempted; + ReadyState = EReadyState.NotLoaded; + } - public enum EReadyState - { - NotLoaded, + public enum EReadyState + { + NotLoaded, - /// - /// Currently loading from the database. - /// - Loading, + /// + /// Currently loading from the database. + /// + Loading, - /// - /// Locations loaded, no import running. - /// - Ready, + /// + /// Locations loaded, no import running. + /// + Ready, - /// - /// Import running, should probably not interact with this too much. - /// - Importing, - } + /// + /// Import running, should probably not interact with this too much. + /// + Importing, } } diff --git a/Pal.Client/Floors/ObjectTableDebug.cs b/Pal.Client/Floors/ObjectTableDebug.cs index 78cbab0..605d0ef 100644 --- a/Pal.Client/Floors/ObjectTableDebug.cs +++ b/Pal.Client/Floors/ObjectTableDebug.cs @@ -9,93 +9,92 @@ using Dalamud.Game.Gui; using Dalamud.Plugin; using ImGuiNET; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +/// +/// This isn't very useful for running deep dungeons normally, but it is for plugin dev. +/// +/// Needs the corresponding beta feature to be enabled. +/// +internal sealed class ObjectTableDebug : IDisposable { - /// - /// This isn't very useful for running deep dungeons normally, but it is for plugin dev. - /// - /// Needs the corresponding beta feature to be enabled. - /// - internal sealed class ObjectTableDebug : IDisposable + public const string FeatureName = nameof(ObjectTableDebug); + + private readonly DalamudPluginInterface _pluginInterface; + private readonly ObjectTable _objectTable; + private readonly GameGui _gameGui; + private readonly ClientState _clientState; + + public ObjectTableDebug(DalamudPluginInterface pluginInterface, ObjectTable objectTable, GameGui gameGui, ClientState clientState) { - public const string FeatureName = nameof(ObjectTableDebug); + _pluginInterface = pluginInterface; + _objectTable = objectTable; + _gameGui = gameGui; + _clientState = clientState; - private readonly DalamudPluginInterface _pluginInterface; - private readonly ObjectTable _objectTable; - private readonly GameGui _gameGui; - private readonly ClientState _clientState; + _pluginInterface.UiBuilder.Draw += Draw; + } - public ObjectTableDebug(DalamudPluginInterface pluginInterface, ObjectTable objectTable, GameGui gameGui, ClientState clientState) + private void Draw() + { + int index = 0; + foreach (GameObject obj in _objectTable) { - _pluginInterface = pluginInterface; - _objectTable = objectTable; - _gameGui = gameGui; - _clientState = clientState; - - _pluginInterface.UiBuilder.Draw += Draw; - } - - private void Draw() - { - int index = 0; - foreach (GameObject obj in _objectTable) + if (obj is EventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString())) { - if (obj is EventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString())) + ++index; + int model = Marshal.ReadInt32(obj.Address + 128); + + if (_gameGui.WorldToScreen(obj.Position, out var screenCoords)) { - ++index; - int model = Marshal.ReadInt32(obj.Address + 128); + // So, while WorldToScreen will return false if the point is off of game client screen, to + // to avoid performance issues, we have to manually determine if creating a window would + // produce a new viewport, and skip rendering it if so + float distance = DistanceToPlayer(obj.Position); + var objectText = + $"{obj.Address.ToInt64():X}:{obj.ObjectId:X}[{index}]\nkind: {obj.ObjectKind} sub: {obj.SubKind}\nmodel: {model}\nname: {obj.Name}\ndata id: {obj.DataId}"; - if (_gameGui.WorldToScreen(obj.Position, out var screenCoords)) - { - // So, while WorldToScreen will return false if the point is off of game client screen, to - // to avoid performance issues, we have to manually determine if creating a window would - // produce a new viewport, and skip rendering it if so - float distance = DistanceToPlayer(obj.Position); - var objectText = - $"{obj.Address.ToInt64():X}:{obj.ObjectId:X}[{index}]\nkind: {obj.ObjectKind} sub: {obj.SubKind}\nmodel: {model}\nname: {obj.Name}\ndata id: {obj.DataId}"; + var screenPos = ImGui.GetMainViewport().Pos; + var screenSize = ImGui.GetMainViewport().Size; - var screenPos = ImGui.GetMainViewport().Pos; - var screenSize = ImGui.GetMainViewport().Size; + var windowSize = ImGui.CalcTextSize(objectText); - var windowSize = ImGui.CalcTextSize(objectText); + // Add some extra safety padding + windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; + windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; - // Add some extra safety padding - windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; - windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; + if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || + screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) + continue; - if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || - screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) - continue; + if (distance > 50f) + continue; - if (distance > 50f) - continue; + ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); - ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); - - ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f)); - if (ImGui.Begin( - $"PalacePal_{nameof(ObjectTableDebug)}_{index}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoMouseInputs | - ImGuiWindowFlags.NoDocking | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoNav)) - ImGui.Text(objectText); - ImGui.End(); - } + ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f)); + if (ImGui.Begin( + $"PalacePal_{nameof(ObjectTableDebug)}_{index}", + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDocking | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoNav)) + ImGui.Text(objectText); + ImGui.End(); } } } + } - private float DistanceToPlayer(Vector3 center) - => Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, center); + private float DistanceToPlayer(Vector3 center) + => Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, center); - public void Dispose() - { - _pluginInterface.UiBuilder.Draw -= Draw; - } + public void Dispose() + { + _pluginInterface.UiBuilder.Draw -= Draw; } } diff --git a/Pal.Client/Floors/PersistentLocation.cs b/Pal.Client/Floors/PersistentLocation.cs index e6f8ad6..a4eb021 100644 --- a/Pal.Client/Floors/PersistentLocation.cs +++ b/Pal.Client/Floors/PersistentLocation.cs @@ -2,54 +2,53 @@ using System.Collections.Generic; using Pal.Client.Database; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +/// +/// A loaded in memory, with certain extra attributes as needed. +/// +internal sealed class PersistentLocation : MemoryLocation { + /// + public int? LocalId { get; set; } + /// - /// A loaded in memory, with certain extra attributes as needed. + /// Network id for the server you're currently connected to. /// - internal sealed class PersistentLocation : MemoryLocation + 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. + /// + public bool UploadRequested { get; set; } + + /// + /// + public List RemoteSeenOn { get; set; } = new(); + + /// + /// Whether this marker was requested to be seen, to avoid duplicate requests. + /// + public bool RemoteSeenRequested { get; set; } + + public ClientLocation.ESource Source { get; init; } + + public override bool Equals(object? obj) => obj is PersistentLocation && base.Equals(obj); + + public override int GetHashCode() => base.GetHashCode(); + + public static bool operator ==(PersistentLocation? a, object? b) { - /// - public int? LocalId { get; set; } + return Equals(a, b); + } - /// - /// Network id for the server you're currently connected to. - /// - public Guid? NetworkId { get; set; } + public static bool operator !=(PersistentLocation? a, object? b) + { + return !Equals(a, b); + } - /// - /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests. - /// - public bool UploadRequested { get; set; } - - /// - /// - public List RemoteSeenOn { get; set; } = new(); - - /// - /// Whether this marker was requested to be seen, to avoid duplicate requests. - /// - public bool RemoteSeenRequested { get; set; } - - public ClientLocation.ESource Source { get; init; } - - public override bool Equals(object? obj) => obj is PersistentLocation && base.Equals(obj); - - public override int GetHashCode() => base.GetHashCode(); - - public static bool operator ==(PersistentLocation? a, object? b) - { - return Equals(a, b); - } - - public static bool operator !=(PersistentLocation? a, object? b) - { - return !Equals(a, b); - } - - public override string ToString() - { - return $"PersistentLocation(Position={Position}, Type={Type})"; - } + public override string ToString() + { + return $"PersistentLocation(Position={Position}, Type={Type})"; } } diff --git a/Pal.Client/Floors/Tasks/DbTask.cs b/Pal.Client/Floors/Tasks/DbTask.cs index 0d8a6c9..c7a1747 100644 --- a/Pal.Client/Floors/Tasks/DbTask.cs +++ b/Pal.Client/Floors/Tasks/DbTask.cs @@ -4,38 +4,37 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; -namespace Pal.Client.Floors.Tasks +namespace Pal.Client.Floors.Tasks; + +internal abstract class DbTask + where T : DbTask { - internal abstract class DbTask - where T : DbTask + private readonly IServiceScopeFactory _serviceScopeFactory; + + protected DbTask(IServiceScopeFactory serviceScopeFactory) { - private readonly IServiceScopeFactory _serviceScopeFactory; - - protected DbTask(IServiceScopeFactory serviceScopeFactory) - { - _serviceScopeFactory = serviceScopeFactory; - } - - public void Start() - { - Task.Run(() => - { - try - { - using var scope = _serviceScopeFactory.CreateScope(); - ILogger logger = scope.ServiceProvider.GetRequiredService>(); - using var dbContext = scope.ServiceProvider.GetRequiredService(); - - Run(dbContext, logger); - } - catch (Exception e) - { - DependencyInjectionContext.LoggerProvider.CreateLogger>() - .LogError(e, "Failed to run DbTask"); - } - }); - } - - protected abstract void Run(PalClientContext dbContext, ILogger logger); + _serviceScopeFactory = serviceScopeFactory; } + + public void Start() + { + Task.Run(() => + { + try + { + using var scope = _serviceScopeFactory.CreateScope(); + ILogger logger = scope.ServiceProvider.GetRequiredService>(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + + Run(dbContext, logger); + } + catch (Exception e) + { + DependencyInjectionContext.LoggerProvider.CreateLogger>() + .LogError(e, "Failed to run DbTask"); + } + }); + } + + protected abstract void Run(PalClientContext dbContext, ILogger logger); } diff --git a/Pal.Client/Floors/Tasks/LoadTerritory.cs b/Pal.Client/Floors/Tasks/LoadTerritory.cs index 7e11b2f..d3c7bcd 100644 --- a/Pal.Client/Floors/Tasks/LoadTerritory.cs +++ b/Pal.Client/Floors/Tasks/LoadTerritory.cs @@ -7,73 +7,72 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; -namespace Pal.Client.Floors.Tasks +namespace Pal.Client.Floors.Tasks; + +internal sealed class LoadTerritory : DbTask { - internal sealed class LoadTerritory : DbTask + private readonly Cleanup _cleanup; + private readonly MemoryTerritory _territory; + + public LoadTerritory(IServiceScopeFactory serviceScopeFactory, + Cleanup cleanup, + MemoryTerritory territory) + : base(serviceScopeFactory) { - private readonly Cleanup _cleanup; - private readonly MemoryTerritory _territory; + _cleanup = cleanup; + _territory = territory; + } - public LoadTerritory(IServiceScopeFactory serviceScopeFactory, - Cleanup cleanup, - MemoryTerritory territory) - : base(serviceScopeFactory) + protected override void Run(PalClientContext dbContext, ILogger logger) + { + lock (_territory.LockObj) { - _cleanup = cleanup; - _territory = territory; - } - - protected override void Run(PalClientContext dbContext, ILogger logger) - { - lock (_territory.LockObj) + if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading) { - if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading) - { - logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType, - _territory.ReadyState); - return; - } - - logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType); - - // purge outdated locations - _cleanup.Purge(dbContext, _territory.TerritoryType); - - // load good locations - List 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); + logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType, + _territory.ReadyState); + return; } - } - public static PersistentLocation ToMemoryLocation(ClientLocation location) - { - return new PersistentLocation - { - LocalId = location.LocalId, - Type = ToMemoryLocationType(location.Type), - Position = new Vector3(location.X, location.Y, location.Z), - Seen = location.Seen, - Source = location.Source, - RemoteSeenOn = location.RemoteEncounters.Select(o => o.AccountId).ToList(), - }; - } + logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType); - private static MemoryLocation.EType ToMemoryLocationType(ClientLocation.EType type) - { - return type switch - { - ClientLocation.EType.Trap => MemoryLocation.EType.Trap, - ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; + // purge outdated locations + _cleanup.Purge(dbContext, _territory.TerritoryType); + + // load good locations + List 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); } } + + public static PersistentLocation ToMemoryLocation(ClientLocation location) + { + return new PersistentLocation + { + LocalId = location.LocalId, + Type = ToMemoryLocationType(location.Type), + Position = new Vector3(location.X, location.Y, location.Z), + Seen = location.Seen, + Source = location.Source, + RemoteSeenOn = location.RemoteEncounters.Select(o => o.AccountId).ToList(), + }; + } + + private static MemoryLocation.EType ToMemoryLocationType(ClientLocation.EType type) + { + return type switch + { + ClientLocation.EType.Trap => MemoryLocation.EType.Trap, + ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } } diff --git a/Pal.Client/Floors/Tasks/MarkLocalSeen.cs b/Pal.Client/Floors/Tasks/MarkLocalSeen.cs index 59b99bb..79cb36c 100644 --- a/Pal.Client/Floors/Tasks/MarkLocalSeen.cs +++ b/Pal.Client/Floors/Tasks/MarkLocalSeen.cs @@ -5,33 +5,32 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; -namespace Pal.Client.Floors.Tasks +namespace Pal.Client.Floors.Tasks; + +internal sealed class MarkLocalSeen : DbTask { - internal sealed class MarkLocalSeen : DbTask + private readonly MemoryTerritory _territory; + private readonly IReadOnlyList _locations; + + public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + IReadOnlyList locations) + : base(serviceScopeFactory) { - private readonly MemoryTerritory _territory; - private readonly IReadOnlyList _locations; + _territory = territory; + _locations = locations; + } - public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, - IReadOnlyList locations) - : base(serviceScopeFactory) + protected override void Run(PalClientContext dbContext, ILogger logger) + { + lock (_territory.LockObj) { - _territory = territory; - _locations = locations; - } - - protected override void Run(PalClientContext dbContext, ILogger logger) - { - lock (_territory.LockObj) - { - logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count, - _territory.TerritoryType); - List localIds = _locations.Select(l => l.LocalId).Where(x => x != null).Cast().ToList(); - dbContext.Locations - .Where(loc => localIds.Contains(loc.LocalId)) - .ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true)); - dbContext.SaveChanges(); - } + logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count, + _territory.TerritoryType); + List localIds = _locations.Select(l => l.LocalId).Where(x => x != null).Cast().ToList(); + dbContext.Locations + .Where(loc => localIds.Contains(loc.LocalId)) + .ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true)); + dbContext.SaveChanges(); } } } diff --git a/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs index 7a63741..2dbf3ba 100644 --- a/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs +++ b/Pal.Client/Floors/Tasks/MarkRemoteSeen.cs @@ -5,47 +5,46 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Pal.Client.Database; -namespace Pal.Client.Floors.Tasks +namespace Pal.Client.Floors.Tasks; + +internal sealed class MarkRemoteSeen : DbTask { - internal sealed class MarkRemoteSeen : DbTask + private readonly MemoryTerritory _territory; + private readonly IReadOnlyList _locations; + private readonly string _accountId; + + public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory, + MemoryTerritory territory, + IReadOnlyList locations, + string accountId) + : base(serviceScopeFactory) { - private readonly MemoryTerritory _territory; - private readonly IReadOnlyList _locations; - private readonly string _accountId; + _territory = territory; + _locations = locations; + _accountId = accountId; + } - public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory, - MemoryTerritory territory, - IReadOnlyList locations, - string accountId) - : base(serviceScopeFactory) + protected override void Run(PalClientContext dbContext, ILogger logger) + { + lock (_territory.LockObj) { - _territory = territory; - _locations = locations; - _accountId = accountId; - } + logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}", + _locations.Count, _accountId, _territory.TerritoryType); - protected override void Run(PalClientContext dbContext, ILogger logger) - { - lock (_territory.LockObj) + List locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast().ToList(); + List 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) { - logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}", - _locations.Count, _accountId, _territory.TerritoryType); - - List locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast().ToList(); - List 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(); + clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId)); } + + dbContext.SaveChanges(); } } } diff --git a/Pal.Client/Floors/Tasks/SaveNewLocations.cs b/Pal.Client/Floors/Tasks/SaveNewLocations.cs index 345986a..908f489 100644 --- a/Pal.Client/Floors/Tasks/SaveNewLocations.cs +++ b/Pal.Client/Floors/Tasks/SaveNewLocations.cs @@ -6,72 +6,71 @@ using Microsoft.Extensions.Logging; using Pal.Client.Database; using Pal.Common; -namespace Pal.Client.Floors.Tasks +namespace Pal.Client.Floors.Tasks; + +internal sealed class SaveNewLocations : DbTask { - internal sealed class SaveNewLocations : DbTask + private readonly MemoryTerritory _territory; + private readonly List _newLocations; + + public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, + List newLocations) + : base(serviceScopeFactory) { - private readonly MemoryTerritory _territory; - private readonly List _newLocations; + _territory = territory; + _newLocations = newLocations; + } - public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory, - List newLocations) - : base(serviceScopeFactory) - { - _territory = territory; - _newLocations = newLocations; - } + protected override void Run(PalClientContext dbContext, ILogger logger) + { + Run(_territory, dbContext, logger, _newLocations); + } - protected override void Run(PalClientContext dbContext, ILogger logger) + public static void Run( + MemoryTerritory territory, + PalClientContext dbContext, + ILogger logger, + List locations) + { + lock (territory.LockObj) { - Run(_territory, dbContext, logger, _newLocations); - } + logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count, + territory.TerritoryType); - public static void Run( - MemoryTerritory territory, - PalClientContext dbContext, - ILogger logger, - List locations) - { - lock (territory.LockObj) + Dictionary mapping = + locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType)); + dbContext.Locations.AddRange(mapping.Values); + dbContext.SaveChanges(); + + foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping) { - logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count, - territory.TerritoryType); - - Dictionary mapping = - locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType)); - dbContext.Locations.AddRange(mapping.Values); - dbContext.SaveChanges(); - - foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping) - { - persistentLocation.LocalId = clientLocation.LocalId; - } + persistentLocation.LocalId = clientLocation.LocalId; } } + } - private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType) + private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType) + { + return new ClientLocation { - return new ClientLocation - { - TerritoryType = (ushort)territoryType, - Type = ToDatabaseType(location.Type), - X = location.Position.X, - Y = location.Position.Y, - Z = location.Position.Z, - Seen = location.Seen, - Source = location.Source, - SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), - }; - } + TerritoryType = (ushort)territoryType, + Type = ToDatabaseType(location.Type), + X = location.Position.X, + Y = location.Position.Y, + Z = location.Position.Z, + Seen = location.Seen, + Source = location.Source, + SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), + }; + } - private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type) + private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type) + { + return type switch { - return type switch - { - MemoryLocation.EType.Trap => ClientLocation.EType.Trap, - MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - } + MemoryLocation.EType.Trap => ClientLocation.EType.Trap, + MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; } } diff --git a/Pal.Client/Floors/TerritoryState.cs b/Pal.Client/Floors/TerritoryState.cs index febff4c..2ba1071 100644 --- a/Pal.Client/Floors/TerritoryState.cs +++ b/Pal.Client/Floors/TerritoryState.cs @@ -2,35 +2,34 @@ using Dalamud.Game.ClientState.Conditions; using Pal.Common; -namespace Pal.Client.Floors +namespace Pal.Client.Floors; + +public sealed class TerritoryState { - public sealed class TerritoryState + private readonly ClientState _clientState; + private readonly Condition _condition; + + public TerritoryState(ClientState clientState, Condition condition) { - private readonly ClientState _clientState; - private readonly Condition _condition; - - public TerritoryState(ClientState clientState, Condition condition) - { - _clientState = clientState; - _condition = condition; - } - - public ushort LastTerritory { get; set; } - public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; - public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; - - public bool IsInDeepDungeon() => - _clientState.IsLoggedIn - && _condition[ConditionFlag.InDeepDungeon] - && typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType); - + _clientState = clientState; + _condition = condition; } - public enum PomanderState - { - Inactive, - Active, - FoundOnCurrentFloor, - PomanderOfSafetyUsed, - } + public ushort LastTerritory { get; set; } + public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; + public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; + + public bool IsInDeepDungeon() => + _clientState.IsLoggedIn + && _condition[ConditionFlag.InDeepDungeon] + && typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType); + +} + +public enum PomanderState +{ + Inactive, + Active, + FoundOnCurrentFloor, + PomanderOfSafetyUsed, } diff --git a/Pal.Client/ILanguageChanged.cs b/Pal.Client/ILanguageChanged.cs index 8f3f519..847340b 100644 --- a/Pal.Client/ILanguageChanged.cs +++ b/Pal.Client/ILanguageChanged.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Pal.Client +namespace Pal.Client; + +internal interface ILanguageChanged { - internal interface ILanguageChanged - { - void LanguageChanged(); - } + void LanguageChanged(); } diff --git a/Pal.Client/Net/JwtClaims.cs b/Pal.Client/Net/JwtClaims.cs index cd2796a..3927c77 100644 --- a/Pal.Client/Net/JwtClaims.cs +++ b/Pal.Client/Net/JwtClaims.cs @@ -1,95 +1,77 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; -namespace Pal.Client.Net +namespace Pal.Client.Net; + +internal sealed class JwtClaims { - internal sealed class JwtClaims + [JsonPropertyName("nameid")] + public Guid NameId { get; set; } + + [JsonPropertyName("role")] + [JsonConverter(typeof(JwtRoleConverter))] + public List Roles { get; set; } = new(); + + [JsonPropertyName("nbf")] + [JsonConverter(typeof(JwtDateConverter))] + public DateTimeOffset NotBefore { get; set; } + + [JsonPropertyName("exp")] + [JsonConverter(typeof(JwtDateConverter))] + public DateTimeOffset ExpiresAt { get; set; } + + public static JwtClaims FromAuthToken(string authToken) { - [JsonPropertyName("nameid")] - public Guid NameId { get; set; } + if (string.IsNullOrEmpty(authToken)) + throw new ArgumentException("Server sent no auth token", nameof(authToken)); - [JsonPropertyName("role")] - [JsonConverter(typeof(JwtRoleConverter))] - public List Roles { get; set; } = new(); + string[] parts = authToken.Split('.'); + if (parts.Length != 3) + throw new ArgumentException("Unsupported token type", nameof(authToken)); - [JsonPropertyName("nbf")] - [JsonConverter(typeof(JwtDateConverter))] - public DateTimeOffset NotBefore { get; set; } + // fix padding manually + string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_"); + if (payload.Length % 4 == 2) + payload += "=="; + else if (payload.Length % 4 == 3) + payload += "="; - [JsonPropertyName("exp")] - [JsonConverter(typeof(JwtDateConverter))] - public DateTimeOffset ExpiresAt { get; set; } - - public static JwtClaims FromAuthToken(string authToken) - { - if (string.IsNullOrEmpty(authToken)) - throw new ArgumentException("Server sent no auth token", nameof(authToken)); - - string[] parts = authToken.Split('.'); - if (parts.Length != 3) - throw new ArgumentException("Unsupported token type", nameof(authToken)); - - // fix padding manually - string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_"); - if (payload.Length % 4 == 2) - payload += "=="; - else if (payload.Length % 4 == 3) - payload += "="; - - string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload)); - return JsonSerializer.Deserialize(content) ?? throw new InvalidOperationException("token deserialization returned null"); - } - } - - internal sealed class JwtRoleConverter : JsonConverter> - { - public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - return new List { reader.GetString() ?? throw new JsonException("no value present") }; - else if (reader.TokenType == JsonTokenType.StartArray) - { - List result = new(); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - { - result.Sort(); - return result; - } - - if (reader.TokenType != JsonTokenType.String) - throw new JsonException("string expected"); - - result.Add(reader.GetString() ?? throw new JsonException("no value present")); - } - - throw new JsonException("read to end of document"); - } - else - throw new JsonException("bad token type"); - } - - public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) => throw new NotImplementedException(); - } - - public sealed class JwtDateConverter : JsonConverter - { - static readonly DateTimeOffset Zero = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.Number) - throw new JsonException("bad token type"); - - return Zero.AddSeconds(reader.GetInt64()); - } - - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) => throw new NotImplementedException(); + string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload)); + return JsonSerializer.Deserialize(content) ?? throw new InvalidOperationException("token deserialization returned null"); } } + +internal sealed class JwtRoleConverter : JsonConverter> +{ + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + return new List { reader.GetString() ?? throw new JsonException("no value present") }; + else if (reader.TokenType == JsonTokenType.StartArray) + { + List result = new(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + result.Sort(); + return result; + } + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException("string expected"); + + result.Add(reader.GetString() ?? throw new JsonException("no value present")); + } + + throw new JsonException("read to end of document"); + } + else + throw new JsonException("bad token type"); + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) => throw new NotImplementedException(); +} diff --git a/Pal.Client/Net/JwtDateConverter.cs b/Pal.Client/Net/JwtDateConverter.cs new file mode 100644 index 0000000..11a7508 --- /dev/null +++ b/Pal.Client/Net/JwtDateConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Pal.Client.Net; + +public sealed class JwtDateConverter : JsonConverter +{ + static readonly DateTimeOffset Zero = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + throw new JsonException("bad token type"); + + return Zero.AddSeconds(reader.GetInt64()); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) => throw new NotImplementedException(); +} diff --git a/Pal.Client/Net/RemoteApi.AccountService.cs b/Pal.Client/Net/RemoteApi.AccountService.cs index 558eebc..cc25d89 100644 --- a/Pal.Client/Net/RemoteApi.AccountService.cs +++ b/Pal.Client/Net/RemoteApi.AccountService.cs @@ -12,221 +12,220 @@ using Pal.Client.Configuration; using Pal.Client.Extensions; using Pal.Client.Properties; -namespace Pal.Client.Net +namespace Pal.Client.Net; + +internal partial class RemoteApi { - internal partial class RemoteApi + private readonly SemaphoreSlim _connectLock = new(1, 1); + + private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, + ILoggerFactory? loggerFactory = null, bool retry = true) { - private readonly SemaphoreSlim _connectLock = new(1, 1); + using IDisposable? logScope = _logger.BeginScope("TryConnect"); - private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, - ILoggerFactory? loggerFactory = null, bool retry = true) + var result = await TryConnectImpl(cancellationToken, loggerFactory); + if (retry && result.ShouldRetry) + result = await TryConnectImpl(cancellationToken, loggerFactory); + + return (result.Success, result.Error); + } + + private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl( + CancellationToken cancellationToken, + ILoggerFactory? loggerFactory) + { + if (_configuration.Mode != EMode.Online) { - using IDisposable? logScope = _logger.BeginScope("TryConnect"); - - var result = await TryConnectImpl(cancellationToken, loggerFactory); - if (retry && result.ShouldRetry) - result = await TryConnectImpl(cancellationToken, loggerFactory); - - return (result.Success, result.Error); + _logger.LogDebug("Not Online, not attempting to establish a connection"); + return (false, Localization.ConnectionError_NotOnline, false); } - private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl( - CancellationToken cancellationToken, - ILoggerFactory? loggerFactory) + if (_channel == null || + !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) { - if (_configuration.Mode != EMode.Online) - { - _logger.LogDebug("Not Online, not attempting to establish a connection"); - return (false, Localization.ConnectionError_NotOnline, false); - } + Dispose(); - if (_channel == null || - !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) + _logger.LogInformation("Creating new gRPC channel"); + _channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions { - Dispose(); - - _logger.LogInformation("Creating new gRPC channel"); - _channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions + HttpHandler = new SocketsHttpHandler { - HttpHandler = new SocketsHttpHandler - { - ConnectTimeout = TimeSpan.FromSeconds(5), - SslOptions = GetSslClientAuthenticationOptions(), - }, - LoggerFactory = loggerFactory, - }); + ConnectTimeout = TimeSpan.FromSeconds(5), + SslOptions = GetSslClientAuthenticationOptions(), + }, + LoggerFactory = loggerFactory, + }); - _logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl); - await _channel.ConnectAsync(cancellationToken); + _logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl); + await _channel.ConnectAsync(cancellationToken); + } + + 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); + if (createAccountReply.Success) + { + if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) + throw new InvalidOperationException("invalid account id returned"); + + configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); + _logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId()); + + _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); + _warnedAboutUpgrade = true; + } + + return (false, + string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error), + false); + } } cancellationToken.ThrowIfCancellationRequested(); - _logger.LogTrace("Acquiring connect lock"); - await _connectLock.WaitAsync(cancellationToken); - _logger.LogTrace("Obtained connect lock"); - - try + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (configuredAccount == null) { - var accountClient = new AccountService.AccountServiceClient(_channel); - IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl); - if (configuredAccount == null) + _logger.LogWarning("No account to login with"); + return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false); + } + + 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("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); - if (createAccountReply.Success) - { - if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId)) - throw new InvalidOperationException("invalid account id returned"); - - configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); - _logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId()); - - _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); - _warnedAboutUpgrade = true; - } - - return (false, - string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error), - false); - } - } - - cancellationToken.ThrowIfCancellationRequested(); - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (configuredAccount == null) - { - _logger.LogWarning("No account to login with"); - return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false); - } - - if (!_loginInfo.IsValid) - { - _logger.LogInformation("Logging in with account id {AccountId}", + _logger.LogInformation("Login successful 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); + _loginInfo = new LoginInfo(loginReply.AuthToken); - if (loginReply.Success) + bool save = configuredAccount.EncryptIfNeeded(); + + List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); + if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) { - _logger.LogInformation("Login successful with account id: {AccountId}", - configuredAccount.AccountId.ToPartialId()); - _loginInfo = new LoginInfo(loginReply.AuthToken); - - bool save = configuredAccount.EncryptIfNeeded(); - - List newRoles = _loginInfo.Claims?.Roles.ToList() ?? new(); - if (!newRoles.SequenceEqual(configuredAccount.CachedRoles)) - { - configuredAccount.CachedRoles = newRoles; - save = true; - } - - if (save) - _configurationManager.Save(_configuration); + configuredAccount.CachedRoles = newRoles; + save = true; } - 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); - _logger.LogInformation("Attempting connection retry without account id"); - return (false, Localization.ConnectionError_InvalidAccountId, true); - } - - if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) - { - _chat.Error(Localization.ConnectionError_OldVersion); - _warnedAboutUpgrade = true; - } - - return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error), - false); - } - } - - if (!_loginInfo.IsValid) - { - _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, - _loginInfo.IsExpired); - return (false, Localization.ConnectionError_LoginReturnedNoToken, false); - } - - cancellationToken.ThrowIfCancellationRequested(); - return (true, string.Empty, false); - } - finally - { - _logger.LogTrace("Releasing connectLock"); - _connectLock.Release(); - } - } - - private async Task Connect(CancellationToken cancellationToken) - { - var result = await TryConnect(cancellationToken); - return result.Success; - } - - public async Task VerifyConnection(CancellationToken cancellationToken = default) - { - using IDisposable? logScope = _logger.BeginScope("VerifyConnection"); - - _warnedAboutUpgrade = false; - - var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory); - if (!connectionResult.Success) - return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error); - - _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); - - _logger.LogInformation("Verification returned no errors."); - return Localization.ConnectionSuccessful; - } - - internal sealed class LoginInfo - { - public LoginInfo(string? authToken) - { - if (!string.IsNullOrEmpty(authToken)) - { - IsLoggedIn = true; - AuthToken = authToken; - Claims = JwtClaims.FromAuthToken(authToken); + if (save) + _configurationManager.Save(_configuration); } else - IsLoggedIn = false; + { + _logger.LogError("Login failed with error {Error}", loginReply.Error); + _loginInfo = new LoginInfo(null); + if (loginReply.Error == LoginError.InvalidAccountId) + { + _configuration.RemoveAccount(RemoteUrl); + _configurationManager.Save(_configuration); + + _logger.LogInformation("Attempting connection retry without account id"); + return (false, Localization.ConnectionError_InvalidAccountId, true); + } + + if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade) + { + _chat.Error(Localization.ConnectionError_OldVersion); + _warnedAboutUpgrade = true; + } + + return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error), + false); + } } - public bool IsLoggedIn { get; } - public string? AuthToken { get; } - public JwtClaims? Claims { get; } + if (!_loginInfo.IsValid) + { + _logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, + _loginInfo.IsExpired); + return (false, Localization.ConnectionError_LoginReturnedNoToken, false); + } - private DateTimeOffset ExpiresAt => - Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; - - public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; - - public bool IsValid => IsLoggedIn && !IsExpired; + cancellationToken.ThrowIfCancellationRequested(); + return (true, string.Empty, false); + } + finally + { + _logger.LogTrace("Releasing connectLock"); + _connectLock.Release(); } } + + private async Task Connect(CancellationToken cancellationToken) + { + var result = await TryConnect(cancellationToken); + return result.Success; + } + + public async Task VerifyConnection(CancellationToken cancellationToken = default) + { + using IDisposable? logScope = _logger.BeginScope("VerifyConnection"); + + _warnedAboutUpgrade = false; + + var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory); + if (!connectionResult.Success) + return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error); + + _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); + + _logger.LogInformation("Verification returned no errors."); + return Localization.ConnectionSuccessful; + } + + internal sealed class LoginInfo + { + public LoginInfo(string? authToken) + { + if (!string.IsNullOrEmpty(authToken)) + { + IsLoggedIn = true; + AuthToken = authToken; + Claims = JwtClaims.FromAuthToken(authToken); + } + else + IsLoggedIn = false; + } + + public bool IsLoggedIn { get; } + public string? AuthToken { get; } + public JwtClaims? Claims { get; } + + private DateTimeOffset ExpiresAt => + Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue; + + public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow; + + public bool IsValid => IsLoggedIn && !IsExpired; + } } diff --git a/Pal.Client/Net/RemoteApi.ExportService.cs b/Pal.Client/Net/RemoteApi.ExportService.cs index 79e2e31..7489f82 100644 --- a/Pal.Client/Net/RemoteApi.ExportService.cs +++ b/Pal.Client/Net/RemoteApi.ExportService.cs @@ -3,21 +3,20 @@ using System.Threading; using System.Threading.Tasks; using Export; -namespace Pal.Client.Net -{ - internal partial class RemoteApi - { - public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default) - { - if (!await Connect(cancellationToken)) - return new(false, new()); +namespace Pal.Client.Net; - var exportClient = new ExportService.ExportServiceClient(_channel); - var exportReply = await exportClient.ExportAsync(new ExportRequest - { - ServerUrl = RemoteUrl, - }, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken); - return (exportReply.Success, exportReply.Data); - } +internal partial class RemoteApi +{ + public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return new(false, new()); + + var exportClient = new ExportService.ExportServiceClient(_channel); + var exportReply = await exportClient.ExportAsync(new ExportRequest + { + ServerUrl = RemoteUrl, + }, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken); + return (exportReply.Success, exportReply.Data); } } diff --git a/Pal.Client/Net/RemoteApi.PalaceService.cs b/Pal.Client/Net/RemoteApi.PalaceService.cs index 140d92f..b1f34c8 100644 --- a/Pal.Client/Net/RemoteApi.PalaceService.cs +++ b/Pal.Client/Net/RemoteApi.PalaceService.cs @@ -8,80 +8,79 @@ using Pal.Client.Database; using Pal.Client.Floors; using Palace; -namespace Pal.Client.Net +namespace Pal.Client.Net; + +internal partial class RemoteApi { - internal partial class RemoteApi + public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) { - public async Task<(bool, List)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default) + if (!await Connect(cancellationToken)) + return (false, new()); + + 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(CreateLocationFromNetworkObject).ToList()); + } + + public async Task<(bool, List)> UploadLocations(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) + { + if (locations.Count == 0) + return (true, new()); + + if (!await Connect(cancellationToken)) + return (false, new()); + + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var uploadRequest = new UploadFloorsRequest { - if (!await Connect(cancellationToken)) - return (false, new()); - - 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(CreateLocationFromNetworkObject).ToList()); - } - - public async Task<(bool, List)> UploadLocations(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) + TerritoryType = territoryType, + }; + uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject { - if (locations.Count == 0) - return (true, new()); + Type = m.Type.ToObjectType(), + X = m.Position.X, + Y = m.Position.Y, + Z = m.Position.Z + })); + var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); + return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList()); + } - if (!await Connect(cancellationToken)) - return (false, new()); + public async Task MarkAsSeen(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) + { + if (locations.Count == 0) + return true; - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var uploadRequest = new UploadFloorsRequest - { - TerritoryType = territoryType, - }; - uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject - { - Type = m.Type.ToObjectType(), - X = m.Position.X, - Y = m.Position.Y, - Z = m.Position.Z - })); - var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken); - return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList()); - } + if (!await Connect(cancellationToken)) + return false; - public async Task MarkAsSeen(ushort territoryType, IReadOnlyList locations, CancellationToken cancellationToken = default) + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; + foreach (var marker in locations) + 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 PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj) + { + return new PersistentLocation { - if (locations.Count == 0) - return true; + Type = obj.Type.ToMemoryType(), + Position = new Vector3(obj.X, obj.Y, obj.Z), + NetworkId = Guid.Parse(obj.NetworkId), + Source = ClientLocation.ESource.Download, + }; + } - if (!await Connect(cancellationToken)) - return false; + public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) + { + if (!await Connect(cancellationToken)) + return new(false, new List()); - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType }; - foreach (var marker in locations) - 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 PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj) - { - return new PersistentLocation - { - Type = obj.Type.ToMemoryType(), - Position = new Vector3(obj.X, obj.Y, obj.Z), - NetworkId = Guid.Parse(obj.NetworkId), - Source = ClientLocation.ESource.Download, - }; - } - - public async Task<(bool, List)> FetchStatistics(CancellationToken cancellationToken = default) - { - if (!await Connect(cancellationToken)) - return new(false, new List()); - - var palaceClient = new PalaceService.PalaceServiceClient(_channel); - var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken); - return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList()); - } + var palaceClient = new PalaceService.PalaceServiceClient(_channel); + var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken); + return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList()); } } diff --git a/Pal.Client/Net/RemoteApi.Utils.cs b/Pal.Client/Net/RemoteApi.Utils.cs index f520bd0..eebd864 100644 --- a/Pal.Client/Net/RemoteApi.Utils.cs +++ b/Pal.Client/Net/RemoteApi.Utils.cs @@ -5,54 +5,53 @@ using Dalamud.Logging; using Grpc.Core; using Microsoft.Extensions.Logging; -namespace Pal.Client.Net +namespace Pal.Client.Net; + +internal partial class RemoteApi { - internal partial class RemoteApi + private Metadata UnauthorizedHeaders() => new() { - private Metadata UnauthorizedHeaders() => new() - { - { "User-Agent", _userAgent }, - }; + { "User-Agent", _userAgent }, + }; - private Metadata AuthorizedHeaders() => new() - { - { "Authorization", $"Bearer {_loginInfo.AuthToken}" }, - { "User-Agent", _userAgent }, - }; + private Metadata AuthorizedHeaders() => new() + { + { "Authorization", $"Bearer {_loginInfo.AuthToken}" }, + { "User-Agent", _userAgent }, + }; - private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions() - { + private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions() + { #if !DEBUG - var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets"); - if (secrets == null) - return null; - - var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string; - if (pass == null) - return null; - - var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx"); - if (manifestResourceStream == null) - return null; - - var bytes = new byte[manifestResourceStream.Length]; - int read = manifestResourceStream.Read(bytes, 0, bytes.Length); - if (read != bytes.Length) - throw new InvalidOperationException(); - - var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet); - _logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString()); - return new SslClientAuthenticationOptions - { - ClientCertificates = new X509CertificateCollection() - { - certificate, - }, - }; -#else - _logger.LogDebug("Not using client certificate"); + var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets"); + if (secrets == null) return null; + + var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string; + if (pass == null) + return null; + + var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx"); + if (manifestResourceStream == null) + return null; + + var bytes = new byte[manifestResourceStream.Length]; + int read = manifestResourceStream.Read(bytes, 0, bytes.Length); + if (read != bytes.Length) + throw new InvalidOperationException(); + + var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet); + _logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString()); + return new SslClientAuthenticationOptions + { + ClientCertificates = new X509CertificateCollection() + { + certificate, + }, + }; +#else + _logger.LogDebug("Not using client certificate"); + return null; #endif - } } } diff --git a/Pal.Client/Net/RemoteApi.cs b/Pal.Client/Net/RemoteApi.cs index 0fad434..f5cf15d 100644 --- a/Pal.Client/Net/RemoteApi.cs +++ b/Pal.Client/Net/RemoteApi.cs @@ -6,47 +6,46 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.DependencyInjection; -namespace Pal.Client.Net +namespace Pal.Client.Net; + +internal sealed partial class RemoteApi : IDisposable { - internal sealed partial class RemoteApi : IDisposable - { #if DEBUG - public const string RemoteUrl = "http://localhost:5415"; + public const string RemoteUrl = "http://localhost:5415"; #else - public const string RemoteUrl = "https://pal.liza.sh"; + public const string RemoteUrl = "https://pal.liza.sh"; #endif - private readonly string _userAgent = - $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; + private readonly string _userAgent = + $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly Chat _chat; - private readonly ConfigurationManager _configurationManager; - private readonly IPalacePalConfiguration _configuration; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private readonly Chat _chat; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; - private GrpcChannel? _channel; - private LoginInfo _loginInfo = new(null); - private bool _warnedAboutUpgrade; + private GrpcChannel? _channel; + private LoginInfo _loginInfo = new(null); + private bool _warnedAboutUpgrade; - public RemoteApi( - ILoggerFactory loggerFactory, - ILogger logger, - Chat chat, - ConfigurationManager configurationManager, - IPalacePalConfiguration configuration) - { - _loggerFactory = loggerFactory; - _logger = logger; - _chat = chat; - _configurationManager = configurationManager; - _configuration = configuration; - } + public RemoteApi( + ILoggerFactory loggerFactory, + ILogger logger, + Chat chat, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + { + _loggerFactory = loggerFactory; + _logger = logger; + _chat = chat; + _configurationManager = configurationManager; + _configuration = configuration; + } - public void Dispose() - { - _logger.LogDebug("Disposing gRPC channel"); - _channel?.Dispose(); - _channel = null; - } + public void Dispose() + { + _logger.LogDebug("Disposing gRPC channel"); + _channel?.Dispose(); + _channel = null; } } diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index 5a8392c..1e7f1b1 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -29,18 +29,18 @@ - + - + - - - - + + + + all @@ -48,24 +48,24 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + - - - + + + @@ -124,10 +124,10 @@ - + - + diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index f45ae99..33cd44e 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -21,217 +21,216 @@ using Pal.Client.DependencyInjection; using Pal.Client.Properties; using Pal.Client.Rendering; -namespace Pal.Client +namespace Pal.Client; + +/// +/// With all DI logic elsewhere, this plugin shell really only takes care of a few things around events that +/// need to be sent to different receivers depending on priority or configuration . +/// +/// +internal sealed class Plugin : IDalamudPlugin { - /// - /// With all DI logic elsewhere, this plugin shell really only takes care of a few things around events that - /// need to be sent to different receivers depending on priority or configuration . - /// - /// - internal sealed class Plugin : IDalamudPlugin + private readonly CancellationTokenSource _initCts = new(); + + private readonly DalamudPluginInterface _pluginInterface; + private readonly CommandManager _commandManager; + private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + private readonly Framework _framework; + + private readonly TaskCompletionSource _rootScopeCompletionSource = new(); + private ELoadState _loadState = ELoadState.Initializing; + + private DependencyInjectionContext? _dependencyInjectionContext; + private ILogger _logger = DependencyInjectionContext.LoggerProvider.CreateLogger(); + private WindowSystem? _windowSystem; + private IServiceScope? _rootScope; + private Action? _loginAction; + + public Plugin( + DalamudPluginInterface pluginInterface, + CommandManager commandManager, + ClientState clientState, + ChatGui chatGui, + Framework framework) { - private readonly CancellationTokenSource _initCts = new(); + _pluginInterface = pluginInterface; + _commandManager = commandManager; + _clientState = clientState; + _chatGui = chatGui; + _framework = framework; - private readonly DalamudPluginInterface _pluginInterface; - private readonly CommandManager _commandManager; - private readonly ClientState _clientState; - private readonly ChatGui _chatGui; - private readonly Framework _framework; + // set up the current UI language before creating anything + Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage); - private readonly TaskCompletionSource _rootScopeCompletionSource = new(); - private ELoadState _loadState = ELoadState.Initializing; - - private DependencyInjectionContext? _dependencyInjectionContext; - private ILogger _logger = DependencyInjectionContext.LoggerProvider.CreateLogger(); - private WindowSystem? _windowSystem; - private IServiceScope? _rootScope; - private Action? _loginAction; - - public Plugin( - DalamudPluginInterface pluginInterface, - CommandManager commandManager, - ClientState clientState, - ChatGui chatGui, - Framework framework) + _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) { - _pluginInterface = pluginInterface; - _commandManager = commandManager; - _clientState = clientState; - _chatGui = chatGui; - _framework = framework; + HelpMessage = Localization.Command_pal_HelpText + }); - // set up the current UI language before creating anything - Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage); + // Using TickScheduler requires ECommons to at least be partially initialized + // ECommonsMain.Dispose leaves this untouched. + Svc.Init(pluginInterface); - _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) + Task.Run(async () => await CreateDependencyContext()); + } + + public string Name => Localization.Palace_Pal; + + private async Task CreateDependencyContext() + { + try + { + _dependencyInjectionContext = _pluginInterface.Create(this) + ?? throw new Exception("Could not create DI root context class"); + var serviceProvider = _dependencyInjectionContext.BuildServiceContainer(); + _initCts.Token.ThrowIfCancellationRequested(); + + _logger = serviceProvider.GetRequiredService>(); + _windowSystem = serviceProvider.GetRequiredService(); + _rootScope = serviceProvider.CreateScope(); + + var loader = _rootScope.ServiceProvider.GetRequiredService(); + await loader.InitializeAsync(_initCts.Token); + + await _framework.RunOnFrameworkThread(() => { - HelpMessage = Localization.Command_pal_HelpText + _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; + _pluginInterface.LanguageChanged += LanguageChanged; + _clientState.Login += Login; }); - - // Using TickScheduler requires ECommons to at least be partially initialized - // ECommonsMain.Dispose leaves this untouched. - Svc.Init(pluginInterface); - - Task.Run(async () => await CreateDependencyContext()); + _rootScopeCompletionSource.SetResult(_rootScope); + _loadState = ELoadState.Loaded; } - - public string Name => Localization.Palace_Pal; - - private async Task CreateDependencyContext() + catch (Exception e) when (e is ObjectDisposedException + or OperationCanceledException + or RepoVerification.RepoVerificationFailedException + || (e is FileLoadException && _pluginInterface.IsDev)) { + _rootScopeCompletionSource.SetException(e); + _loadState = ELoadState.Error; + } + catch (Exception e) + { + _rootScopeCompletionSource.SetException(e); + _logger.LogError(e, "Async load failed"); + ShowErrorOnLogin(() => + new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed, + $"{e.GetType()} - {e.Message}"))); + + _loadState = ELoadState.Error; + } + } + + private void ShowErrorOnLogin(Action? loginAction) + { + if (_clientState.IsLoggedIn) + { + loginAction?.Invoke(); + _loginAction = null; + } + else + _loginAction = loginAction; + } + + private void Login(object? sender, EventArgs eventArgs) + { + _loginAction?.Invoke(); + _loginAction = null; + } + + private void OnCommand(string command, string arguments) + { + arguments = arguments.Trim(); + + Task.Run(async () => + { + IServiceScope rootScope; + Chat chat; + try { - _dependencyInjectionContext = _pluginInterface.Create(this) - ?? throw new Exception("Could not create DI root context class"); - var serviceProvider = _dependencyInjectionContext.BuildServiceContainer(); - _initCts.Token.ThrowIfCancellationRequested(); - - _logger = serviceProvider.GetRequiredService>(); - _windowSystem = serviceProvider.GetRequiredService(); - _rootScope = serviceProvider.CreateScope(); - - var loader = _rootScope.ServiceProvider.GetRequiredService(); - await loader.InitializeAsync(_initCts.Token); - - await _framework.RunOnFrameworkThread(() => - { - _pluginInterface.UiBuilder.Draw += Draw; - _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; - _pluginInterface.LanguageChanged += LanguageChanged; - _clientState.Login += Login; - }); - _rootScopeCompletionSource.SetResult(_rootScope); - _loadState = ELoadState.Loaded; - } - catch (Exception e) when (e is ObjectDisposedException - or OperationCanceledException - or RepoVerification.RepoVerificationFailedException - || (e is FileLoadException && _pluginInterface.IsDev)) - { - _rootScopeCompletionSource.SetException(e); - _loadState = ELoadState.Error; + rootScope = await _rootScopeCompletionSource.Task; + chat = rootScope.ServiceProvider.GetRequiredService(); } catch (Exception e) { - _rootScopeCompletionSource.SetException(e); - _logger.LogError(e, "Async load failed"); - ShowErrorOnLogin(() => - new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed, - $"{e.GetType()} - {e.Message}"))); - - _loadState = ELoadState.Error; + _logger.LogError(e, "Could not wait for command root scope"); + return; } - } - private void ShowErrorOnLogin(Action? loginAction) - { - if (_clientState.IsLoggedIn) + try { - loginAction?.Invoke(); - _loginAction = null; - } - else - _loginAction = loginAction; - } - - private void Login(object? sender, EventArgs eventArgs) - { - _loginAction?.Invoke(); - _loginAction = null; - } - - private void OnCommand(string command, string arguments) - { - arguments = arguments.Trim(); - - Task.Run(async () => - { - IServiceScope rootScope; - Chat chat; - - try + IPalacePalConfiguration configuration = + rootScope.ServiceProvider.GetRequiredService(); + if (configuration.FirstUse && arguments != "" && arguments != "config") { - rootScope = await _rootScopeCompletionSource.Task; - chat = rootScope.ServiceProvider.GetRequiredService(); - } - catch (Exception e) - { - _logger.LogError(e, "Could not wait for command root scope"); + chat.Error(Localization.Error_FirstTimeSetupRequired); return; } - try - { - IPalacePalConfiguration configuration = - rootScope.ServiceProvider.GetRequiredService(); - if (configuration.FirstUse && arguments != "" && arguments != "config") + Action commandHandler = rootScope.ServiceProvider + .GetRequiredService>() + .SelectMany(cmd => cmd.GetHandlers()) + .Where(cmd => cmd.Key == arguments.ToLowerInvariant()) + .Select(cmd => cmd.Value) + .SingleOrDefault(missingCommand => { - chat.Error(Localization.Error_FirstTimeSetupRequired); - return; - } - - Action commandHandler = rootScope.ServiceProvider - .GetRequiredService>() - .SelectMany(cmd => cmd.GetHandlers()) - .Where(cmd => cmd.Key == arguments.ToLowerInvariant()) - .Select(cmd => cmd.Value) - .SingleOrDefault(missingCommand => - { - chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand, - command)); - }); - commandHandler.Invoke(arguments); - } - catch (Exception e) - { - _logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command, - arguments); - chat.Error(string.Format(Localization.Error_CommandFailed, - $"{e.GetType()} - {e.Message}")); - } - }); - } - - private void OpenConfigUi() - => _rootScope!.ServiceProvider.GetRequiredService().Execute(); - - private void LanguageChanged(string languageCode) - { - _logger.LogInformation("Language set to '{Language}'", languageCode); - - Localization.Culture = new CultureInfo(languageCode); - _windowSystem!.Windows.OfType() - .Each(w => w.LanguageChanged()); - } - - private void Draw() - { - _rootScope!.ServiceProvider.GetRequiredService().DrawLayers(); - _windowSystem!.Draw(); - } - - public void Dispose() - { - _commandManager.RemoveHandler("/pal"); - - if (_loadState == ELoadState.Loaded) - { - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - _pluginInterface.LanguageChanged -= LanguageChanged; - _clientState.Login -= Login; + chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand, + command)); + }); + commandHandler.Invoke(arguments); } + catch (Exception e) + { + _logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command, + arguments); + chat.Error(string.Format(Localization.Error_CommandFailed, + $"{e.GetType()} - {e.Message}")); + } + }); + } - _initCts.Cancel(); - _rootScope?.Dispose(); - _dependencyInjectionContext?.Dispose(); - } + private void OpenConfigUi() + => _rootScope!.ServiceProvider.GetRequiredService().Execute(); - private enum ELoadState + private void LanguageChanged(string languageCode) + { + _logger.LogInformation("Language set to '{Language}'", languageCode); + + Localization.Culture = new CultureInfo(languageCode); + _windowSystem!.Windows.OfType() + .Each(w => w.LanguageChanged()); + } + + private void Draw() + { + _rootScope!.ServiceProvider.GetRequiredService().DrawLayers(); + _windowSystem!.Draw(); + } + + public void Dispose() + { + _commandManager.RemoveHandler("/pal"); + + if (_loadState == ELoadState.Loaded) { - Initializing, - Loaded, - Error + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; + _clientState.Login -= Login; } + + _initCts.Cancel(); + _rootScope?.Dispose(); + _dependencyInjectionContext?.Dispose(); + } + + private enum ELoadState + { + Initializing, + Loaded, + Error } } diff --git a/Pal.Client/Rendering/ELayer.cs b/Pal.Client/Rendering/ELayer.cs index 1027f27..0f89ff9 100644 --- a/Pal.Client/Rendering/ELayer.cs +++ b/Pal.Client/Rendering/ELayer.cs @@ -1,9 +1,8 @@ -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal enum ELayer { - internal enum ELayer - { - TrapHoard, - RegularCoffers, - Test, - } + TrapHoard, + RegularCoffers, + Test, } diff --git a/Pal.Client/Rendering/IRenderElement.cs b/Pal.Client/Rendering/IRenderElement.cs index 8f11a82..6424fda 100644 --- a/Pal.Client/Rendering/IRenderElement.cs +++ b/Pal.Client/Rendering/IRenderElement.cs @@ -1,9 +1,8 @@ -namespace Pal.Client.Rendering -{ - public interface IRenderElement - { - bool IsValid { get; } +namespace Pal.Client.Rendering; - uint Color { get; set; } - } +public interface IRenderElement +{ + bool IsValid { get; } + + uint Color { get; set; } } diff --git a/Pal.Client/Rendering/IRenderer.cs b/Pal.Client/Rendering/IRenderer.cs index 1856403..17da135 100644 --- a/Pal.Client/Rendering/IRenderer.cs +++ b/Pal.Client/Rendering/IRenderer.cs @@ -3,18 +3,17 @@ using System.Numerics; using Pal.Client.Configuration; using Pal.Client.Floors; -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal interface IRenderer { - internal interface IRenderer - { - ERenderer GetConfigValue(); + ERenderer GetConfigValue(); - void SetLayer(ELayer layer, IReadOnlyList elements); + void SetLayer(ELayer layer, IReadOnlyList elements); - void ResetLayer(ELayer layer); + void ResetLayer(ELayer layer); - IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false); + IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false); - void DrawDebugItems(uint trapColor, uint hoardColor); - } + void DrawDebugItems(uint trapColor, uint hoardColor); } diff --git a/Pal.Client/Rendering/MarkerConfig.cs b/Pal.Client/Rendering/MarkerConfig.cs index 830ea4a..60b71e1 100644 --- a/Pal.Client/Rendering/MarkerConfig.cs +++ b/Pal.Client/Rendering/MarkerConfig.cs @@ -1,24 +1,23 @@ using System.Collections.Generic; using Pal.Client.Floors; -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal sealed class MarkerConfig { - internal sealed class MarkerConfig + private static readonly MarkerConfig EmptyConfig = new(); + + private static readonly Dictionary MarkerConfigs = new() { - private static readonly MarkerConfig EmptyConfig = new(); + { MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } }, + { MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, + { MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, + { MemoryLocation.EType.GoldCoffer, new MarkerConfig { Radius = 1f } }, + }; - private static readonly Dictionary MarkerConfigs = new() - { - { MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } }, - { MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } }, - { MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } }, - { MemoryLocation.EType.GoldCoffer, new MarkerConfig { Radius = 1f } }, - }; + public float OffsetY { get; private init; } + public float Radius { get; private init; } = 0.25f; - public float OffsetY { get; private init; } - public float Radius { get; private init; } = 0.25f; - - public static MarkerConfig ForType(MemoryLocation.EType type) => - MarkerConfigs.GetValueOrDefault(type, EmptyConfig); - } + public static MarkerConfig ForType(MemoryLocation.EType type) => + MarkerConfigs.GetValueOrDefault(type, EmptyConfig); } diff --git a/Pal.Client/Rendering/RenderAdapter.cs b/Pal.Client/Rendering/RenderAdapter.cs index 6659f24..09c36bd 100644 --- a/Pal.Client/Rendering/RenderAdapter.cs +++ b/Pal.Client/Rendering/RenderAdapter.cs @@ -6,73 +6,72 @@ using Microsoft.Extensions.Logging; using Pal.Client.Configuration; using Pal.Client.Floors; -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal sealed class RenderAdapter : IRenderer, IDisposable { - internal sealed class RenderAdapter : IRenderer, IDisposable + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private readonly IPalacePalConfiguration _configuration; + + private IServiceScope? _renderScope; + private IRenderer _implementation; + + public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger logger, + IPalacePalConfiguration configuration) { - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly ILogger _logger; - private readonly IPalacePalConfiguration _configuration; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + _configuration = configuration; - private IServiceScope? _renderScope; - private IRenderer _implementation; + _implementation = Recreate(null); + } - public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger logger, - IPalacePalConfiguration configuration) - { - _serviceScopeFactory = serviceScopeFactory; - _logger = logger; - _configuration = configuration; + public bool RequireRedraw { get; set; } - _implementation = Recreate(null); - } + private IRenderer Recreate(ERenderer? currentRenderer) + { + ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer; + if (targetRenderer == currentRenderer) + return _implementation; - public bool RequireRedraw { get; set; } + _renderScope?.Dispose(); - private IRenderer Recreate(ERenderer? currentRenderer) - { - ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer; - if (targetRenderer == currentRenderer) - return _implementation; + _logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer); + _renderScope = _serviceScopeFactory.CreateScope(); + if (targetRenderer == ERenderer.Splatoon) + return _renderScope.ServiceProvider.GetRequiredService(); + else + return _renderScope.ServiceProvider.GetRequiredService(); + } - _renderScope?.Dispose(); + public void ConfigUpdated() + { + _implementation = Recreate(_implementation.GetConfigValue()); + RequireRedraw = true; + } - _logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer); - _renderScope = _serviceScopeFactory.CreateScope(); - if (targetRenderer == ERenderer.Splatoon) - return _renderScope.ServiceProvider.GetRequiredService(); - else - return _renderScope.ServiceProvider.GetRequiredService(); - } + public void Dispose() + => _renderScope?.Dispose(); - public void ConfigUpdated() - { - _implementation = Recreate(_implementation.GetConfigValue()); - RequireRedraw = true; - } + public void SetLayer(ELayer layer, IReadOnlyList elements) + => _implementation.SetLayer(layer, elements); - public void Dispose() - => _renderScope?.Dispose(); + public void ResetLayer(ELayer layer) + => _implementation.ResetLayer(layer); - public void SetLayer(ELayer layer, IReadOnlyList elements) - => _implementation.SetLayer(layer, elements); + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) + => _implementation.CreateElement(type, pos, color, fill); - public void ResetLayer(ELayer layer) - => _implementation.ResetLayer(layer); + public ERenderer GetConfigValue() + => throw new NotImplementedException(); - public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) - => _implementation.CreateElement(type, pos, color, fill); + public void DrawDebugItems(uint trapColor, uint hoardColor) + => _implementation.DrawDebugItems(trapColor, hoardColor); - public ERenderer GetConfigValue() - => throw new NotImplementedException(); - - public void DrawDebugItems(uint trapColor, uint hoardColor) - => _implementation.DrawDebugItems(trapColor, hoardColor); - - public void DrawLayers() - { - if (_implementation is SimpleRenderer sr) - sr.DrawLayers(); - } + public void DrawLayers() + { + if (_implementation is SimpleRenderer sr) + sr.DrawLayers(); } } diff --git a/Pal.Client/Rendering/RenderData.cs b/Pal.Client/Rendering/RenderData.cs index 41c64ed..a8f0e57 100644 --- a/Pal.Client/Rendering/RenderData.cs +++ b/Pal.Client/Rendering/RenderData.cs @@ -1,8 +1,7 @@ -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal static class RenderData { - internal static class RenderData - { - public static readonly uint ColorInvisible = 0; - public static readonly long TestLayerTimeout = 10_000; - } + public static readonly uint ColorInvisible = 0; + public static readonly long TestLayerTimeout = 10_000; } diff --git a/Pal.Client/Rendering/SimpleRenderer.cs b/Pal.Client/Rendering/SimpleRenderer.cs index 6220faf..4d11765 100644 --- a/Pal.Client/Rendering/SimpleRenderer.cs +++ b/Pal.Client/Rendering/SimpleRenderer.cs @@ -11,194 +11,193 @@ using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Floors; -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +/// +/// Simple renderer that only draws basic stuff. +/// +/// This is based on what SliceIsRight uses, and what PalacePal used before it was +/// remade into PalacePal (which is the third or fourth iteration on the same idea +/// I made, just with a clear vision). +/// +internal sealed class SimpleRenderer : IRenderer, IDisposable { - /// - /// Simple renderer that only draws basic stuff. - /// - /// This is based on what SliceIsRight uses, and what PalacePal used before it was - /// remade into PalacePal (which is the third or fourth iteration on the same idea - /// I made, just with a clear vision). - /// - internal sealed class SimpleRenderer : IRenderer, IDisposable + private const int SegmentCount = 20; + + private readonly ClientState _clientState; + private readonly GameGui _gameGui; + private readonly IPalacePalConfiguration _configuration; + private readonly TerritoryState _territoryState; + private readonly ConcurrentDictionary _layers = new(); + + public SimpleRenderer(ClientState clientState, GameGui gameGui, IPalacePalConfiguration configuration, + TerritoryState territoryState) { - private const int SegmentCount = 20; + _clientState = clientState; + _gameGui = gameGui; + _configuration = configuration; + _territoryState = territoryState; + } - private readonly ClientState _clientState; - private readonly GameGui _gameGui; - private readonly IPalacePalConfiguration _configuration; - private readonly TerritoryState _territoryState; - private readonly ConcurrentDictionary _layers = new(); - - public SimpleRenderer(ClientState clientState, GameGui gameGui, IPalacePalConfiguration configuration, - TerritoryState territoryState) + public void SetLayer(ELayer layer, IReadOnlyList elements) + { + _layers[layer] = new SimpleLayer { - _clientState = clientState; - _gameGui = gameGui; - _configuration = configuration; - _territoryState = territoryState; - } + TerritoryType = _clientState.TerritoryType, + Elements = elements.Cast().ToList() + }; + } - public void SetLayer(ELayer layer, IReadOnlyList elements) + public void ResetLayer(ELayer layer) + { + if (_layers.Remove(layer, out var l)) + l.Dispose(); + } + + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) + { + var config = MarkerConfig.ForType(type); + return new SimpleElement { - _layers[layer] = new SimpleLayer + Type = type, + Position = pos + new Vector3(0, config.OffsetY, 0), + Color = color, + Radius = config.Radius, + Fill = fill, + }; + } + + public void DrawDebugItems(uint trapColor, uint hoardColor) + { + _layers[ELayer.Test] = new SimpleLayer + { + TerritoryType = _clientState.TerritoryType, + Elements = new List { - TerritoryType = _clientState.TerritoryType, - Elements = elements.Cast().ToList() - }; - } + (SimpleElement)CreateElement( + MemoryLocation.EType.Trap, + _clientState.LocalPlayer?.Position ?? default, + trapColor), + (SimpleElement)CreateElement( + MemoryLocation.EType.Hoard, + _clientState.LocalPlayer?.Position ?? default, + hoardColor) + }, + ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout + }; + } - public void ResetLayer(ELayer layer) - { - if (_layers.Remove(layer, out var l)) - l.Dispose(); - } + public void DrawLayers() + { + if (_layers.Count == 0) + return; - public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero); + ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); + if (ImGui.Begin("###PalacePalSimpleRender", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.AlwaysUseWindowPadding)) { - var config = MarkerConfig.ForType(type); - return new SimpleElement + foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState))) { - Type = type, - Position = pos + new Vector3(0, config.OffsetY, 0), - Color = color, - Radius = config.Radius, - Fill = fill, - }; - } - - public void DrawDebugItems(uint trapColor, uint hoardColor) - { - _layers[ELayer.Test] = new SimpleLayer - { - TerritoryType = _clientState.TerritoryType, - Elements = new List - { - (SimpleElement)CreateElement( - MemoryLocation.EType.Trap, - _clientState.LocalPlayer?.Position ?? default, - trapColor), - (SimpleElement)CreateElement( - MemoryLocation.EType.Hoard, - _clientState.LocalPlayer?.Position ?? default, - hoardColor) - }, - ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout - }; - } - - public void DrawLayers() - { - if (_layers.Count == 0) - return; - - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero); - ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); - if (ImGui.Begin("###PalacePalSimpleRender", - ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | - ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.AlwaysUseWindowPadding)) - { - foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState))) - { - foreach (var e in layer.Elements) - Draw(e); - } - - foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState)) - .Select(l => l.Key) - .ToList()) - ResetLayer(key); - - ImGui.End(); + foreach (var e in layer.Elements) + Draw(e); } - ImGui.PopStyleVar(); + foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState)) + .Select(l => l.Key) + .ToList()) + ResetLayer(key); + + ImGui.End(); } - private void Draw(SimpleElement e) + ImGui.PopStyleVar(); + } + + private void Draw(SimpleElement e) + { + if (e.Color == RenderData.ColorInvisible) + return; + + switch (e.Type) { - if (e.Color == RenderData.ColorInvisible) - return; - - switch (e.Type) - { - case MemoryLocation.EType.Hoard: - // ignore distance if this is a found hoard coffer - if (_territoryState.PomanderOfIntuition == PomanderState.Active && - _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) - break; - - goto case MemoryLocation.EType.Trap; - - case MemoryLocation.EType.Trap: - var playerPos = _clientState.LocalPlayer?.Position; - if (playerPos == null) - return; - - if ((playerPos.Value - e.Position).Length() > 65) - return; + case MemoryLocation.EType.Hoard: + // ignore distance if this is a found hoard coffer + if (_territoryState.PomanderOfIntuition == PomanderState.Active && + _configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) break; - } - bool onScreen = false; - for (int index = 0; index < 2 * SegmentCount; ++index) - { - onScreen |= _gameGui.WorldToScreen(new Vector3( - e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index), - e.Position.Y, - e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)), - out Vector2 vector2); + goto case MemoryLocation.EType.Trap; - ImGui.GetWindowDrawList().PathLineTo(vector2); - } + case MemoryLocation.EType.Trap: + var playerPos = _clientState.LocalPlayer?.Position; + if (playerPos == null) + return; - if (onScreen) - { - if (e.Fill) - ImGui.GetWindowDrawList().PathFillConvex(e.Color); - else - ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2); - } - else - ImGui.GetWindowDrawList().PathClear(); + if ((playerPos.Value - e.Position).Length() > 65) + return; + break; } - public ERenderer GetConfigValue() - => ERenderer.Simple; + bool onScreen = false; + for (int index = 0; index < 2 * SegmentCount; ++index) + { + onScreen |= _gameGui.WorldToScreen(new Vector3( + e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index), + e.Position.Y, + e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)), + out Vector2 vector2); + + ImGui.GetWindowDrawList().PathLineTo(vector2); + } + + if (onScreen) + { + if (e.Fill) + ImGui.GetWindowDrawList().PathFillConvex(e.Color); + else + ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2); + } + else + ImGui.GetWindowDrawList().PathClear(); + } + + public ERenderer GetConfigValue() + => ERenderer.Simple; + + public void Dispose() + { + foreach (var l in _layers.Values) + l.Dispose(); + } + + public sealed class SimpleLayer : IDisposable + { + public required ushort TerritoryType { get; init; } + public required IReadOnlyList Elements { get; init; } + public long ExpiresAt { get; init; } = long.MaxValue; + + public bool IsValid(ClientState clientState) => + TerritoryType == clientState.TerritoryType && ExpiresAt >= Environment.TickCount64; public void Dispose() { - foreach (var l in _layers.Values) - l.Dispose(); - } - - public sealed class SimpleLayer : IDisposable - { - public required ushort TerritoryType { get; init; } - public required IReadOnlyList Elements { get; init; } - public long ExpiresAt { get; init; } = long.MaxValue; - - public bool IsValid(ClientState clientState) => - TerritoryType == clientState.TerritoryType && ExpiresAt >= Environment.TickCount64; - - public void Dispose() - { - foreach (var e in Elements) - e.IsValid = false; - } - } - - public sealed class SimpleElement : IRenderElement - { - public bool IsValid { get; set; } = true; - public required MemoryLocation.EType Type { get; init; } - public required Vector3 Position { get; init; } - public required uint Color { get; set; } - public required float Radius { get; init; } - public required bool Fill { get; init; } + foreach (var e in Elements) + e.IsValid = false; } } + + public sealed class SimpleElement : IRenderElement + { + public bool IsValid { get; set; } = true; + public required MemoryLocation.EType Type { get; init; } + public required Vector3 Position { get; init; } + public required uint Color { get; set; } + public required float Radius { get; init; } + public required bool Fill { get; init; } + } } diff --git a/Pal.Client/Rendering/SplatoonRenderer.cs b/Pal.Client/Rendering/SplatoonRenderer.cs index 7e2a482..fe708e1 100644 --- a/Pal.Client/Rendering/SplatoonRenderer.cs +++ b/Pal.Client/Rendering/SplatoonRenderer.cs @@ -15,182 +15,181 @@ using Pal.Client.Configuration; using Pal.Client.DependencyInjection; using Pal.Client.Floors; -namespace Pal.Client.Rendering +namespace Pal.Client.Rendering; + +internal sealed class SplatoonRenderer : IRenderer, IDisposable { - internal sealed class SplatoonRenderer : IRenderer, IDisposable + private const long OnTerritoryChange = -2; + + private readonly ILogger _logger; + private readonly DebugState _debugState; + private readonly ClientState _clientState; + private readonly Chat _chat; + + public SplatoonRenderer( + ILogger logger, + DalamudPluginInterface pluginInterface, + IDalamudPlugin dalamudPlugin, + DebugState debugState, + ClientState clientState, + Chat chat) { - private const long OnTerritoryChange = -2; + _logger = logger; + _debugState = debugState; + _clientState = clientState; + _chat = chat; - private readonly ILogger _logger; - private readonly DebugState _debugState; - private readonly ClientState _clientState; - private readonly Chat _chat; + _logger.LogInformation("Initializing splatoon"); + ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); + } - public SplatoonRenderer( - ILogger logger, - DalamudPluginInterface pluginInterface, - IDalamudPlugin dalamudPlugin, - DebugState debugState, - ClientState clientState, - Chat chat) - { - _logger = logger; - _debugState = debugState; - _clientState = clientState; - _chat = chat; + private bool IsDisposed { get; set; } - _logger.LogInformation("Initializing splatoon"); - ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); - } - - private bool IsDisposed { get; set; } - - public void SetLayer(ELayer layer, IReadOnlyList elements) - { - // we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout - _ = new TickScheduler(delegate - { - try - { - Splatoon.AddDynamicElements(ToLayerName(layer), - elements.Cast().Select(x => x.Delegate).ToArray(), - new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); - } - catch (Exception e) - { - _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, - elements.Count); - _debugState.SetFromException(e); - } - }); - } - - public void ResetLayer(ELayer layer) + public void SetLayer(ELayer layer, IReadOnlyList elements) + { + // we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout + _ = new TickScheduler(delegate { try { - Splatoon.RemoveDynamicElements(ToLayerName(layer)); + Splatoon.AddDynamicElements(ToLayerName(layer), + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange }); } catch (Exception e) { - _logger.LogError(e, "Could not reset splatoon layer {Layer}", layer); + _logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, + elements.Count); + _debugState.SetFromException(e); + } + }); + } + + public void ResetLayer(ELayer layer) + { + try + { + Splatoon.RemoveDynamicElements(ToLayerName(layer)); + } + catch (Exception e) + { + _logger.LogError(e, "Could not reset splatoon layer {Layer}", layer); + } + } + + private string ToLayerName(ELayer layer) + => $"PalacePal.{layer}"; + + public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) + { + MarkerConfig config = MarkerConfig.ForType(type); + Element element = new Element(ElementType.CircleAtFixedCoordinates) + { + refX = pos.X, + refY = pos.Z, // z and y are swapped + refZ = pos.Y, + offX = 0, + offY = 0, + offZ = config.OffsetY, + Filled = fill, + radius = config.Radius, + FillStep = 1, + color = color, + thicc = 2, + }; + return new SplatoonElement(this, element); + } + + public void DrawDebugItems(uint trapColor, uint hoardColor) + { + try + { + Vector3? pos = _clientState.LocalPlayer?.Position; + if (pos != null) + { + ResetLayer(ELayer.Test); + + var elements = new List + { + CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor), + CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor), + }; + + if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), + elements.Cast().Select(x => x.Delegate).ToArray(), + new[] { Environment.TickCount64 + RenderData.TestLayerTimeout })) + { + _chat.Message("Could not draw markers :("); + } } } - - private string ToLayerName(ELayer layer) - => $"PalacePal.{layer}"; - - public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false) - { - MarkerConfig config = MarkerConfig.ForType(type); - Element element = new Element(ElementType.CircleAtFixedCoordinates) - { - refX = pos.X, - refY = pos.Z, // z and y are swapped - refZ = pos.Y, - offX = 0, - offY = 0, - offZ = config.OffsetY, - Filled = fill, - radius = config.Radius, - FillStep = 1, - color = color, - thicc = 2, - }; - return new SplatoonElement(this, element); - } - - public void DrawDebugItems(uint trapColor, uint hoardColor) + catch (Exception) { try { - Vector3? pos = _clientState.LocalPlayer?.Position; - if (pos != null) + var pluginManager = DalamudReflector.GetPluginManager(); + IList installedPlugins = + pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ?? + new List(); + + foreach (var t in installedPlugins) { - ResetLayer(ELayer.Test); - - var elements = new List + AssemblyName? assemblyName = + (AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t); + string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); + if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") { - CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor), - CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor), - }; - - if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), - elements.Cast().Select(x => x.Delegate).ToArray(), - new[] { Environment.TickCount64 + RenderData.TestLayerTimeout })) - { - _chat.Message("Could not draw markers :("); + _chat.Error( + $"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); + _chat.Message( + "You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); + return; } } } catch (Exception) { - try - { - var pluginManager = DalamudReflector.GetPluginManager(); - IList installedPlugins = - pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ?? - new List(); - - foreach (var t in installedPlugins) - { - AssemblyName? assemblyName = - (AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t); - string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); - if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon") - { - _chat.Error( - $"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); - _chat.Message( - "You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins."); - return; - } - } - } - catch (Exception) - { - // not relevant - } - - _chat.Error("Could not draw markers, is Splatoon installed and enabled?"); + // not relevant } + + _chat.Error("Could not draw markers, is Splatoon installed and enabled?"); + } + } + + public ERenderer GetConfigValue() + => ERenderer.Splatoon; + + public void Dispose() + { + _logger.LogInformation("Disposing splatoon"); + + IsDisposed = true; + + ResetLayer(ELayer.TrapHoard); + ResetLayer(ELayer.RegularCoffers); + ResetLayer(ELayer.Test); + + ECommonsMain.Dispose(); + } + + private sealed class SplatoonElement : IRenderElement + { + private readonly SplatoonRenderer _renderer; + + public SplatoonElement(SplatoonRenderer renderer, Element element) + { + _renderer = renderer; + Delegate = element; } - public ERenderer GetConfigValue() - => ERenderer.Splatoon; + public Element Delegate { get; } - public void Dispose() + public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid(); + + public uint Color { - _logger.LogInformation("Disposing splatoon"); - - IsDisposed = true; - - ResetLayer(ELayer.TrapHoard); - ResetLayer(ELayer.RegularCoffers); - ResetLayer(ELayer.Test); - - ECommonsMain.Dispose(); - } - - private sealed class SplatoonElement : IRenderElement - { - private readonly SplatoonRenderer _renderer; - - public SplatoonElement(SplatoonRenderer renderer, Element element) - { - _renderer = renderer; - Delegate = element; - } - - public Element Delegate { get; } - - public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid(); - - public uint Color - { - get => Delegate.color; - set => Delegate.color = value; - } + get => Delegate.color; + set => Delegate.color = value; } } } diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index 5397be3..5bdcc33 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -2,38 +2,37 @@ using Dalamud.Logging; using Microsoft.Extensions.Logging; -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled; + +internal interface IQueueOnFrameworkThread { - internal interface IQueueOnFrameworkThread + internal interface IHandler { - internal interface IHandler + void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout); + } + + internal abstract class Handler : IHandler + where T : IQueueOnFrameworkThread + { + protected readonly ILogger> _logger; + + protected Handler(ILogger> logger) { - void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout); + _logger = logger; } - internal abstract class Handler : IHandler - where T : IQueueOnFrameworkThread + protected abstract void Run(T queued, ref bool recreateLayout); + + public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout) { - protected readonly ILogger> _logger; - - protected Handler(ILogger> logger) + if (queued is T t) { - _logger = logger; + _logger.LogDebug("Handling {QueuedType}", queued.GetType()); + Run(t, ref recreateLayout); } - - protected abstract void Run(T queued, ref bool recreateLayout); - - public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout) + else { - if (queued is T t) - { - _logger.LogDebug("Handling {QueuedType}", queued.GetType()); - Run(t, ref recreateLayout); - } - else - { - _logger.LogError("Could not use queue handler {QueuedType}", queued.GetType()); - } + _logger.LogError("Could not use queue handler {QueuedType}", queued.GetType()); } } } diff --git a/Pal.Client/Scheduled/QueuedConfigUpdate.cs b/Pal.Client/Scheduled/QueuedConfigUpdate.cs index 485120c..2a91f8e 100644 --- a/Pal.Client/Scheduled/QueuedConfigUpdate.cs +++ b/Pal.Client/Scheduled/QueuedConfigUpdate.cs @@ -4,26 +4,25 @@ using Pal.Client.DependencyInjection; using Pal.Client.Floors; using Pal.Client.Rendering; -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled; + +internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread { - internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread + internal sealed class Handler : IQueueOnFrameworkThread.Handler { - internal sealed class Handler : IQueueOnFrameworkThread.Handler + private readonly RenderAdapter _renderAdapter; + + public Handler( + ILogger logger, + RenderAdapter renderAdapter) + : base(logger) { - private readonly RenderAdapter _renderAdapter; + _renderAdapter = renderAdapter; + } - public Handler( - ILogger logger, - RenderAdapter renderAdapter) - : base(logger) - { - _renderAdapter = renderAdapter; - } - - protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout) - { - _renderAdapter.ConfigUpdated(); - } + protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout) + { + _renderAdapter.ConfigUpdated(); } } } diff --git a/Pal.Client/Scheduled/QueuedImport.cs b/Pal.Client/Scheduled/QueuedImport.cs index 7799a53..3f2bdbc 100644 --- a/Pal.Client/Scheduled/QueuedImport.cs +++ b/Pal.Client/Scheduled/QueuedImport.cs @@ -10,114 +10,113 @@ using Pal.Client.Properties; using Pal.Client.Windows; using Pal.Common; -namespace Pal.Client.Scheduled -{ - internal sealed class QueuedImport : IQueueOnFrameworkThread - { - private ExportRoot Export { get; } - private Guid ExportId { get; set; } - private int ImportedTraps { get; set; } - private int ImportedHoardCoffers { get; set; } +namespace Pal.Client.Scheduled; - public QueuedImport(string sourcePath) +internal sealed class QueuedImport : IQueueOnFrameworkThread +{ + private ExportRoot Export { get; } + private Guid ExportId { get; set; } + private int ImportedTraps { get; set; } + private int ImportedHoardCoffers { get; set; } + + public QueuedImport(string sourcePath) + { + using var input = File.OpenRead(sourcePath); + Export = ExportRoot.Parser.ParseFrom(input); + } + + internal sealed class Handler : IQueueOnFrameworkThread.Handler + { + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly Chat _chat; + private readonly ImportService _importService; + private readonly ConfigWindow _configWindow; + + public Handler( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + Chat chat, + ImportService importService, + ConfigWindow configWindow) + : base(logger) { - using var input = File.OpenRead(sourcePath); - Export = ExportRoot.Parser.ParseFrom(input); + _serviceScopeFactory = serviceScopeFactory; + _chat = chat; + _importService = importService; + _configWindow = configWindow; } - internal sealed class Handler : IQueueOnFrameworkThread.Handler + protected override void Run(QueuedImport import, ref bool recreateLayout) { - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly Chat _chat; - private readonly ImportService _importService; - private readonly ConfigWindow _configWindow; + recreateLayout = true; - public Handler( - ILogger logger, - IServiceScopeFactory serviceScopeFactory, - Chat chat, - ImportService importService, - ConfigWindow configWindow) - : base(logger) + try { - _serviceScopeFactory = serviceScopeFactory; - _chat = chat; - _importService = importService; - _configWindow = configWindow; - } + if (!Validate(import)) + return; - protected override void Run(QueuedImport import, ref bool recreateLayout) - { - recreateLayout = true; - - try + Task.Run(() => { - if (!Validate(import)) - return; - - Task.Run(() => + try { - try + using (var scope = _serviceScopeFactory.CreateScope()) { - using (var scope = _serviceScopeFactory.CreateScope()) - { - using var dbContext = scope.ServiceProvider.GetRequiredService(); - (import.ImportedTraps, import.ImportedHoardCoffers) = - _importService.Import(import.Export); - } - - _configWindow.UpdateLastImport(); - - _logger.LogInformation( - "Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId, - import.ImportedTraps, import.ImportedHoardCoffers); - _chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, - import.ImportedHoardCoffers)); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + (import.ImportedTraps, import.ImportedHoardCoffers) = + _importService.Import(import.Export); } - catch (Exception e) - { - _logger.LogError(e, "Import failed in inner task"); - _chat.Error(string.Format(Localization.Error_ImportFailed, e)); - } - }); - } - catch (Exception e) - { - _logger.LogError(e, "Import failed"); - _chat.Error(string.Format(Localization.Error_ImportFailed, e)); - } + + _configWindow.UpdateLastImport(); + + _logger.LogInformation( + "Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId, + import.ImportedTraps, import.ImportedHoardCoffers); + _chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, + import.ImportedHoardCoffers)); + } + catch (Exception e) + { + _logger.LogError(e, "Import failed in inner task"); + _chat.Error(string.Format(Localization.Error_ImportFailed, e)); + } + }); } - - private bool Validate(QueuedImport import) + catch (Exception e) { - if (import.Export.ExportVersion != ExportConfig.ExportVersion) - { - _logger.LogError( - "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", - import.Export.ExportVersion, ExportConfig.ExportVersion); - _chat.Error(Localization.Error_ImportFailed_IncompatibleVersion); - return false; - } - - if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty) - { - _logger.LogError("Import: Invalid export id '{Id}'", import.Export.ExportId); - _chat.Error(Localization.Error_ImportFailed_InvalidFile); - return false; - } - - import.ExportId = exportId; - - if (string.IsNullOrEmpty(import.Export.ServerUrl)) - { - // If we allow for backups as import/export, this should be removed - _logger.LogError("Import: No server URL"); - _chat.Error(Localization.Error_ImportFailed_InvalidFile); - return false; - } - - return true; + _logger.LogError(e, "Import failed"); + _chat.Error(string.Format(Localization.Error_ImportFailed, e)); } } + + private bool Validate(QueuedImport import) + { + if (import.Export.ExportVersion != ExportConfig.ExportVersion) + { + _logger.LogError( + "Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}", + import.Export.ExportVersion, ExportConfig.ExportVersion); + _chat.Error(Localization.Error_ImportFailed_IncompatibleVersion); + return false; + } + + if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty) + { + _logger.LogError("Import: Invalid export id '{Id}'", import.Export.ExportId); + _chat.Error(Localization.Error_ImportFailed_InvalidFile); + return false; + } + + import.ExportId = exportId; + + if (string.IsNullOrEmpty(import.Export.ServerUrl)) + { + // If we allow for backups as import/export, this should be removed + _logger.LogError("Import: No server URL"); + _chat.Error(Localization.Error_ImportFailed_InvalidFile); + return false; + } + + return true; + } } } diff --git a/Pal.Client/Scheduled/QueuedSyncResponse.cs b/Pal.Client/Scheduled/QueuedSyncResponse.cs index 519f5f1..efb254e 100644 --- a/Pal.Client/Scheduled/QueuedSyncResponse.cs +++ b/Pal.Client/Scheduled/QueuedSyncResponse.cs @@ -11,151 +11,150 @@ using Pal.Client.Floors.Tasks; using Pal.Client.Net; using Pal.Common; -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled; + +internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread { - internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread + public required SyncType Type { get; init; } + public required ushort TerritoryType { get; init; } + public required bool Success { get; init; } + public required IReadOnlyList Locations { get; init; } + + internal sealed class Handler : IQueueOnFrameworkThread.Handler { - public required SyncType Type { get; init; } - public required ushort TerritoryType { get; init; } - public required bool Success { get; init; } - public required IReadOnlyList Locations { get; init; } + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IPalacePalConfiguration _configuration; + private readonly FloorService _floorService; + private readonly TerritoryState _territoryState; + private readonly DebugState _debugState; - internal sealed class Handler : IQueueOnFrameworkThread.Handler + public Handler( + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + IPalacePalConfiguration configuration, + FloorService floorService, + TerritoryState territoryState, + DebugState debugState) + : base(logger) { - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly IPalacePalConfiguration _configuration; - private readonly FloorService _floorService; - private readonly TerritoryState _territoryState; - private readonly DebugState _debugState; + _serviceScopeFactory = serviceScopeFactory; + _configuration = configuration; + _floorService = floorService; + _territoryState = territoryState; + _debugState = debugState; + } - public Handler( - ILogger logger, - IServiceScopeFactory serviceScopeFactory, - IPalacePalConfiguration configuration, - FloorService floorService, - TerritoryState territoryState, - DebugState debugState) - : base(logger) + protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout) + { + 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) { - _serviceScopeFactory = serviceScopeFactory; - _configuration = configuration; - _floorService = floorService; - _territoryState = territoryState; - _debugState = debugState; + _logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready", + (ETerritoryType)queued.TerritoryType); + return; } - protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout) + try { - 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) + var remoteMarkers = queued.Locations; + if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) { - _logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready", - (ETerritoryType)queued.TerritoryType); + switch (queued.Type) + { + case SyncType.Download: + case SyncType.Upload: + List newLocations = new(); + 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. + PersistentLocation? localLocation = + memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); + if (localLocation != null) + { + localLocation.NetworkId = remoteMarker.NetworkId; + continue; + } + + if (queued.Type == SyncType.Download) + { + memoryTerritory.Locations.Add(remoteMarker); + newLocations.Add(remoteMarker); + } + } + + if (newLocations.Count > 0) + new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start(); + + break; + + case SyncType.MarkSeen: + var partialAccountId = + _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); + if (partialAccountId == null) + break; + + List locationsToUpdate = new(); + foreach (var remoteMarker in remoteMarkers) + { + PersistentLocation? localLocation = + memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); + if (localLocation != null) + { + localLocation.RemoteSeenOn.Add(partialAccountId); + locationsToUpdate.Add(localLocation); + } + } + + if (locationsToUpdate.Count > 0) + { + new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate, + partialAccountId).Start(); + } + + break; + } + } + + // don't modify state for outdated floors + if (_territoryState.LastTerritory != queued.TerritoryType) return; - } - try + if (queued.Type == SyncType.Download) { - var remoteMarkers = queued.Locations; - if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0) - { - switch (queued.Type) - { - case SyncType.Download: - case SyncType.Upload: - List newLocations = new(); - 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. - PersistentLocation? localLocation = - memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); - if (localLocation != null) - { - localLocation.NetworkId = remoteMarker.NetworkId; - continue; - } - - if (queued.Type == SyncType.Download) - { - memoryTerritory.Locations.Add(remoteMarker); - newLocations.Add(remoteMarker); - } - } - - if (newLocations.Count > 0) - new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start(); - - break; - - case SyncType.MarkSeen: - var partialAccountId = - _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId(); - if (partialAccountId == null) - break; - - List locationsToUpdate = new(); - foreach (var remoteMarker in remoteMarkers) - { - PersistentLocation? localLocation = - memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker); - if (localLocation != null) - { - localLocation.RemoteSeenOn.Add(partialAccountId); - locationsToUpdate.Add(localLocation); - } - } - - if (locationsToUpdate.Count > 0) - { - new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate, - partialAccountId).Start(); - } - - break; - } - } - - // don't modify state for outdated floors - if (_territoryState.LastTerritory != queued.TerritoryType) - return; - - if (queued.Type == SyncType.Download) - { - if (queued.Success) - memoryTerritory.SyncState = ESyncState.Complete; - else - 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) + if (queued.Success) + memoryTerritory.SyncState = ESyncState.Complete; + else 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) + memoryTerritory.SyncState = ESyncState.Failed; + } } } - - public enum ESyncState - { - NotAttempted, - NotNeeded, - Started, - Complete, - Failed, - } - - public enum SyncType - { - Upload, - Download, - MarkSeen, - } +} + +public enum ESyncState +{ + NotAttempted, + NotNeeded, + Started, + Complete, + Failed, +} + +public enum SyncType +{ + Upload, + Download, + MarkSeen, } diff --git a/Pal.Client/Scheduled/QueuedUndoImport.cs b/Pal.Client/Scheduled/QueuedUndoImport.cs index f5b0d3c..e9acdc4 100644 --- a/Pal.Client/Scheduled/QueuedUndoImport.cs +++ b/Pal.Client/Scheduled/QueuedUndoImport.cs @@ -7,36 +7,35 @@ using Pal.Client.Floors; using Pal.Client.Windows; using Pal.Common; -namespace Pal.Client.Scheduled +namespace Pal.Client.Scheduled; + +internal sealed class QueuedUndoImport : IQueueOnFrameworkThread { - internal sealed class QueuedUndoImport : IQueueOnFrameworkThread + public QueuedUndoImport(Guid exportId) { - public QueuedUndoImport(Guid exportId) + ExportId = exportId; + } + + private Guid ExportId { get; } + + internal sealed class Handler : IQueueOnFrameworkThread.Handler + { + private readonly ImportService _importService; + private readonly ConfigWindow _configWindow; + + public Handler(ILogger logger, ImportService importService, ConfigWindow configWindow) + : base(logger) { - ExportId = exportId; + _importService = importService; + _configWindow = configWindow; } - private Guid ExportId { get; } - - internal sealed class Handler : IQueueOnFrameworkThread.Handler + protected override void Run(QueuedUndoImport queued, ref bool recreateLayout) { - private readonly ImportService _importService; - private readonly ConfigWindow _configWindow; + recreateLayout = true; - public Handler(ILogger logger, ImportService importService, ConfigWindow configWindow) - : base(logger) - { - _importService = importService; - _configWindow = configWindow; - } - - protected override void Run(QueuedUndoImport queued, ref bool recreateLayout) - { - recreateLayout = true; - - _importService.RemoveById(queued.ExportId); - _configWindow.UpdateLastImport(); - } + _importService.RemoveById(queued.ExportId); + _configWindow.UpdateLastImport(); } } } diff --git a/Pal.Client/Windows/AgreementWindow.cs b/Pal.Client/Windows/AgreementWindow.cs index b791076..41bb2d7 100644 --- a/Pal.Client/Windows/AgreementWindow.cs +++ b/Pal.Client/Windows/AgreementWindow.cs @@ -8,98 +8,97 @@ using Pal.Client.Configuration; using Pal.Client.Extensions; using Pal.Client.Properties; -namespace Pal.Client.Windows +namespace Pal.Client.Windows; + +internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged { - internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged + private const string WindowId = "###PalPalaceAgreement"; + private readonly WindowSystem _windowSystem; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private int _choice; + + public AgreementWindow( + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration) + : base(WindowId) { - private const string WindowId = "###PalPalaceAgreement"; - private readonly WindowSystem _windowSystem; - private readonly ConfigurationManager _configurationManager; - private readonly IPalacePalConfiguration _configuration; - private int _choice; + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; - public AgreementWindow( - WindowSystem windowSystem, - ConfigurationManager configurationManager, - IPalacePalConfiguration configuration) - : base(WindowId) + LanguageChanged(); + + Flags = ImGuiWindowFlags.NoCollapse; + Size = new Vector2(500, 500); + SizeCondition = ImGuiCond.FirstUseEver; + PositionCondition = ImGuiCond.FirstUseEver; + Position = new Vector2(310, 310); + + SizeConstraints = new WindowSizeConstraints { - _windowSystem = windowSystem; - _configurationManager = configurationManager; - _configuration = configuration; + MinimumSize = new Vector2(500, 500), + MaximumSize = new Vector2(2000, 2000), + }; - LanguageChanged(); + IsOpen = configuration.FirstUse; + _windowSystem.AddWindow(this); + } - Flags = ImGuiWindowFlags.NoCollapse; - Size = new Vector2(500, 500); - SizeCondition = ImGuiCond.FirstUseEver; - PositionCondition = ImGuiCond.FirstUseEver; - Position = new Vector2(310, 310); + public void Dispose() + => _windowSystem.RemoveWindow(this); - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(500, 500), - MaximumSize = new Vector2(2000, 2000), - }; + public void LanguageChanged() + => WindowName = $"{Localization.Palace_Pal}{WindowId}"; - IsOpen = configuration.FirstUse; - _windowSystem.AddWindow(this); + public override void OnOpen() + { + _choice = -1; + } + + public override void Draw() + { + ImGui.TextWrapped(Localization.Explanation_1); + ImGui.TextWrapped(Localization.Explanation_2); + + ImGui.Spacing(); + + ImGui.TextWrapped(Localization.Explanation_3); + ImGui.TextWrapped(Localization.Explanation_4); + + PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, + (int)EMode.Online); + PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, + (int)EMode.Offline); + + ImGui.Separator(); + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextWrapped(Localization.Agreement_Warning1); + ImGui.TextWrapped(Localization.Agreement_Warning2); + ImGui.TextWrapped(Localization.Agreement_Warning3); + ImGui.PopStyleColor(); + + ImGui.Separator(); + + if (_choice == -1) + ImGui.TextDisabled(Localization.Agreement_PickOneOption); + ImGui.BeginDisabled(_choice == -1); + if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk)) + { + _configuration.Mode = (EMode)_choice; + _configuration.FirstUse = false; + _configurationManager.Save(_configuration); + + IsOpen = false; } - public void Dispose() - => _windowSystem.RemoveWindow(this); + ImGui.EndDisabled(); - public void LanguageChanged() - => WindowName = $"{Localization.Palace_Pal}{WindowId}"; + ImGui.Separator(); - public override void OnOpen() - { - _choice = -1; - } - - public override void Draw() - { - ImGui.TextWrapped(Localization.Explanation_1); - ImGui.TextWrapped(Localization.Explanation_2); - - ImGui.Spacing(); - - ImGui.TextWrapped(Localization.Explanation_3); - ImGui.TextWrapped(Localization.Explanation_4); - - PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice, - (int)EMode.Online); - PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice, - (int)EMode.Offline); - - ImGui.Separator(); - - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.TextWrapped(Localization.Agreement_Warning1); - ImGui.TextWrapped(Localization.Agreement_Warning2); - ImGui.TextWrapped(Localization.Agreement_Warning3); - ImGui.PopStyleColor(); - - ImGui.Separator(); - - if (_choice == -1) - ImGui.TextDisabled(Localization.Agreement_PickOneOption); - ImGui.BeginDisabled(_choice == -1); - if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk)) - { - _configuration.Mode = (EMode)_choice; - _configuration.FirstUse = false; - _configurationManager.Save(_configuration); - - IsOpen = false; - } - - ImGui.EndDisabled(); - - ImGui.Separator(); - - if (ImGui.Button(Localization.Agreement_ViewPluginAndServerSourceCode)) - GenericHelpers.ShellStart("https://github.com/carvelli/PalPalace"); - } + if (ImGui.Button(Localization.Agreement_ViewPluginAndServerSourceCode)) + GenericHelpers.ShellStart("https://github.com/carvelli/PalPalace"); } } diff --git a/Pal.Client/Windows/ConfigWindow.cs b/Pal.Client/Windows/ConfigWindow.cs index d7d99ea..850f64b 100644 --- a/Pal.Client/Windows/ConfigWindow.cs +++ b/Pal.Client/Windows/ConfigWindow.cs @@ -23,557 +23,556 @@ using Pal.Client.Properties; using Pal.Client.Rendering; using Pal.Client.Scheduled; -namespace Pal.Client.Windows +namespace Pal.Client.Windows; + +internal sealed class ConfigWindow : Window, ILanguageChanged, IDisposable { - internal sealed class ConfigWindow : Window, ILanguageChanged, IDisposable + private const string WindowId = "###PalPalaceConfig"; + + private readonly ILogger _logger; + private readonly WindowSystem _windowSystem; + private readonly ConfigurationManager _configurationManager; + private readonly IPalacePalConfiguration _configuration; + private readonly RenderAdapter _renderAdapter; + private readonly TerritoryState _territoryState; + private readonly FrameworkService _frameworkService; + private readonly FloorService _floorService; + private readonly DebugState _debugState; + private readonly Chat _chat; + private readonly RemoteApi _remoteApi; + private readonly ImportService _importService; + + private int _mode; + private int _renderer; + private ConfigurableMarker _trapConfig = new(); + private ConfigurableMarker _hoardConfig = new(); + private ConfigurableMarker _silverConfig = new(); + private ConfigurableMarker _goldConfig = new(); + + private string? _connectionText; + private bool _switchToCommunityTab; + private string _openImportPath = string.Empty; + private string _saveExportPath = string.Empty; + private string? _openImportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + private readonly FileDialogManager _importDialog; + private readonly FileDialogManager _exportDialog; + private ImportHistory? _lastImport; + + private CancellationTokenSource? _testConnectionCts; + private CancellationTokenSource? _lastImportCts; + + public ConfigWindow( + ILogger logger, + WindowSystem windowSystem, + ConfigurationManager configurationManager, + IPalacePalConfiguration configuration, + RenderAdapter renderAdapter, + TerritoryState territoryState, + FrameworkService frameworkService, + FloorService floorService, + DebugState debugState, + Chat chat, + RemoteApi remoteApi, + ImportService importService) + : base(WindowId) { - private const string WindowId = "###PalPalaceConfig"; + _logger = logger; + _windowSystem = windowSystem; + _configurationManager = configurationManager; + _configuration = configuration; + _renderAdapter = renderAdapter; + _territoryState = territoryState; + _frameworkService = frameworkService; + _floorService = floorService; + _debugState = debugState; + _chat = chat; + _remoteApi = remoteApi; + _importService = importService; - private readonly ILogger _logger; - private readonly WindowSystem _windowSystem; - private readonly ConfigurationManager _configurationManager; - private readonly IPalacePalConfiguration _configuration; - private readonly RenderAdapter _renderAdapter; - private readonly TerritoryState _territoryState; - private readonly FrameworkService _frameworkService; - private readonly FloorService _floorService; - private readonly DebugState _debugState; - private readonly Chat _chat; - private readonly RemoteApi _remoteApi; - private readonly ImportService _importService; + LanguageChanged(); - private int _mode; - private int _renderer; - private ConfigurableMarker _trapConfig = new(); - private ConfigurableMarker _hoardConfig = new(); - private ConfigurableMarker _silverConfig = new(); - private ConfigurableMarker _goldConfig = new(); + Size = new Vector2(500, 400); + SizeCondition = ImGuiCond.FirstUseEver; + Position = new Vector2(300, 300); + PositionCondition = ImGuiCond.FirstUseEver; - private string? _connectionText; - private bool _switchToCommunityTab; - private string _openImportPath = string.Empty; - private string _saveExportPath = string.Empty; - private string? _openImportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - private readonly FileDialogManager _importDialog; - private readonly FileDialogManager _exportDialog; - private ImportHistory? _lastImport; + _importDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; + _exportDialog = new FileDialogManager + { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - private CancellationTokenSource? _testConnectionCts; - private CancellationTokenSource? _lastImportCts; + _windowSystem.AddWindow(this); + } - public ConfigWindow( - ILogger logger, - WindowSystem windowSystem, - ConfigurationManager configurationManager, - IPalacePalConfiguration configuration, - RenderAdapter renderAdapter, - TerritoryState territoryState, - FrameworkService frameworkService, - FloorService floorService, - DebugState debugState, - Chat chat, - RemoteApi remoteApi, - ImportService importService) - : base(WindowId) + public void Dispose() + { + _windowSystem.RemoveWindow(this); + _lastImportCts?.Cancel(); + _testConnectionCts?.Cancel(); + } + + public void LanguageChanged() + { + var version = typeof(Plugin).Assembly.GetName().Version!.ToString(2); + WindowName = $"{Localization.Palace_Pal} v{version}{WindowId}"; + } + + public override void OnOpen() + { + _mode = (int)_configuration.Mode; + _renderer = (int)_configuration.Renderer.SelectedRenderer; + _trapConfig = new ConfigurableMarker(_configuration.DeepDungeons.Traps); + _hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers); + _silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers); + _goldConfig = new ConfigurableMarker(_configuration.DeepDungeons.GoldCoffers); + _connectionText = null; + + UpdateLastImport(); + } + + public override void OnClose() + { + _importDialog.Reset(); + _exportDialog.Reset(); + _testConnectionCts?.Cancel(); + _testConnectionCts = null; + } + + public override void Draw() + { + bool save = false; + bool saveAndClose = false; + if (ImGui.BeginTabBar("PalTabs")) { - _logger = logger; - _windowSystem = windowSystem; - _configurationManager = configurationManager; - _configuration = configuration; - _renderAdapter = renderAdapter; - _territoryState = territoryState; - _frameworkService = frameworkService; - _floorService = floorService; - _debugState = debugState; - _chat = chat; - _remoteApi = remoteApi; - _importService = importService; + DrawDeepDungeonItemsTab(ref save, ref saveAndClose); + DrawCommunityTab(ref saveAndClose); + DrawImportTab(); + DrawExportTab(); + DrawRenderTab(ref save, ref saveAndClose); + DrawDebugTab(); - LanguageChanged(); - - Size = new Vector2(500, 400); - SizeCondition = ImGuiCond.FirstUseEver; - Position = new Vector2(300, 300); - PositionCondition = ImGuiCond.FirstUseEver; - - _importDialog = new FileDialogManager - { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - _exportDialog = new FileDialogManager - { AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking }; - - _windowSystem.AddWindow(this); + ImGui.EndTabBar(); } - public void Dispose() + _importDialog.Draw(); + + if (save || saveAndClose) { - _windowSystem.RemoveWindow(this); - _lastImportCts?.Cancel(); - _testConnectionCts?.Cancel(); + _configuration.Mode = (EMode)_mode; + _configuration.Renderer.SelectedRenderer = (ERenderer)_renderer; + _configuration.DeepDungeons.Traps = _trapConfig.Build(); + _configuration.DeepDungeons.HoardCoffers = _hoardConfig.Build(); + _configuration.DeepDungeons.SilverCoffers = _silverConfig.Build(); + _configuration.DeepDungeons.GoldCoffers = _goldConfig.Build(); + + _configurationManager.Save(_configuration); + + if (saveAndClose) + IsOpen = false; } + } - public void LanguageChanged() + private void DrawDeepDungeonItemsTab(ref bool save, ref bool saveAndClose) + { + if (ImGui.BeginTabItem($"{Localization.ConfigTab_DeepDungeons}###TabDeepDungeons")) { - var version = typeof(Plugin).Assembly.GetName().Version!.ToString(2); - WindowName = $"{Localization.Palace_Pal} v{version}{WindowId}"; + ImGui.PushID("trap"); + ImGui.Checkbox(Localization.Config_Traps_Show, ref _trapConfig.Show); + ImGui.Indent(); + ImGui.BeginDisabled(!_trapConfig.Show); + ImGui.Spacing(); + ImGui.ColorEdit4(Localization.Config_Traps_Color, ref _trapConfig.Color, ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_Traps_HideImpossible, ref _trapConfig.OnlyVisibleAfterPomander); + ImGui.SameLine(); + ImGuiComponents.HelpMarker(Localization.Config_Traps_HideImpossible_ToolTip); + ImGui.EndDisabled(); + ImGui.Unindent(); + ImGui.PopID(); + + ImGui.Separator(); + + ImGui.PushID("hoard"); + ImGui.Checkbox(Localization.Config_HoardCoffers_Show, ref _hoardConfig.Show); + ImGui.Indent(); + ImGui.BeginDisabled(!_hoardConfig.Show); + ImGui.Spacing(); + ImGui.ColorEdit4(Localization.Config_HoardCoffers_Color, ref _hoardConfig.Color, + ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_HoardCoffers_HideImpossible, + ref _hoardConfig.OnlyVisibleAfterPomander); + ImGui.SameLine(); + ImGuiComponents.HelpMarker(Localization.Config_HoardCoffers_HideImpossible_ToolTip); + ImGui.EndDisabled(); + ImGui.Unindent(); + ImGui.PopID(); + + ImGui.Separator(); + + ImGui.PushID("silver"); + ImGui.Checkbox(Localization.Config_SilverCoffer_Show, ref _silverConfig.Show); + ImGuiComponents.HelpMarker(Localization.Config_SilverCoffers_ToolTip); + ImGui.Indent(); + ImGui.BeginDisabled(!_silverConfig.Show); + ImGui.Spacing(); + ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, + ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _silverConfig.Fill); + ImGui.EndDisabled(); + ImGui.Unindent(); + ImGui.PopID(); + + ImGui.Separator(); + + ImGui.PushID("gold"); + ImGui.Checkbox(Localization.Config_GoldCoffer_Show, ref _goldConfig.Show); + ImGuiComponents.HelpMarker(Localization.Config_GoldCoffers_ToolTip); + ImGui.Indent(); + ImGui.BeginDisabled(!_goldConfig.Show); + ImGui.Spacing(); + ImGui.ColorEdit4(Localization.Config_GoldCoffer_Color, ref _goldConfig.Color, + ImGuiColorEditFlags.NoInputs); + ImGui.Checkbox(Localization.Config_GoldCoffer_Filled, ref _goldConfig.Fill); + ImGui.EndDisabled(); + ImGui.Unindent(); + ImGui.PopID(); + + ImGui.Separator(); + + save = ImGui.Button(Localization.Save); + ImGui.SameLine(); + saveAndClose = ImGui.Button(Localization.SaveAndClose); + + ImGui.EndTabItem(); } + } - public override void OnOpen() + private void DrawCommunityTab(ref bool saveAndClose) + { + if (PalImGui.BeginTabItemWithFlags($"{Localization.ConfigTab_Community}###TabCommunity", + _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) { - _mode = (int)_configuration.Mode; - _renderer = (int)_configuration.Renderer.SelectedRenderer; - _trapConfig = new ConfigurableMarker(_configuration.DeepDungeons.Traps); - _hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers); - _silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers); - _goldConfig = new ConfigurableMarker(_configuration.DeepDungeons.GoldCoffers); - _connectionText = null; + _switchToCommunityTab = false; - UpdateLastImport(); + ImGui.TextWrapped(Localization.Explanation_3); + ImGui.TextWrapped(Localization.Explanation_4); + + PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, + (int)EMode.Online); + PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, + (int)EMode.Offline); + saveAndClose = ImGui.Button(Localization.SaveAndClose); + + ImGui.Separator(); + + ImGui.BeginDisabled(_configuration.Mode != EMode.Online); + if (ImGui.Button(Localization.Config_TestConnection)) + TestConnection(); + + if (_connectionText != null) + ImGui.Text(_connectionText); + + ImGui.EndDisabled(); + ImGui.EndTabItem(); } + } - public override void OnClose() + private void DrawImportTab() + { + if (ImGui.BeginTabItem($"{Localization.ConfigTab_Import}###TabImport")) { - _importDialog.Reset(); - _exportDialog.Reset(); - _testConnectionCts?.Cancel(); - _testConnectionCts = null; - } - - public override void Draw() - { - bool save = false; - bool saveAndClose = false; - if (ImGui.BeginTabBar("PalTabs")) + ImGui.TextWrapped(Localization.Config_ImportExplanation1); + ImGui.TextWrapped(Localization.Config_ImportExplanation2); + ImGui.TextWrapped(Localization.Config_ImportExplanation3); + ImGui.Separator(); + ImGui.TextWrapped(string.Format(Localization.Config_ImportDownloadLocation, + "https://github.com/carvelli/PalacePal/releases/")); + if (ImGui.Button(Localization.Config_Import_VisitGitHub)) + GenericHelpers.ShellStart("https://github.com/carvelli/PalacePal/releases/latest"); + ImGui.Separator(); + ImGui.Text(Localization.Config_SelectImportFile); + ImGui.SameLine(); + ImGui.InputTextWithHint("", Localization.Config_SelectImportFile_Hint, ref _openImportPath, 260); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) { - DrawDeepDungeonItemsTab(ref save, ref saveAndClose); - DrawCommunityTab(ref saveAndClose); - DrawImportTab(); - DrawExportTab(); - DrawRenderTab(ref save, ref saveAndClose); - DrawDebugTab(); - - ImGui.EndTabBar(); - } - - _importDialog.Draw(); - - if (save || saveAndClose) - { - _configuration.Mode = (EMode)_mode; - _configuration.Renderer.SelectedRenderer = (ERenderer)_renderer; - _configuration.DeepDungeons.Traps = _trapConfig.Build(); - _configuration.DeepDungeons.HoardCoffers = _hoardConfig.Build(); - _configuration.DeepDungeons.SilverCoffers = _silverConfig.Build(); - _configuration.DeepDungeons.GoldCoffers = _goldConfig.Build(); - - _configurationManager.Save(_configuration); - - if (saveAndClose) - IsOpen = false; - } - } - - private void DrawDeepDungeonItemsTab(ref bool save, ref bool saveAndClose) - { - if (ImGui.BeginTabItem($"{Localization.ConfigTab_DeepDungeons}###TabDeepDungeons")) - { - ImGui.PushID("trap"); - ImGui.Checkbox(Localization.Config_Traps_Show, ref _trapConfig.Show); - ImGui.Indent(); - ImGui.BeginDisabled(!_trapConfig.Show); - ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_Traps_Color, ref _trapConfig.Color, ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_Traps_HideImpossible, ref _trapConfig.OnlyVisibleAfterPomander); - ImGui.SameLine(); - ImGuiComponents.HelpMarker(Localization.Config_Traps_HideImpossible_ToolTip); - ImGui.EndDisabled(); - ImGui.Unindent(); - ImGui.PopID(); - - ImGui.Separator(); - - ImGui.PushID("hoard"); - ImGui.Checkbox(Localization.Config_HoardCoffers_Show, ref _hoardConfig.Show); - ImGui.Indent(); - ImGui.BeginDisabled(!_hoardConfig.Show); - ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_HoardCoffers_Color, ref _hoardConfig.Color, - ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_HoardCoffers_HideImpossible, - ref _hoardConfig.OnlyVisibleAfterPomander); - ImGui.SameLine(); - ImGuiComponents.HelpMarker(Localization.Config_HoardCoffers_HideImpossible_ToolTip); - ImGui.EndDisabled(); - ImGui.Unindent(); - ImGui.PopID(); - - ImGui.Separator(); - - ImGui.PushID("silver"); - ImGui.Checkbox(Localization.Config_SilverCoffer_Show, ref _silverConfig.Show); - ImGuiComponents.HelpMarker(Localization.Config_SilverCoffers_ToolTip); - ImGui.Indent(); - ImGui.BeginDisabled(!_silverConfig.Show); - ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_SilverCoffer_Color, ref _silverConfig.Color, - ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_SilverCoffer_Filled, ref _silverConfig.Fill); - ImGui.EndDisabled(); - ImGui.Unindent(); - ImGui.PopID(); - - ImGui.Separator(); - - ImGui.PushID("gold"); - ImGui.Checkbox(Localization.Config_GoldCoffer_Show, ref _goldConfig.Show); - ImGuiComponents.HelpMarker(Localization.Config_GoldCoffers_ToolTip); - ImGui.Indent(); - ImGui.BeginDisabled(!_goldConfig.Show); - ImGui.Spacing(); - ImGui.ColorEdit4(Localization.Config_GoldCoffer_Color, ref _goldConfig.Color, - ImGuiColorEditFlags.NoInputs); - ImGui.Checkbox(Localization.Config_GoldCoffer_Filled, ref _goldConfig.Fill); - ImGui.EndDisabled(); - ImGui.Unindent(); - ImGui.PopID(); - - ImGui.Separator(); - - save = ImGui.Button(Localization.Save); - ImGui.SameLine(); - saveAndClose = ImGui.Button(Localization.SaveAndClose); - - ImGui.EndTabItem(); - } - } - - private void DrawCommunityTab(ref bool saveAndClose) - { - if (PalImGui.BeginTabItemWithFlags($"{Localization.ConfigTab_Community}###TabCommunity", - _switchToCommunityTab ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) - { - _switchToCommunityTab = false; - - ImGui.TextWrapped(Localization.Explanation_3); - ImGui.TextWrapped(Localization.Explanation_4); - - PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _mode, - (int)EMode.Online); - PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _mode, - (int)EMode.Offline); - saveAndClose = ImGui.Button(Localization.SaveAndClose); - - ImGui.Separator(); - - ImGui.BeginDisabled(_configuration.Mode != EMode.Online); - if (ImGui.Button(Localization.Config_TestConnection)) - TestConnection(); - - if (_connectionText != null) - ImGui.Text(_connectionText); - - ImGui.EndDisabled(); - ImGui.EndTabItem(); - } - } - - private void DrawImportTab() - { - if (ImGui.BeginTabItem($"{Localization.ConfigTab_Import}###TabImport")) - { - ImGui.TextWrapped(Localization.Config_ImportExplanation1); - ImGui.TextWrapped(Localization.Config_ImportExplanation2); - ImGui.TextWrapped(Localization.Config_ImportExplanation3); - ImGui.Separator(); - ImGui.TextWrapped(string.Format(Localization.Config_ImportDownloadLocation, - "https://github.com/carvelli/PalacePal/releases/")); - if (ImGui.Button(Localization.Config_Import_VisitGitHub)) - GenericHelpers.ShellStart("https://github.com/carvelli/PalacePal/releases/latest"); - ImGui.Separator(); - ImGui.Text(Localization.Config_SelectImportFile); - ImGui.SameLine(); - ImGui.InputTextWithHint("", Localization.Config_SelectImportFile_Hint, ref _openImportPath, 260); - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) - { - _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", - (success, paths) => - { - if (success && paths.Count == 1) - { - _openImportPath = paths.First(); - } - }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); - _openImportDialogStartPath = - null; // only use this once, FileDialogManager will save path between calls - } - - ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath) || _floorService.IsImportRunning); - if (ImGui.Button(Localization.Config_StartImport)) - DoImport(_openImportPath); - ImGui.EndDisabled(); - - ImportHistory? importHistory = _lastImport; - if (importHistory != null) - { - ImGui.Separator(); - ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, - importHistory.ImportedAt.ToLocalTime(), - importHistory.RemoteUrl, - importHistory.ExportedAt.ToUniversalTime())); - ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); - - ImGui.BeginDisabled(_floorService.IsImportRunning); - if (ImGui.Button(Localization.Config_UndoImport)) - UndoImport(importHistory.Id); - ImGui.EndDisabled(); - } - - ImGui.EndTabItem(); - } - } - - private void DrawExportTab() - { - if (_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "export:run") && - ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) - { - string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; - if (string.IsNullOrEmpty(_saveExportPath) && !string.IsNullOrEmpty(_saveExportDialogStartPath)) - _saveExportPath = Path.Join(_saveExportDialogStartPath, todaysFileName); - - ImGui.TextWrapped(string.Format(Localization.Config_ExportSource, RemoteApi.RemoteUrl)); - ImGui.Text(Localization.Config_Export_SaveAs); - ImGui.SameLine(); - ImGui.InputTextWithHint("", Localization.Config_SelectImportFile_Hint, ref _saveExportPath, 260); - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) - { - _importDialog.SaveFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", - todaysFileName, "pal", (success, path) => - { - if (success && !string.IsNullOrEmpty(path)) - { - _saveExportPath = path; - } - }, startPath: _saveExportDialogStartPath, isModal: false); - _saveExportDialogStartPath = - null; // only use this once, FileDialogManager will save path between calls - } - - ImGui.BeginDisabled(string.IsNullOrEmpty(_saveExportPath) || File.Exists(_saveExportPath)); - if (ImGui.Button(Localization.Config_StartExport)) - DoExport(_saveExportPath); - ImGui.EndDisabled(); - - ImGui.EndTabItem(); - } - } - - private void DrawRenderTab(ref bool save, ref bool saveAndClose) - { - if (ImGui.BeginTabItem($"{Localization.ConfigTab_Renderer}###TabRenderer")) - { - ImGui.Text(Localization.Config_SelectRenderBackend); - ImGui.RadioButton( - $"{Localization.Config_Renderer_Splatoon} ({Localization.Config_Renderer_Splatoon_Hint})", - ref _renderer, (int)ERenderer.Splatoon); - ImGui.RadioButton($"{Localization.Config_Renderer_Simple} ({Localization.Config_Renderer_Simple_Hint})", - ref _renderer, (int)ERenderer.Simple); - - ImGui.Separator(); - - save = ImGui.Button(Localization.Save); - ImGui.SameLine(); - saveAndClose = ImGui.Button(Localization.SaveAndClose); - - ImGui.Separator(); - if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) - _renderAdapter.DrawDebugItems(ImGui.ColorConvertFloat4ToU32(_trapConfig.Color), - ImGui.ColorConvertFloat4ToU32(_hoardConfig.Color)); - - ImGui.EndTabItem(); - } - } - - private void DrawDebugTab() - { - if (ImGui.BeginTabItem($"{Localization.ConfigTab_Debug}###TabDebug")) - { - 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 = {memoryTerritory?.SyncState.ToString() ?? "Unknown"}"); - ImGui.Text($"{_debugState.DebugMessage}"); - - ImGui.Indent(); - if (memoryTerritory != null) + _importDialog.OpenFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + (success, paths) => { - if (_trapConfig.Show) + if (success && paths.Count == 1) { - int traps = memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Trap); - ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); + _openImportPath = paths.First(); } + }, selectionCountMax: 1, startPath: _openImportDialogStartPath, isModal: false); + _openImportDialogStartPath = + null; // only use this once, FileDialogManager will save path between calls + } - if (_hoardConfig.Show) + ImGui.BeginDisabled(string.IsNullOrEmpty(_openImportPath) || !File.Exists(_openImportPath) || _floorService.IsImportRunning); + if (ImGui.Button(Localization.Config_StartImport)) + DoImport(_openImportPath); + ImGui.EndDisabled(); + + ImportHistory? importHistory = _lastImport; + if (importHistory != null) + { + ImGui.Separator(); + ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, + importHistory.ImportedAt.ToLocalTime(), + importHistory.RemoteUrl, + importHistory.ExportedAt.ToUniversalTime())); + ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); + + ImGui.BeginDisabled(_floorService.IsImportRunning); + if (ImGui.Button(Localization.Config_UndoImport)) + UndoImport(importHistory.Id); + ImGui.EndDisabled(); + } + + ImGui.EndTabItem(); + } + } + + private void DrawExportTab() + { + if (_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "export:run") && + ImGui.BeginTabItem($"{Localization.ConfigTab_Export}###TabExport")) + { + string todaysFileName = $"export-{DateTime.Today:yyyy-MM-dd}.pal"; + if (string.IsNullOrEmpty(_saveExportPath) && !string.IsNullOrEmpty(_saveExportDialogStartPath)) + _saveExportPath = Path.Join(_saveExportDialogStartPath, todaysFileName); + + ImGui.TextWrapped(string.Format(Localization.Config_ExportSource, RemoteApi.RemoteUrl)); + ImGui.Text(Localization.Config_Export_SaveAs); + ImGui.SameLine(); + ImGui.InputTextWithHint("", Localization.Config_SelectImportFile_Hint, ref _saveExportPath, 260); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) + { + _importDialog.SaveFileDialog(Localization.Palace_Pal, $"{Localization.Palace_Pal} (*.pal) {{.pal}}", + todaysFileName, "pal", (success, path) => + { + if (success && !string.IsNullOrEmpty(path)) { - int hoardCoffers = - memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Hoard); - ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); + _saveExportPath = path; } + }, startPath: _saveExportDialogStartPath, isModal: false); + _saveExportDialogStartPath = + null; // only use this once, FileDialogManager will save path between calls + } - if (_silverConfig.Show) - { - int silverCoffers = - _floorService.EphemeralLocations.Count(x => - x.Type == MemoryLocation.EType.SilverCoffer); - ImGui.Text( - $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); - } + ImGui.BeginDisabled(string.IsNullOrEmpty(_saveExportPath) || File.Exists(_saveExportPath)); + if (ImGui.Button(Localization.Config_StartExport)) + DoExport(_saveExportPath); + ImGui.EndDisabled(); - if (_goldConfig.Show) - { - int goldCoffers = - _floorService.EphemeralLocations.Count(x => - x.Type == MemoryLocation.EType.GoldCoffer); - ImGui.Text( - $"{goldCoffers} silver coffer{(goldCoffers == 1 ? "" : "s")} visible on current floor"); - } + ImGui.EndTabItem(); + } + } - ImGui.Text($"Pomander of Sight: {_territoryState.PomanderOfSight}"); - ImGui.Text($"Pomander of Intuition: {_territoryState.PomanderOfIntuition}"); + private void DrawRenderTab(ref bool save, ref bool saveAndClose) + { + if (ImGui.BeginTabItem($"{Localization.ConfigTab_Renderer}###TabRenderer")) + { + ImGui.Text(Localization.Config_SelectRenderBackend); + ImGui.RadioButton( + $"{Localization.Config_Renderer_Splatoon} ({Localization.Config_Renderer_Splatoon_Hint})", + ref _renderer, (int)ERenderer.Splatoon); + ImGui.RadioButton($"{Localization.Config_Renderer_Simple} ({Localization.Config_Renderer_Simple_Hint})", + ref _renderer, (int)ERenderer.Simple); + + ImGui.Separator(); + + save = ImGui.Button(Localization.Save); + ImGui.SameLine(); + saveAndClose = ImGui.Button(Localization.SaveAndClose); + + ImGui.Separator(); + if (ImGui.Button(Localization.Config_Splatoon_DrawCircles)) + _renderAdapter.DrawDebugItems(ImGui.ColorConvertFloat4ToU32(_trapConfig.Color), + ImGui.ColorConvertFloat4ToU32(_hoardConfig.Color)); + + ImGui.EndTabItem(); + } + } + + private void DrawDebugTab() + { + if (ImGui.BeginTabItem($"{Localization.ConfigTab_Debug}###TabDebug")) + { + 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 = {memoryTerritory?.SyncState.ToString() ?? "Unknown"}"); + ImGui.Text($"{_debugState.DebugMessage}"); + + ImGui.Indent(); + if (memoryTerritory != null) + { + if (_trapConfig.Show) + { + int traps = memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Trap); + ImGui.Text($"{traps} known trap{(traps == 1 ? "" : "s")}"); } - else - ImGui.Text("Could not query current trap/coffer count."); - ImGui.Unindent(); - ImGui.TextWrapped( - "Traps and coffers may not be discovered even after using a pomander if they're far away (around 1,5-2 rooms)."); + if (_hoardConfig.Show) + { + int hoardCoffers = + memoryTerritory.Locations.Count(x => x.Type == MemoryLocation.EType.Hoard); + ImGui.Text($"{hoardCoffers} known hoard coffer{(hoardCoffers == 1 ? "" : "s")}"); + } + + if (_silverConfig.Show) + { + int silverCoffers = + _floorService.EphemeralLocations.Count(x => + x.Type == MemoryLocation.EType.SilverCoffer); + ImGui.Text( + $"{silverCoffers} silver coffer{(silverCoffers == 1 ? "" : "s")} visible on current floor"); + } + + if (_goldConfig.Show) + { + int goldCoffers = + _floorService.EphemeralLocations.Count(x => + x.Type == MemoryLocation.EType.GoldCoffer); + ImGui.Text( + $"{goldCoffers} silver coffer{(goldCoffers == 1 ? "" : "s")} visible on current floor"); + } + + ImGui.Text($"Pomander of Sight: {_territoryState.PomanderOfSight}"); + ImGui.Text($"Pomander of Intuition: {_territoryState.PomanderOfIntuition}"); } else - ImGui.Text(Localization.Config_Debug_NotInADeepDungeon); + ImGui.Text("Could not query current trap/coffer count."); - ImGui.EndTabItem(); + ImGui.Unindent(); + ImGui.TextWrapped( + "Traps and coffers may not be discovered even after using a pomander if they're far away (around 1,5-2 rooms)."); } - } + else + ImGui.Text(Localization.Config_Debug_NotInADeepDungeon); - internal void TestConnection() + ImGui.EndTabItem(); + } + } + + internal void TestConnection() + { + Task.Run(async () => { - Task.Run(async () => - { - _connectionText = Localization.Config_TestConnection_Connecting; - _switchToCommunityTab = true; - - _testConnectionCts?.Cancel(); - - CancellationTokenSource cts = new(); - cts.CancelAfter(TimeSpan.FromSeconds(60)); - _testConnectionCts = cts; - - try - { - _connectionText = await _remoteApi.VerifyConnection(cts.Token); - } - catch (Exception e) - { - if (cts == _testConnectionCts) - { - _logger.LogError(e, "Could not establish remote connection"); - _connectionText = e.ToString(); - } - else - _logger.LogWarning(e, - "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); - } - }); - } - - private void DoImport(string sourcePath) - { - _frameworkService.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); - } - - private void UndoImport(Guid importId) - { - _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); - } - - internal void UpdateLastImport() - { - _lastImportCts?.Cancel(); - CancellationTokenSource cts = new CancellationTokenSource(); - _lastImportCts = cts; - - Task.Run(async () => - { - try - { - _lastImport = await _importService.FindLast(cts.Token); - } - catch (Exception e) - { - _logger.LogError(e, "Unable to fetch last import"); - } - }, cts.Token); - } - - private void DoExport(string destinationPath) - { - Task.Run(async () => - { - try - { - (bool success, ExportRoot export) = await _remoteApi.DoExport(); - if (success) - { - await using var output = File.Create(destinationPath); - export.WriteTo(output); - - _chat.Message($"Export saved as {destinationPath}."); - } - else - { - _chat.Error("Export failed due to server error."); - } - } - catch (Exception e) - { - _logger.LogError(e, "Export failed"); - _chat.Error($"Export failed: {e}"); - } - }); - } - - private sealed class ConfigurableMarker - { - public bool Show; - public Vector4 Color; - public bool OnlyVisibleAfterPomander; - public bool Fill; - - public ConfigurableMarker() + _connectionText = Localization.Config_TestConnection_Connecting; + _switchToCommunityTab = true; + + _testConnectionCts?.Cancel(); + + CancellationTokenSource cts = new(); + cts.CancelAfter(TimeSpan.FromSeconds(60)); + _testConnectionCts = cts; + + try { + _connectionText = await _remoteApi.VerifyConnection(cts.Token); } - - public ConfigurableMarker(MarkerConfiguration config) + catch (Exception e) { - Show = config.Show; - Color = ImGui.ColorConvertU32ToFloat4(config.Color); - OnlyVisibleAfterPomander = config.OnlyVisibleAfterPomander; - Fill = config.Fill; - } - - public MarkerConfiguration Build() - { - return new MarkerConfiguration + if (cts == _testConnectionCts) { - Show = Show, - Color = ImGui.ColorConvertFloat4ToU32(Color), - OnlyVisibleAfterPomander = OnlyVisibleAfterPomander, - Fill = Fill - }; + _logger.LogError(e, "Could not establish remote connection"); + _connectionText = e.ToString(); + } + else + _logger.LogWarning(e, + "Could not establish a remote connection, but user also clicked 'test connection' again so not updating UI"); } + }); + } + + private void DoImport(string sourcePath) + { + _frameworkService.EarlyEventQueue.Enqueue(new QueuedImport(sourcePath)); + } + + private void UndoImport(Guid importId) + { + _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); + } + + internal void UpdateLastImport() + { + _lastImportCts?.Cancel(); + CancellationTokenSource cts = new CancellationTokenSource(); + _lastImportCts = cts; + + Task.Run(async () => + { + try + { + _lastImport = await _importService.FindLast(cts.Token); + } + catch (Exception e) + { + _logger.LogError(e, "Unable to fetch last import"); + } + }, cts.Token); + } + + private void DoExport(string destinationPath) + { + Task.Run(async () => + { + try + { + (bool success, ExportRoot export) = await _remoteApi.DoExport(); + if (success) + { + await using var output = File.Create(destinationPath); + export.WriteTo(output); + + _chat.Message($"Export saved as {destinationPath}."); + } + else + { + _chat.Error("Export failed due to server error."); + } + } + catch (Exception e) + { + _logger.LogError(e, "Export failed"); + _chat.Error($"Export failed: {e}"); + } + }); + } + + private sealed class ConfigurableMarker + { + public bool Show; + public Vector4 Color; + public bool OnlyVisibleAfterPomander; + public bool Fill; + + public ConfigurableMarker() + { + } + + public ConfigurableMarker(MarkerConfiguration config) + { + Show = config.Show; + Color = ImGui.ColorConvertU32ToFloat4(config.Color); + OnlyVisibleAfterPomander = config.OnlyVisibleAfterPomander; + Fill = config.Fill; + } + + public MarkerConfiguration Build() + { + return new MarkerConfiguration + { + Show = Show, + Color = ImGui.ColorConvertFloat4ToU32(Color), + OnlyVisibleAfterPomander = OnlyVisibleAfterPomander, + Fill = Fill + }; } } } diff --git a/Pal.Client/Windows/StatisticsWindow.cs b/Pal.Client/Windows/StatisticsWindow.cs index 13ccff5..046cea8 100644 --- a/Pal.Client/Windows/StatisticsWindow.cs +++ b/Pal.Client/Windows/StatisticsWindow.cs @@ -8,119 +8,118 @@ using Pal.Client.Properties; using Pal.Common; using Palace; -namespace Pal.Client.Windows +namespace Pal.Client.Windows; + +internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged { - internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged + private const string WindowId = "###PalacePalStats"; + private readonly WindowSystem _windowSystem; + private readonly SortedDictionary _territoryStatistics = new(); + + public StatisticsWindow(WindowSystem windowSystem) + : base(WindowId) { - private const string WindowId = "###PalacePalStats"; - private readonly WindowSystem _windowSystem; - private readonly SortedDictionary _territoryStatistics = new(); + _windowSystem = windowSystem; - public StatisticsWindow(WindowSystem windowSystem) - : base(WindowId) + LanguageChanged(); + + Size = new Vector2(500, 500); + SizeCondition = ImGuiCond.FirstUseEver; + Flags = ImGuiWindowFlags.AlwaysAutoResize; + + foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) { - _windowSystem = windowSystem; - - LanguageChanged(); - - Size = new Vector2(500, 500); - SizeCondition = ImGuiCond.FirstUseEver; - Flags = ImGuiWindowFlags.AlwaysAutoResize; - - foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) - { - _territoryStatistics[territory] = new TerritoryStatistics(territory.ToString()); - } - - _windowSystem.AddWindow(this); + _territoryStatistics[territory] = new TerritoryStatistics(territory.ToString()); } - public void Dispose() - => _windowSystem.RemoveWindow(this); + _windowSystem.AddWindow(this); + } - public void LanguageChanged() - => WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}"; + public void Dispose() + => _windowSystem.RemoveWindow(this); - public override void Draw() + public void LanguageChanged() + => WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}"; + + public override void Draw() + { + if (ImGui.BeginTabBar("Tabs")) { - if (ImGui.BeginTabBar("Tabs")) - { - DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10, - ETerritoryType.Palace_191_200); - DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10, - ETerritoryType.HeavenOnHigh_91_100); - DrawDungeonStats("Eureka Orthos", Localization.EurekaOrthos, ETerritoryType.EurekaOrthos_1_10, - ETerritoryType.EurekaOrthos_91_100); - } + DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10, + ETerritoryType.Palace_191_200); + DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10, + ETerritoryType.HeavenOnHigh_91_100); + DrawDungeonStats("Eureka Orthos", Localization.EurekaOrthos, ETerritoryType.EurekaOrthos_1_10, + ETerritoryType.EurekaOrthos_91_100); } + } - private void DrawDungeonStats(string id, string name, ETerritoryType minTerritory, ETerritoryType maxTerritory) + private void DrawDungeonStats(string id, string name, ETerritoryType minTerritory, ETerritoryType maxTerritory) + { + if (ImGui.BeginTabItem($"{name}###{id}")) { - if (ImGui.BeginTabItem($"{name}###{id}")) + if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4, + ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) { - if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4, - ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) + ImGui.TableSetupColumn(Localization.Statistics_TerritoryId); + ImGui.TableSetupColumn(Localization.Statistics_InstanceName); + ImGui.TableSetupColumn(Localization.Statistics_Traps); + ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers); + ImGui.TableHeadersRow(); + + foreach (var (territoryType, stats) in _territoryStatistics + .Where(x => x.Key >= minTerritory && x.Key <= maxTerritory) + .OrderBy(x => x.Key.GetOrder() ?? (int)x.Key)) { - ImGui.TableSetupColumn(Localization.Statistics_TerritoryId); - ImGui.TableSetupColumn(Localization.Statistics_InstanceName); - ImGui.TableSetupColumn(Localization.Statistics_Traps); - ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers); - ImGui.TableHeadersRow(); + ImGui.TableNextRow(); + if (ImGui.TableNextColumn()) + ImGui.Text($"{(uint)territoryType}"); - foreach (var (territoryType, stats) in _territoryStatistics - .Where(x => x.Key >= minTerritory && x.Key <= maxTerritory) - .OrderBy(x => x.Key.GetOrder() ?? (int)x.Key)) - { - ImGui.TableNextRow(); - if (ImGui.TableNextColumn()) - ImGui.Text($"{(uint)territoryType}"); + if (ImGui.TableNextColumn()) + ImGui.Text(stats.TerritoryName); - if (ImGui.TableNextColumn()) - ImGui.Text(stats.TerritoryName); + if (ImGui.TableNextColumn()) + ImGui.Text(stats.TrapCount?.ToString() ?? "-"); - if (ImGui.TableNextColumn()) - ImGui.Text(stats.TrapCount?.ToString() ?? "-"); - - if (ImGui.TableNextColumn()) - ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); - } - - ImGui.EndTable(); + if (ImGui.TableNextColumn()) + ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); } - ImGui.EndTabItem(); + ImGui.EndTable(); } + + ImGui.EndTabItem(); + } + } + + internal void SetFloorData(IEnumerable floorStatistics) + { + foreach (var territoryStatistics in _territoryStatistics.Values) + { + territoryStatistics.TrapCount = null; + territoryStatistics.HoardCofferCount = null; } - internal void SetFloorData(IEnumerable floorStatistics) + foreach (var floor in floorStatistics) { - foreach (var territoryStatistics in _territoryStatistics.Values) + if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, + out TerritoryStatistics? territoryStatistics)) { - territoryStatistics.TrapCount = null; - territoryStatistics.HoardCofferCount = null; - } - - foreach (var floor in floorStatistics) - { - if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, - out TerritoryStatistics? territoryStatistics)) - { - territoryStatistics.TrapCount = floor.TrapCount; - territoryStatistics.HoardCofferCount = floor.HoardCount; - } - } - } - - private sealed class TerritoryStatistics - { - public string TerritoryName { get; } - public uint? TrapCount { get; set; } - public uint? HoardCofferCount { get; set; } - - public TerritoryStatistics(string territoryName) - { - TerritoryName = territoryName; + territoryStatistics.TrapCount = floor.TrapCount; + territoryStatistics.HoardCofferCount = floor.HoardCount; } } } + + private sealed class TerritoryStatistics + { + public string TerritoryName { get; } + public uint? TrapCount { get; set; } + public uint? HoardCofferCount { get; set; } + + public TerritoryStatistics(string territoryName) + { + TerritoryName = territoryName; + } + } } diff --git a/Pal.Common/ETerritoryType.cs b/Pal.Common/ETerritoryType.cs index 71788c0..9b5c418 100644 --- a/Pal.Common/ETerritoryType.cs +++ b/Pal.Common/ETerritoryType.cs @@ -1,63 +1,62 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; -namespace Pal.Common +namespace Pal.Common; + +[SuppressMessage("ReSharper", "UnusedMember.Global")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum ETerritoryType : ushort { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum ETerritoryType : ushort - { - Palace_1_10 = 561, - Palace_11_20, - Palace_21_30, - Palace_31_40, - Palace_41_50, - Palace_51_60 = 593, - Palace_61_70, - Palace_71_80, - Palace_81_90, - Palace_91_100, - Palace_101_110, - Palace_111_120, - Palace_121_130, - Palace_131_140, - Palace_141_150, - Palace_151_160, - Palace_161_170, - Palace_171_180, - Palace_181_190, - Palace_191_200, + Palace_1_10 = 561, + Palace_11_20, + Palace_21_30, + Palace_31_40, + Palace_41_50, + Palace_51_60 = 593, + Palace_61_70, + Palace_71_80, + Palace_81_90, + Palace_91_100, + Palace_101_110, + Palace_111_120, + Palace_121_130, + Palace_131_140, + Palace_141_150, + Palace_151_160, + Palace_161_170, + Palace_171_180, + Palace_181_190, + Palace_191_200, - [Display(Order = 1)] - HeavenOnHigh_1_10 = 770, - [Display(Order = 2)] - HeavenOnHigh_11_20 = 771, - [Display(Order = 3)] - HeavenOnHigh_21_30 = 772, - [Display(Order = 4)] - HeavenOnHigh_31_40 = 782, - [Display(Order = 5)] - HeavenOnHigh_41_50 = 773, - [Display(Order = 6)] - HeavenOnHigh_51_60 = 783, - [Display(Order = 7)] - HeavenOnHigh_61_70 = 774, - [Display(Order = 8)] - HeavenOnHigh_71_80 = 784, - [Display(Order = 9)] - HeavenOnHigh_81_90 = 775, - [Display(Order = 10)] - HeavenOnHigh_91_100 = 785, + [Display(Order = 1)] + HeavenOnHigh_1_10 = 770, + [Display(Order = 2)] + HeavenOnHigh_11_20 = 771, + [Display(Order = 3)] + HeavenOnHigh_21_30 = 772, + [Display(Order = 4)] + HeavenOnHigh_31_40 = 782, + [Display(Order = 5)] + HeavenOnHigh_41_50 = 773, + [Display(Order = 6)] + HeavenOnHigh_51_60 = 783, + [Display(Order = 7)] + HeavenOnHigh_61_70 = 774, + [Display(Order = 8)] + HeavenOnHigh_71_80 = 784, + [Display(Order = 9)] + HeavenOnHigh_81_90 = 775, + [Display(Order = 10)] + HeavenOnHigh_91_100 = 785, - EurekaOrthos_1_10 = 1099, - EurekaOrthos_11_20, - EurekaOrthos_21_30, - EurekaOrthos_31_40, - EurekaOrthos_41_50, - EurekaOrthos_51_60, - EurekaOrthos_61_70, - EurekaOrthos_71_80, - EurekaOrthos_81_90, - EurekaOrthos_91_100 - } + EurekaOrthos_1_10 = 1099, + EurekaOrthos_11_20, + EurekaOrthos_21_30, + EurekaOrthos_31_40, + EurekaOrthos_41_50, + EurekaOrthos_51_60, + EurekaOrthos_61_70, + EurekaOrthos_71_80, + EurekaOrthos_81_90, + EurekaOrthos_91_100 } diff --git a/Pal.Common/EnumExtensions.cs b/Pal.Common/EnumExtensions.cs index e978d8e..532ce67 100644 --- a/Pal.Common/EnumExtensions.cs +++ b/Pal.Common/EnumExtensions.cs @@ -1,16 +1,15 @@ using System.ComponentModel.DataAnnotations; using System.Reflection; -namespace Pal.Common +namespace Pal.Common; + +public static class EnumExtensions { - public static class EnumExtensions + public static int? GetOrder(this Enum e) { - public static int? GetOrder(this Enum e) - { - Type type = e.GetType(); - MemberInfo field = type.GetMember(e.ToString()).Single(); - DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast().FirstOrDefault(); - return attribute?.Order; - } + Type type = e.GetType(); + MemberInfo field = type.GetMember(e.ToString()).Single(); + DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast().FirstOrDefault(); + return attribute?.Order; } } diff --git a/Pal.Common/ExportConfig.cs b/Pal.Common/ExportConfig.cs index 408b6f2..6c1ffe2 100644 --- a/Pal.Common/ExportConfig.cs +++ b/Pal.Common/ExportConfig.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Pal.Common +namespace Pal.Common; + +public static class ExportConfig { - public static class ExportConfig - { - public static int ExportVersion => 2; - } + public static int ExportVersion => 2; } diff --git a/Pal.Common/PalaceMath.cs b/Pal.Common/PalaceMath.cs index 19c890c..a3354eb 100644 --- a/Pal.Common/PalaceMath.cs +++ b/Pal.Common/PalaceMath.cs @@ -1,22 +1,21 @@ using System.Numerics; -namespace Pal.Common +namespace Pal.Common; + +public class PalaceMath { - public class PalaceMath + private static readonly Vector3 ScaleFactor = new(5); + + public static bool IsNearlySamePosition(Vector3 a, Vector3 b) { - private static readonly Vector3 ScaleFactor = new(5); + a *= ScaleFactor; + b *= ScaleFactor; + return (int)a.X == (int)b.X && (int)a.Y == (int)b.Y && (int)a.Z == (int)b.Z; + } - public static bool IsNearlySamePosition(Vector3 a, Vector3 b) - { - a *= ScaleFactor; - b *= ScaleFactor; - return (int)a.X == (int)b.X && (int)a.Y == (int)b.Y && (int)a.Z == (int)b.Z; - } - - public static int GetHashCode(Vector3 v) - { - v *= ScaleFactor; - return HashCode.Combine((int)v.X, (int)v.Y, (int)v.Z); - } + public static int GetHashCode(Vector3 v) + { + v *= ScaleFactor; + return HashCode.Combine((int)v.X, (int)v.Y, (int)v.Z); } }